Created
October 23, 2018 10:58
-
-
Save vidartf/043ee5a2e691ba185271658c6af02a07 to your computer and use it in GitHub Desktop.
Eventful jupyter widgets model
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
class EventfulModel extends Widgetmodel { | |
// ... | |
setupListeners() { | |
// The names of the attributes that are widget references: | |
const referencePropNames = ['foo', 'bar']; | |
// The names of the attributes that are collections of widget references: | |
const nestedPropNames = ['baz', 'alice']; | |
// Handle changes in references | |
for (let propName of referencePropNames) { | |
// register listener for current child value | |
var curValue = this.get(propName); | |
if (curValue) { | |
this.listenTo(curValue, 'change', this.onChildChanged.bind(this)); | |
this.listenTo(curValue, 'childchange', this.onChildChanged.bind(this)); | |
} | |
// make sure to (un)hook listeners when child points to new object | |
this.on('change:' + propName, function(model, value) { | |
var prevModel = this.previous(propName); | |
var currModel = value; | |
if (prevModel) { | |
this.stopListening(prevModel); | |
} | |
if (currModel) { | |
this.listenTo(currModel, 'change', this.onChildChanged.bind(this)); | |
this.listenTo(currModel, 'childchange', this.onChildChanged.bind(this)); | |
} | |
}, this); | |
}; | |
// Handle changes in three instance nested props (arrays/dicts, possibly nested) | |
listenNested(this, nestedPropNames, this.onChildChanged.bind(this)); | |
this.on('change', this.onChange, this); | |
} | |
onChange(model, options) { | |
// Handle a direct change on this model | |
} | |
onChildChange(model, options) { | |
// Propagate event up to parents: | |
this.trigger('childchange', this); | |
// Handle a change in a child/reference | |
} | |
} | |
/** | |
* Helper function for listening to child models in lists/dicts | |
* | |
* @param {any} model The parent model | |
* @param {any} propNames The propetry names that are lists/dicts | |
* @param {any} callback The callback to call when child changes | |
*/ | |
function listenNested(model, propNames, callback) { | |
for (let propName of propNames) { | |
// listen to current values in array | |
var curr = model.get(propName) || []; | |
// support properties that are either an instance, or a | |
// sequence of instances: | |
if (curr instanceof Widgetmodel) { | |
model.listenTo(curr, 'change', callback); | |
model.listenTo(curr, 'childchange', callback); | |
} else { | |
utils.childModelsNested(curr).forEach(function(childModel) { | |
model.listenTo(childModel, 'change', callback); | |
model.listenTo(childModel, 'childchange', callback); | |
}); | |
} | |
// make sure to (un)hook listeners when array changes | |
model.on('change:' + propName, function(model, value) { | |
var prev = model.previous(propName) || []; | |
var curr = value || []; | |
// Check for instance values: | |
if (prev instanceof Widgetmodel) { | |
model.stopListening(prev); | |
} | |
if (curr instanceof Widgetmodel) { | |
model.listenTo(curr, 'change', callback); | |
model.listenTo(curr, 'childchange', callback); | |
} | |
// Done if both are instance values: | |
if (prev instanceof Widgetmodel && curr instanceof Widgetmodel) { | |
return; | |
} | |
if (prev instanceof Widgetmodel) { | |
// Implies curr is array | |
utils.childModelsNested(curr).forEach(function(childModel) { | |
model.listenTo(childModel, 'change', callback); | |
model.listenTo(childModel, 'childchange', callback); | |
}); | |
} else if (curr instanceof Widgetmodel) { | |
// Implies prev is array | |
utils.childModelsNested(prev).forEach(function(childModel) { | |
model.stopListening(childModel); | |
}); | |
} else { | |
// Both are arrays | |
var diff = utils.nestedDiff(curr, prev); | |
diff.added.forEach(function(childModel) { | |
model.listenTo(childModel, 'change', callback); | |
model.listenTo(childModel, 'childchange', callback); | |
}); | |
diff.removed.forEach(function(childModel) { | |
model.stopListening(childModel); | |
}); | |
} | |
}); | |
} | |
} | |
/** | |
* Gets the child models of an arbitrarily nested combination of | |
* arrays and dicts (hash maps). | |
* | |
* @param {any} obj nested array/dict structure with WidgetModels as leaf nodes. | |
* @returns The child models | |
*/ | |
function childModelsNested(obj) { | |
let children; | |
if (Array.isArray(obj)) { | |
children = obj; | |
} else { | |
children = Object.keys(obj).map(function(childModelKey) { | |
return obj[childModelKey]; | |
}); | |
} | |
if (children.length === 0) { | |
return children; | |
} | |
if (children[0] instanceof Widgetmodel) { | |
// Bottom level (children are leaf nodes) | |
return children; | |
} | |
return _.flatten(children.map(function(child) { | |
return childModelsNested(child); | |
}), true); | |
} | |
/** | |
* Get the diff of two array. | |
* | |
* @param {any[]} newArray | |
* @param {any[]} oldArray | |
* @returns An object with three attributes 'added', 'removed' and 'kept', | |
* each an array of child values; | |
*/ | |
function arrayDiff(newArray, oldArray) { | |
const added = _.difference(newArray, oldArray); | |
const removed = _.difference(oldArray, newArray); | |
const kept = _.intersection(oldArray, newArray); | |
return {added: added, removed: removed, kept: kept}; | |
} | |
/** | |
* Get the diff of two dicts (hash maps). | |
* | |
* @param {any} newDict | |
* @param {any} oldDict | |
* @returns An object with three attributes 'added', 'removed' and 'kept', | |
* each an array of child values; | |
*/ | |
function dictDiff(newDict, oldDict) { | |
const newKeys = Object.keys(newDict); | |
const oldKeys = Object.keys(oldDict); | |
const added = _.difference(newKeys, oldKeys).map(function(key) { return newDict[key]; }); | |
const removed = _.difference(oldKeys, newKeys).map(function(key) { return oldDict[key]; }); | |
const kept = _.intersection(newKeys, oldKeys).map(function(key) { return newDict[key]; }); | |
return {added: added, removed: removed, kept: kept}; | |
} | |
/** | |
* Get the diff of two arbitrarily nested combinations of | |
* arrays an dicts (hash maps). | |
* | |
* Note: This function assumes the structure of both are the same, | |
* i.e. they both have the same type at the same nesting level. | |
* | |
* @param {any | any[]} newObj | |
* @param {any | any[]} oldObj | |
* @returns An object with three attributes 'added', 'removed' and 'kept', | |
* each an array of child models; | |
*/ | |
function nestedDiff(newObj, oldObj) { | |
let diff; | |
if (Array.isArray(newObj)) { | |
diff = arrayDiff(newObj, oldObj); | |
} else { | |
diff = dictDiff(newObj, oldObj); | |
} | |
var all = _.flatten([diff.added, diff.removed, diff.kept]); | |
if (all.length === 0) { | |
return all; | |
} | |
if (all[0] instanceof WidgetModel) { | |
// Bottom level | |
return diff; | |
} | |
var ret = { | |
added: childModelsNested(diff.added), | |
removed: childModelsNested(diff.removed), | |
}; | |
return ret; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
TODO: Give
childModelsNested
a memo argument to prevent infinite recursion on dicts/arrays with circular references, or clearly document that this is unsupported.