diff --git a/src/parse/converters/mustache/content.js b/src/parse/converters/mustache/content.js index 365fab658f..2e29e8a8c6 100644 --- a/src/parse/converters/mustache/content.js +++ b/src/parse/converters/mustache/content.js @@ -4,6 +4,7 @@ import handlebarsBlockCodes from 'parse/converters/mustache/handlebarsBlockCodes import 'legacy'; var indexRefPattern = /^\s*:\s*([a-zA-Z_$][a-zA-Z_$0-9]*)/, + keyIndexRefPattern = /^\s*,\s*([a-zA-Z_$][a-zA-Z_$0-9]*)/, arrayMemberPattern = /^[0-9][1-9]*$/, handlebarsBlockPattern = new RegExp( '^(' + Object.keys( handlebarsBlockCodes ).join( '|' ) + ')\\b' ), legalReference; @@ -181,9 +182,15 @@ export default function ( parser, delimiterType ) { ]; } - // optional index reference + // optional index and key references if ( i = parser.matchPattern( indexRefPattern ) ) { - mustache.i = i; + let extra; + + if ( extra = parser.matchPattern( keyIndexRefPattern ) ) { + mustache.i = i + ',' + extra; + } else { + mustache.i = i; + } } return mustache; diff --git a/src/shared/keypaths/decode.js b/src/shared/keypaths/decode.js index bdf036fadd..38371b4ec7 100644 --- a/src/shared/keypaths/decode.js +++ b/src/shared/keypaths/decode.js @@ -1,6 +1,11 @@ import isNumeric from 'utils/isNumeric'; export default function decodeKeypath ( keypath ) { - var value = keypath.slice( 1 ); - return isNumeric( value ) ? +value : value; -} \ No newline at end of file + var value = keypath.slice( 2 ); + + if ( keypath[1] === 'i' ) { + return isNumeric( value ) ? +value : value; + } else { + return value; + } +} diff --git a/src/shared/parameters/ComplexParameter.js b/src/shared/parameters/ComplexParameter.js index 35e7df1875..e93a49ddbd 100644 --- a/src/shared/parameters/ComplexParameter.js +++ b/src/shared/parameters/ComplexParameter.js @@ -40,8 +40,8 @@ ComplexParameter.prototype = { this.dirty = false; }, - rebind: function ( indexRef, newIndex, oldKeypath, newKeypath ) { - this.fragment.rebind( indexRef, newIndex, oldKeypath, newKeypath ); + rebind: function ( oldKeypath, newKeypath ) { + this.fragment.rebind( oldKeypath, newKeypath ); }, unbind: function () { diff --git a/src/shared/resolveRef.js b/src/shared/resolveRef.js index 62b2a476f4..f70fb89977 100644 --- a/src/shared/resolveRef.js +++ b/src/shared/resolveRef.js @@ -5,7 +5,6 @@ import resolveAncestorRef from 'shared/resolveAncestorRef'; export default function resolveRef ( ractive, ref, fragment, isParentLookup ) { var context, key, - index, keypath, parentValue, hasContextChain, @@ -61,15 +60,6 @@ export default function resolveRef ( ractive, ref, fragment, isParentLookup ) { hasContextChain = true; fragment = ractive.component.parentFragment; - // Special case - index refs - if ( fragment.indexRefs && ( index = fragment.indexRefs[ ref ] ) !== undefined ) { - // Create an index ref binding, so that it can be rebound letter if necessary. - // It doesn't have an alias since it's an implicit binding, hence `...[ ref ] = ref` - ractive.component.indexRefBindings[ ref ] = ref; - ractive.viewmodel.set( ref, index, { silent: true } ); - return; - } - keypath = resolveRef( ractive.parent, ref, fragment, true ); if ( keypath ) { diff --git a/src/virtualdom/Fragment.js b/src/virtualdom/Fragment.js index 22b67ba453..ae299182f2 100644 --- a/src/virtualdom/Fragment.js +++ b/src/virtualdom/Fragment.js @@ -34,9 +34,19 @@ Fragment.prototype = { getValue: getValue, init: init, rebind: rebind, + registerIndexRef: function( idx ) { + var idxs = this.registeredIndexRefs; + if ( idxs.indexOf( idx ) === -1 ) { + idxs.push( idx ); + } + }, render: render, toString: toString, unbind: unbind, + unregisterIndexRef: function( idx ) { + var idxs = this.registeredIndexRefs; + idxs.splice( idxs.indexOf( idx ), 1 ); + }, unrender: unrender }; diff --git a/src/virtualdom/Fragment/prototype/init.js b/src/virtualdom/Fragment/prototype/init.js index da930cbb62..ccb3e3c372 100644 --- a/src/virtualdom/Fragment/prototype/init.js +++ b/src/virtualdom/Fragment/prototype/init.js @@ -1,5 +1,3 @@ -import types from 'config/types'; -import create from 'utils/create'; import createItem from 'virtualdom/Fragment/prototype/init/createItem'; export default function Fragment$init ( options ) { @@ -14,19 +12,9 @@ export default function Fragment$init ( options ) { this.root = options.root; this.pElement = options.pElement; this.context = options.context; - - // If parent item is a section, this may not be the only fragment - // that belongs to it - we need to make a note of the index - if ( this.owner.type === types.SECTION ) { - this.index = options.index; - } - - // index references (the 'i' in {{#section:i}}...{{/section}}) need to cascade - // down the tree - this.indexRefs = create( parentFragment ? parentFragment.indexRefs : null ); - if ( options.indexRef ) { - this.indexRefs[ options.indexRef ] = options.index; - } + this.index = options.index; + this.key = options.key; + this.registeredIndexRefs = []; // Time to create this fragment's child items diff --git a/src/virtualdom/Fragment/prototype/rebind.js b/src/virtualdom/Fragment/prototype/rebind.js index 81447e8eca..8a957422f5 100644 --- a/src/virtualdom/Fragment/prototype/rebind.js +++ b/src/virtualdom/Fragment/prototype/rebind.js @@ -1,21 +1,13 @@ import assignNewKeypath from 'shared/keypaths/assignNew'; -export default function Fragment$rebind ( indexRef, newIndex, oldKeypath, newKeypath ) { - - if ( newIndex !== undefined ) { - this.index = newIndex; - } +export default function Fragment$rebind ( oldKeypath, newKeypath ) { // assign new context keypath if needed assignNewKeypath( this, 'context', oldKeypath, newKeypath ); - if ( this.indexRefs && this.indexRefs[ indexRef ] !== undefined ) { - this.indexRefs[ indexRef ] = newIndex; - } - this.items.forEach( item => { if ( item.rebind ) { - item.rebind( indexRef, newIndex, oldKeypath, newKeypath ); + item.rebind( oldKeypath, newKeypath ); } }); } diff --git a/src/virtualdom/items/Component/prototype/rebind.js b/src/virtualdom/items/Component/prototype/rebind.js index 730ff099a1..cb9b868f16 100644 --- a/src/virtualdom/items/Component/prototype/rebind.js +++ b/src/virtualdom/items/Component/prototype/rebind.js @@ -1,9 +1,5 @@ -import runloop from 'global/runloop'; - -export default function Component$rebind ( indexRef, newIndex, oldKeypath, newKeypath ) { - var childInstance = this.instance, - indexRefAlias, - query; +export default function Component$rebind ( oldKeypath, newKeypath ) { + var query; this.resolvers.forEach( rebind ); @@ -13,16 +9,11 @@ export default function Component$rebind ( indexRef, newIndex, oldKeypath, newKe } } - if ( indexRefAlias = this.indexRefBindings[ indexRef ] ) { - runloop.addViewmodel( childInstance.viewmodel ); - childInstance.viewmodel.set( indexRefAlias, newIndex ); - } - if ( query = this.root._liveComponentQueries[ '_' + this.name ] ) { query._makeDirty(); } function rebind ( x ) { - x.rebind( indexRef, newIndex, oldKeypath, newKeypath ); + x.rebind( oldKeypath, newKeypath ); } } diff --git a/src/virtualdom/items/Element/Attribute/prototype/rebind.js b/src/virtualdom/items/Element/Attribute/prototype/rebind.js index a8fd1cd30d..84a48b0c17 100644 --- a/src/virtualdom/items/Element/Attribute/prototype/rebind.js +++ b/src/virtualdom/items/Element/Attribute/prototype/rebind.js @@ -1,5 +1,5 @@ -export default function Attribute$rebind ( indexRef, newIndex, oldKeypath, newKeypath ) { +export default function Attribute$rebind ( oldKeypath, newKeypath ) { if ( this.fragment ) { - this.fragment.rebind( indexRef, newIndex, oldKeypath, newKeypath ); + this.fragment.rebind( oldKeypath, newKeypath ); } } diff --git a/src/virtualdom/items/Element/Binding/RadioNameBinding.js b/src/virtualdom/items/Element/Binding/RadioNameBinding.js index 4243f8a263..1a585fea88 100644 --- a/src/virtualdom/items/Element/Binding/RadioNameBinding.js +++ b/src/virtualdom/items/Element/Binding/RadioNameBinding.js @@ -53,10 +53,10 @@ var RadioNameBinding = Binding.extend({ } }, - rebound: function ( indexRef, newIndex, oldKeypath, newKeypath ) { + rebound: function ( oldKeypath, newKeypath ) { var node; - Binding.prototype.rebound.call( this, indexRef, newIndex, oldKeypath, newKeypath ); + Binding.prototype.rebound.call( this, oldKeypath, newKeypath ); if ( node = this.element.node ) { node.name = '{{' + this.keypath + '}}'; diff --git a/src/virtualdom/items/Element/ConditionalAttribute/_ConditionalAttribute.js b/src/virtualdom/items/Element/ConditionalAttribute/_ConditionalAttribute.js index 1f6ea8614f..3f81d4b995 100644 --- a/src/virtualdom/items/Element/ConditionalAttribute/_ConditionalAttribute.js +++ b/src/virtualdom/items/Element/ConditionalAttribute/_ConditionalAttribute.js @@ -36,8 +36,8 @@ ConditionalAttribute.prototype = { this.element.bubble(); }, - rebind: function ( indexRef, newIndex, oldKeypath, newKeypath ) { - this.fragment.rebind( indexRef, newIndex, oldKeypath, newKeypath ); + rebind: function ( oldKeypath, newKeypath ) { + this.fragment.rebind( oldKeypath, newKeypath ); }, render: function ( node ) { diff --git a/src/virtualdom/items/Element/Decorator/_Decorator.js b/src/virtualdom/items/Element/Decorator/_Decorator.js index 797d70336c..b20d22a69a 100644 --- a/src/virtualdom/items/Element/Decorator/_Decorator.js +++ b/src/virtualdom/items/Element/Decorator/_Decorator.js @@ -104,9 +104,9 @@ Decorator.prototype = { } }, - rebind: function ( indexRef, newIndex, oldKeypath, newKeypath ) { + rebind: function ( oldKeypath, newKeypath ) { if ( this.fragment ) { - this.fragment.rebind( indexRef, newIndex, oldKeypath, newKeypath ); + this.fragment.rebind( oldKeypath, newKeypath ); } }, diff --git a/src/virtualdom/items/Element/EventHandler/prototype/rebind.js b/src/virtualdom/items/Element/EventHandler/prototype/rebind.js index 2d61345861..a5a2b85757 100644 --- a/src/virtualdom/items/Element/EventHandler/prototype/rebind.js +++ b/src/virtualdom/items/Element/EventHandler/prototype/rebind.js @@ -1,4 +1,4 @@ -export default function EventHandler$rebind ( indexRef, newIndex, oldKeypath, newKeypath ) { +export default function EventHandler$rebind ( oldKeypath, newKeypath ) { var fragment; if ( this.method ) { fragment = this.element.parentFragment; @@ -16,6 +16,6 @@ export default function EventHandler$rebind ( indexRef, newIndex, oldKeypath, ne } function rebind ( thing ) { - thing && thing.rebind( indexRef, newIndex, oldKeypath, newKeypath ); + thing && thing.rebind( oldKeypath, newKeypath ); } } diff --git a/src/virtualdom/items/Element/EventHandler/shared/genericHandler.js b/src/virtualdom/items/Element/EventHandler/shared/genericHandler.js index e8334b9ffd..6b879371ee 100644 --- a/src/virtualdom/items/Element/EventHandler/shared/genericHandler.js +++ b/src/virtualdom/items/Element/EventHandler/shared/genericHandler.js @@ -1,13 +1,23 @@ +import findIndexRefs from 'virtualdom/items/shared/Resolvers/findIndexRefs'; + export default function genericHandler ( event ) { - var storage, handler; + var storage, handler, indices, index = {}; storage = this._ractive; handler = storage.events[ event.type ]; + if ( indices = findIndexRefs( handler.element.parentFragment ) ) { + let k, ref; + for ( k in indices.refs ) { + ref = indices.refs[k]; + index[ ref.ref.n ] = ref.ref.t === 'k' ? ref.fragment.key : ref.fragment.index; + } + } + handler.fire({ node: this, original: event, - index: storage.index, + index: index, keypath: storage.keypath, context: storage.root.get( storage.keypath ) }); diff --git a/src/virtualdom/items/Element/prototype/init.js b/src/virtualdom/items/Element/prototype/init.js index baa73037c8..842689b256 100644 --- a/src/virtualdom/items/Element/prototype/init.js +++ b/src/virtualdom/items/Element/prototype/init.js @@ -35,6 +35,7 @@ export default function Element$init ( options ) { this.root = ractive = parentFragment.root; this.index = options.index; + this.key = options.key; this.name = enforceCase( template.e ); diff --git a/src/virtualdom/items/Element/prototype/rebind.js b/src/virtualdom/items/Element/prototype/rebind.js index c408281a41..bfb01b3e09 100644 --- a/src/virtualdom/items/Element/prototype/rebind.js +++ b/src/virtualdom/items/Element/prototype/rebind.js @@ -1,6 +1,6 @@ import assignNewKeypath from 'shared/keypaths/assignNew'; -export default function Element$rebind ( indexRef, newIndex, oldKeypath, newKeypath ) { +export default function Element$rebind ( oldKeypath, newKeypath ) { var i, storage, liveQueries, ractive; if ( this.attributes ) { @@ -38,13 +38,9 @@ export default function Element$rebind ( indexRef, newIndex, oldKeypath, newKeyp // adjust keypath if needed assignNewKeypath( storage, 'keypath', oldKeypath, newKeypath ); - - if ( indexRef != undefined ) { - storage.index[ indexRef ] = newIndex; - } } function rebind ( thing ) { - thing.rebind( indexRef, newIndex, oldKeypath, newKeypath ); + thing.rebind( oldKeypath, newKeypath ); } } diff --git a/src/virtualdom/items/Element/prototype/render.js b/src/virtualdom/items/Element/prototype/render.js index 5a4d242d40..9d48c472f0 100644 --- a/src/virtualdom/items/Element/prototype/render.js +++ b/src/virtualdom/items/Element/prototype/render.js @@ -64,7 +64,6 @@ export default function Element$render () { value: { proxy: this, keypath: getInnerContext( this.parentFragment ), - index: create( this.parentFragment.indexRefs ), events: create( null ), root: root } diff --git a/src/virtualdom/items/Partial/_Partial.js b/src/virtualdom/items/Partial/_Partial.js index 08b5ec7528..01044640ac 100644 --- a/src/virtualdom/items/Partial/_Partial.js +++ b/src/virtualdom/items/Partial/_Partial.js @@ -82,13 +82,13 @@ Partial.prototype = { return this.fragment.getValue(); }, - rebind: function ( indexRef, newIndex, oldKeypath, newKeypath ) { + rebind: function ( oldKeypath, newKeypath ) { // named partials aren't bound, so don't rebind if ( !this.isNamed ) { - rebind.call( this, indexRef, newIndex, oldKeypath, newKeypath ); + rebind.call( this, oldKeypath, newKeypath ); } - - this.fragment.rebind( indexRef, newIndex, oldKeypath, newKeypath ); + + this.fragment.rebind( oldKeypath, newKeypath ); }, render: function () { diff --git a/src/virtualdom/items/Section/_Section.js b/src/virtualdom/items/Section/_Section.js index 68989fa91e..f747044d82 100644 --- a/src/virtualdom/items/Section/_Section.js +++ b/src/virtualdom/items/Section/_Section.js @@ -20,7 +20,7 @@ import update from 'virtualdom/items/Section/prototype/update'; var Section = function ( options ) { this.type = types.SECTION; - this.subtype = options.template.n; + this.subtype = this.currentSubtype = options.template.n; this.inverted = this.subtype === types.SECTION_UNLESS; @@ -31,6 +31,12 @@ var Section = function ( options ) { this.fragmentsToRender = []; this.fragmentsToUnrender = []; + if ( options.template.i ) { + this.indexRefs = options.template.i.split(',').map( ( k, i ) => { + return { n: k, t: i === 0 ? 'k' : 'i' }; + }); + } + this.renderedFragments = []; this.length = 0; // number of times this section is rendered @@ -47,6 +53,15 @@ Section.prototype = { findComponent: findComponent, findNextNode: findNextNode, firstNode: firstNode, + getIndexRef: function( name ) { + if ( this.indexRefs ) { + for ( let ref of this.indexRefs ) { + if ( ref.n === name ) { + return ref; + } + } + } + }, getValue: Mustache.getValue, shuffle: shuffle, rebind: rebind, diff --git a/src/virtualdom/items/Section/prototype/rebind.js b/src/virtualdom/items/Section/prototype/rebind.js index 6d18120276..77df626965 100644 --- a/src/virtualdom/items/Section/prototype/rebind.js +++ b/src/virtualdom/items/Section/prototype/rebind.js @@ -1,14 +1,5 @@ import Mustache from 'virtualdom/items/shared/Mustache/_Mustache'; -import types from 'config/types'; -export default function( indexRef, newIndex, oldKeypath, newKeypath ) { - var ref, idx; - - if ( indexRef !== undefined || this.currentSubtype !== types.SECTION_EACH ) { - ref = indexRef; - idx = newIndex; - } - - // If the new index belonged to us, we'd be shuffling instead - Mustache.rebind.call( this, ref, idx, oldKeypath, newKeypath ); +export default function( oldKeypath, newKeypath ) { + Mustache.rebind.call( this, oldKeypath, newKeypath ); } diff --git a/src/virtualdom/items/Section/prototype/setValue.js b/src/virtualdom/items/Section/prototype/setValue.js index f026df38b5..a7f17aae46 100644 --- a/src/virtualdom/items/Section/prototype/setValue.js +++ b/src/virtualdom/items/Section/prototype/setValue.js @@ -36,8 +36,7 @@ export default function Section$setValue ( value ) { template: this.template.f, root: this.root, pElement: this.pElement, - owner: this, - indexRef: this.template.i + owner: this }; this.fragmentsToCreate.forEach( index => { @@ -65,6 +64,28 @@ export default function Section$setValue ( value ) { this.updating = false; } +function changeCurrentSubtype ( section, value, obj ) { + if ( value === types.SECTION_EACH ) { + // make sure ref type is up to date for key or value indices + if ( section.indexRefs && section.indexRefs[0] ) { + let ref = section.indexRefs[0]; + + // when switching flavors, make sure the section gets updated + if ( ( obj && ref.t === 'i' ) || ( !obj && ref.t === 'k' ) ) { + // if switching from object to list, unbind all of the old fragments + if ( !obj ) { + section.length = 0; + section.fragmentsToUnrender = section.fragments.slice( 0 ); + section.fragmentsToUnrender.forEach( f => f.unbind() ); + } + } + + ref.t = obj ? 'k' : 'i'; + } + } + + section.currentSubtype = value; +} function reevaluateSection ( section, value ) { var fragmentOptions = { @@ -78,8 +99,6 @@ function reevaluateSection ( section, value ) { // TODO can this be optimised? i.e. pick an reevaluateSection function during init // and avoid doing this each time? if ( section.subtype ) { - section.currentSubtype = section.subtype; - switch ( section.subtype ) { case types.SECTION_IF: return reevaluateConditionalSection( section, value, false, fragmentOptions ); @@ -95,6 +114,7 @@ function reevaluateSection ( section, value ) { case types.SECTION_EACH: if ( isObject( value ) ) { + changeCurrentSubtype( section, section.subtype, true ); return reevaluateListObjectSection( section, value, fragmentOptions ); } @@ -107,7 +127,7 @@ function reevaluateSection ( section, value ) { // Ordered list section if ( section.ordered ) { - section.currentSubtype = types.SECTION_EACH; + changeCurrentSubtype( section, types.SECTION_EACH, false ); return reevaluateListSection( section, value, fragmentOptions ); } @@ -115,17 +135,17 @@ function reevaluateSection ( section, value ) { if ( isObject( value ) || typeof value === 'function' ) { // Index reference indicates section should be treated as a list if ( section.template.i ) { - section.currentSubtype = types.SECTION_EACH; + changeCurrentSubtype( section, types.SECTION_EACH, true ); return reevaluateListObjectSection( section, value, fragmentOptions ); } // Otherwise, object provides context for contents - section.currentSubtype = types.SECTION_WITH; + changeCurrentSubtype( section, types.SECTION_WITH, false ); return reevaluateContextSection( section, fragmentOptions ); } // Conditional section - section.currentSubtype = types.SECTION_IF; + changeCurrentSubtype( section, types.SECTION_IF, false ); return reevaluateConditionalSection( section, value, false, fragmentOptions ); } @@ -154,10 +174,6 @@ function reevaluateListSection ( section, value, fragmentOptions ) { fragmentOptions.context = section.keypath + '.' + i; fragmentOptions.index = i; - if ( section.template.i ) { - fragmentOptions.indexRef = section.template.i; - } - fragment = new Fragment( fragmentOptions ); section.fragmentsToRender.push( section.fragments[i] = fragment ); } @@ -169,7 +185,7 @@ function reevaluateListSection ( section, value, fragmentOptions ) { } function reevaluateListObjectSection ( section, value, fragmentOptions ) { - var id, i, hasKey, fragment, changed; + var id, i, hasKey, fragment, changed, deps; hasKey = section.hasKey || ( section.hasKey = {} ); @@ -178,28 +194,42 @@ function reevaluateListObjectSection ( section, value, fragmentOptions ) { while ( i-- ) { fragment = section.fragments[i]; - if ( !( fragment.index in value ) ) { + if ( !( fragment.key in value ) ) { changed = true; fragment.unbind(); section.fragmentsToUnrender.push( fragment ); section.fragments.splice( i, 1 ); - hasKey[ fragment.index ] = false; + hasKey[ fragment.key ] = false; + } + } + + // notify any dependents about changed indices + i = section.fragments.length; + while ( i-- ) { + fragment = section.fragments[i]; + + if ( fragment.index !== i ){ + fragment.index = i; + if ( deps = fragment.registeredIndexRefs ) { + for ( let d of deps ) { + // the keypath doesn't actually matter here as it won't have changed + d.rebind( '', '' ); + } + } } } // add any that haven't been created yet + i = section.fragments.length; for ( id in value ) { if ( !hasKey[ id ] ) { changed = true; fragmentOptions.context = section.keypath + '.' + id; - fragmentOptions.index = id; - - if ( section.template.i ) { - fragmentOptions.indexRef = section.template.i; - } + fragmentOptions.key = id; + fragmentOptions.index = i++; fragment = new Fragment( fragmentOptions ); @@ -214,7 +244,7 @@ function reevaluateListObjectSection ( section, value, fragmentOptions ) { } function reevaluateConditionalContextSection ( section, value, fragmentOptions ) { - if(value){ + if ( value ) { return reevaluateContextSection( section, fragmentOptions ); } else { return removeSectionFragments( section ); @@ -263,7 +293,7 @@ function reevaluateConditionalSection ( section, value, inverted, fragmentOption if ( doRender ) { if ( !section.length ) { // no change to context stack - fragmentOptions.index = undefined; + fragmentOptions.index = 0; fragment = new Fragment( fragmentOptions ); section.fragmentsToRender.push( section.fragments[0] = fragment ); diff --git a/src/virtualdom/items/Section/prototype/shuffle.js b/src/virtualdom/items/Section/prototype/shuffle.js index 3d3ad0e7fb..557bcb911a 100644 --- a/src/virtualdom/items/Section/prototype/shuffle.js +++ b/src/virtualdom/items/Section/prototype/shuffle.js @@ -19,7 +19,7 @@ export default function Section$shuffle ( newIndices ) { // short circuit any double-updates, and ensure that this isn't applied to // non-list sections - if ( this.shuffling || this.unbound || ( this.subtype && this.subtype !== types.SECTION_EACH ) ) { + if ( this.shuffling || this.unbound || ( this.currentSubtype !== types.SECTION_EACH ) ) { return; } @@ -30,9 +30,10 @@ export default function Section$shuffle ( newIndices ) { reboundFragments = []; + // TODO: need to update this // first, rebind existing fragments newIndices.forEach( ( newIndex, oldIndex ) => { - var fragment, by, oldKeypath, newKeypath; + var fragment, by, oldKeypath, newKeypath, deps; if ( newIndex === oldIndex ) { reboundFragments[ newIndex ] = this.fragments[ oldIndex ]; @@ -57,7 +58,17 @@ export default function Section$shuffle ( newIndices ) { oldKeypath = this.keypath + '.' + oldIndex; newKeypath = this.keypath + '.' + newIndex; - fragment.rebind( this.template.i, newIndex, oldKeypath, newKeypath ); + fragment.index = newIndex; + + // notify any registered index refs directly + if ( deps = fragment.registeredIndexRefs ) { + for ( let d of deps ) { + // the keypath doesn't actually matter here + d.rebind( '', '' ); + } + } + + fragment.rebind( oldKeypath, newKeypath ); reboundFragments[ newIndex ] = fragment; }); @@ -87,10 +98,6 @@ export default function Section$shuffle ( newIndices ) { owner: this }; - if ( this.template.i ) { - fragmentOptions.indexRef = this.template.i; - } - // Add as many new fragments as we need to, or add back existing // (detached) fragments for ( i = firstChange; i < newLength; i += 1 ) { diff --git a/src/virtualdom/items/Yielder.js b/src/virtualdom/items/Yielder.js index 94fc87efb9..d8655be1af 100644 --- a/src/virtualdom/items/Yielder.js +++ b/src/virtualdom/items/Yielder.js @@ -93,8 +93,8 @@ Yielder.prototype = { removeFromArray( this.component.yielders[ this.name ], this ); }, - rebind: function ( indexRef, newIndex, oldKeypath, newKeypath ) { - this.fragment.rebind( indexRef, newIndex, oldKeypath, newKeypath ); + rebind: function ( oldKeypath, newKeypath ) { + this.fragment.rebind( oldKeypath, newKeypath ); }, toString: function () { diff --git a/src/virtualdom/items/shared/Mustache/initialise.js b/src/virtualdom/items/shared/Mustache/initialise.js index 8d8f4c392d..224a8f1cc4 100644 --- a/src/virtualdom/items/shared/Mustache/initialise.js +++ b/src/virtualdom/items/shared/Mustache/initialise.js @@ -16,6 +16,7 @@ export default function Mustache$init ( mustache, options ) { mustache.template = options.template; mustache.index = options.index || 0; + mustache.key = options.key; mustache.isStatic = options.template.s; mustache.type = options.template.t; @@ -54,7 +55,7 @@ export default function Mustache$init ( mustache, options ) { if ( oldKeypath !== undefined ) { mustache.fragments && mustache.fragments.forEach( f => { - f.rebind( null, null, oldKeypath, newKeypath ); + f.rebind( oldKeypath, newKeypath ); }); } } diff --git a/src/virtualdom/items/shared/Mustache/rebind.js b/src/virtualdom/items/shared/Mustache/rebind.js index 8e68390f85..2d9b730c8c 100644 --- a/src/virtualdom/items/shared/Mustache/rebind.js +++ b/src/virtualdom/items/shared/Mustache/rebind.js @@ -1,11 +1,11 @@ -export default function Mustache$rebind ( indexRef, newIndex, oldKeypath, newKeypath ) { +export default function Mustache$rebind ( oldKeypath, newKeypath ) { // Children first if ( this.fragments ) { - this.fragments.forEach( f => f.rebind( indexRef, newIndex, oldKeypath, newKeypath ) ); + this.fragments.forEach( f => f.rebind( oldKeypath, newKeypath ) ); } // Expression mustache? if ( this.resolver ) { - this.resolver.rebind( indexRef, newIndex, oldKeypath, newKeypath ); + this.resolver.rebind( oldKeypath, newKeypath ); } } diff --git a/src/virtualdom/items/shared/Resolvers/ExpressionResolver.js b/src/virtualdom/items/shared/Resolvers/ExpressionResolver.js index 4372165033..0989a4fcd9 100644 --- a/src/virtualdom/items/shared/Resolvers/ExpressionResolver.js +++ b/src/virtualdom/items/shared/Resolvers/ExpressionResolver.js @@ -9,7 +9,7 @@ var ExpressionResolver, bind = Function.prototype.bind; ExpressionResolver = function ( owner, parentFragment, expression, callback ) { - var ractive, indexRefs; + var ractive; ractive = owner.root; @@ -20,8 +20,6 @@ ExpressionResolver = function ( owner, parentFragment, expression, callback ) { this.str = expression.s; this.keypaths = []; - indexRefs = parentFragment.indexRefs; - // Create resolvers for each reference this.pending = expression.r.length; this.refResolvers = expression.r.map( ( ref, i ) => { @@ -105,9 +103,9 @@ ExpressionResolver.prototype = { } }, - rebind: function ( indexRef, newIndex, oldKeypath, newKeypath ) { + rebind: function ( oldKeypath, newKeypath ) { // TODO only bubble once, no matter how many references are affected by the rebind - this.refResolvers.forEach( r => r.rebind( indexRef, newIndex, oldKeypath, newKeypath ) ); + this.refResolvers.forEach( r => r.rebind( oldKeypath, newKeypath ) ); } }; diff --git a/src/virtualdom/items/shared/Resolvers/IndexResolver.js b/src/virtualdom/items/shared/Resolvers/IndexResolver.js index e67a5be5f3..22e3e1e533 100644 --- a/src/virtualdom/items/shared/Resolvers/IndexResolver.js +++ b/src/virtualdom/items/shared/Resolvers/IndexResolver.js @@ -3,21 +3,29 @@ var IndexResolver = function ( owner, ref, callback ) { this.ref = ref; this.callback = callback; + ref.ref.fragment.registerIndexRef( this ); + this.rebind(); }; IndexResolver.prototype = { rebind: function () { - var ref = this.ref, - indexRefs = this.parentFragment.indexRefs, - index = indexRefs[ ref ]; + var index, ref = this.ref.ref; + + if ( ref.ref.t === 'k' ) { + index = 'k' + ref.fragment.key; + } else { + index = 'i' + ref.fragment.index; + } if ( index !== undefined ) { this.callback( '@' + index ); } }, - unbind: function () {} // noop + unbind: function () { + this.ref.ref.fragment.unregisterIndexRef( this ); + } }; -export default IndexResolver; \ No newline at end of file +export default IndexResolver; diff --git a/src/virtualdom/items/shared/Resolvers/ReferenceExpressionResolver/MemberResolver.js b/src/virtualdom/items/shared/Resolvers/ReferenceExpressionResolver/MemberResolver.js index 6b323a7688..95cc31fcb7 100644 --- a/src/virtualdom/items/shared/Resolvers/ReferenceExpressionResolver/MemberResolver.js +++ b/src/virtualdom/items/shared/Resolvers/ReferenceExpressionResolver/MemberResolver.js @@ -47,9 +47,9 @@ MemberResolver.prototype = { this.viewmodel.register( this.keypath, this ); }, - rebind: function ( indexRef, newIndex, oldKeypath, newKeypath ) { + rebind: function ( oldKeypath, newKeypath ) { if ( this.refResolver ) { - this.refResolver.rebind( indexRef, newIndex, oldKeypath, newKeypath ); + this.refResolver.rebind( oldKeypath, newKeypath ); } }, diff --git a/src/virtualdom/items/shared/Resolvers/ReferenceExpressionResolver/ReferenceExpressionResolver.js b/src/virtualdom/items/shared/Resolvers/ReferenceExpressionResolver/ReferenceExpressionResolver.js index 3653691f13..3c8e6efc8b 100644 --- a/src/virtualdom/items/shared/Resolvers/ReferenceExpressionResolver/ReferenceExpressionResolver.js +++ b/src/virtualdom/items/shared/Resolvers/ReferenceExpressionResolver/ReferenceExpressionResolver.js @@ -55,11 +55,11 @@ ReferenceExpressionResolver.prototype = { this.members.forEach( unbind ); }, - rebind: function ( indexRef, newIndex, oldKeypath, newKeypath ) { + rebind: function ( oldKeypath, newKeypath ) { var changed; this.members.forEach( members => { - if ( members.rebind( indexRef, newIndex, oldKeypath, newKeypath ) ) { + if ( members.rebind( oldKeypath, newKeypath ) ) { changed = true; } }); diff --git a/src/virtualdom/items/shared/Resolvers/ReferenceResolver.js b/src/virtualdom/items/shared/Resolvers/ReferenceResolver.js index 754a19a950..49cb0bf83c 100644 --- a/src/virtualdom/items/shared/Resolvers/ReferenceResolver.js +++ b/src/virtualdom/items/shared/Resolvers/ReferenceResolver.js @@ -34,7 +34,7 @@ ReferenceResolver.prototype = { this.resolve( this.ref ); }, - rebind: function ( indexRef, newIndex, oldKeypath, newKeypath ) { + rebind: function ( oldKeypath, newKeypath ) { var keypath; if ( this.keypath !== undefined ) { diff --git a/src/virtualdom/items/shared/Resolvers/SpecialResolver.js b/src/virtualdom/items/shared/Resolvers/SpecialResolver.js index 753720f9bc..bef7cd5843 100644 --- a/src/virtualdom/items/shared/Resolvers/SpecialResolver.js +++ b/src/virtualdom/items/shared/Resolvers/SpecialResolver.js @@ -1,3 +1,5 @@ +import types from 'config/types'; + var SpecialResolver = function ( owner, ref, callback ) { this.parentFragment = owner.parentFragment; this.ref = ref; @@ -7,29 +9,71 @@ var SpecialResolver = function ( owner, ref, callback ) { }; var props = { - '@keypath': 'context', - '@index': 'index', - '@key': 'index' + '@keypath': { prefix: 'c', prop: [ 'context' ] }, + '@index': { prefix: 'i', prop: [ 'index' ] }, + '@key': { prefix: 'k', prop: [ 'key', 'index' ] } }; +function getProp( target, prop ) { + var value; + for ( let i = 0; i < prop.prop.length; i++ ) { + if ( ( value = target[prop.prop[i]] ) !== undefined ) { + return value; + } + } +} + SpecialResolver.prototype = { rebind: function () { - var ref = this.ref, fragment = this.parentFragment, prop = props[ref]; + var ref = this.ref, fragment = this.parentFragment, prop = props[ref], value; if ( !prop ) { throw new Error( 'Unknown special reference "' + ref + '" - valid references are @index, @key and @keypath' ); } - while ( fragment ) { - if ( fragment[prop] !== undefined ) { - return this.callback( '@' + fragment[prop] ); + // have we already found the nearest parent? + if ( this.cached ) { + return this.callback( '@' + prop.prefix + getProp( this.cached, prop ) ); + } + + // special case for indices, which may cross component boundaries + if ( prop.prop.indexOf( 'index' ) !== -1 || prop.prop.indexOf( 'key' ) !== -1 ) { + while ( fragment ) { + if ( fragment.owner.currentSubtype === types.SECTION_EACH && ( value = getProp( fragment, prop ) ) !== undefined ) { + this.cached = fragment; + + fragment.registerIndexRef( this ); + + return this.callback( '@' + prop.prefix + value ); + } + + // watch for component boundaries + if ( !fragment.parent && fragment.owner && + fragment.owner.component && fragment.owner.component.parentFragment && + !fragment.owner.component.instance.isolated ) { + fragment = fragment.owner.component.parentFragment; + } else { + fragment = fragment.parent; + } } + } - fragment = fragment.parent; + else { + while ( fragment ) { + if ( ( value = getProp( fragment, prop ) ) !== undefined ) { + return this.callback( '@' + prop.prefix + value ); + } + + fragment = fragment.parent; + } } }, - unbind: function () {} // noop + unbind: function () { + if ( this.cached ) { + this.cached.unregisterIndexRef( this ); + } + } }; export default SpecialResolver; diff --git a/src/virtualdom/items/shared/Resolvers/createReferenceResolver.js b/src/virtualdom/items/shared/Resolvers/createReferenceResolver.js index e676feaec2..9713d94cfb 100644 --- a/src/virtualdom/items/shared/Resolvers/createReferenceResolver.js +++ b/src/virtualdom/items/shared/Resolvers/createReferenceResolver.js @@ -1,18 +1,18 @@ import ReferenceResolver from 'virtualdom/items/shared/Resolvers/ReferenceResolver'; import SpecialResolver from 'virtualdom/items/shared/Resolvers/SpecialResolver'; import IndexResolver from 'virtualdom/items/shared/Resolvers/IndexResolver'; +import findIndexRefs from 'virtualdom/items/shared/Resolvers/findIndexRefs'; export default function createReferenceResolver ( owner, ref, callback ) { - var indexRefs, index; + var indexRef; if ( ref.charAt( 0 ) === '@' ) { return new SpecialResolver( owner, ref, callback ); } - indexRefs = owner.parentFragment.indexRefs; - if ( indexRefs && ( index = indexRefs[ ref ] ) !== undefined ) { - return new IndexResolver( owner, ref, callback ); + if ( indexRef = findIndexRefs( owner.parentFragment, ref ) ) { + return new IndexResolver( owner, indexRef, callback ); } return new ReferenceResolver( owner, ref, callback ); -} \ No newline at end of file +} diff --git a/src/virtualdom/items/shared/Resolvers/findIndexRefs.js b/src/virtualdom/items/shared/Resolvers/findIndexRefs.js new file mode 100644 index 0000000000..c5438f43db --- /dev/null +++ b/src/virtualdom/items/shared/Resolvers/findIndexRefs.js @@ -0,0 +1,53 @@ +export default function findIndexRefs( fragment, refName ) { + var result = {}, refs, fragRefs, ref, i, owner, hit = false; + + if ( !refName ) { + result.refs = refs = {}; + } + + while ( fragment ) { + if ( ( owner = fragment.owner ) && ( fragRefs = owner.indexRefs ) ) { + + // we're looking for a particular ref, and it's here + if ( refName && ( ref = owner.getIndexRef( refName ) ) ) { + result.ref = { + fragment: fragment, + ref: ref + }; + return result; + } + + // we're collecting refs up-tree + else if ( !refName ) { + for ( i in fragRefs ) { + ref = fragRefs[i]; + + // don't overwrite existing refs - they should shadow parents + if ( !refs[ref.n] ) { + hit = true; + refs[ref.n] = { + fragment: fragment, + ref: ref + }; + } + } + } + } + + // watch for component boundaries + if ( !fragment.parent && fragment.owner && + fragment.owner.component && fragment.owner.component.parentFragment && + !fragment.owner.component.instance.isolated ) { + result.componentBoundary = true; + fragment = fragment.owner.component.parentFragment; + } else { + fragment = fragment.parent; + } + } + + if ( !hit ) { + return undefined; + } else { + return result; + } +} diff --git a/test/modules/rebind.js b/test/modules/rebind.js index 2d6ba12391..7e395d4653 100644 --- a/test/modules/rebind.js +++ b/test/modules/rebind.js @@ -74,7 +74,8 @@ define([ fragment.findNextNode = function () { return null; }; fragment.render(); - fragment.rebind( 'i', opt.newKeypath.replace('items.',''), opt.oldKeypath, opt.newKeypath); + fragment.index = opt.newKeypath.replace( 'items.', '' ); + fragment.rebind( opt.oldKeypath, opt.newKeypath ); t.equal( fragment.context, opt.expected ); t.equal( fragment.items[0].node._ractive.keypath, opt.expected ); @@ -405,7 +406,7 @@ define([ test( 'index rebinds do not go past new index providers (#1457)', function ( t ) { var ractive = new Ractive({ el: fixture, - template: '{{#each foo}}{{@index}}{{#each .bar}}{{@index}}{{/each}}{{/each}}', + template: '{{#each foo}}{{@index}}{{#each .bar}}{{@index}}{{/each}}
{{/each}}', data: { foo: [ { bar: [ 1, 2 ] }, @@ -415,17 +416,17 @@ define([ } }); - t.htmlEqual( fixture.innerHTML, '0011020123' ); + t.htmlEqual( fixture.innerHTML, '001
10
20123
' ); ractive.splice( 'foo', 1, 1 ); - t.htmlEqual( fixture.innerHTML, '00110123' ); + t.htmlEqual( fixture.innerHTML, '001
10123
' ); }); test( 'index rebinds get passed through conditional sections correctly', t => { var ractive = new Ractive({ el: fixture, - template: '{{#each foo}}{{@index}}{{#.bar}}{{@index}}{{/}}{{/each}}', + template: '{{#each foo}}{{@index}}{{#.bar}}{{@index}}{{/}}
{{/each}}', data: { foo: [ { bar: true }, @@ -436,11 +437,11 @@ define([ } }); - t.htmlEqual( fixture.innerHTML, '0011233' ); + t.htmlEqual( fixture.innerHTML, '00
11
2
33
' ); ractive.splice( 'foo', 1, 1 ); - t.htmlEqual( fixture.innerHTML, '00122' ); + t.htmlEqual( fixture.innerHTML, '00
1
22
' ); }); }; diff --git a/test/modules/render.js b/test/modules/render.js index 86c0e91409..7b7c98b49d 100644 --- a/test/modules/render.js +++ b/test/modules/render.js @@ -225,6 +225,32 @@ define([ 'ractive', 'samples/render' ], function ( Ractive, tests ) { t.htmlEqual( fixture.innerHTML, '
foo
' ); }); + test( 'Value changes in object iteration should cause updates (#1476)', t => { + var ractive = new Ractive({ + el: fixture, + template: '{{#obj[sel]:sk}}{{sk}} {{@key}} {{.}}{{/}}', + data: { + obj: { + key1: { a: 'a1', b: 'b1' }, + key2: { a: 'a2', b: 'b2', c: 'c2' }, + key3: { c: 'c3' } + }, + sel: 'key1' + } + }); + + t.htmlEqual( fixture.innerHTML, 'a a a1b b b1' ); + + ractive.set( 'sel', 'key2' ); + t.htmlEqual( fixture.innerHTML, 'a a a2b b b2c c c2' ); + + ractive.set( 'sel', 'key3' ); + t.htmlEqual( fixture.innerHTML, 'c c c3' ); + + ractive.set( 'sel', 'key1' ); + t.htmlEqual( fixture.innerHTML, 'a a a1b b b1' ); + }); + }; function deepClone ( source ) { diff --git a/test/samples/parse.js b/test/samples/parse.js index 831b7bef54..24a7478927 100644 --- a/test/samples/parse.js +++ b/test/samples/parse.js @@ -64,6 +64,11 @@ var parseTests = [ template: "{{#items:i}}{{i}}: {{name}}{{/items}}", parsed: {v:1,t:[{"f":[{r:"i",t:2},": ",{r:"name",t:2}],"i":"i",r:"items",t:4}]} }, + { + name: "Named key and index", + template: "{{#items:k,i}}{{i}}: {{name}}{{/items}}", + parsed: {v:1,t:[{"f":[{r:"i",t:2},": ",{r:"name",t:2}],"i":"k,i",r:"items",t:4}]} + }, { name: "Element with unquoted attributes", template: "
", diff --git a/test/samples/render.js b/test/samples/render.js index e9785c8fa9..90622e6bf6 100644 --- a/test/samples/render.js +++ b/test/samples/render.js @@ -617,6 +617,31 @@ var renderTests = [ new_data: { object: { bar: 2, baz: 4, qux: 5 } }, new_result: '

2

4

5

' }, + { + name: 'two indices in an #each with object give access to the key and index', + handlebars: true, + template: '{{#object:k,i}}

{{k}} {{i}} {{.}}

{{/each}}', + data: { object: { foo: 1, bar: 2, baz: 3 } }, + result: '

foo 0 1

bar 1 2

baz 2 3

' + }, + { + name: 'the key ref in an #each switches to index if the value turns into an array', + handlebars: true, + template: '{{#object:k,i}}

{{k}} {{i}} {{.}}

{{/each}}', + data: { object: { foo: 1, bar: 2, baz: 3 } }, + result: '

foo 0 1

bar 1 2

baz 2 3

', + new_data: { object: [ 1, 2, 3 ] }, + new_result: '

0 0 1

1 1 2

2 2 3

' + }, + { + name: 'the key ref in an #each switches to key if the value turns into an object', + handlebars: true, + template: '{{#object:k,i}}

{{k}} {{i}} {{.}}

{{/each}}', + data: { object: [ 1, 2, 3 ] }, + result: '

0 0 1

1 1 2

2 2 3

', + new_data: { object: { foo: 1, bar: 2, baz: 3 } }, + new_result: '

foo 0 1

bar 1 2

baz 2 3

' + }, { name: '@index can be used as an index reference', handlebars: true, @@ -625,12 +650,37 @@ var renderTests = [ result: '

0: a

1: b

2: c

' }, { - name: '@key can be used as an key reference', + name: '@key can be used as a key reference', handlebars: true, template: '{{#each object}}

{{@key}}: {{this}}

{{/each}}', data: { object: { foo: 1, bar: 2, baz: 3 } }, result: '

foo: 1

bar: 2

baz: 3

' }, + { + name: '@key can be used as an index reference for arrays', + handlebars: true, + template: '{{#each array}}

{{@key}}: {{this}}

{{/each}}', + data: { array: [ 'foo', 'bar', 'baz' ] }, + result: '

0: foo

1: bar

2: baz

' + }, + { + name: '@index can be used as an index reference with object sections', + template: '{{#each object}}

{{@key}} {{@index}} {{.}}

{{/each}}', + data: { object: { foo: 1, bar: 2, baz: 3 } }, + result: '

foo 0 1

bar 1 2

baz 2 3

' + }, + { + name: '@key and @index can be used in an expression with object sections', + template: '{{#each object}}

{{@key + "!"}} {{@index + 1}} {{.}}

{{/each}}', + data: { object: { foo: 1, bar: 2, baz: 3 } }, + result: '

foo! 1 1

bar! 2 2

baz! 3 3

' + }, + { + name: 'key and index refs can be used in an expression with object sections', + template: '{{#each object: k, i }}

{{k + "!"}} {{i + 1}} {{.}}

{{/each}}', + data: { object: { foo: 1, bar: 2, baz: 3 } }, + result: '

foo! 1 1

bar! 2 2

baz! 3 3

' + }, { name: '@index can be used in an expression', handlebars: true, @@ -942,12 +992,17 @@ var renderTests = [ template: '', result: '' }, - { name: '@keypath may be used to refer to the current context', template: ``, data: { items: [ { some: { path: 'a' } }, { notsome: { path: 'b' } }, { some: { path: 'c' } } ] }, result: `` + }, + { + name: '@key references can be strings that look like numbers', + template: '{{#each object:k}}{{@key}} {{k}}{{/each}}', + data: { object: { '0001': 1 } }, + result: '0001 0001' } ];