Last active
August 29, 2015 14:17
-
-
Save marbemac/a18582da8d2912285950 to your computer and use it in GitHub Desktop.
A query mixin for Baobab, supporting local and remote fetching, error handling, and loading handling.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// actions.js provides functions to fetch | |
// remote data and store it in the tree | |
// these remote functions should return | |
// promises | |
require('es6-promise').polyfill(); | |
var stateTree = require('./stateTree'), | |
PostModel = require('./post'), | |
fetch = require('isomorphic-fetch'), | |
utils = require('./utils'), | |
_merge = require('lodash.merge'), | |
format = require('util').format, | |
debug = require('../mixins/debug'); | |
///////////////////// | |
// MODEL CONSTANTS // | |
///////////////////// | |
var modelMeta = { | |
post: { | |
url: '/posts', | |
stateStore: 'posts', | |
instance: PostModel | |
} | |
} | |
var models = { | |
POST: 'post' | |
} | |
/////////////////// | |
// QUERY METHODS // | |
/////////////////// | |
var findOneQueries = {}; | |
function findOne(model, id, force) { | |
// utils.buildQueryId creates a consistent, unique signature for this model/id | |
var queryId = utils.buildQueryId(model, id); | |
// If we're forcing a lookup, or we haven't looked this up before | |
if (force || !findOneQueries[queryId]) { | |
var r = get(format(modelMeta[model].url + '/%s', id), null) | |
var p = new Promise(function(resolve, reject) { | |
r.then(function(res) { | |
findOneQueries[queryId] = true; | |
if (!res.data) { | |
reject(res); | |
return; | |
} | |
setModel(model, res.data); | |
resolve(res.data); | |
}).catch(function(err) { | |
findOneQueries[queryId] = true; | |
debug.error('store:error', err); | |
reject(err); | |
}); | |
}); | |
findOneQueries[queryId] = p; | |
} | |
return findOneQueries[queryId]; | |
} | |
var findQueries = {}; | |
function find(model, query, modifiers, force) { | |
// utils.buildQueryId creates a consistent, unique signature for this model/params | |
var params = _merge(query, modifiers), | |
queryId = utils.buildQueryId(model, params); | |
// If we're forcing a lookup, or we haven't looked this up before | |
if (force || !findQueries[queryId]) { | |
var r = get(modelMeta[model].url, params); | |
var p = new Promise(function(resolve, reject) { | |
r.then(function(res) { | |
findQueries[queryId] = true; | |
if (!res.data) { | |
reject(res); | |
return; | |
} | |
// Loop through the response data and set/overwrite the models in the store | |
var storedData = stateTree.get(['models', modelMeta[model].stateStore]); | |
for (var k in res.data) { | |
storedData[res.data[k].id] = new modelMeta[model].instance(res.data[k]); | |
} | |
// update the tree, overwriting the entire models.[modelType] subtree | |
stateTree.set(['models', modelMeta[model].stateStore], storedData); | |
resolve(res.data); | |
}).catch(function(err) { | |
findQueries[queryId] = true; | |
debug.error('store:error', err); | |
reject(err); | |
}); | |
}); | |
findQueries[queryId] = p; | |
} | |
return findQueries[queryId]; | |
} | |
///////////////////// | |
// GENERIC HELPERS // | |
///////////////////// | |
function setModel(model, data) { | |
// update the tree, setting models.[modelType].[modelId] to this model data | |
stateTree.set(['models', modelMeta[model].stateStore, data.id], new modelMeta[model].instance(data)); | |
} | |
function get(url, query) { | |
var session = JSON.parse(localStorage.session); | |
var options = { | |
method: 'GET', | |
headers: {} | |
} | |
if (session && session.authToken) { | |
options.headers['Authorization'] = 'Bearer ' + session.authToken; | |
} | |
if (query) { | |
url += utils.queryToParams(query); | |
} | |
return fetch(__ENV__.API_HOST + url, options).then(function(res) { | |
return res.json(); | |
}); | |
} | |
///////////// | |
// EXPORTS // | |
///////////// | |
module.exports = { | |
models: models, | |
findOne: findOne, | |
find: find | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
// This mixin allows one to define a queries | |
// structure on components. The definition looks like this: | |
// | |
// mixins: [ stateMixins.query ], | |
// { | |
// queries: { | |
// post: { | |
// cursors: { | |
// posts: ['posts'] | |
// }, | |
// local: function(_this, cursors) { | |
// // _this is a reference to the component | |
// // cursors is an object of cursors defined above | |
// // so in this example you'd have access to cursors.posts | |
// // return the data or null | |
// }, | |
// remote: function(_this, cursors) { | |
// // return a promise or null | |
// } | |
// } | |
// } | |
// } | |
var stateTree = require('./state'), | |
Combination = require('baobab/src/combination'); | |
var QueryMixin = { | |
getInitialState() { | |
// Are there any queries to create? | |
if (!this.queries) | |
return {}; | |
var initialState = {}; | |
this.__bbQueries = { | |
listeners: [], | |
cursors: {} | |
}; | |
// will be called when any of the | |
// queryCursors receives and update from the tree | |
this.queryUpdateHandler = (function(k) { | |
var d = {}; | |
d[k] = this.queryfetchData(k); | |
// only update the state if the data has changed | |
if (this.state[k] !== d[k]) { | |
this.setState(d); | |
} | |
}); | |
// used to populate the data for a particular query | |
this.queryfetchData = (function(queryIndex) { | |
var query = this.queries[queryIndex], | |
cursors = this.__bbQueries.cursors[queryIndex]; | |
// Each query holds all it's meta data | |
// So if you have a query: | |
// queries: { | |
// post: { | |
// cursors: [], | |
// local: function(){} | |
// remote: function(){} | |
// } | |
// } | |
// this.state.post will equal the structure below | |
// so, for example, check this.state.post.loading to see if it's | |
// in the loading state, or this.state.post.result for the actual | |
// data returned from local/remote | |
var data = { | |
cursors: cursors, | |
loading: false, | |
error: null, | |
result: null | |
} | |
// Try the local function | |
data.result = query.local(this, cursors); | |
// No local data (either null or empty array)? Try remote | |
if ((!data.result || (data.result instanceof Array && data.result.length === 0)) && query.remote) { | |
var r = query.remote(this, cursors); | |
// query.remote returns a promise, or null | |
if (r && r instanceof Promise) { | |
data.loading = true; | |
var _this = this; | |
r.then(function() { | |
// no extra processing on success | |
// this is because an update to the tree will | |
// cause this function to be re-run anyways | |
}, function(err) { | |
// on error, set the error, because we don't | |
// update the tree in the error case | |
var d = _this.state[queryIndex]; | |
d.loading = false; | |
d.error = err; | |
_this.setState(d); | |
}); | |
} else { | |
// if query.remote returned null, we're not loading anything | |
data.loading = false; | |
} | |
} | |
return data; | |
}).bind(this); | |
// build the internal query structures for each defined query | |
for (var k in this.queries) { | |
var data = {}, | |
query = this.queries[k]; | |
// can't have a query without cursors | |
if (!query.cursors) | |
continue; | |
// Build the cursors and store them for this query | |
var queryCursors = {}; | |
for (var j in query.cursors) { | |
queryCursors[j] = stateTree.select(query.cursors[j]); | |
} | |
this.__bbQueries.cursors[k] = queryCursors; | |
// Set the initial state for this query | |
// This sets this.state.{queryKey} | |
initialState[k] = this.queryfetchData(k); | |
} | |
return initialState; | |
}, | |
componentDidMount() { | |
if (!this.queries) { | |
return; | |
} | |
// For each query, subscribe to the updates | |
// for the cursors involved in the query. | |
// When an update happens, call the updateHandler | |
// for this query. | |
for (var k in this.queries) { | |
var query = this.queries[k]; | |
var combo = new Combination( | |
'or', | |
Object.keys(query.cursors).map(function(k) { | |
return stateTree.select(query.cursors[k]); | |
}, this) | |
); | |
combo.on('update', this.queryUpdateHandler.bind(this, k)); | |
this.__bbQueries.listeners.push(combo); | |
} | |
}, | |
componentWillUnmount() { | |
for (var k in this.__bbQueries.listeners) { | |
this.__bbQueries.listeners[k].release(); | |
} | |
}, | |
componentWillReceiveProps() { | |
// If the props have changed | |
// we might want to re-run the queries | |
// For example if using react-router and | |
// query.local or query.remote depends on | |
// a prop in the url | |
// | |
// we can probably make this smarter | |
for (var k in this.queries) { | |
this.queryUpdateHandler(k); | |
} | |
} | |
}; | |
module.exports = { | |
query: QueryMixin | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Immutable = require('immutable'); | |
class Post extends Immutable.Record({ | |
id: null, | |
title: null, | |
body: null | |
}) { | |
// Methods go here | |
// foo() { | |
// | |
// } | |
}; | |
module.exports = Post; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
var React = require('react'), | |
{ State } = require('react-router'), | |
StateQuery = require('./query'), | |
StateActions = require('./actions'), | |
StateMixins = require('./mixins'); | |
var Requests = React.createClass({ | |
mixins: [ State, StateMixins.query ], | |
queries: { | |
post: { | |
cursors: { | |
posts: ['models', 'posts'], | |
}, | |
local: function(_this, cursors) { | |
return StateQuery.findOne(cursors.posts.get(), {id: _this.getParams().postId}); | |
}, | |
remote: function(_this, cursors) { | |
return StateActions.findOne(StateActions.models.POST, {id: _this.getParams().postId}, false); | |
} | |
} | |
}, | |
render() { | |
var post = this.state.post; | |
if (post.loading) { | |
return <div>Loading</div>; | |
} else if (post.error) { | |
return <div>Error: {post.error}</div>; | |
} | |
return <div>The post is {post.result.title}</div>; | |
}, | |
}); | |
module.exports = Post; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Provides helper functions to filter | |
// data locally. | |
var StateActions = require('./actions'), | |
_find = require('lodash.find'), | |
_findWhere = require('lodash.findwhere'), | |
_sortByAll = require('lodash.sortbyall'), | |
_values = require('lodash.values'); | |
function findOne(target, query) { | |
var found = false, | |
k; | |
var result = _find(target, function(i) { | |
found = true; | |
for (k in query) { | |
if (i.get(k) != query[k]) { | |
found = false; | |
} | |
} | |
return found; | |
}); | |
return result ? result : null; | |
} | |
function find(target, query) { | |
// we want an array | |
if (target instanceof Object) { | |
target = _values(target); | |
} | |
var found = false, | |
result, | |
k; | |
if (query && Object.keys(query).length > 0) { | |
result = _findWhere(target, function(i) { | |
found = true; | |
for (k in query) { | |
if (i.get(k) != query[k]) { | |
found = false; | |
} | |
} | |
return found; | |
}); | |
} else { | |
result = target; | |
} | |
return result && result instanceof Array ? result : []; | |
} | |
module.exports = { | |
findOne: findOne, | |
find: find | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Baobab = require('baobab'); | |
var state = new Baobab({ | |
models: { | |
posts: {} | |
} | |
}); | |
// Easier debugging in the console | |
window.stateTree = state; | |
module.exports = state; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi! This seems useful. Why a gist instead of a repo?