diff --git a/src/generators/dom/index.js b/src/generators/dom/index.js index 24a8753476..38c5f81ec9 100644 --- a/src/generators/dom/index.js +++ b/src/generators/dom/index.js @@ -14,6 +14,11 @@ class DomGenerator extends Generator { this.renderers = []; this.uses = new Set(); + // initial values for e.g. window.innerWidth, if there's a <:Window> meta tag + this.builders = { + metaBindings: new CodeBuilder() + }; + this.importedComponents = new Map(); } @@ -329,6 +334,10 @@ export default function dom ( parsed, source, options, names ) { `this._state = ${templateProperties.data ? `Object.assign( ${generator.alias( 'template' )}.data(), options.data )` : `options.data || {}`};` ); + if ( !generator.builders.metaBindings.isEmpty() ) { + constructorBlock.addBlock( generator.builders.metaBindings ); + } + if ( templateProperties.computed ) { constructorBlock.addLine( `${generator.alias( 'applyComputations' )}( this._state, this._state, {}, true );` diff --git a/src/generators/dom/visitors/Element.js b/src/generators/dom/visitors/Element.js index 0526e5fc71..478bc39e2e 100644 --- a/src/generators/dom/visitors/Element.js +++ b/src/generators/dom/visitors/Element.js @@ -2,10 +2,20 @@ import CodeBuilder from '../../../utils/CodeBuilder.js'; import deindent from '../../../utils/deindent.js'; import addElementAttributes from './attributes/addElementAttributes.js'; import Component from './Component.js'; +import Window from './meta/Window.js'; + +const meta = { + ':Window': Window +}; export default { enter ( generator, node ) { + if ( node.name in meta ) { + return meta[ node.name ].enter( generator, node ); + } + const isComponent = generator.components.has( node.name ) || node.name === ':Self'; + if ( isComponent ) { return Component.enter( generator, node ); } @@ -100,7 +110,13 @@ export default { }, leave ( generator, node ) { + if ( node.name in meta ) { + if ( meta[ node.name ].leave ) meta[ node.name ].leave( generator, node ); + return; + } + const isComponent = generator.components.has( node.name ); + if ( isComponent ) { return Component.leave( generator, node ); } diff --git a/src/generators/dom/visitors/meta/Window.js b/src/generators/dom/visitors/meta/Window.js new file mode 100644 index 0000000000..e1d5cb7b43 --- /dev/null +++ b/src/generators/dom/visitors/meta/Window.js @@ -0,0 +1,84 @@ +import flattenReference from '../../../../utils/flattenReference.js'; +import deindent from '../../../../utils/deindent.js'; + +const associatedEvents = { + innerWidth: 'resize', + innerHeight: 'resize', + outerWidth: 'resize', + outerHeight: 'resize', + + scrollX: 'scroll', + scrollY: 'scroll' +}; + +export default { + enter ( generator, node ) { + const events = {}; + + node.attributes.forEach( attribute => { + if ( attribute.type === 'EventHandler' ) { + // TODO verify that it's a valid callee (i.e. built-in or declared method) + generator.addSourcemapLocations( attribute.expression ); + + const flattened = flattenReference( attribute.expression.callee ); + if ( flattened.name !== 'event' && flattened.name !== 'this' ) { + // allow event.stopPropagation(), this.select() etc + generator.code.prependRight( attribute.expression.start, 'component.' ); + } + + const handlerName = generator.current.getUniqueName( `onwindow${attribute.name}` ); + + generator.current.builders.init.addBlock( deindent` + var ${handlerName} = function ( event ) { + [✂${attribute.expression.start}-${attribute.expression.end}✂]; + }; + window.addEventListener( '${attribute.name}', ${handlerName} ); + ` ); + + generator.current.builders.teardown.addBlock( deindent` + window.removeEventListener( '${attribute.name}', ${handlerName} ); + ` ); + } + + 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}'` ); + } + + if ( !events[ associatedEvent ] ) events[ associatedEvent ] = []; + events[ associatedEvent ].push( `${attribute.value.name}: this.${attribute.name}` ); + + // add initial value + generator.builders.metaBindings.addLine( + `this._state.${attribute.value.name} = window.${attribute.name};` + ); + } + }); + + Object.keys( events ).forEach( event => { + const handlerName = generator.current.getUniqueName( `onwindow${event}` ); + + const props = events[ event ].join( ',\n' ); + + generator.current.builders.init.addBlock( deindent` + var ${handlerName} = function ( event ) { + component.set({ + ${props} + }); + }; + window.addEventListener( '${event}', ${handlerName} ); + ` ); + + generator.current.builders.teardown.addBlock( deindent` + window.removeEventListener( '${event}', ${handlerName} ); + ` ); + }); + } +}; diff --git a/src/generators/server-side-rendering/visitors/Element.js b/src/generators/server-side-rendering/visitors/Element.js index 971b54d658..d50f795b6c 100644 --- a/src/generators/server-side-rendering/visitors/Element.js +++ b/src/generators/server-side-rendering/visitors/Element.js @@ -1,8 +1,17 @@ import Component from './Component.js'; import isVoidElementName from '../../../utils/isVoidElementName.js'; +import Window from './meta/Window.js'; + +const meta = { + ':Window': Window +}; export default { enter ( generator, node ) { + if ( node.name in meta ) { + return meta[ node.name ].enter( generator, node ); + } + if ( generator.components.has( node.name ) || node.name === ':Self' ) { Component.enter( generator, node ); return; @@ -39,6 +48,11 @@ export default { }, leave ( generator, node ) { + if ( node.name in meta ) { + if ( meta[ node.name ].leave ) meta[ node.name ].leave( generator, node ); + return; + } + if ( generator.components.has( node.name ) || node.name === ':Self' ) { Component.leave( generator, node ); return; diff --git a/src/generators/server-side-rendering/visitors/meta/Window.js b/src/generators/server-side-rendering/visitors/meta/Window.js new file mode 100644 index 0000000000..7df7d8bdaf --- /dev/null +++ b/src/generators/server-side-rendering/visitors/meta/Window.js @@ -0,0 +1,9 @@ +export default { + enter () { + // noop + }, + + leave () { + // noop + } +}; diff --git a/src/parse/index.js b/src/parse/index.js index ee0c1d8dfb..29122d69bc 100644 --- a/src/parse/index.js +++ b/src/parse/index.js @@ -32,6 +32,7 @@ export default function parse ( template, options = {} ) { index: 0, template, stack: [], + metaTags: {}, current () { return this.stack[ this.stack.length - 1 ]; diff --git a/src/parse/state/tag.js b/src/parse/state/tag.js index 361f6f49f0..5069c5c85c 100644 --- a/src/parse/state/tag.js +++ b/src/parse/state/tag.js @@ -11,6 +11,10 @@ const invalidUnquotedAttributeCharacters = /[\s"'=<>\/`]/; const SELF = ':Self'; +const metaTags = { + ':Window': true +}; + const specials = new Map( [ [ 'script', { read: readScript, @@ -80,6 +84,22 @@ export default function tag ( parser ) { const name = readTagName( parser ); + if ( name in metaTags ) { + if ( name in parser.metaTags ) { + if ( isClosingTag && parser.current().children.length ) { + parser.error( `<${name}> cannot have children`, parser.current().children[0].start ); + } + + parser.error( `A component can only have one <${name}> tag`, start ); + } + + parser.metaTags[ name ] = true; + + if ( parser.stack.length > 1 ) { + parser.error( `<${name}> tags cannot be inside elements or blocks`, start ); + } + } + parser.allowWhitespace(); if ( isClosingTag ) { @@ -192,6 +212,9 @@ function readTagName ( parser ) { } const name = parser.readUntil( /(\s|\/|>)/ ); + + if ( name in metaTags ) return name; + if ( !validTagName.test( name ) ) { parser.error( `Expected valid tag name`, start ); } diff --git a/test/generator/index.js b/test/generator/index.js index f52831b7c2..04599c6f2b 100644 --- a/test/generator/index.js +++ b/test/generator/index.js @@ -93,6 +93,8 @@ describe( 'generate', () => { return env() .then( window => { + global.window = window; + // Put the constructor on window for testing window.SvelteComponent = SvelteComponent; diff --git a/test/generator/samples/window-binding-resize/_config.js b/test/generator/samples/window-binding-resize/_config.js new file mode 100644 index 0000000000..ee991552a7 --- /dev/null +++ b/test/generator/samples/window-binding-resize/_config.js @@ -0,0 +1,29 @@ +export default { + html: `