diff --git a/src/generators/dom/visitors/Element/EventHandler.js b/src/generators/dom/visitors/Element/EventHandler.js index 189bb536c4..3800b528a5 100644 --- a/src/generators/dom/visitors/Element/EventHandler.js +++ b/src/generators/dom/visitors/Element/EventHandler.js @@ -45,7 +45,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/src/generators/dom/visitors/Element/meta/Window.js b/src/generators/dom/visitors/Element/meta/Window.js index b1dc8f8ecf..252ac1e2f7 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' ) { @@ -40,17 +42,22 @@ export default function visitWindow ( generator, block, node ) { } if ( attribute.type === 'Binding' ) { + 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}'` ); + } + + 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 ( 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}'` ); - } - if ( !events[ associatedEvent ] ) events[ associatedEvent ] = []; events[ associatedEvent ].push( `${attribute.value.name}: this.${attribute.name}` ); @@ -61,16 +68,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` + ${block.component}.set({ + ${props} + }); + ` ); + + if ( event === 'scroll' ) { + handlerBody.addLine( `${lock} = false;` ); + } + block.builders.create.addBlock( deindent` - var ${handlerName} = function ( event ) { - ${block.component}.set({ - ${props} - }); + function ${handlerName} ( event ) { + ${handlerBody} }; window.addEventListener( '${event}', ${handlerName} ); ` ); @@ -79,4 +101,52 @@ 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' } ); + }); + ` ); + } + + // 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 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 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