Created
July 24, 2017 00:17
-
-
Save josiahbryan/75d8c298f0f7b9fe0085fc077564fa34 to your computer and use it in GitHub Desktop.
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
import Ember from 'ember'; | |
/** | |
@function LiveDependantListMixinFactory | |
@param listPropertyName {String} or {Object} - if a string, will be the name | |
of the destination property that you can use in your template as the list. | |
If this param is an object with at least `{selectAs,from}` defined, | |
it will be used as the values for the following args (they will be overwritten | |
with values from the object) | |
@param fromStoreModel - ember-data model name to select from | |
@param whereChildPropertyName - property on the model (`fromStoreModel`) | |
to match as the dependant key | |
@param equalsParentPropertyName - property on the parent object | |
that we are using this mixin with, defaults to 'model' | |
@param sortBy - List of sort args for passing to Ember.computed.sort | |
NOTE: Final property (`listPropertyName`) will have the `isPending` flag | |
set to true while waiting for the initial response from the server. | |
Example usage: | |
```javascript | |
// ...other imports... | |
import LiveDependantListMixinFactory from '../../../utils/models/live-dependant-list'; | |
export default Ember.Controller.extend(LiveDependantListMixinFactory({ | |
selectAs: 'sortedCheckins', | |
from: 'checkin', | |
where: 'contact', | |
equals: 'model', | |
sortBy: ['date:desc'] | |
}), { | |
/// ...the rest of your controller definiton here.... | |
}); | |
``` | |
NOTE: Even though a simple query() would accurately returns the list of linked items for the model, | |
we do the set of live filters here, because when a new item is added to the store, if we only did the query(), | |
the UI *DOES NOT* automatically update with the new record as soon as it's added - | |
a page reload would be required! | |
**/ | |
// Caches if we've already hit the server once during this app's lifecycle | |
// for the given set of query params | |
const TriggerStatusCache = {}; | |
export default function LiveDependantListMixinFactory( | |
listPropertyName, | |
fromStoreModel, | |
whereChildPropertyName, | |
equalsParentPropertyName, | |
sortBy, | |
limitBy | |
) { | |
//const _this = controller; | |
// If {selectAs,from} are on the first arg, assume all options are there | |
// and use that as an object | |
if(listPropertyName.selectAs, | |
listPropertyName.from) { | |
fromStoreModel | |
= listPropertyName.from; | |
whereChildPropertyName | |
= listPropertyName.where; | |
equalsParentPropertyName | |
= listPropertyName.equals; | |
sortBy | |
= listPropertyName.sortBy; | |
limitBy | |
= listPropertyName.limitBy; | |
// NOTE: This must be last ... | |
listPropertyName | |
= listPropertyName.selectAs; | |
} | |
// Don't assume a join - JB 20170619 | |
// if(!equalsParentPropertyName) | |
// equalsParentPropertyName = 'model'; | |
// Require sorting regardless for limit to work - JB 20170619 | |
if(!sortBy) | |
sortBy = ['id']; | |
const mixin = { | |
// Simply doing 'this.get(equalsParentPropertyName).get(whereChildPropertyName)' doesnt work, so | |
// we have to first query the server for our child items, then filter and sort ourselves | |
}; | |
// When adding this mixin to a DS.model, the store is already injected | |
// and will crash with an error if we attempt to re-inject via our mixin | |
if(equalsParentPropertyName != 'this') | |
mixin.store = Ember.inject.service('store'); | |
const mixinPropertyPrefix = `_${listPropertyName}`, | |
trigger = `${mixinPropertyPrefix}_trigger`, | |
peek = `${mixinPropertyPrefix}_peek`, | |
filter = `${mixinPropertyPrefix}_filter`, | |
sortOpts = `${mixinPropertyPrefix}_sortOpts`, | |
sorted = `${mixinPropertyPrefix}_sort`, | |
limit = `${mixinPropertyPrefix}_limit`, | |
// Cache key for "trigger" hit flag on TriggerStatusCache | |
tsCache = `${mixinPropertyPrefix}_tsCacheKey`, | |
// This loading prop will be used to coordinate the query | |
// loading state on the server to the output's `isPending` property | |
// to comply with convention for Ember's promise results | |
loading = `${mixinPropertyPrefix}_loading`; | |
// This variable is used to chain the set of computed filters below | |
// based on what options were actually given. | |
let previousProp = null; | |
const literalParentProperty = equalsParentPropertyName ? equalsParentPropertyName : 'this'; | |
// The initial trigger computed prop does the actual query to the server. | |
// It will only query the server once per app lifecycle (e.g. resets after page | |
// reload) thanks to the singleton TriggerStatusCache defined above. | |
// We can get away with this since the 'store' caches the loaded objects | |
// in memory, so we don't need to requery eveyr time an object using | |
// our mixin is create()ed - we also don't have to cache the data internally | |
// in our mixin - again, the store will have all that data cached, so we | |
// just get it via peek instead of re-querying on subsequent creations | |
// of our dependant objects. | |
mixin[trigger] = Ember.computed(literalParentProperty, function() { | |
// console.error('[LiveDependantListMixinFactory]', literalParentProperty, 'changed'); | |
const query = {}; | |
// If a 'where' option given, set the actual filter value on the query | |
if(whereChildPropertyName) { | |
const parentVal = equalsParentPropertyName == 'this' ? this : this.get(equalsParentPropertyName); | |
query[whereChildPropertyName] = (parentVal ? parentVal.get('id') : null); | |
} | |
// If limit given, set an integer limit on the query (assuming FeathersJS-style $limit) | |
if(limitBy) | |
query.$limit = parseInt(limitBy); | |
// If sorting given, convert from ember-style sort opts to FeathersJS-style $sort opts) | |
if(sortBy) { | |
const sort = {}; | |
// Convert "timestamp:desc" (ember style) | |
// to { timestamp: -1 } for FeathersJS-style sorting | |
sortBy.forEach( (arg) => { | |
let [ key, dir ] = arg.split(':'); | |
dir = (dir + '').toLowerCase() == 'desc' ? -1 : 1; | |
sort[key] = dir; | |
}); | |
query.$sort = sort; | |
} | |
// Build cache key | |
const cacheKey = [tsCache, JSON.stringify(query)].join(':'); | |
// this.set(tsCache, cacheKey); | |
// | |
// if(TriggerStatusCache[cacheKey]) { | |
// // console.log("[query] [before] TriggerStatusCache: ", {tsCache, cacheKey}); | |
// return; | |
// } | |
// console.log("[LiveDependantListMixinFactory] query:",query); | |
// Indicate on our final output property that we are waiting on the server | |
if(!TriggerStatusCache[cacheKey]) | |
this.set(loading, true); | |
// Execute query on server | |
return this.get('store').query(fromStoreModel, {query}).then( () => { | |
// Response received, notify output property | |
if(!this.get('isDestroyed')) { | |
// Cache a flag indicating that we have done our initial query load | |
// from the server. | |
TriggerStatusCache[cacheKey] = true; | |
// console.log("[query] [then] TriggerStatusCache: ", {tsCache, cacheKey}); | |
// Set our loading prop | |
this.set(loading, false); | |
} | |
}); | |
}); | |
previousProp = trigger; | |
// query() does not return a live list, so we have to "peekAll" to get | |
// a live-updated list. | |
mixin[peek] = Ember.computed(previousProp, literalParentProperty, function() { | |
// Check our trigger flag - if we have already queried the server | |
// for this set of query values once in this app, we assume | |
// we don't need to query again because (A) FeathersJS will keep us | |
// updated with new remote objects and (B) peekAll() will get | |
// any new objects added from the store since we last loaded. | |
// const cacheKey = this.get(tsCache); | |
// if(!cacheKey || !TriggerStatusCache[cacheKey]) { | |
// console.log("TriggerStatusCache - cache miss, getting ",trigger," for ", cacheKey); | |
this.get(trigger); | |
// } | |
// else { | |
// console.log("TriggerStatusCache - cache hit for ", cacheKey, TriggerStatusCache); | |
// } | |
return this.get('store').peekAll(fromStoreModel); | |
}); | |
previousProp = peek; | |
// Future items being pushed onto the store (e.g. from server) may hit this | |
// filter so make sure we only show items for parent model, if a parent prop specified | |
if(whereChildPropertyName) { | |
mixin[filter] = Ember.computed.filter( | |
previousProp+'.@each.{'+whereChildPropertyName+'}', | |
function(child) { | |
const parentVal = equalsParentPropertyName == 'this' ? this : this.get(equalsParentPropertyName); | |
const childVal = child.get(whereChildPropertyName); | |
// console.warn("[LiveDependantListMixinFactory] ", { previousProp, filter, whereChildPropertyName, equalsParentPropertyName, parentVal, childVal, child }); | |
return (childVal ? childVal.get('id') : null) === (parentVal ? parentVal.get('id') : null); | |
}); | |
previousProp = filter; | |
} | |
// Sort results by given sort options | |
if(sortBy) { | |
mixin[sortOpts] = sortBy; | |
mixin[sorted] = Ember.computed.sort(previousProp, sortOpts); | |
previousProp = sorted; | |
} | |
// Limit the results to just the top X results, if a 'limitBy' value specified | |
if(limitBy) { | |
const watchProp = previousProp; | |
mixin[limit] = Ember.computed(watchProp, function() { | |
const limitInt = parseInt(limitBy); | |
const array = this.get(watchProp); | |
return array.slice(0, limitInt); | |
}); | |
previousProp = limit; | |
} | |
// The final output property | |
mixin[listPropertyName] = Ember.computed(previousProp, loading, function() { | |
let result = this.get(previousProp), | |
isPending = this.get(loading); | |
// For some weird reason, on subsequent route loads, | |
// the result.isPending will be stuck on true, even though our own loading prop | |
// is false, so we just explicitly override it here | |
if(result && result.set && result.get && !result.get('isDestroyed')) // && result.get('isPending') === undefined) | |
result.set('isPending', isPending); | |
else | |
result = { isPending }; | |
//console.log("[LiveDependantListMixinFactory] final:",listPropertyName,", result:", result, ", result.isPending: ", result.get('isPending'), ", loading? ", isPending ); | |
// const id = (equalsParentPropertyName == 'this' ? this : this.get(equalsParentPropertyName)).get('id'); | |
// console.log("[LiveDependantListMixinFactory] {", listPropertyName, "}: ", { id, result, "result.isPending": result.get('isPending'), isPending }); | |
return result; | |
}); | |
// console.log(mixin, {listPropertyName, | |
// fromStoreModel, | |
// whereChildPropertyName, | |
// equalsParentPropertyName, | |
// sortBy, | |
// limitBy | |
// }); | |
return mixin; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment