diff --git a/compiler/generate/binding/index.js b/compiler/generate/binding/index.js new file mode 100644 index 0000000000..c3449d8ab5 --- /dev/null +++ b/compiler/generate/binding/index.js @@ -0,0 +1,60 @@ +import deindent from '../utils/deindent.js'; + +export default function createBinding ( node, name, attribute, current, initStatements, updateStatements, teardownStatements, allUsedContexts ) { + const parts = attribute.value.split( '.' ); + + const deep = parts.length > 1; + const contextual = parts[0] in current.contexts; + if ( contextual ) allUsedContexts.add( parts[0] ); + + const handler = current.counter( `${name}ChangeHandler` ); + let setter; + + let eventName = 'change'; + if ( node.name === 'input' ) { + const type = node.attributes.find( attr => attr.type === 'Attribute' && attr.name === 'type' ); + if ( !type || type.value[0].data === 'text' ) { + // TODO in validation, should throw if type attribute is not static + eventName = 'input'; + } + } + + if ( deep && contextual ) { + // TODO can we target only things that have changed? + setter = deindent` + var context = this.__context.${parts[0]}; + context.${parts.slice( 1 ).join( '.' )} = this.${attribute.name}; + component.set({}); + `; + } else if ( deep ) { + setter = deindent` + var ${parts[0]} = component.get( '${parts[0]}' ); + ${parts[0]}.${parts.slice( 1 ).join( '.' )} = this.${attribute.name}; + component.set({ ${parts[0]}: ${parts[0]} }); + `; + } else if ( contextual ) { + throw new Error( `Reassigning context is not currently supported` ); + } else { + setter = `component.set({ ${attribute.value}: ${name}.${attribute.name} });`; + } + + initStatements.push( deindent` + var ${name}_updating = false; + + function ${handler} () { + ${name}_updating = true; + ${setter} + ${name}_updating = false; + } + + ${name}.addEventListener( '${eventName}', ${handler}, false ); + ` ); + + updateStatements.push( deindent` + if ( !${name}_updating ) ${name}.${attribute.name} = ${contextual ? attribute.value : `root.${attribute.value}`} + ` ); + + teardownStatements.push( deindent` + ${name}.removeEventListener( '${eventName}', ${handler}, false ); + ` ); +} diff --git a/compiler/generate/index.js b/compiler/generate/index.js index 22b37ee52c..8c599bf321 100644 --- a/compiler/generate/index.js +++ b/compiler/generate/index.js @@ -6,6 +6,7 @@ import isReference from './utils/isReference.js'; import contextualise from './utils/contextualise.js'; import counter from './utils/counter.js'; import attributeLookup from './attributes/lookup.js'; +import createBinding from './binding/index.js'; function createRenderer ( fragment ) { return deindent` @@ -235,42 +236,7 @@ export default function generate ( parsed, template ) { } else if ( attribute.type === 'Binding' ) { - const parts = attribute.value.split( '.' ); - const contextual = parts[0] in current.contexts; - - const handler = current.counter( `${name}ChangeHandler` ); - let setter; - - if ( contextual ) { - allUsedContexts.add( parts[0] ); - setter = deindent` - var context = this.__context.${parts[0]}; - context.${parts.slice( 1 ).join( '.' )} = this.value; - component.set({ ${current.contextChain[0]}: component.get( '${current.contextChain[0]}' ) }); - `; - } else { - setter = `component.set({ ${attribute.value}: ${name}.value });`; - } - - initStatements.push( deindent` - var ${name}_updating = false; - - function ${handler} () { - ${name}_updating = true; - ${setter} - ${name}_updating = false; - } - - ${name}.addEventListener( 'input', ${handler}, false ); - ` ); - - updateStatements.push( deindent` - if ( !${name}_updating ) ${name}.value = ${contextual ? attribute.value : `root.${attribute.value}`} - ` ); - - teardownStatements.push( deindent` - ${name}.removeEventListener( 'input', ${handler}, false ); - ` ); + createBinding( node, name, attribute, current, initStatements, updateStatements, teardownStatements, allUsedContexts ); } else { diff --git a/test/compiler/binding-input-checkbox/_config.js b/test/compiler/binding-input-checkbox/_config.js new file mode 100644 index 0000000000..634504a4d0 --- /dev/null +++ b/test/compiler/binding-input-checkbox/_config.js @@ -0,0 +1,23 @@ +import * as assert from 'assert'; + +export default { + data: { + foo: true + }, + html: `\n

true

`, + test ( component, target, window ) { + const input = target.querySelector( 'input' ); + assert.equal( input.checked, true ); + + const event = new window.Event( 'change' ); + + input.checked = false; + input.dispatchEvent( event ); + + assert.equal( target.innerHTML, `\n

false

` ); + + component.set({ foo: true }); + assert.equal( input.checked, true ); + assert.equal( target.innerHTML, `\n

true

` ); + } +}; diff --git a/test/compiler/binding-input-checkbox/main.svelte b/test/compiler/binding-input-checkbox/main.svelte new file mode 100644 index 0000000000..2ab8592831 --- /dev/null +++ b/test/compiler/binding-input-checkbox/main.svelte @@ -0,0 +1,2 @@ + +

{{foo}}

diff --git a/test/compiler/binding-input-text-contextual/_config.js b/test/compiler/binding-input-text-contextual/_config.js new file mode 100644 index 0000000000..24a9a81fc6 --- /dev/null +++ b/test/compiler/binding-input-text-contextual/_config.js @@ -0,0 +1,31 @@ +import * as assert from 'assert'; + +export default { + data: { + items: [ + 'one', + 'two', + 'three' + ] + }, + html: `

one

two

three

`, + test ( component, target, window ) { + const inputs = [ ...target.querySelectorAll( 'input' ) ]; + + assert.equal( inputs[0].value, 'one' ); + + const event = new window.Event( 'input' ); + + inputs[1].value = 'four'; + inputs[1].dispatchEvent( event ); + + assert.equal( target.innerHTML, `

one

four

three

` ); + + const items = component.get( 'items' ); + items[2] = 'five'; + + component.set({ items }); + assert.equal( inputs[2].value, 'five' ); + assert.equal( target.innerHTML, `

one

four

five

` ); + } +}; diff --git a/test/compiler/binding-input-text-contextual/main.svelte b/test/compiler/binding-input-text-contextual/main.svelte new file mode 100644 index 0000000000..5f07fadc26 --- /dev/null +++ b/test/compiler/binding-input-text-contextual/main.svelte @@ -0,0 +1,3 @@ +{{#each items as item}} +

{{item}}

+{{/each}} diff --git a/test/compiler/binding-input-text-deep-contextual/_config.js b/test/compiler/binding-input-text-deep-contextual/_config.js new file mode 100644 index 0000000000..ada981c7a5 --- /dev/null +++ b/test/compiler/binding-input-text-deep-contextual/_config.js @@ -0,0 +1,31 @@ +import * as assert from 'assert'; + +export default { + data: { + items: [ + { description: 'one' }, + { description: 'two' }, + { description: 'three' } + ] + }, + html: `

one

two

three

`, + test ( component, target, window ) { + const inputs = [ ...target.querySelectorAll( 'input' ) ]; + + assert.equal( inputs[0].value, 'one' ); + + const event = new window.Event( 'input' ); + + inputs[1].value = 'four'; + inputs[1].dispatchEvent( event ); + + assert.equal( target.innerHTML, `

one

four

three

` ); + + const items = component.get( 'items' ); + items[2].description = 'five'; + + component.set({ items }); + assert.equal( inputs[2].value, 'five' ); + assert.equal( target.innerHTML, `

one

four

five

` ); + } +}; diff --git a/test/compiler/binding-input-text-deep-contextual/main.svelte b/test/compiler/binding-input-text-deep-contextual/main.svelte new file mode 100644 index 0000000000..210e4e4fea --- /dev/null +++ b/test/compiler/binding-input-text-deep-contextual/main.svelte @@ -0,0 +1,3 @@ +{{#each items as item}} +

{{item.description}}

+{{/each}} diff --git a/test/compiler/binding-input-text-deep/_config.js b/test/compiler/binding-input-text-deep/_config.js index ada981c7a5..a1c919c6ac 100644 --- a/test/compiler/binding-input-text-deep/_config.js +++ b/test/compiler/binding-input-text-deep/_config.js @@ -2,30 +2,28 @@ import * as assert from 'assert'; export default { data: { - items: [ - { description: 'one' }, - { description: 'two' }, - { description: 'three' } - ] + user: { + name: 'alice' + } }, - html: `

one

two

three

`, + html: `\n

hello alice

`, test ( component, target, window ) { - const inputs = [ ...target.querySelectorAll( 'input' ) ]; + const input = target.querySelector( 'input' ); - assert.equal( inputs[0].value, 'one' ); + assert.equal( input.value, 'alice' ); const event = new window.Event( 'input' ); - inputs[1].value = 'four'; - inputs[1].dispatchEvent( event ); + input.value = 'bob'; + input.dispatchEvent( event ); - assert.equal( target.innerHTML, `

one

four

three

` ); + assert.equal( target.innerHTML, `\n

hello bob

` ); - const items = component.get( 'items' ); - items[2].description = 'five'; + const user = component.get( 'user' ); + user.name = 'carol'; - component.set({ items }); - assert.equal( inputs[2].value, 'five' ); - assert.equal( target.innerHTML, `

one

four

five

` ); + component.set({ user }); + assert.equal( input.value, 'carol' ); + assert.equal( target.innerHTML, `\n

hello carol

` ); } }; diff --git a/test/compiler/binding-input-text-deep/main.svelte b/test/compiler/binding-input-text-deep/main.svelte index 210e4e4fea..8ed32a7e90 100644 --- a/test/compiler/binding-input-text-deep/main.svelte +++ b/test/compiler/binding-input-text-deep/main.svelte @@ -1,3 +1,2 @@ -{{#each items as item}} -

{{item.description}}

-{{/each}} + +

hello {{user.name}}