From fa60968ae1a20b93b360483cfa6696e1cfa55389 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sat, 19 Nov 2016 22:25:02 -0500 Subject: [PATCH] basic two-way binding --- compiler/generate/index.js | 27 ++++++++++++++++++++ compiler/parse/read/directives.js | 23 +++++++++++++++++ compiler/parse/state/tag.js | 7 ++++- test/compiler/binding-input-text/_config.js | 23 +++++++++++++++++ test/compiler/binding-input-text/main.svelte | 2 ++ test/parser/binding/input.svelte | 1 + test/parser/binding/output.json | 27 ++++++++++++++++++++ 7 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 test/compiler/binding-input-text/_config.js create mode 100644 test/compiler/binding-input-text/main.svelte create mode 100644 test/parser/binding/input.svelte create mode 100644 test/parser/binding/output.json diff --git a/compiler/generate/index.js b/compiler/generate/index.js index b865a1ff90..5e405566f2 100644 --- a/compiler/generate/index.js +++ b/compiler/generate/index.js @@ -234,6 +234,33 @@ export default function generate ( parsed, template ) { ` ); } + else if ( attribute.type === 'Binding' ) { + if ( attribute.value in current.contexts ) { + throw new Error( `Can only bind top-level properties` ); + } + + const handler = current.counter( `${name}ChangeHandler` ); + + initStatements.push( deindent` + var ${name}_updating = false; + function ${handler} () { + ${name}_updating = true; + component.set({ ${attribute.value}: ${name}.value }); + ${name}_updating = false; + } + + ${name}.addEventListener( 'input', ${handler}, false ); + ` ); + + updateStatements.push( deindent` + if ( !${name}_updating ) ${name}.value = root.${attribute.value}; + ` ); + + teardownStatements.push( deindent` + ${name}.removeEventListener( 'input', ${handler}, false ); + ` ); + } + else { throw new Error( `Not implemented: ${attribute.type}` ); } diff --git a/compiler/parse/read/directives.js b/compiler/parse/read/directives.js index 55e5f2e7e0..3573a5ded3 100644 --- a/compiler/parse/read/directives.js +++ b/compiler/parse/read/directives.js @@ -41,3 +41,26 @@ export function readEventHandlerDirective ( parser, start, name ) { expression }; } + +export function readBindingDirective ( parser, start, name ) { + const quoteMark = ( + parser.eat( `'` ) ? `'` : + parser.eat( `"` ) ? `"` : + null + ); + + const value = parser.read( /[a-zA-Z_$][a-zA-Z0-9_$]*/ ); // TODO – keypaths? /([a-zA-Z_$][a-zA-Z0-9_$]*)(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*/ + if ( !value ) parser.error( `Expected valid property name` ); + + if ( quoteMark ) { + parser.eat( quoteMark, true ); + } + + return { + start, + end: parser.index, + type: 'Binding', + name, + value + }; +} diff --git a/compiler/parse/state/tag.js b/compiler/parse/state/tag.js index cb2178e30d..7332bccd66 100644 --- a/compiler/parse/state/tag.js +++ b/compiler/parse/state/tag.js @@ -1,7 +1,7 @@ import readExpression from '../read/expression.js'; import readScript from '../read/script.js'; import readStyle from '../read/style.js'; -import { readEventHandlerDirective } from '../read/directives.js'; +import { readEventHandlerDirective, readBindingDirective } from '../read/directives.js'; import { trimStart, trimEnd } from '../utils/trim.js'; const validTagName = /^[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; @@ -144,6 +144,11 @@ function readAttribute ( parser ) { return readEventHandlerDirective( parser, start, name.slice( 3 ) ); } + if ( /^bind:/.test( name ) ) { + parser.eat( '=', true ); + return readBindingDirective( parser, start, name.slice( 5 ) ); + } + const value = parser.eat( '=' ) ? readAttributeValue( parser ) : true; return { diff --git a/test/compiler/binding-input-text/_config.js b/test/compiler/binding-input-text/_config.js new file mode 100644 index 0000000000..572205dc29 --- /dev/null +++ b/test/compiler/binding-input-text/_config.js @@ -0,0 +1,23 @@ +import * as assert from 'assert'; + +export default { + data: { + name: 'world' + }, + html: `\n

hello world

`, + test ( component, target, window ) { + const input = target.querySelector( 'input' ); + assert.equal( input.value, 'world' ); + + const event = new window.Event( 'input' ); + + input.value = 'everybody'; + input.dispatchEvent( event ); + + assert.equal( target.innerHTML, `\n

hello everybody

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

hello goodbye

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

hello {{name}}

diff --git a/test/parser/binding/input.svelte b/test/parser/binding/input.svelte new file mode 100644 index 0000000000..6a7bf8566c --- /dev/null +++ b/test/parser/binding/input.svelte @@ -0,0 +1 @@ + diff --git a/test/parser/binding/output.json b/test/parser/binding/output.json new file mode 100644 index 0000000000..ec1fcd2862 --- /dev/null +++ b/test/parser/binding/output.json @@ -0,0 +1,27 @@ +{ + "html": { + "start": 0, + "end": 25, + "type": "Fragment", + "children": [ + { + "start": 0, + "end": 25, + "type": "Element", + "name": "input", + "attributes": [ + { + "start": 7, + "end": 24, + "type": "Binding", + "name": "value", + "value": "name" + } + ], + "children": [] + } + ] + }, + "css": null, + "js": null +}