ampersand-view
Advanced tools
Comparing version 4.2.3 to 5.0.0
@@ -6,4 +6,5 @@ var State = require('ampersand-state'); | ||
var events = require('events-mixin'); | ||
var classes = require('component-classes'); | ||
var matches = require('matches-selector'); | ||
var bindings = require('dom-bindings'); | ||
var getPath = require('get-object-path'); | ||
@@ -17,3 +18,3 @@ | ||
this.on('change:el', this.handleElementChange, this); | ||
this._parsedBindings = {}; | ||
this._parsedBindings = bindings(this.bindings); | ||
this._initializeBindings(); | ||
@@ -40,11 +41,11 @@ this.initialize.apply(this, arguments); | ||
}, | ||
model: { | ||
collection: { | ||
set: function (newVal) { | ||
return { | ||
val: newVal, | ||
type: (newVal && newVal.initialize) ? 'model' : typeof newVal | ||
type: newVal && newVal.isCollection ? 'collection' : typeof newVal | ||
}; | ||
}, | ||
compare: function (m1, m2) { | ||
return m1 === m2; | ||
compare: function (currentVal, newVal) { | ||
return currentVal === newVal; | ||
} | ||
@@ -54,5 +55,5 @@ } | ||
props: { | ||
model: 'model', | ||
model: 'state', | ||
el: 'element', | ||
collection: 'model' | ||
collection: 'collection' | ||
}, | ||
@@ -117,6 +118,3 @@ derived: { | ||
render: function () { | ||
this.renderWithTemplate({ | ||
model: this.model, | ||
collection: this.collection | ||
}); | ||
this.renderWithTemplate(this); | ||
return this; | ||
@@ -151,3 +149,3 @@ }, | ||
this.delegateEvents(); | ||
this._applyAllBindings(); | ||
this._applyBindingsForKey(); | ||
return this; | ||
@@ -215,168 +213,11 @@ }, | ||
_applyAllBindings: function () { | ||
_applyBindingsForKey: function (name) { | ||
if (!this.el) return; | ||
var self = this; | ||
_.each(this._parsedBindings, function (value, key) { | ||
self._applyBindingsForModel(key); | ||
}); | ||
}, | ||
_applyBindingsForModel: function (name) { | ||
if (!this.el) return; | ||
var self = this; | ||
var model = this[name]; | ||
var bindings, fns; | ||
if (!model) return; | ||
bindings = this._parsedBindings[name]; | ||
if (!bindings) return; | ||
_.each(bindings, function (fns, key) { | ||
_.each(fns, function (fn) { | ||
fn(); | ||
}); | ||
}); | ||
}, | ||
_applyBindingsForModelProperty: function (modelName, prop) { | ||
if (!this.el) return; | ||
var bindings = this._parsedBindings[modelName]; | ||
var fns = bindings && bindings[prop]; | ||
if (!fns) return; | ||
_.each(fns, function (fn) { | ||
fn(); | ||
}); | ||
}, | ||
// Builds array of functions grouped by model name and property | ||
// that can be used to apply a binding rule based on model name | ||
// and optionally property. | ||
// The result of this parsing looks something like this: | ||
// | ||
// _parsedBindings: { | ||
// model: { | ||
// active: [ | ||
// fn1, | ||
// fn2, | ||
// ..etc.. | ||
// ] | ||
// } | ||
// } | ||
// | ||
// Then, we can use these as callbacks for change handlers or simply | ||
// or call them all if the root element is replaced | ||
_parseBindings: function (modelName, bindings) { | ||
var self = this; | ||
var processedBindings = {}; | ||
var bindingsForModel = this._parsedBindings[modelName] || (this._parsedBindings[modelName] = {}); | ||
// create new object with same keys but | ||
// with arrays for values. | ||
// This is where we'll push our results | ||
_.each(_.keys(bindings), function (key) { | ||
processedBindings[key] = []; | ||
}); | ||
function addBinding(propName, definition) { | ||
var processedDef = processedBindings[propName]; | ||
var selector, attr, name, split; | ||
if (typeof definition === 'string') { | ||
processedDef.push([definition, 'text']); | ||
} else { | ||
selector = definition[0]; | ||
attr = definition[1]; | ||
name = definition[2]; | ||
split = attr.split(' '); | ||
if (split.length > 1) { | ||
_.each(split, function (attributeName) { | ||
processedDef.push([selector, attributeName, name]); | ||
}); | ||
} else { | ||
processedDef.push([selector, attr, name]); | ||
} | ||
} | ||
var fns = this._parsedBindings.getGrouped(name); | ||
var item; | ||
for (item in fns) { | ||
fns[item].forEach(function (fn) { | ||
fn(this.el, getPath(this, item), _.last(item.split('.'))); | ||
}, this); | ||
} | ||
// extract any nested bindings | ||
_.each(bindings, function (value, propertyName) { | ||
if (_.isArray(value) && _.isArray(value[0])) { | ||
_.each(value, function (item) { | ||
addBinding(propertyName, item); | ||
}); | ||
} else { | ||
addBinding(propertyName, value); | ||
} | ||
}); | ||
_.each(processedBindings, function (value, propertyName) { | ||
_.each(value, function (value) { | ||
var selector = value[0]; | ||
var attr = value[1]; | ||
var attributeName = value[2]; | ||
var fns = bindingsForModel[propertyName] || (bindingsForModel[propertyName] = []); | ||
// handle text bindings | ||
if (attr === 'text') { | ||
fns.push(function () { | ||
_.each(self.getAll(selector), function (el) { | ||
var model = self[modelName]; | ||
var value = model && model.get(propertyName); | ||
el.textContent = value || ''; | ||
}); | ||
}); | ||
return; | ||
} | ||
if (attr === 'html') { | ||
fns.push(function () { | ||
_.each(self.getAll(selector), function (el) { | ||
var model = self[modelName]; | ||
var value = model && model.get(propertyName); | ||
el.innerHTML = value || ''; | ||
}); | ||
}); | ||
return; | ||
} | ||
if (attr === 'class') { | ||
fns.push(function () { | ||
_.each(self.getAll(selector), function (el) { | ||
var model = self[modelName]; | ||
var newVal = model && model.get(propertyName); | ||
var classList = classes(el); | ||
var prevVal; | ||
if (_.isBoolean(newVal)) { | ||
classList.toggle(propertyName, newVal); | ||
} else { | ||
prevVal = model.previous(propertyName); | ||
if (prevVal) classList.remove(prevVal); | ||
if (newVal) classList.add(newVal); | ||
} | ||
}); | ||
}); | ||
return; | ||
} | ||
// treat 'classList' attrs like | ||
// set/get for class attr | ||
if (attr === 'classList') { | ||
attr = 'class'; | ||
} | ||
fns.push(function () { | ||
_.each(self.getAll(selector), function (el) { | ||
var model = self[modelName]; | ||
var newVal = model && model.get(propertyName); | ||
// coerce undefined to empty string | ||
if (_.isUndefined(newVal)) newVal = ''; | ||
// now we can treat them all the same | ||
if (_.isBoolean(newVal) && !newVal) { | ||
el.removeAttribute(attributeName); | ||
} else { | ||
el.setAttribute(attr, attributeName || newVal); | ||
} | ||
}); | ||
}); | ||
}); | ||
}); | ||
}, | ||
@@ -387,35 +228,7 @@ | ||
if (!this.bindings) return; | ||
var self = this; | ||
var firstVal; | ||
function register(name) { | ||
var model = self[name]; | ||
var modelChangeHandler = function (view, newModel) { | ||
var oldModel = self.previous(name); | ||
if (oldModel) self.stopListening(oldModel); | ||
// apply previuos bindings with new model present | ||
self._applyBindingsForModel(name); | ||
_.each(self._parsedBindings[name], function (thing, key) { | ||
self.listenTo(newModel, 'change:' + key, function (model, newVal) { | ||
self._applyBindingsForModelProperty(name, key); | ||
}); | ||
}); | ||
}; | ||
// | ||
self.on('change:' + name, modelChangeHandler); | ||
if (model) modelChangeHandler(self, model); | ||
} | ||
firstVal = _.values(this.bindings)[0]; | ||
// we can assume we're only binding on `model` here. | ||
if (_.isArray(firstVal) || _.isString(firstVal)) { | ||
self._parseBindings('model', this.bindings); | ||
register('model'); | ||
} else { | ||
_.each(this.bindings, function (value, key) { | ||
self._parseBindings(key, value); | ||
register(key); | ||
}); | ||
} | ||
this.on('all', function (eventName) { | ||
if (eventName.slice(0, 7) === 'change:') { | ||
this._applyBindingsForKey(eventName.split(':')[1]); | ||
} | ||
}, this); | ||
}, | ||
@@ -441,4 +254,3 @@ | ||
if (!template) throw new Error('Template string or function needed.'); | ||
var newDom = _.isString(template) ? template : template(context || {model: this.model}); | ||
if (_.isString(newDom)) newDom = domify(newDom); | ||
var newDom = domify(_.isString(template) ? template : template(context || this)); | ||
var parent = this.el && this.el.parentNode; | ||
@@ -445,0 +257,0 @@ if (parent) parent.replaceChild(newDom, this.el); |
{ | ||
"name": "ampersand-view", | ||
"description": "A smart base view for Backbone apps, to make it easy to bind collections and properties to the DOM.", | ||
"version": "4.2.3", | ||
"version": "5.0.0", | ||
"author": "Henrik Joreteg <henrik@andyet.net>", | ||
@@ -9,7 +9,10 @@ "browser": "./ampersand-view.js", | ||
"dependencies": { | ||
"ampersand-state": "^4.0.0", | ||
"ampersand-state": "~4.2.1", | ||
"backbone-events-standalone": "0.x.x", | ||
"component-classes": "1.x.x", | ||
"dom-bindings": "~0.2.0", | ||
"domify": "1.x.x", | ||
"events-mixin": "^1.1.0", | ||
"get-object-path": "0.0.2", | ||
"key-tree-store": "~0.3.0", | ||
"matches-selector": "1.x.x", | ||
@@ -19,4 +22,4 @@ "underscore": "1.x.x" | ||
"devDependencies": { | ||
"ampersand-model": "~2.7.2", | ||
"ampersand-rest-collection": "~0.1.6", | ||
"ampersand-model": "~2.10.6", | ||
"ampersand-rest-collection": "^1.0.3", | ||
"browserify": "~3.41.0", | ||
@@ -23,0 +26,0 @@ "precommit-hook": "0.x.x", |
@@ -43,2 +43,7 @@ # ampersand-view | ||
Full description of the available binding types can be found here: https://github.com/henrikjoreteg/dom-bindings#binding-types | ||
A few examples below: | ||
```javascript | ||
@@ -56,40 +61,52 @@ var MyView = AmpersandView.extend({ | ||
// if it's changed. | ||
name: '.userName', | ||
'model.name': '.userName', | ||
// If the `active` property is a boolean the class | ||
// will be added/removed based on the boolean property. | ||
// But, if the `active` property were a string, the previous | ||
// value would be removed and the new one added from the class | ||
// list without affecting other classes that may alreay be there. | ||
active: ['li', 'class'], | ||
// You can add/remove a class based on a boolean property. | ||
// Based on the truthiness of the `active` property an | ||
// "active" class will be added or removed from element(s) | ||
// that match the selector. without affecting other classes | ||
// that may alreay be there. | ||
'model.active': { | ||
type: 'booleanClass', | ||
selector: 'li', | ||
// you can optionally specify a name like this | ||
// name: 'is-active' | ||
// or even a true/false case like this: | ||
// yes: 'is-active', | ||
// no: 'is-not-active' | ||
// if no name is given it will use the property name | ||
// by default. | ||
}, | ||
// If you've got a boolean property, you can also specify a third | ||
// item in the array. It will be used to determine what the class | ||
// is that will be toggled. This way your property name can be | ||
// different than the class. | ||
isActive: ['li', 'class', 'active'], // will toggle the 'active' class | ||
// If you prefer, you *can* also bind to the whole class list. Which | ||
// will wipe out all existing classes. | ||
myClasses: ['li', 'classList'], | ||
// As you might have guessed, you can bind to any attribute you want | ||
userUrl: ['a', 'href'], | ||
'model.userUrl': { | ||
type: 'attribute', | ||
name: 'href', | ||
selector: 'a' | ||
}, | ||
// This works for boolean attributes. The following would add and remove | ||
// the entire `checked` attribute (assuming the property value was a boolean) | ||
selected: ['input', 'checked'] | ||
selected: { | ||
type: 'booleanAttribute', | ||
selector: 'input', | ||
name: 'checked' | ||
}, | ||
// If you really need to, you can even bind the same attribute to different | ||
// types of things with different options. If "superActive" was a string, the following would put | ||
// the text value of it, inside `.userName` and add it as a class on the `li`. | ||
// If you really need to, you can have as many bindings as you want for the same | ||
// property, just pass an array of binding definitions: | ||
superActive: [ | ||
// the *only* restriction here is that if you pass an array of binding | ||
// declarations for a single property, each sub-item must also be an | ||
// array. | ||
['.userName'], | ||
['li', 'class'], | ||
// you can even get crazy... this would bind both | ||
// data attributes to both the li and .username elements | ||
['li, .username', 'data-attribute1 data-attribute2'], | ||
{ | ||
type: 'text', | ||
selector: '.username' | ||
}, | ||
{ | ||
type: 'class', | ||
selector: 'li' | ||
}, | ||
// btw, also works for selectors that match multiple elements | ||
{ | ||
type: 'booleanAttribute', | ||
selector: '.username, .thing' | ||
} | ||
] | ||
@@ -96,0 +113,0 @@ }, |
153
test/main.js
@@ -42,2 +42,3 @@ var test = require('tape'); | ||
test('registerSubview', function (t) { | ||
@@ -104,3 +105,3 @@ var removeCalled = 0; | ||
bindings: { | ||
name: 'span' | ||
'model.name': 'span' | ||
} | ||
@@ -117,3 +118,6 @@ }); | ||
bindings: { | ||
name: ['', 'html'] | ||
'model.name': { | ||
type: 'innerHTML', | ||
selector: '' | ||
} | ||
} | ||
@@ -130,3 +134,7 @@ }); | ||
bindings: { | ||
url: ['img', 'src'] | ||
'model.url': { | ||
type: 'attribute', | ||
name: 'src', | ||
selector: 'img' | ||
} | ||
} | ||
@@ -145,3 +153,7 @@ }); | ||
bindings: { | ||
url: ['', 'href'] | ||
'model.url': { | ||
type: 'attribute', | ||
name: 'href', | ||
selector: '' | ||
} | ||
} | ||
@@ -160,3 +172,7 @@ }); | ||
bindings: { | ||
something: ['input', 'value'] | ||
'model.something': { | ||
type: 'attribute', | ||
selector: 'input', | ||
name: 'value' | ||
} | ||
} | ||
@@ -180,4 +196,8 @@ }); | ||
bindings: { | ||
fireDanger: ['', 'class'], | ||
active: ['', 'class'] | ||
'model.fireDanger': { | ||
type: 'class' | ||
}, | ||
'model.active': { | ||
type: 'booleanClass' | ||
} | ||
} | ||
@@ -199,26 +219,6 @@ }, model); | ||
test('classList bindings', function (t) { | ||
var model = new Model(); | ||
model.set({ | ||
fireDanger: 'high', | ||
active: true, | ||
something: 'cool' | ||
}); | ||
var view = getView({ | ||
autoRender: true, | ||
template: '<li class="something else perhaps"></li>', | ||
bindings: { | ||
classes: ['', 'classList'] | ||
} | ||
}, model); | ||
t.equal(view.el.className, 'cooltrue', 'wipes out existing classes'); | ||
t.end(); | ||
}); | ||
test('nested binding definitions', function (t) { | ||
var model = new Model(); | ||
model.set({ | ||
fireDanger: 'high', | ||
active: true, | ||
something: 'cool' | ||
active: true | ||
}); | ||
@@ -229,7 +229,19 @@ var View = AmpersandView.extend({ | ||
bindings: { | ||
active: [ | ||
['div', 'data-active data-something'], | ||
['div', 'text'], | ||
['', 'class'], | ||
['div', 'classList', 'selected'] | ||
'model.active': [ | ||
{ | ||
type: 'booleanAttribute', | ||
name: 'data-active', | ||
selector: 'div' | ||
}, | ||
{ | ||
type: 'booleanAttribute', | ||
name: 'data-something', | ||
selector: 'div' | ||
}, | ||
{ | ||
selector: 'div' | ||
}, | ||
{ | ||
type: 'booleanClass' | ||
} | ||
] | ||
@@ -245,3 +257,2 @@ } | ||
t.ok(contains(li.className, 'active')); | ||
t.equal(div.className, 'selected'); | ||
t.end(); | ||
@@ -335,33 +346,32 @@ }); | ||
/* | ||
test('focus/blur events should work in events hash. Issue #8', function (t) { | ||
t.plan(2); | ||
var View = AmpersandView.extend({ | ||
events: { | ||
'focus #thing': 'handleFocus', | ||
'blur #thing': 'handleBlur' | ||
}, | ||
autoRender: true, | ||
template: '<div><input id="thing"/></div></div>', | ||
handleFocus: function () { | ||
t.pass('focus called'); | ||
}, | ||
handleBlur: function () { | ||
t.pass('blur called'); | ||
t.end(); | ||
} | ||
}); | ||
var view = new View(); | ||
// should be able to do this without | ||
// ending up with too many handlers | ||
view.delegateEvents(); | ||
view.delegateEvents(); | ||
view.delegateEvents(); | ||
document.body.appendChild(view.el); | ||
view.el.firstChild.focus(); | ||
view.el.firstChild.blur(); | ||
document.body.removeChild(view.el); | ||
}); | ||
*/ | ||
//test('focus/blur events should work in events hash. Issue #8', function (t) { | ||
// t.plan(2); | ||
// var View = AmpersandView.extend({ | ||
// events: { | ||
// 'focus #thing': 'handleFocus', | ||
// 'blur #thing': 'handleBlur' | ||
// }, | ||
// autoRender: true, | ||
// template: '<div><input id="thing"/></div></div>', | ||
// handleFocus: function () { | ||
// t.pass('focus called'); | ||
// }, | ||
// handleBlur: function () { | ||
// t.pass('blur called'); | ||
// t.end(); | ||
// } | ||
// }); | ||
// var view = new View(); | ||
// // should be able to do this without | ||
// // ending up with too many handlers | ||
// view.delegateEvents(); | ||
// view.delegateEvents(); | ||
// view.delegateEvents(); | ||
// | ||
// document.body.appendChild(view.el); | ||
// view.el.firstChild.focus(); | ||
// view.el.firstChild.blur(); | ||
// document.body.removeChild(view.el); | ||
//}); | ||
@@ -458,7 +468,6 @@ test('ability to mix in state properties', function (t) { | ||
bindings: { | ||
model1: { | ||
name: '#model1' | ||
}, | ||
model2: { | ||
name: ['#model2', 'class'] | ||
'model1.name': '#model1', | ||
'model2.name': { | ||
type: 'class', | ||
selector: '#model2' | ||
} | ||
@@ -482,3 +491,3 @@ } | ||
bindings: { | ||
name: '' | ||
'model.name': '' | ||
} | ||
@@ -501,3 +510,3 @@ }); | ||
bindings: { | ||
name: '' | ||
'model.name': '' | ||
} | ||
@@ -525,3 +534,3 @@ }); | ||
bindings: { | ||
name: '' | ||
'model.name': '' | ||
} | ||
@@ -528,0 +537,0 @@ }); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
378
0
52117
10
9
1040
+ Addeddom-bindings@~0.2.0
+ Addedget-object-path@0.0.2
+ Addedkey-tree-store@~0.3.0
+ Addedampersand-dom@0.1.2(transitive)
+ Addedampersand-state@4.2.7(transitive)
+ Addedbackbone-events-standalone@0.2.1(transitive)
+ Addeddeep-equal@0.2.2(transitive)
+ Addeddefined@0.0.0(transitive)
+ Addeddom-bindings@0.2.1(transitive)
+ Addedget-object-path@0.0.2(transitive)
+ Addedglob@3.2.11(transitive)
+ Addedis-array@1.0.1(transitive)
+ Addedkey-tree-store@0.1.20.3.0(transitive)
+ Addedlru-cache@2.7.3(transitive)
+ Addedminimatch@0.3.0(transitive)
+ Addedobject-inspect@0.4.0(transitive)
+ Addedresumer@0.0.0(transitive)
+ Addedsigmund@1.0.1(transitive)
+ Addedtape@2.13.4(transitive)
+ Addedthrough@2.3.8(transitive)
+ Addedupdate@0.1.0(transitive)
- Removedampersand-events@1.1.1(transitive)
- Removedampersand-state@4.9.1(transitive)
- Removedampersand-version@1.0.2(transitive)
- Removedcore-util-is@1.0.3(transitive)
- Removedfind-root@0.1.2(transitive)
- Removedisarray@0.0.1(transitive)
- Removedkey-tree-store@1.3.0(transitive)
- Removedlodash._arrayeach@3.0.0(transitive)
- Removedlodash._arraymap@3.0.0(transitive)
- Removedlodash._baseassign@3.2.0(transitive)
- Removedlodash._basecopy@3.0.1(transitive)
- Removedlodash._basedifference@3.0.3(transitive)
- Removedlodash._baseeach@3.0.4(transitive)
- Removedlodash._baseflatten@3.1.4(transitive)
- Removedlodash._basefor@3.0.3(transitive)
- Removedlodash._baseget@3.7.2(transitive)
- Removedlodash._baseindexof@3.1.0(transitive)
- Removedlodash._baseisequal@3.0.7(transitive)
- Removedlodash._baseslice@3.0.3(transitive)
- Removedlodash._baseuniq@3.0.3(transitive)
- Removedlodash._basevalues@3.0.0(transitive)
- Removedlodash._bindcallback@3.0.1(transitive)
- Removedlodash._cacheindexof@3.0.2(transitive)
- Removedlodash._createassigner@3.1.1(transitive)
- Removedlodash._createcache@3.1.2(transitive)
- Removedlodash._createwrapper@3.2.0(transitive)
- Removedlodash._getnative@3.9.1(transitive)
- Removedlodash._isiterateecall@3.0.9(transitive)
- Removedlodash._pickbyarray@3.0.2(transitive)
- Removedlodash._pickbycallback@3.0.0(transitive)
- Removedlodash._replaceholders@3.0.0(transitive)
- Removedlodash._root@3.0.1(transitive)
- Removedlodash._topath@3.8.1(transitive)
- Removedlodash.assign@3.2.0(transitive)
- Removedlodash.before@3.0.3(transitive)
- Removedlodash.bind@3.1.0(transitive)
- Removedlodash.escape@3.2.0(transitive)
- Removedlodash.foreach@3.0.3(transitive)
- Removedlodash.forown@3.0.2(transitive)
- Removedlodash.has@3.2.1(transitive)
- Removedlodash.includes@3.1.3(transitive)
- Removedlodash.isarguments@3.1.0(transitive)
- Removedlodash.isarray@3.0.4(transitive)
- Removedlodash.isdate@3.0.3(transitive)
- Removedlodash.isempty@3.0.4(transitive)
- Removedlodash.isequal@3.0.4(transitive)
- Removedlodash.isfunction@3.0.9(transitive)
- Removedlodash.isobject@3.0.2(transitive)
- Removedlodash.isstring@3.0.1(transitive)
- Removedlodash.istypedarray@3.0.6(transitive)
- Removedlodash.keys@3.1.2(transitive)
- Removedlodash.keysin@3.0.8(transitive)
- Removedlodash.omit@3.1.0(transitive)
- Removedlodash.once@3.0.1(transitive)
- Removedlodash.restparam@3.6.1(transitive)
- Removedlodash.result@3.1.2(transitive)
- Removedlodash.union@3.1.0(transitive)
- Removedlodash.uniqueid@3.2.0(transitive)
- Removedreadable-stream@1.0.34(transitive)
- Removedstring_decoder@0.10.31(transitive)
- Removedthrough2@0.6.5(transitive)
- Removedxtend@4.0.2(transitive)
Updatedampersand-state@~4.2.1