Skip to content

Instantly share code, notes, and snippets.

@josiahbryan
Created July 24, 2017 00:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save josiahbryan/75d8c298f0f7b9fe0085fc077564fa34 to your computer and use it in GitHub Desktop.
Save josiahbryan/75d8c298f0f7b9fe0085fc077564fa34 to your computer and use it in GitHub Desktop.
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