ractive
Version:
Next-generation DOM manipulation
116 lines (91 loc) • 3.44 kB
JavaScript
import runloop from 'global/runloop';
import warn from 'utils/warn';
import create from 'utils/create';
import extend from 'utils/extend';
import removeFromArray from 'utils/removeFromArray';
var Binding = function ( element ) {
var interpolator, keypath, value;
this.element = element;
this.root = element.root;
this.attribute = element.attributes[ this.name || 'value' ];
interpolator = this.attribute.interpolator;
interpolator.twowayBinding = this;
if ( interpolator.keypath && interpolator.keypath.substr === '${' ) {
warn( 'Two-way binding does not work with expressions: ' + interpolator.keypath );
return false;
}
// A mustache may be *ambiguous*. Let's say we were given
// `value="{{bar}}"`. If the context was `foo`, and `foo.bar`
// *wasn't* `undefined`, the keypath would be `foo.bar`.
// Then, any user input would result in `foo.bar` being updated.
//
// If, however, `foo.bar` *was* undefined, and so was `bar`, we would be
// left with an unresolved partial keypath - so we are forced to make an
// assumption. That assumption is that the input in question should
// be forced to resolve to `bar`, and any user input would affect `bar`
// and not `foo.bar`.
//
// Did that make any sense? No? Oh. Sorry. Well the moral of the story is
// be explicit when using two-way data-binding about what keypath you're
// updating. Using it in lists is probably a recipe for confusion...
if ( !interpolator.keypath ) {
if ( interpolator.ref ) {
interpolator.resolve( interpolator.ref );
}
// If we have a reference expression resolver, we have to force
// members to attach themselves to the root
if ( interpolator.resolver ) {
interpolator.resolver.forceResolution();
}
}
this.keypath = keypath = interpolator.keypath;
// initialise value, if it's undefined
// TODO could we use a similar mechanism instead of the convoluted
// select/checkbox init logic?
if ( this.root.viewmodel.get( keypath ) === undefined && this.getInitialValue ) {
value = this.getInitialValue();
if ( value !== undefined ) {
this.root.viewmodel.set( keypath, value );
}
}
};
Binding.prototype = {
handleChange: function () {
runloop.start( this.root );
this.attribute.locked = true;
this.root.viewmodel.set( this.keypath, this.getValue() );
runloop.scheduleTask( () => this.attribute.locked = false );
runloop.end();
},
rebound: function () {
var bindings, oldKeypath, newKeypath;
oldKeypath = this.keypath;
newKeypath = this.attribute.interpolator.keypath;
// The attribute this binding is linked to has already done the work
if ( oldKeypath === newKeypath ) {
return;
}
removeFromArray( this.root._twowayBindings[ oldKeypath ], this );
this.keypath = newKeypath;
bindings = this.root._twowayBindings[ newKeypath ] || ( this.root._twowayBindings[ newKeypath ] = [] );
bindings.push( this );
},
unbind: function () {
// this is called when the element is unbound.
// Specialised bindings can override it
}
};
Binding.extend = function ( properties ) {
var Parent = this, SpecialisedBinding;
SpecialisedBinding = function ( element ) {
Binding.call( this, element );
if ( this.init ) {
this.init();
}
};
SpecialisedBinding.prototype = create( Parent.prototype );
extend( SpecialisedBinding.prototype, properties );
SpecialisedBinding.extend = Binding.extend;
return SpecialisedBinding;
};
export default Binding;