Skip to content

Instantly share code, notes, and snippets.

@vidartf
Created October 23, 2018 10:58
Show Gist options
  • Save vidartf/043ee5a2e691ba185271658c6af02a07 to your computer and use it in GitHub Desktop.
Save vidartf/043ee5a2e691ba185271658c6af02a07 to your computer and use it in GitHub Desktop.
Eventful jupyter widgets model
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;
}
@vidartf
Copy link
Author

vidartf commented Oct 23, 2018

TODO: Give childModelsNested a memo argument to prevent infinite recursion on dicts/arrays with circular references, or clearly document that this is unsupported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment