From 4a20d3d946ec307984c1829389290da7ddd0c605 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 10 Apr 2017 16:08:19 -0400 Subject: [PATCH 1/3] window scroll bindings are bidirectional (#404) --- .../dom/visitors/Element/meta/Window.js | 53 +++++++++++++++++-- .../window-bind-scroll-update/_config.js | 12 +++++ .../window-bind-scroll-update/main.html | 3 ++ 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 test/runtime/samples/window-bind-scroll-update/_config.js create mode 100644 test/runtime/samples/window-bind-scroll-update/main.html diff --git a/src/generators/dom/visitors/Element/meta/Window.js b/src/generators/dom/visitors/Element/meta/Window.js index c0f9f3d181..837578ef69 100644 --- a/src/generators/dom/visitors/Element/meta/Window.js +++ b/src/generators/dom/visitors/Element/meta/Window.js @@ -1,5 +1,6 @@ import flattenReference from '../../../../../utils/flattenReference.js'; import deindent from '../../../../../utils/deindent.js'; +import CodeBuilder from '../../../../../utils/CodeBuilder.js'; const associatedEvents = { innerWidth: 'resize', @@ -13,6 +14,7 @@ const associatedEvents = { export default function visitWindow ( generator, block, node ) { const events = {}; + const bindings = {}; node.attributes.forEach( attribute => { if ( attribute.type === 'EventHandler' ) { @@ -51,6 +53,8 @@ export default function visitWindow ( generator, block, node ) { throw new Error( `Bindings on <:Window/> must be to top-level properties, e.g. '${parts.pop()}' rather than '${keypath}'` ); } + bindings[ attribute.name ] = attribute.value.name; + if ( !events[ associatedEvent ] ) events[ associatedEvent ] = []; events[ associatedEvent ].push( `${attribute.value.name}: this.${attribute.name}` ); @@ -61,16 +65,31 @@ export default function visitWindow ( generator, block, node ) { } }); + const lock = block.getUniqueName( `window_updating` ); + Object.keys( events ).forEach( event => { const handlerName = block.getUniqueName( `onwindow${event}` ); - const props = events[ event ].join( ',\n' ); + const handlerBody = new CodeBuilder(); + if ( event === 'scroll' ) { // TODO other bidirectional bindings... + block.builders.create.addLine( `var ${lock} = false;` ); + handlerBody.addLine( `${lock} = true;` ); + } + + handlerBody.addBlock( deindent` + component.set({ + ${props} + }); + ` ); + + if ( event === 'scroll' ) { + handlerBody.addLine( `${lock} = false;` ); + } + block.builders.create.addBlock( deindent` var ${handlerName} = function ( event ) { - component.set({ - ${props} - }); + ${handlerBody} }; window.addEventListener( '${event}', ${handlerName} ); ` ); @@ -79,4 +98,30 @@ export default function visitWindow ( generator, block, node ) { window.removeEventListener( '${event}', ${handlerName} ); ` ); }); + + // special case... might need to abstract this out if we add more special cases + if ( bindings.scrollX && bindings.scrollY ) { + const observerCallback = block.getUniqueName( `scrollobserver` ); + + block.builders.create.addBlock( deindent` + function ${observerCallback} () { + if ( ${lock} ) return; + var x = ${bindings.scrollX ? `component.get( '${bindings.scrollX}' )` : `window.scrollX`}; + var y = ${bindings.scrollY ? `component.get( '${bindings.scrollY}' )` : `window.scrollY`}; + window.scrollTo( x, y ); + }; + ` ); + + if ( bindings.scrollX ) block.builders.create.addLine( `component.observe( '${bindings.scrollX}', ${observerCallback} );` ); + if ( bindings.scrollY ) block.builders.create.addLine( `component.observe( '${bindings.scrollY}', ${observerCallback} );` ); + } else if ( bindings.scrollX || bindings.scrollY ) { + const isX = !!bindings.scrollX; + + block.builders.create.addBlock( deindent` + component.observe( '${bindings.scrollX || bindings.scrollY}', function ( ${isX ? 'x' : 'y'} ) { + if ( ${lock} ) return; + window.scrollTo( ${isX ? 'x, window.scrollY' : 'window.scrollX, y' } ); + }); + ` ); + } } \ No newline at end of file diff --git a/test/runtime/samples/window-bind-scroll-update/_config.js b/test/runtime/samples/window-bind-scroll-update/_config.js new file mode 100644 index 0000000000..3ae13c33ce --- /dev/null +++ b/test/runtime/samples/window-bind-scroll-update/_config.js @@ -0,0 +1,12 @@ +export default { + skip: true, // JSDOM + + test ( assert, component, target, window ) { + assert.equal( window.scrollY, 0 ); + + component.set({ scrollY: 100 }); + assert.equal( window.scrollY, 100 ); + + component.destroy(); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/window-bind-scroll-update/main.html b/test/runtime/samples/window-bind-scroll-update/main.html new file mode 100644 index 0000000000..a95028fb7a --- /dev/null +++ b/test/runtime/samples/window-bind-scroll-update/main.html @@ -0,0 +1,3 @@ +<:Window bind:scrollY/> + +
\ No newline at end of file From dee035fee2adc7b5c635442a3b5aa16a46998a34 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 10 Apr 2017 16:31:44 -0400 Subject: [PATCH 2/3] add bind:online to <:Window/> (#404) --- .../dom/visitors/Element/meta/Window.js | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/generators/dom/visitors/Element/meta/Window.js b/src/generators/dom/visitors/Element/meta/Window.js index 837578ef69..8030d68090 100644 --- a/src/generators/dom/visitors/Element/meta/Window.js +++ b/src/generators/dom/visitors/Element/meta/Window.js @@ -42,12 +42,6 @@ export default function visitWindow ( generator, block, node ) { } if ( attribute.type === 'Binding' ) { - const associatedEvent = associatedEvents[ attribute.name ]; - - if ( !associatedEvent ) { - throw new Error( `Cannot bind to ${attribute.name} on <:Window>` ); - } - if ( attribute.value.type !== 'Identifier' ) { const { parts, keypath } = flattenReference( attribute.value ); throw new Error( `Bindings on <:Window/> must be to top-level properties, e.g. '${parts.pop()}' rather than '${keypath}'` ); @@ -55,6 +49,15 @@ export default function visitWindow ( generator, block, node ) { bindings[ attribute.name ] = attribute.value.name; + // bind:online is a special case, we need to listen for two separate events + if ( attribute.name === 'online' ) return; + + const associatedEvent = associatedEvents[ attribute.name ]; + + if ( !associatedEvent ) { + throw new Error( `Cannot bind to ${attribute.name} on <:Window>` ); + } + if ( !events[ associatedEvent ] ) events[ associatedEvent ] = []; events[ associatedEvent ].push( `${attribute.value.name}: this.${attribute.name}` ); @@ -88,7 +91,7 @@ export default function visitWindow ( generator, block, node ) { } block.builders.create.addBlock( deindent` - var ${handlerName} = function ( event ) { + function ${handlerName} ( event ) { ${handlerBody} }; window.addEventListener( '${event}', ${handlerName} ); @@ -124,4 +127,26 @@ export default function visitWindow ( generator, block, node ) { }); ` ); } + + // another special case. (I'm starting to think these are all special cases.) + if ( bindings.online ) { + const handlerName = block.getUniqueName( `onlinestatuschanged` ); + block.builders.create.addBlock( deindent` + function ${handlerName} ( event ) { + component.set({ ${bindings.online}: navigator.onLine }); + }; + window.addEventListener( 'online', ${handlerName} ); + window.addEventListener( 'offline', ${handlerName} ); + ` ); + + // add initial value + generator.builders.metaBindings.addLine( + `this._state.${bindings.online} = navigator.onLine;` + ); + + block.builders.destroy.addBlock( deindent` + window.removeEventListener( 'online', ${handlerName} ); + window.removeEventListener( 'offline', ${handlerName} ); + ` ); + } } \ No newline at end of file From 05e31185924525ca82478bb997463ef74e3b86a1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Apr 2017 09:38:34 -0400 Subject: [PATCH 3/3] =?UTF-8?q?ensure=20hoisted=20event=20handler=20names?= =?UTF-8?q?=20are=20globally=20unique=20=E2=80=94=20fixes=20#466?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dom/visitors/Element/EventHandler.js | 2 +- .../_config.js | 36 +++++++++++++++++++ .../event-handler-each-deconflicted/main.html | 9 +++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 test/runtime/samples/event-handler-each-deconflicted/_config.js create mode 100644 test/runtime/samples/event-handler-each-deconflicted/main.html diff --git a/src/generators/dom/visitors/Element/EventHandler.js b/src/generators/dom/visitors/Element/EventHandler.js index 2a51cdc1ff..1099238b83 100644 --- a/src/generators/dom/visitors/Element/EventHandler.js +++ b/src/generators/dom/visitors/Element/EventHandler.js @@ -44,7 +44,7 @@ export default function visitEventHandler ( generator, block, state, node, attri // get a name for the event handler that is globally unique // if hoisted, locally unique otherwise const handlerName = shouldHoist ? - generator.alias( `${name}_handler` ) : + generator.getUniqueName( `${name}_handler` ) : block.getUniqueName( `${name}_handler` ); // create the handler body diff --git a/test/runtime/samples/event-handler-each-deconflicted/_config.js b/test/runtime/samples/event-handler-each-deconflicted/_config.js new file mode 100644 index 0000000000..0c11a15d3d --- /dev/null +++ b/test/runtime/samples/event-handler-each-deconflicted/_config.js @@ -0,0 +1,36 @@ +export default { + data: { + foo: [ 1 ], + bar: [ 2 ], + clicked: 'neither' + }, + + html: ` + + +

clicked: neither

+ `, + + test ( assert, component, target, window ) { + const buttons = target.querySelectorAll( 'button' ); + const event = new window.MouseEvent( 'click' ); + + buttons[0].dispatchEvent( event ); + assert.equal( component.get( 'clicked' ), 'foo' ); + assert.htmlEqual( target.innerHTML, ` + + +

clicked: foo

+ ` ); + + buttons[1].dispatchEvent( event ); + assert.equal( component.get( 'clicked' ), 'bar' ); + assert.htmlEqual( target.innerHTML, ` + + +

clicked: bar

+ ` ); + + component.destroy(); + } +}; diff --git a/test/runtime/samples/event-handler-each-deconflicted/main.html b/test/runtime/samples/event-handler-each-deconflicted/main.html new file mode 100644 index 0000000000..4dcc770e83 --- /dev/null +++ b/test/runtime/samples/event-handler-each-deconflicted/main.html @@ -0,0 +1,9 @@ +{{#each foo as f}} + +{{/each}} + +{{#each bar as b}} + +{{/each}} + +

clicked: {{clicked}}

\ No newline at end of file