diff --git a/CHANGELOG.md b/CHANGELOG.md index 98068b6b26..615b566ab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Svelte changelog +## 1.42.0 + +* Implement `indeterminate` binding for checkbox inputs ([#910](https://github.com/sveltejs/svelte/issues/910)) +* Use `` children as `value` attribute if none exists ([#928](https://github.com/sveltejs/svelte/issues/928)) +* Allow quoted property names in default export and sub-properties ([#914](https://github.com/sveltejs/svelte/issues/914)) +* Various improvements to generated code for bindings + +## 1.41.4 + +* Handle self-destructive bindings ([#917](https://github.com/sveltejs/svelte/issues/917)) +* Prevent `innerHTML` with `` elements ([#915](https://github.com/sveltejs/svelte/issues/915)) +* Use `dataset` unless `legacy` is true ([#858](https://github.com/sveltejs/svelte/issues/858)) +* Add `prepare` script to facilitate installing from git ([#923](https://github.com/sveltejs/svelte/pull/923)) + ## 1.41.3 * Prevent argument name clashes ([#911](https://github.com/sveltejs/svelte/issues/911)) diff --git a/package.json b/package.json index e40e408efb..a57d6cdedf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "1.41.3", + "version": "1.42.0", "description": "The magical disappearing UI framework", "main": "compiler/svelte.js", "files": [ @@ -18,6 +18,7 @@ "precodecov": "npm run coverage", "lint": "eslint src test/*.js", "build": "node src/shared/_build.js && rollup -c", + "prepare": "npm run build", "dev": "node src/shared/_build.js && rollup -c -w", "pretest": "npm run build", "prepublishOnly": "npm run lint && npm test", diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 6e78d0fc74..8f2dad24e4 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -12,6 +12,7 @@ import namespaces from '../utils/namespaces'; import { removeNode, removeObjectKey } from '../utils/removeNode'; import wrapModule from './shared/utils/wrapModule'; import annotateWithScopes from '../utils/annotateWithScopes'; +import getName from '../utils/getName'; import clone from '../utils/clone'; import DomBlock from './dom/Block'; import SsrBlock from './server-side-rendering/Block'; @@ -497,13 +498,13 @@ export default class Generator { if (defaultExport) { defaultExport.declaration.properties.forEach((prop: Node) => { - templateProperties[prop.key.name] = prop; + templateProperties[getName(prop.key)] = prop; }); ['helpers', 'events', 'components', 'transitions'].forEach(key => { if (templateProperties[key]) { templateProperties[key].value.properties.forEach((prop: Node) => { - this[key].add(prop.key.name); + this[key].add(getName(prop.key)); }); } }); @@ -574,7 +575,7 @@ export default class Generator { if (templateProperties.components) { templateProperties.components.value.properties.forEach((property: Node) => { - addDeclaration(property.key.name, property.value, 'components'); + addDeclaration(getName(property.key), property.value, 'components'); }); } @@ -582,7 +583,7 @@ export default class Generator { const dependencies = new Map(); templateProperties.computed.value.properties.forEach((prop: Node) => { - const key = prop.key.name; + const key = getName(prop.key); const value = prop.value; const deps = value.params.map( @@ -605,12 +606,12 @@ export default class Generator { computations.push({ key, deps }); - const prop = templateProperties.computed.value.properties.find((prop: Node) => prop.key.name === key); + const prop = templateProperties.computed.value.properties.find((prop: Node) => getName(prop.key) === key); addDeclaration(key, prop.value, 'computed'); }; templateProperties.computed.value.properties.forEach((prop: Node) => - visit(prop.key.name) + visit(getName(prop.key)) ); } @@ -620,13 +621,13 @@ export default class Generator { if (templateProperties.events && dom) { templateProperties.events.value.properties.forEach((property: Node) => { - addDeclaration(property.key.name, property.value, 'events'); + addDeclaration(getName(property.key), property.value, 'events'); }); } if (templateProperties.helpers) { templateProperties.helpers.value.properties.forEach((property: Node) => { - addDeclaration(property.key.name, property.value, 'helpers'); + addDeclaration(getName(property.key), property.value, 'helpers'); }); } @@ -663,7 +664,7 @@ export default class Generator { if (templateProperties.transitions) { templateProperties.transitions.value.properties.forEach((property: Node) => { - addDeclaration(property.key.name, property.value, 'transitions'); + addDeclaration(getName(property.key), property.value, 'transitions'); }); } } diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts index a93b2b4048..33a305146f 100644 --- a/src/generators/dom/preprocess.ts +++ b/src/generators/dom/preprocess.ts @@ -309,7 +309,7 @@ const preprocessors = { stripWhitespace: boolean, nextSibling: Node ) => { - if (node.name === 'slot') { + if (node.name === 'slot' || node.name === 'option') { cannotUseInnerHTML(node); } @@ -358,6 +358,19 @@ const preprocessors = { } }); + const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value'); + + // Treat these the same way: + // {{foo}} + // {{foo}} + if (node.name === 'option' && !valueAttribute) { + node.attributes.push({ + type: 'Attribute', + name: 'value', + value: node.children + }); + } + // special case — in a case like this... // // @@ -369,14 +382,9 @@ const preprocessors = { // so that if `foo.qux` changes, we know that we need to // mark `bar` and `baz` as dirty too if (node.name === 'select') { - cannotUseInnerHTML(node); - - const value = node.attributes.find( - (attribute: Node) => attribute.name === 'value' - ); - if (value) { + if (valueAttribute) { // TODO does this also apply to e.g. ``? - const dependencies = block.findDependencies(value.value); + const dependencies = block.findDependencies(valueAttribute.value); state.selectBindingDependencies = dependencies; dependencies.forEach((prop: string) => { generator.indirectDependencies.set(prop, new Set()); diff --git a/src/generators/dom/visitors/Component.ts b/src/generators/dom/visitors/Component.ts index 1a06ebadec..5ac1e36ead 100644 --- a/src/generators/dom/visitors/Component.ts +++ b/src/generators/dom/visitors/Component.ts @@ -7,21 +7,10 @@ import getTailSnippet from '../../../utils/getTailSnippet'; import getObject from '../../../utils/getObject'; import getExpressionPrecedence from '../../../utils/getExpressionPrecedence'; import { stringify } from '../../../utils/stringify'; +import stringifyProps from '../../../utils/stringifyProps'; import { Node } from '../../../interfaces'; import { State } from '../interfaces'; -function stringifyProps(props: string[]) { - if (!props.length) return '{}'; - - const joined = props.join(', '); - if (joined.length > 40) { - // make larger data objects readable - return `{\n\t${props.join(',\n\t')}\n}`; - } - - return `{ ${joined} }`; -} - interface Attribute { name: string; value: any; diff --git a/src/generators/dom/visitors/Element/Attribute.ts b/src/generators/dom/visitors/Element/Attribute.ts index 837e0945db..2baf4a7d96 100644 --- a/src/generators/dom/visitors/Element/Attribute.ts +++ b/src/generators/dom/visitors/Element/Attribute.ts @@ -3,7 +3,6 @@ import deindent from '../../../../utils/deindent'; import visitStyleAttribute, { optimizeStyle } from './StyleAttribute'; import { stringify } from '../../../../utils/stringify'; import getExpressionPrecedence from '../../../../utils/getExpressionPrecedence'; -import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue'; import { DomGenerator } from '../../index'; import Block from '../../Block'; import { Node } from '../../../../interfaces'; @@ -56,6 +55,11 @@ export default function visitAttribute( const isLegacyInputType = generator.legacy && name === 'type' && node.name === 'input'; + const isDataSet = /^data-/.test(name) && !generator.legacy; + const camelCaseName = isDataSet ? name.replace('data-', '').replace(/(-\w)/g, function (m) { + return m[1].toUpperCase(); + }) : name; + if (isDynamic) { let value; @@ -163,6 +167,11 @@ export default function visitAttribute( `${state.parentNode}.${propertyName} = ${init};` ); updater = `${state.parentNode}.${propertyName} = ${shouldCache || isSelectValueAttribute ? last : value};`; + } else if (isDataSet) { + block.builders.hydrate.addLine( + `${state.parentNode}.dataset.${camelCaseName} = ${init};` + ); + updater = `${state.parentNode}.dataset.${camelCaseName} = ${shouldCache || isSelectValueAttribute ? last : value};`; } else { block.builders.hydrate.addLine( `${method}(${state.parentNode}, "${name}", ${init});` @@ -198,6 +207,7 @@ export default function visitAttribute( const statement = ( isLegacyInputType ? `@setInputType(${state.parentNode}, ${value});` : propertyName ? `${state.parentNode}.${propertyName} = ${value};` : + isDataSet ? `${state.parentNode}.dataset.${camelCaseName} = ${value};` : `${method}(${state.parentNode}, "${name}", ${value});` ); @@ -221,4 +231,4 @@ export default function visitAttribute( block.builders.hydrate.addLine(updateValue); if (isDynamic) block.builders.update.addLine(updateValue); } -} \ No newline at end of file +} diff --git a/src/generators/dom/visitors/Element/Binding.ts b/src/generators/dom/visitors/Element/Binding.ts deleted file mode 100644 index 6be11e86a5..0000000000 --- a/src/generators/dom/visitors/Element/Binding.ts +++ /dev/null @@ -1,349 +0,0 @@ -import deindent from '../../../../utils/deindent'; -import flattenReference from '../../../../utils/flattenReference'; -import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue'; -import { DomGenerator } from '../../index'; -import Block from '../../Block'; -import { Node } from '../../../../interfaces'; -import { State } from '../../interfaces'; -import getObject from '../../../../utils/getObject'; -import getTailSnippet from '../../../../utils/getTailSnippet'; - -const readOnlyMediaAttributes = new Set([ - 'duration', - 'buffered', - 'seekable', - 'played' -]); - -export default function visitBinding( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - attribute: Node -) { - const { name } = getObject(attribute.value); - const { snippet, contexts, dependencies } = block.contextualise( - attribute.value - ); - - contexts.forEach(context => { - if (!~state.allUsedContexts.indexOf(context)) - state.allUsedContexts.push(context); - }); - - const eventNames = getBindingEventName(node, attribute); - const handler = block.getUniqueName( - `${state.parentNode}_${eventNames.join('_')}_handler` - ); - const isMultipleSelect = - node.name === 'select' && - node.attributes.find( - (attr: Node) => attr.name.toLowerCase() === 'multiple' - ); // TODO use getStaticAttributeValue - const type = getStaticAttributeValue(node, 'type'); - const bindingGroup = attribute.name === 'group' - ? getBindingGroup(generator, attribute.value) - : null; - - const isMediaElement = node.name === 'audio' || node.name === 'video'; - const isReadOnly = isMediaElement && readOnlyMediaAttributes.has(attribute.name) - - const value = getBindingValue( - generator, - block, - state, - node, - attribute, - isMultipleSelect, - isMediaElement, - bindingGroup, - type - ); - - let setter = getSetter(generator, block, name, snippet, state.parentNode, attribute, dependencies, value); - let updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`; - - const needsLock = !isReadOnly && node.name !== 'input' || !/radio|checkbox|range|color/.test(type); // TODO others? - const lock = `#${state.parentNode}_updating`; - let updateConditions = needsLock ? [`!${lock}`] : []; - - if (needsLock) block.addVariable(lock, 'false'); - - // special case - if (node.name === 'select') { - if (!isMultipleSelect) { - setter = `var selectedOption = ${state.parentNode}.querySelector(':checked') || ${state.parentNode}.options[0];\n${setter}`; - } - - const value = block.getUniqueName('value'); - const option = block.getUniqueName('option'); - - const ifStatement = isMultipleSelect - ? deindent` - ${option}.selected = ~${value}.indexOf(${option}.__value);` - : deindent` - if (${option}.__value === ${value}) { - ${option}.selected = true; - break; - }`; - - const { name } = getObject(attribute.value); - const tailSnippet = getTailSnippet(attribute.value); - - updateElement = deindent` - var ${value} = ${snippet}; - for (var #i = 0; #i < ${state.parentNode}.options.length; #i += 1) { - var ${option} = ${state.parentNode}.options[#i]; - - ${ifStatement} - } - `; - - generator.hasComplexBindings = true; - block.builders.hydrate.addBlock( - `if (!('${name}' in state)) #component._root._beforecreate.push(${handler});` - ); - } else if (attribute.name === 'group') { - // special case - if (type === 'radio') { - setter = deindent` - if (!${state.parentNode}.checked) return; - ${setter} - `; - } - - const condition = type === 'checkbox' - ? `~${snippet}.indexOf(${state.parentNode}.__value)` - : `${state.parentNode}.__value === ${snippet}`; - - block.builders.hydrate.addLine( - `#component._bindingGroups[${bindingGroup}].push(${state.parentNode});` - ); - - block.builders.destroy.addBlock( - `#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${state.parentNode}), 1);` - ); - - updateElement = `${state.parentNode}.checked = ${condition};`; - } else if (isMediaElement) { - generator.hasComplexBindings = true; - block.builders.hydrate.addBlock(`#component._root._beforecreate.push(${handler});`); - - if (attribute.name === 'currentTime') { - const frame = block.getUniqueName(`${state.parentNode}_animationframe`); - block.addVariable(frame); - setter = deindent` - cancelAnimationFrame(${frame}); - if (!${state.parentNode}.paused) ${frame} = requestAnimationFrame(${handler}); - ${setter} - `; - - updateConditions.push(`!isNaN(${snippet})`); - } else if (attribute.name === 'paused') { - // this is necessary to prevent the audio restarting by itself - const last = block.getUniqueName(`${state.parentNode}_paused_value`); - block.addVariable(last, 'true'); - - updateConditions = [`${last} !== (${last} = ${snippet})`]; - updateElement = `${state.parentNode}[${last} ? "pause" : "play"]();`; - } - } - - block.builders.init.addBlock(deindent` - function ${handler}() { - ${needsLock && `${lock} = true;`} - ${setter} - ${needsLock && `${lock} = false;`} - } - `); - - if (node.name === 'input' && type === 'range') { - // need to bind to `input` and `change`, for the benefit of IE - block.builders.hydrate.addBlock(deindent` - @addListener(${state.parentNode}, "input", ${handler}); - @addListener(${state.parentNode}, "change", ${handler}); - `); - - block.builders.destroy.addBlock(deindent` - @removeListener(${state.parentNode}, "input", ${handler}); - @removeListener(${state.parentNode}, "change", ${handler}); - `); - } else { - eventNames.forEach(eventName => { - block.builders.hydrate.addLine( - `@addListener(${state.parentNode}, "${eventName}", ${handler});` - ); - - block.builders.destroy.addLine( - `@removeListener(${state.parentNode}, "${eventName}", ${handler});` - ); - }); - } - - if (!isMediaElement) { - node.initialUpdate = updateElement; - node.initialUpdateNeedsStateObject = !block.contexts.has(name); - } - - if (!isReadOnly) { // audio/video duration is read-only, it never updates - if (updateConditions.length) { - block.builders.update.addBlock(deindent` - if (${updateConditions.join(' && ')}) { - ${updateElement} - } - `); - } else { - block.builders.update.addBlock(deindent` - ${updateElement} - `); - } - } - - if (attribute.name === 'paused') { - block.builders.create.addLine( - `@addListener(${state.parentNode}, "play", ${handler});` - ); - block.builders.destroy.addLine( - `@removeListener(${state.parentNode}, "play", ${handler});` - ); - } -} - -function getBindingEventName(node: Node, attribute: Node) { - if (node.name === 'input') { - const typeAttribute = node.attributes.find( - (attr: Node) => attr.type === 'Attribute' && attr.name === 'type' - ); - const type = typeAttribute ? typeAttribute.value[0].data : 'text'; // TODO in validation, should throw if type attribute is not static - - return [type === 'checkbox' || type === 'radio' ? 'change' : 'input']; - } - - if (node.name === 'textarea') return ['input']; - if (attribute.name === 'currentTime') return ['timeupdate']; - if (attribute.name === 'duration') return ['durationchange']; - if (attribute.name === 'paused') return ['pause']; - if (attribute.name === 'buffered') return ['progress', 'loadedmetadata']; - if (attribute.name === 'seekable') return ['loadedmetadata']; - if (attribute.name === 'played') return ['timeupdate']; - - return ['change']; -} - -function getBindingValue( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - attribute: Node, - isMultipleSelect: boolean, - isMediaElement: boolean, - bindingGroup: number, - type: string -) { - // - if (attribute.name === 'group') { - if (type === 'checkbox') { - return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`; - } - - return `${state.parentNode}.__value`; - } - - // - if (type === 'range' || type === 'number') { - return `@toNumber(${state.parentNode}.${attribute.name})`; - } - - if (isMediaElement && (attribute.name === 'buffered' || attribute.name === 'seekable' || attribute.name === 'played')) { - return `@timeRangesToArray(${state.parentNode}.${attribute.name})` - } - - // everything else - return `${state.parentNode}.${attribute.name}`; -} - -function getBindingGroup(generator: DomGenerator, value: Node) { - const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions - const keypath = parts.join('.'); - - // TODO handle contextual bindings — `keypath` should include unique ID of - // each block that provides context - let index = generator.bindingGroups.indexOf(keypath); - if (index === -1) { - index = generator.bindingGroups.length; - generator.bindingGroups.push(keypath); - } - - return index; -} - -function getSetter( - generator: DomGenerator, - block: Block, - name: string, - snippet: string, - _this: string, - attribute: Node, - dependencies: string[], - value: string, -) { - const tail = attribute.value.type === 'MemberExpression' - ? getTailSnippet(attribute.value) - : ''; - - if (block.contexts.has(name)) { - const prop = dependencies[0]; - const computed = isComputed(attribute.value); - - return deindent` - var list = ${_this}._svelte.${block.listNames.get(name)}; - var index = ${_this}._svelte.${block.indexNames.get(name)}; - ${computed && `var state = #component.get();`} - list[index]${tail} = ${value}; - - ${computed - ? `#component.set({${dependencies.map((prop: string) => `${prop}: state.${prop}`).join(', ')} });` - : `#component.set({${dependencies.map((prop: string) => `${prop}: #component.get('${prop}')`).join(', ')} });`} - `; - } - - if (attribute.value.type === 'MemberExpression') { - // This is a little confusing, and should probably be tidied up - // at some point. It addresses a tricky bug (#893), wherein - // Svelte tries to `set()` a computed property, which throws an - // error in dev mode. a) it's possible that we should be - // replacing computations with *their* dependencies, and b) - // we should probably populate `generator.readonly` sooner so - // that we don't have to do the `.some()` here - dependencies = dependencies.filter(prop => !generator.computations.some(computation => computation.key === prop)); - - return deindent` - var state = #component.get(); - ${snippet} = ${value}; - #component.set({ ${dependencies.map((prop: string) => `${prop}: state.${prop}`).join(', ')} }); - `; - } - - return `#component.set({ ${name}: ${value} });`; -} - -function isComputed(node: Node) { - while (node.type === 'MemberExpression') { - if (node.computed) return true; - node = node.object; - } - - return false; -} diff --git a/src/generators/dom/visitors/Element/Element.ts b/src/generators/dom/visitors/Element/Element.ts index 6c7a307fab..8de38c766d 100644 --- a/src/generators/dom/visitors/Element/Element.ts +++ b/src/generators/dom/visitors/Element/Element.ts @@ -4,9 +4,9 @@ import visitSlot from '../Slot'; import visitComponent from '../Component'; import visitWindow from './meta/Window'; import visitAttribute from './Attribute'; -import visitEventHandler from './EventHandler'; -import visitBinding from './Binding'; -import visitRef from './Ref'; +import addBindings from './addBindings'; +import flattenReference from '../../../../utils/flattenReference'; +import validCalleeObjects from '../../../../utils/validCalleeObjects'; import * as namespaces from '../../../../utils/namespaces'; import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue'; import isVoidElementName from '../../../../utils/isVoidElementName'; @@ -18,24 +18,10 @@ import { State } from '../../interfaces'; import reservedNames from '../../../../utils/reservedNames'; import { stringify } from '../../../../utils/stringify'; -const meta = { +const meta: Record = { ':Window': visitWindow, }; -const order = { - Attribute: 1, - Binding: 2, - EventHandler: 3, - Ref: 4, -}; - -const visitors = { - Attribute: visitAttribute, - EventHandler: visitEventHandler, - Binding: visitBinding, - Ref: visitRef, -}; - export default function visitElement( generator: DomGenerator, block: Block, @@ -69,8 +55,6 @@ export default function visitElement( `${componentStack[componentStack.length - 1].var}._slotted.${slot.value[0].data}` : // TODO this looks bonkers state.parentNode; - const isToplevel = !parentNode; - block.addVariable(name); block.builders.create.addLine( `${name} = ${getRenderStatement( @@ -93,6 +77,10 @@ export default function visitElement( ); } else { block.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`); + + // TODO we eventually need to consider what happens to elements + // that belong to the same outgroup as an outroing element... + block.builders.unmount.addLine(`@detachNode(${name});`); } // add CSS encapsulation attribute @@ -109,122 +97,191 @@ export default function visitElement( } } - function visitAttributesAndAddProps() { - let intro; - let outro; - - node.attributes - .sort((a: Node, b: Node) => order[a.type] - order[b.type]) - .forEach((attribute: Node) => { - if (attribute.type === 'Transition') { - if (attribute.intro) intro = attribute; - if (attribute.outro) outro = attribute; - return; - } - - visitors[attribute.type](generator, block, childState, node, attribute); + if (node.name === 'textarea') { + // this is an egregious hack, but it's the easiest way to get + // children treated the same way as a value attribute + if (node.children.length > 0) { + node.attributes.push({ + type: 'Attribute', + name: 'value', + value: node.children, }); - if (intro || outro) - addTransitions(generator, block, childState, node, intro, outro); + node.children = []; + } + } + + // insert static children with textContent or innerHTML + if (!childState.namespace && node.canUseInnerHTML && node.children.length > 0) { + if (node.children.length === 1 && node.children[0].type === 'Text') { + block.builders.create.addLine( + `${name}.textContent = ${stringify(node.children[0].data)};` + ); + } else { + block.builders.create.addLine( + `${name}.innerHTML = ${stringify(node.children.map(toHTML).join(''))};` + ); + } + } else { + node.children.forEach((child: Node) => { + visit(generator, block, childState, child, elementStack.concat(node), componentStack); + }); + } + + addBindings(generator, block, childState, node); - if (childState.allUsedContexts.length || childState.usesComponent) { - const initialProps: string[] = []; - const updates: string[] = []; + node.attributes.filter((a: Node) => a.type === 'Attribute').forEach((attribute: Node) => { + visitAttribute(generator, block, childState, node, attribute); + }); - if (childState.usesComponent) { - initialProps.push(`component: #component`); - } + // event handlers + node.attributes.filter((a: Node) => a.type === 'EventHandler').forEach((attribute: Node) => { + const isCustomEvent = generator.events.has(attribute.name); + const shouldHoist = !isCustomEvent && state.inEachBlock; - childState.allUsedContexts.forEach((contextName: string) => { - if (contextName === 'state') return; + const context = shouldHoist ? null : name; + const usedContexts: string[] = []; - const listName = block.listNames.get(contextName); - const indexName = block.indexNames.get(contextName); + if (attribute.expression) { + generator.addSourcemapLocations(attribute.expression); - initialProps.push( - `${listName}: ${listName},\n${indexName}: ${indexName}` - ); - updates.push( - `${name}._svelte.${listName} = ${listName};\n${name}._svelte.${indexName} = ${indexName};` + const flattened = flattenReference(attribute.expression.callee); + if (!validCalleeObjects.has(flattened.name)) { + // allow event.stopPropagation(), this.select() etc + // TODO verify that it's a valid callee (i.e. built-in or declared method) + generator.code.prependRight( + attribute.expression.start, + `${block.alias('component')}.` ); + if (shouldHoist) childState.usesComponent = true; // this feels a bit hacky but it works! + } + + attribute.expression.arguments.forEach((arg: Node) => { + const { contexts } = block.contextualise(arg, context, true); + + contexts.forEach(context => { + if (!~usedContexts.indexOf(context)) usedContexts.push(context); + if (!~childState.allUsedContexts.indexOf(context)) + childState.allUsedContexts.push(context); + }); }); + } - if (initialProps.length) { - block.builders.hydrate.addBlock(deindent` - ${name}._svelte = { - ${initialProps.join(',\n')} - }; - `); + const _this = context || 'this'; + const declarations = usedContexts.map(name => { + if (name === 'state') { + if (shouldHoist) childState.usesComponent = true; + return `var state = ${block.alias('component')}.get();`; } - if (updates.length) { - block.builders.update.addBlock(updates.join('\n')); - } - } - } + const listName = block.listNames.get(name); + const indexName = block.indexNames.get(name); + const contextName = block.contexts.get(name); - if (isToplevel) { - // TODO we eventually need to consider what happens to elements - // that belong to the same outgroup as an outroing element... - block.builders.unmount.addLine(`@detachNode(${name});`); - } + return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`; + }); - if (node.name !== 'select') { - if (node.name === 'textarea') { - // this is an egregious hack, but it's the easiest way to get - // children treated the same way as a value attribute - if (node.children.length > 0) { - node.attributes.push({ - type: 'Attribute', - name: 'value', - value: node.children, + // get a name for the event handler that is globally unique + // if hoisted, locally unique otherwise + const handlerName = (shouldHoist ? generator : block).getUniqueName( + `${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler` + ); + + // create the handler body + const handlerBody = deindent` + ${childState.usesComponent && + `var ${block.alias('component')} = ${_this}._svelte.component;`} + ${declarations} + ${attribute.expression ? + `[✂${attribute.expression.start}-${attribute.expression.end}✂];` : + `${block.alias('component')}.fire("${attribute.name}", event);`} + `; + + if (isCustomEvent) { + block.addVariable(handlerName); + + block.builders.hydrate.addBlock(deindent` + ${handlerName} = %events-${attribute.name}.call(#component, ${name}, function(event) { + ${handlerBody} }); + `); - node.children = []; + block.builders.destroy.addLine(deindent` + ${handlerName}.teardown(); + `); + } else { + const handler = deindent` + function ${handlerName}(event) { + ${handlerBody} + } + `; + + if (shouldHoist) { + generator.blocks.push(handler); + } else { + block.builders.init.addBlock(handler); } + + block.builders.hydrate.addLine( + `@addListener(${name}, "${attribute.name}", ${handlerName});` + ); + + block.builders.destroy.addLine( + `@removeListener(${name}, "${attribute.name}", ${handlerName});` + ); } + }); - // value attributes are an annoying special case — it must be handled - // *after* its children have been updated - visitAttributesAndAddProps(); - } + // refs + node.attributes.filter((a: Node) => a.type === 'Ref').forEach((attribute: Node) => { + const ref = `#component.refs.${attribute.name}`; - // special case – bound without a value attribute - if ( - node.name === 'option' && - !node.attributes.find( - (attribute: Node) => - attribute.type === 'Attribute' && attribute.name === 'value' - ) - ) { - // TODO check it's bound - const statement = `${name}.__value = ${name}.textContent;`; - node.initialUpdate = node.lateUpdate = statement; - } + block.builders.mount.addLine( + `${ref} = ${name};` + ); - if (!childState.namespace && node.canUseInnerHTML && node.children.length > 0) { - if (node.children.length === 1 && node.children[0].type === 'Text') { - block.builders.create.addLine( - `${name}.textContent = ${stringify(node.children[0].data)};` + block.builders.destroy.addLine( + `if (${ref} === ${name}) ${ref} = null;` + ); + + generator.usesRefs = true; // so component.refs object is created + }); + + addTransitions(generator, block, childState, node); + + if (childState.allUsedContexts.length || childState.usesComponent) { + const initialProps: string[] = []; + const updates: string[] = []; + + if (childState.usesComponent) { + initialProps.push(`component: #component`); + } + + childState.allUsedContexts.forEach((contextName: string) => { + if (contextName === 'state') return; + + const listName = block.listNames.get(contextName); + const indexName = block.indexNames.get(contextName); + + initialProps.push( + `${listName}: ${listName},\n${indexName}: ${indexName}` ); - } else { - block.builders.create.addLine( - `${name}.innerHTML = ${stringify(node.children.map(toHTML).join(''))};` + updates.push( + `${name}._svelte.${listName} = ${listName};\n${name}._svelte.${indexName} = ${indexName};` ); - } - } else { - node.children.forEach((child: Node) => { - visit(generator, block, childState, child, elementStack.concat(node), componentStack); }); - } - if (node.lateUpdate) { - block.builders.update.addLine(node.lateUpdate); - } + if (initialProps.length) { + block.builders.hydrate.addBlock(deindent` + ${name}._svelte = { + ${initialProps.join(',\n')} + }; + `); + } - if (node.name === 'select') { - visitAttributesAndAddProps(); + if (updates.length) { + block.builders.update.addBlock(updates.join('\n')); + } } if (node.initialUpdate) { diff --git a/src/generators/dom/visitors/Element/EventHandler.ts b/src/generators/dom/visitors/Element/EventHandler.ts deleted file mode 100644 index 0a887f63f7..0000000000 --- a/src/generators/dom/visitors/Element/EventHandler.ts +++ /dev/null @@ -1,111 +0,0 @@ -import deindent from '../../../../utils/deindent'; -import flattenReference from '../../../../utils/flattenReference'; -import validCalleeObjects from '../../../../utils/validCalleeObjects'; -import { DomGenerator } from '../../index'; -import Block from '../../Block'; -import { Node } from '../../../../interfaces'; -import { State } from '../../interfaces'; - -export default function visitEventHandler( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - attribute: Node -) { - const name = attribute.name; - const isCustomEvent = generator.events.has(name); - const shouldHoist = !isCustomEvent && state.inEachBlock; - - const context = shouldHoist ? null : state.parentNode; - const usedContexts: string[] = []; - - if (attribute.expression) { - generator.addSourcemapLocations(attribute.expression); - - const flattened = flattenReference(attribute.expression.callee); - if (!validCalleeObjects.has(flattened.name)) { - // allow event.stopPropagation(), this.select() etc - // TODO verify that it's a valid callee (i.e. built-in or declared method) - generator.code.prependRight( - attribute.expression.start, - `${block.alias('component')}.` - ); - if (shouldHoist) state.usesComponent = true; // this feels a bit hacky but it works! - } - - attribute.expression.arguments.forEach((arg: Node) => { - const { contexts } = block.contextualise(arg, context, true); - - contexts.forEach(context => { - if (!~usedContexts.indexOf(context)) usedContexts.push(context); - if (!~state.allUsedContexts.indexOf(context)) - state.allUsedContexts.push(context); - }); - }); - } - - const _this = context || 'this'; - const declarations = usedContexts.map(name => { - if (name === 'state') { - if (shouldHoist) state.usesComponent = true; - return `var state = ${block.alias('component')}.get();`; - } - - const listName = block.listNames.get(name); - const indexName = block.indexNames.get(name); - const contextName = block.contexts.get(name); - - return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`; - }); - - // get a name for the event handler that is globally unique - // if hoisted, locally unique otherwise - const handlerName = (shouldHoist ? generator : block).getUniqueName( - `${name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler` - ); - - // create the handler body - const handlerBody = deindent` - ${state.usesComponent && - `var ${block.alias('component')} = ${_this}._svelte.component;`} - ${declarations} - ${attribute.expression ? - `[✂${attribute.expression.start}-${attribute.expression.end}✂];` : - `${block.alias('component')}.fire("${attribute.name}", event);`} - `; - - if (isCustomEvent) { - block.addVariable(handlerName); - - block.builders.hydrate.addBlock(deindent` - ${handlerName} = %events-${name}.call(#component, ${state.parentNode}, function(event) { - ${handlerBody} - }); - `); - - block.builders.destroy.addLine(deindent` - ${handlerName}.teardown(); - `); - } else { - const handler = deindent` - function ${handlerName}(event) { - ${handlerBody} - } - `; - - if (shouldHoist) { - generator.blocks.push(handler); - } else { - block.builders.init.addBlock(handler); - } - - block.builders.hydrate.addLine( - `@addListener(${state.parentNode}, "${name}", ${handlerName});` - ); - - block.builders.destroy.addLine( - `@removeListener(${state.parentNode}, "${name}", ${handlerName});` - ); - } -} diff --git a/src/generators/dom/visitors/Element/Ref.ts b/src/generators/dom/visitors/Element/Ref.ts deleted file mode 100644 index 8e19c2ae3b..0000000000 --- a/src/generators/dom/visitors/Element/Ref.ts +++ /dev/null @@ -1,25 +0,0 @@ -import deindent from '../../../../utils/deindent'; -import { DomGenerator } from '../../index'; -import Block from '../../Block'; -import { Node } from '../../../../interfaces'; -import { State } from '../../interfaces'; - -export default function visitRef( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - attribute: Node -) { - const name = attribute.name; - - block.builders.mount.addLine( - `#component.refs.${name} = ${state.parentNode};` - ); - - block.builders.destroy.addLine(deindent` - if (#component.refs.${name} === ${state.parentNode}) #component.refs.${name} = null; - `); - - generator.usesRefs = true; // so this component.refs object is created -} diff --git a/src/generators/dom/visitors/Element/addBindings.ts b/src/generators/dom/visitors/Element/addBindings.ts new file mode 100644 index 0000000000..bb00e916bc --- /dev/null +++ b/src/generators/dom/visitors/Element/addBindings.ts @@ -0,0 +1,382 @@ +import deindent from '../../../../utils/deindent'; +import flattenReference from '../../../../utils/flattenReference'; +import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue'; +import { DomGenerator } from '../../index'; +import Block from '../../Block'; +import { Node } from '../../../../interfaces'; +import { State } from '../../interfaces'; +import getObject from '../../../../utils/getObject'; +import getTailSnippet from '../../../../utils/getTailSnippet'; +import stringifyProps from '../../../../utils/stringifyProps'; +import { generateRule } from '../../../../shared/index'; +import flatten from '../../../../utils/flattenReference'; + +interface Binding { + name: string; +} + +const readOnlyMediaAttributes = new Set([ + 'duration', + 'buffered', + 'seekable', + 'played' +]); + +function isMediaNode(name: string) { + return name === 'audio' || name === 'video'; +} + +const events = [ + { + eventNames: ['input'], + filter: (node: Node, binding: Binding) => + node.name === 'textarea' || + node.name === 'input' && !/radio|checkbox/.test(getStaticAttributeValue(node, 'type')) + }, + { + eventNames: ['change'], + filter: (node: Node, binding: Binding) => + node.name === 'select' || + node.name === 'input' && /radio|checkbox|range/.test(getStaticAttributeValue(node, 'type')) + }, + + // media events + { + eventNames: ['timeupdate'], + filter: (node: Node, binding: Binding) => + isMediaNode(node.name) && + (binding.name === 'currentTime' || binding.name === 'played') + }, + { + eventNames: ['durationchange'], + filter: (node: Node, binding: Binding) => + isMediaNode(node.name) && + binding.name === 'duration' + }, + { + eventNames: ['play', 'pause'], + filter: (node: Node, binding: Binding) => + isMediaNode(node.name) && + binding.name === 'paused' + }, + { + eventNames: ['progress'], + filter: (node: Node, binding: Binding) => + isMediaNode(node.name) && + binding.name === 'buffered' + }, + { + eventNames: ['loadedmetadata'], + filter: (node: Node, binding: Binding) => + isMediaNode(node.name) && + (binding.name === 'buffered' || binding.name === 'seekable') + } +]; + +export default function addBindings( + generator: DomGenerator, + block: Block, + state: State, + node: Node +) { + const bindings: Node[] = node.attributes.filter((a: Node) => a.type === 'Binding'); + if (bindings.length === 0) return; + + if (node.name === 'select' || isMediaNode(node.name)) generator.hasComplexBindings = true; + + const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(getStaticAttributeValue(node, 'type')); + + const mungedBindings = bindings.map(binding => { + const isReadOnly = isMediaNode(node.name) && readOnlyMediaAttributes.has(binding.name); + + let updateCondition: string; + + const { name } = getObject(binding.value); + const { snippet, contexts, dependencies } = block.contextualise( + binding.value + ); + + contexts.forEach(context => { + if (!~state.allUsedContexts.indexOf(context)) + state.allUsedContexts.push(context); + }); + + // view to model + const valueFromDom = getValueFromDom(generator, node, binding); + const handler = getEventHandler(generator, block, name, snippet, binding, dependencies, valueFromDom); + + // model to view + let updateDom = getDomUpdater(node, binding, snippet); + let initialUpdate = updateDom; + + // special cases + if (binding.name === 'group') { + const bindingGroup = getBindingGroup(generator, binding.value); + + block.builders.hydrate.addLine( + `#component._bindingGroups[${bindingGroup}].push(${node.var});` + ); + + block.builders.destroy.addLine( + `#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${node.var}), 1);` + ); + } + + if (binding.name === 'currentTime') { + updateCondition = `!isNaN(${snippet})`; + initialUpdate = null; + } + + if (binding.name === 'paused') { + // this is necessary to prevent audio restarting by itself + const last = block.getUniqueName(`${node.var}_is_paused`); + block.addVariable(last, 'true'); + + updateCondition = `${last} !== (${last} = ${snippet})`; + updateDom = `${node.var}[${last} ? "pause" : "play"]();`; + initialUpdate = null; + } + + return { + name: binding.name, + object: name, + handler, + updateDom, + initialUpdate, + needsLock: !isReadOnly && needsLock, + updateCondition + }; + }); + + const lock = mungedBindings.some(binding => binding.needsLock) ? + block.getUniqueName(`${node.var}_updating`) : + null; + + if (lock) block.addVariable(lock, 'false'); + + const groups = events + .map(event => { + return { + events: event.eventNames, + bindings: mungedBindings.filter(binding => event.filter(node, binding)) + }; + }) + .filter(group => group.bindings.length); + + groups.forEach(group => { + const handler = block.getUniqueName(`${node.var}_${group.events.join('_')}_handler`); + + const needsLock = group.bindings.some(binding => binding.needsLock); + + group.bindings.forEach(binding => { + if (!binding.updateDom) return; + + const updateConditions = needsLock ? [`!${lock}`] : []; + if (binding.updateCondition) updateConditions.push(binding.updateCondition); + + block.builders.update.addLine( + updateConditions.length ? `if (${updateConditions.join(' && ')}) ${binding.updateDom}` : binding.updateDom + ); + }); + + const usesContext = group.bindings.some(binding => binding.handler.usesContext); + const usesState = group.bindings.some(binding => binding.handler.usesState); + const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n'); + + const props = new Set(); + group.bindings.forEach(binding => { + binding.handler.props.forEach(prop => { + props.add(prop); + }); + }); // TODO use stringifyProps here, once indenting is fixed + + // media bindings — awkward special case. The native timeupdate events + // fire too infrequently, so we need to take matters into our + // own hands + let animation_frame; + if (group.events[0] === 'timeupdate') { + animation_frame = block.getUniqueName(`${node.var}_animationframe`); + block.addVariable(animation_frame); + } + + block.builders.init.addBlock(deindent` + function ${handler}() { + ${ + animation_frame && deindent` + cancelAnimationFrame(${animation_frame}); + if (!${node.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});` + } + ${usesContext && `var context = ${node.var}._svelte;`} + ${usesState && `var state = #component.get();`} + ${needsLock && `${lock} = true;`} + ${mutations.length > 0 && mutations} + #component.set({ ${Array.from(props).join(', ')} }); + ${needsLock && `${lock} = false;`} + } + `); + + group.events.forEach(name => { + block.builders.hydrate.addLine( + `@addListener(${node.var}, "${name}", ${handler});` + ); + + block.builders.destroy.addLine( + `@removeListener(${node.var}, "${name}", ${handler});` + ); + }); + + const allInitialStateIsDefined = group.bindings + .map(binding => `'${binding.object}' in state`) + .join(' && '); + + if (node.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || readOnlyMediaAttributes.has(binding.name))) { + generator.hasComplexBindings = true; + + block.builders.hydrate.addLine( + `if (!(${allInitialStateIsDefined})) #component._root._beforecreate.push(${handler});` + ); + } + }); + + node.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n'); +} + +function getDomUpdater( + node: Node, + binding: Node, + snippet: string +) { + if (readOnlyMediaAttributes.has(binding.name)) { + return null; + } + + if (node.name === 'select') { + return getStaticAttributeValue(node, 'multiple') === true ? + `@selectOptions(${node.var}, ${snippet})` : + `@selectOption(${node.var}, ${snippet})`; + } + + if (binding.name === 'group') { + const type = getStaticAttributeValue(node, 'type'); + + const condition = type === 'checkbox' + ? `~${snippet}.indexOf(${node.var}.__value)` + : `${node.var}.__value === ${snippet}`; + + return `${node.var}.checked = ${condition};` + } + + return `${node.var}.${binding.name} = ${snippet};`; +} + +function getBindingGroup(generator: DomGenerator, value: Node) { + const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions + const keypath = parts.join('.'); + + // TODO handle contextual bindings — `keypath` should include unique ID of + // each block that provides context + let index = generator.bindingGroups.indexOf(keypath); + if (index === -1) { + index = generator.bindingGroups.length; + generator.bindingGroups.push(keypath); + } + + return index; +} + +function getEventHandler( + generator: DomGenerator, + block: Block, + name: string, + snippet: string, + attribute: Node, + dependencies: string[], + value: string, +) { + if (block.contexts.has(name)) { + const tail = attribute.value.type === 'MemberExpression' + ? getTailSnippet(attribute.value) + : ''; + + const list = `context.${block.listNames.get(name)}`; + const index = `context.${block.indexNames.get(name)}`; + + return { + usesContext: true, + usesState: true, + mutation: `${list}[${index}]${tail} = ${value};`, + props: dependencies.map(prop => `${prop}: state.${prop}`) + }; + } + + if (attribute.value.type === 'MemberExpression') { + // This is a little confusing, and should probably be tidied up + // at some point. It addresses a tricky bug (#893), wherein + // Svelte tries to `set()` a computed property, which throws an + // error in dev mode. a) it's possible that we should be + // replacing computations with *their* dependencies, and b) + // we should probably populate `generator.readonly` sooner so + // that we don't have to do the `.some()` here + dependencies = dependencies.filter(prop => !generator.computations.some(computation => computation.key === prop)); + + return { + usesContext: false, + usesState: true, + mutation: `${snippet} = ${value}`, + props: dependencies.map((prop: string) => `${prop}: state.${prop}`) + }; + } + + return { + usesContext: false, + usesState: false, + mutation: null, + props: [`${name}: ${value}`] + }; +} + +function getValueFromDom( + generator: DomGenerator, + node: Node, + binding: Node +) { + // + if (binding.name === 'group') { + const bindingGroup = getBindingGroup(generator, binding.value); + if (type === 'checkbox') { + return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`; + } + + return `${node.var}.__value`; + } + + // + if (type === 'range' || type === 'number') { + return `@toNumber(${node.var}.${binding.name})`; + } + + if ((binding.name === 'buffered' || binding.name === 'seekable' || binding.name === 'played')) { + return `@timeRangesToArray(${node.var}.${binding.name})` + } + + // everything else + return `${node.var}.${binding.name}`; +} + +function isComputed(node: Node) { + while (node.type === 'MemberExpression') { + if (node.computed) return true; + node = node.object; + } + + return false; +} diff --git a/src/generators/dom/visitors/Element/addTransitions.ts b/src/generators/dom/visitors/Element/addTransitions.ts index b10f404de2..08cc196f7f 100644 --- a/src/generators/dom/visitors/Element/addTransitions.ts +++ b/src/generators/dom/visitors/Element/addTransitions.ts @@ -8,10 +8,13 @@ export default function addTransitions( generator: DomGenerator, block: Block, state: State, - node: Node, - intro, - outro + node: Node ) { + const intro = node.attributes.find((a: Node) => a.type === 'Transition' && a.intro); + const outro = node.attributes.find((a: Node) => a.type === 'Transition' && a.outro); + + if (!intro && !outro) return; + if (intro === outro) { const name = block.getUniqueName(`${node.var}_transition`); const snippet = intro.expression diff --git a/src/generators/dom/visitors/Element/lookup.ts b/src/generators/dom/visitors/Element/lookup.ts index a5096ba19c..83e2815ce6 100644 --- a/src/generators/dom/visitors/Element/lookup.ts +++ b/src/generators/dom/visitors/Element/lookup.ts @@ -112,6 +112,7 @@ const lookup = { 'http-equiv': { propertyName: 'httpEquiv', appliesTo: ['meta'] }, icon: { appliesTo: ['command'] }, id: {}, + indeterminate: { appliesTo: ['input'] }, ismap: { propertyName: 'isMap', appliesTo: ['img'] }, itemprop: {}, keytype: { appliesTo: ['keygen'] }, diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index 5e6bd25d8d..b4590df95d 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -5,6 +5,7 @@ import Block from './Block'; import preprocess from './preprocess'; import visit from './visit'; import { removeNode, removeObjectKey } from '../../utils/removeNode'; +import getName from '../../utils/getName'; import { Parsed, Node, CompileOptions } from '../../interfaces'; import { AppendTarget } from './interfaces'; import { stringify } from '../../utils/stringify'; @@ -132,7 +133,7 @@ export default function ssr( } ${templateProperties.components.value.properties.map((prop: Node) => { - return `addComponent(%components-${prop.key.name});`; + return `addComponent(%components-${getName(prop.key)});`; })} `} diff --git a/src/generators/server-side-rendering/preprocess.ts b/src/generators/server-side-rendering/preprocess.ts index a8a790f754..afddae077c 100644 --- a/src/generators/server-side-rendering/preprocess.ts +++ b/src/generators/server-side-rendering/preprocess.ts @@ -69,6 +69,19 @@ const preprocessors = { if (slot && isChildOfComponent(node, generator)) { node.slotted = true; } + + // Treat these the same way: + // {{foo}} + // {{foo}} + const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value'); + + if (node.name === 'option' && !valueAttribute) { + node.attributes.push({ + type: 'Attribute', + name: 'value', + value: node.children + }); + } } if (node.children.length) { diff --git a/src/shared/dom.js b/src/shared/dom.js index 29cf58e12c..9003faee3e 100644 --- a/src/shared/dom.js +++ b/src/shared/dom.js @@ -148,4 +148,33 @@ export function setInputType(input, type) { export function setStyle(node, key, value) { node.style.setProperty(key, value); +} + +export function selectOption(select, value) { + for (var i = 0; i < select.options.length; i += 1) { + var option = select.options[i]; + + if (option.__value === value) { + option.selected = true; + return; + } + } +} + +export function selectOptions(select, value) { + for (var i = 0; i < select.options.length; i += 1) { + var option = select.options[i]; + option.selected = ~value.indexOf(option.__value); + } +} + +export function selectValue(select) { + var selectedOption = select.querySelector(':checked') || select.options[0]; + return selectedOption && selectedOption.__value; +} + +export function selectMultipleValue(select) { + return [].map.call(select.querySelectorAll(':checked'), function(option) { + return option.__value; + }); } \ No newline at end of file diff --git a/src/shared/index.js b/src/shared/index.js index d55e55c22b..f9d0f91998 100644 --- a/src/shared/index.js +++ b/src/shared/index.js @@ -156,9 +156,12 @@ export function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } export function _setDev(newState) { diff --git a/src/utils/getName.ts b/src/utils/getName.ts new file mode 100644 index 0000000000..d236bbaec8 --- /dev/null +++ b/src/utils/getName.ts @@ -0,0 +1,6 @@ +import { Node } from '../interfaces'; + +export default function getMethodName(node: Node) { + if (node.type === 'Identifier') return node.name; + if (node.type === 'Literal') return String(node.value); +} \ No newline at end of file diff --git a/src/utils/getStaticAttributeValue.ts b/src/utils/getStaticAttributeValue.ts index 67bea12259..c356d84c35 100644 --- a/src/utils/getStaticAttributeValue.ts +++ b/src/utils/getStaticAttributeValue.ts @@ -7,6 +7,7 @@ export default function getStaticAttributeValue(node: Node, name: string) { if (!attribute) return null; + if (attribute.value === true) return true; if (attribute.value.length === 0) return ''; if (attribute.value.length === 1 && attribute.value[0].type === 'Text') { diff --git a/src/utils/removeNode.ts b/src/utils/removeNode.ts index 6abea2f8c9..26095b483c 100644 --- a/src/utils/removeNode.ts +++ b/src/utils/removeNode.ts @@ -1,4 +1,5 @@ import MagicString from 'magic-string'; +import getName from '../utils/getName'; import { Node } from '../interfaces'; const keys = { @@ -51,7 +52,7 @@ export function removeObjectKey(code: MagicString, node: Node, key: string) { let i = node.properties.length; while (i--) { const property = node.properties[i]; - if (property.key.type === 'Identifier' && property.key.name === key) { + if (property.key.type === 'Identifier' && getName(property.key) === key) { removeNode(code, node, property); } } diff --git a/src/utils/stringifyProps.ts b/src/utils/stringifyProps.ts new file mode 100644 index 0000000000..e9c27d687e --- /dev/null +++ b/src/utils/stringifyProps.ts @@ -0,0 +1,11 @@ +export default function stringifyProps(props: string[]) { + if (!props.length) return '{}'; + + const joined = props.join(', '); + if (joined.length > 40) { + // make larger data objects readable + return `{\n\t${props.join(',\n\t')}\n}`; + } + + return `{ ${joined} }`; +} \ No newline at end of file diff --git a/src/validate/html/validateElement.ts b/src/validate/html/validateElement.ts index 606b0399d8..461b5aa52f 100644 --- a/src/validate/html/validateElement.ts +++ b/src/validate/html/validateElement.ts @@ -83,17 +83,17 @@ export default function validateElement( } checkTypeAttribute(validator, node); - } else if (name === 'checked') { + } else if (name === 'checked' || name === 'indeterminate') { if (node.name !== 'input') { validator.error( - `'checked' is not a valid binding on <${node.name}> elements`, + `'${name}' is not a valid binding on <${node.name}> elements`, attribute.start ); } if (checkTypeAttribute(validator, node) !== 'checkbox') { validator.error( - `'checked' binding can only be used with `, + `'${name}' binding can only be used with `, attribute.start ); } diff --git a/src/validate/js/index.ts b/src/validate/js/index.ts index 01754b7d00..be120c5a4a 100644 --- a/src/validate/js/index.ts +++ b/src/validate/js/index.ts @@ -3,6 +3,7 @@ import fuzzymatch from '../utils/fuzzymatch'; import checkForDupes from './utils/checkForDupes'; import checkForComputedKeys from './utils/checkForComputedKeys'; import namespaces from '../../utils/namespaces'; +import getName from '../../utils/getName'; import { Validator } from '../'; import { Node } from '../../interfaces'; @@ -29,7 +30,7 @@ export default function validateJs(validator: Validator, js: Node) { const props = validator.properties; node.declaration.properties.forEach((prop: Node) => { - props.set(prop.key.name, prop); + props.set(getName(prop.key), prop); }); // Remove these checks in version 2 @@ -49,25 +50,26 @@ export default function validateJs(validator: Validator, js: Node) { // ensure all exported props are valid node.declaration.properties.forEach((prop: Node) => { - const propValidator = propValidators[prop.key.name]; + const name = getName(prop.key); + const propValidator = propValidators[name]; if (propValidator) { propValidator(validator, prop); } else { - const match = fuzzymatch(prop.key.name, validPropList); + const match = fuzzymatch(name, validPropList); if (match) { validator.error( - `Unexpected property '${prop.key.name}' (did you mean '${match}'?)`, + `Unexpected property '${name}' (did you mean '${match}'?)`, prop.start ); } else if (/FunctionExpression/.test(prop.value.type)) { validator.error( - `Unexpected property '${prop.key.name}' (did you mean to include it in 'methods'?)`, + `Unexpected property '${name}' (did you mean to include it in 'methods'?)`, prop.start ); } else { validator.error( - `Unexpected property '${prop.key.name}'`, + `Unexpected property '${name}'`, prop.start ); } @@ -86,7 +88,7 @@ export default function validateJs(validator: Validator, js: Node) { ['components', 'methods', 'helpers', 'transitions'].forEach(key => { if (validator.properties.has(key)) { validator.properties.get(key).value.properties.forEach((prop: Node) => { - validator[key].set(prop.key.name, prop.value); + validator[key].set(getName(prop.key), prop.value); }); } }); diff --git a/src/validate/js/propValidators/components.ts b/src/validate/js/propValidators/components.ts index a7ca5eb990..7d46470d0b 100644 --- a/src/validate/js/propValidators/components.ts +++ b/src/validate/js/propValidators/components.ts @@ -1,5 +1,6 @@ import checkForDupes from '../utils/checkForDupes'; import checkForComputedKeys from '../utils/checkForComputedKeys'; +import getName from '../../../utils/getName'; import { Validator } from '../../'; import { Node } from '../../../interfaces'; @@ -16,14 +17,16 @@ export default function components(validator: Validator, prop: Node) { checkForComputedKeys(validator, prop.value.properties); prop.value.properties.forEach((component: Node) => { - if (component.key.name === 'state') { + const name = getName(component.key); + + if (name === 'state') { validator.error( `Component constructors cannot be called 'state' due to technical limitations`, component.start ); } - if (!/^[A-Z]/.test(component.key.name)) { + if (!/^[A-Z]/.test(name)) { validator.warn(`Component names should be capitalised`, component.start); } }); diff --git a/src/validate/js/propValidators/methods.ts b/src/validate/js/propValidators/methods.ts index 7d8547ef39..66fd27eea1 100644 --- a/src/validate/js/propValidators/methods.ts +++ b/src/validate/js/propValidators/methods.ts @@ -2,6 +2,7 @@ import checkForAccessors from '../utils/checkForAccessors'; import checkForDupes from '../utils/checkForDupes'; import checkForComputedKeys from '../utils/checkForComputedKeys'; import usesThisOrArguments from '../utils/usesThisOrArguments'; +import getName from '../../../utils/getName'; import { Validator } from '../../'; import { Node } from '../../../interfaces'; @@ -21,9 +22,11 @@ export default function methods(validator: Validator, prop: Node) { checkForComputedKeys(validator, prop.value.properties); prop.value.properties.forEach((prop: Node) => { - if (builtin.has(prop.key.name)) { + const name = getName(prop.key); + + if (builtin.has(name)) { validator.error( - `Cannot overwrite built-in method '${prop.key.name}'`, + `Cannot overwrite built-in method '${name}'`, prop.start ); } diff --git a/src/validate/js/utils/checkForDupes.ts b/src/validate/js/utils/checkForDupes.ts index 8565ab5388..0473d7a265 100644 --- a/src/validate/js/utils/checkForDupes.ts +++ b/src/validate/js/utils/checkForDupes.ts @@ -1,5 +1,6 @@ import { Validator } from '../../'; import { Node } from '../../../interfaces'; +import getName from '../../../utils/getName'; export default function checkForDupes( validator: Validator, @@ -8,10 +9,12 @@ export default function checkForDupes( const seen = new Set(); properties.forEach(prop => { - if (seen.has(prop.key.name)) { - validator.error(`Duplicate property '${prop.key.name}'`, prop.start); + const name = getName(prop.key); + + if (seen.has(name)) { + validator.error(`Duplicate property '${name}'`, prop.start); } - seen.add(prop.key.name); + seen.add(name); }); } diff --git a/test/js/samples/collapses-text-around-comments/expected-bundle.js b/test/js/samples/collapses-text-around-comments/expected-bundle.js index 7fb59ff390..a5776bc195 100644 --- a/test/js/samples/collapses-text-around-comments/expected-bundle.js +++ b/test/js/samples/collapses-text-around-comments/expected-bundle.js @@ -157,9 +157,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/component-static/expected-bundle.js b/test/js/samples/component-static/expected-bundle.js index c3a2661cc5..94f69ca2ca 100644 --- a/test/js/samples/component-static/expected-bundle.js +++ b/test/js/samples/component-static/expected-bundle.js @@ -133,9 +133,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/computed-collapsed-if/expected-bundle.js b/test/js/samples/computed-collapsed-if/expected-bundle.js index 8a703ed250..c378d78026 100644 --- a/test/js/samples/computed-collapsed-if/expected-bundle.js +++ b/test/js/samples/computed-collapsed-if/expected-bundle.js @@ -133,9 +133,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/css-media-query/expected-bundle.js b/test/js/samples/css-media-query/expected-bundle.js index 6fa07bfcdb..11ffa87f21 100644 --- a/test/js/samples/css-media-query/expected-bundle.js +++ b/test/js/samples/css-media-query/expected-bundle.js @@ -153,9 +153,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js b/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js index 89287471cb..86edac7465 100644 --- a/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js +++ b/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js @@ -145,9 +145,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/do-use-dataset/expected-bundle.js b/test/js/samples/do-use-dataset/expected-bundle.js new file mode 100644 index 0000000000..42de89fd93 --- /dev/null +++ b/test/js/samples/do-use-dataset/expected-bundle.js @@ -0,0 +1,239 @@ +function noop() {} + +function assign(target) { + var k, + source, + i = 1, + len = arguments.length; + for (; i < len; i++) { + source = arguments[i]; + for (k in source) target[k] = source[k]; + } + + return target; +} + +function insertNode(node, target, anchor) { + target.insertBefore(node, anchor); +} + +function detachNode(node) { + node.parentNode.removeChild(node); +} + +function createElement(name) { + return document.createElement(name); +} + +function createText(data) { + return document.createTextNode(data); +} + +function blankObject() { + return Object.create(null); +} + +function destroy(detach) { + this.destroy = noop; + this.fire('destroy'); + this.set = this.get = noop; + + if (detach !== false) this._fragment.u(); + this._fragment.d(); + this._fragment = this._state = null; +} + +function differs(a, b) { + return a !== b || ((a && typeof a === 'object') || typeof a === 'function'); +} + +function dispatchObservers(component, group, changed, newState, oldState) { + for (var key in group) { + if (!changed[key]) continue; + + var newValue = newState[key]; + var oldValue = oldState[key]; + + var callbacks = group[key]; + if (!callbacks) continue; + + for (var i = 0; i < callbacks.length; i += 1) { + var callback = callbacks[i]; + if (callback.__calling) continue; + + callback.__calling = true; + callback.call(component, newValue, oldValue); + callback.__calling = false; + } + } +} + +function fire(eventName, data) { + var handlers = + eventName in this._handlers && this._handlers[eventName].slice(); + if (!handlers) return; + + for (var i = 0; i < handlers.length; i += 1) { + handlers[i].call(this, data); + } +} + +function get(key) { + return key ? this._state[key] : this._state; +} + +function init(component, options) { + component.options = options; + + component._observers = { pre: blankObject(), post: blankObject() }; + component._handlers = blankObject(); + component._root = options._root || component; + component._bind = options._bind; +} + +function observe(key, callback, options) { + var group = options && options.defer + ? this._observers.post + : this._observers.pre; + + (group[key] || (group[key] = [])).push(callback); + + if (!options || options.init !== false) { + callback.__calling = true; + callback.call(this, this._state[key]); + callback.__calling = false; + } + + return { + cancel: function() { + var index = group[key].indexOf(callback); + if (~index) group[key].splice(index, 1); + } + }; +} + +function on(eventName, handler) { + if (eventName === 'teardown') return this.on('destroy', handler); + + var handlers = this._handlers[eventName] || (this._handlers[eventName] = []); + handlers.push(handler); + + return { + cancel: function() { + var index = handlers.indexOf(handler); + if (~index) handlers.splice(index, 1); + } + }; +} + +function set(newState) { + this._set(assign({}, newState)); + if (this._root._lock) return; + this._root._lock = true; + callAll(this._root._beforecreate); + callAll(this._root._oncreate); + callAll(this._root._aftercreate); + this._root._lock = false; +} + +function _set(newState) { + var oldState = this._state, + changed = {}, + dirty = false; + + for (var key in newState) { + if (differs(newState[key], oldState[key])) changed[key] = dirty = true; + } + if (!dirty) return; + + this._state = assign({}, oldState, newState); + this._recompute(changed, this._state); + if (this._bind) this._bind(changed, this._state); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } +} + +function callAll(fns) { + while (fns && fns.length) fns.pop()(); +} + +function _mount(target, anchor) { + this._fragment.m(target, anchor); +} + +function _unmount() { + this._fragment.u(); +} + +var proto = { + destroy: destroy, + get: get, + fire: fire, + observe: observe, + on: on, + set: set, + teardown: destroy, + _recompute: noop, + _set: _set, + _mount: _mount, + _unmount: _unmount +}; + +/* generated by Svelte vX.Y.Z */ +function create_main_fragment(state, component) { + var div, text, div_1; + + return { + c: function create() { + div = createElement("div"); + text = createText("\n"); + div_1 = createElement("div"); + this.h(); + }, + + h: function hydrate() { + div.dataset.foo = "bar"; + div_1.dataset.foo = state.bar; + }, + + m: function mount(target, anchor) { + insertNode(div, target, anchor); + insertNode(text, target, anchor); + insertNode(div_1, target, anchor); + }, + + p: function update(changed, state) { + if (changed.bar) { + div_1.dataset.foo = state.bar; + } + }, + + u: function unmount() { + detachNode(div); + detachNode(text); + detachNode(div_1); + }, + + d: noop + }; +} + +function SvelteComponent(options) { + init(this, options); + this._state = assign({}, options.data); + + this._fragment = create_main_fragment(this._state, this); + + if (options.target) { + this._fragment.c(); + this._fragment.m(options.target, options.anchor || null); + } +} + +assign(SvelteComponent.prototype, proto); + +export default SvelteComponent; diff --git a/test/js/samples/do-use-dataset/expected.js b/test/js/samples/do-use-dataset/expected.js new file mode 100644 index 0000000000..1ae7417469 --- /dev/null +++ b/test/js/samples/do-use-dataset/expected.js @@ -0,0 +1,55 @@ +/* generated by Svelte vX.Y.Z */ +import { assign, createElement, createText, detachNode, init, insertNode, noop, proto } from "svelte/shared.js"; + +function create_main_fragment(state, component) { + var div, text, div_1; + + return { + c: function create() { + div = createElement("div"); + text = createText("\n"); + div_1 = createElement("div"); + this.h(); + }, + + h: function hydrate() { + div.dataset.foo = "bar"; + div_1.dataset.foo = state.bar; + }, + + m: function mount(target, anchor) { + insertNode(div, target, anchor); + insertNode(text, target, anchor); + insertNode(div_1, target, anchor); + }, + + p: function update(changed, state) { + if (changed.bar) { + div_1.dataset.foo = state.bar; + } + }, + + u: function unmount() { + detachNode(div); + detachNode(text); + detachNode(div_1); + }, + + d: noop + }; +} + +function SvelteComponent(options) { + init(this, options); + this._state = assign({}, options.data); + + this._fragment = create_main_fragment(this._state, this); + + if (options.target) { + this._fragment.c(); + this._fragment.m(options.target, options.anchor || null); + } +} + +assign(SvelteComponent.prototype, proto); +export default SvelteComponent; \ No newline at end of file diff --git a/test/js/samples/do-use-dataset/input.html b/test/js/samples/do-use-dataset/input.html new file mode 100644 index 0000000000..acd9d623b4 --- /dev/null +++ b/test/js/samples/do-use-dataset/input.html @@ -0,0 +1,2 @@ + + diff --git a/test/js/samples/dont-use-dataset-in-legacy/_config.js b/test/js/samples/dont-use-dataset-in-legacy/_config.js new file mode 100644 index 0000000000..67924a4ffe --- /dev/null +++ b/test/js/samples/dont-use-dataset-in-legacy/_config.js @@ -0,0 +1,5 @@ +export default { + options: { + legacy: true + } +}; diff --git a/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js b/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js new file mode 100644 index 0000000000..2d25e23a10 --- /dev/null +++ b/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js @@ -0,0 +1,243 @@ +function noop() {} + +function assign(target) { + var k, + source, + i = 1, + len = arguments.length; + for (; i < len; i++) { + source = arguments[i]; + for (k in source) target[k] = source[k]; + } + + return target; +} + +function insertNode(node, target, anchor) { + target.insertBefore(node, anchor); +} + +function detachNode(node) { + node.parentNode.removeChild(node); +} + +function createElement(name) { + return document.createElement(name); +} + +function createText(data) { + return document.createTextNode(data); +} + +function setAttribute(node, attribute, value) { + node.setAttribute(attribute, value); +} + +function blankObject() { + return Object.create(null); +} + +function destroy(detach) { + this.destroy = noop; + this.fire('destroy'); + this.set = this.get = noop; + + if (detach !== false) this._fragment.u(); + this._fragment.d(); + this._fragment = this._state = null; +} + +function differs(a, b) { + return a !== b || ((a && typeof a === 'object') || typeof a === 'function'); +} + +function dispatchObservers(component, group, changed, newState, oldState) { + for (var key in group) { + if (!changed[key]) continue; + + var newValue = newState[key]; + var oldValue = oldState[key]; + + var callbacks = group[key]; + if (!callbacks) continue; + + for (var i = 0; i < callbacks.length; i += 1) { + var callback = callbacks[i]; + if (callback.__calling) continue; + + callback.__calling = true; + callback.call(component, newValue, oldValue); + callback.__calling = false; + } + } +} + +function fire(eventName, data) { + var handlers = + eventName in this._handlers && this._handlers[eventName].slice(); + if (!handlers) return; + + for (var i = 0; i < handlers.length; i += 1) { + handlers[i].call(this, data); + } +} + +function get(key) { + return key ? this._state[key] : this._state; +} + +function init(component, options) { + component.options = options; + + component._observers = { pre: blankObject(), post: blankObject() }; + component._handlers = blankObject(); + component._root = options._root || component; + component._bind = options._bind; +} + +function observe(key, callback, options) { + var group = options && options.defer + ? this._observers.post + : this._observers.pre; + + (group[key] || (group[key] = [])).push(callback); + + if (!options || options.init !== false) { + callback.__calling = true; + callback.call(this, this._state[key]); + callback.__calling = false; + } + + return { + cancel: function() { + var index = group[key].indexOf(callback); + if (~index) group[key].splice(index, 1); + } + }; +} + +function on(eventName, handler) { + if (eventName === 'teardown') return this.on('destroy', handler); + + var handlers = this._handlers[eventName] || (this._handlers[eventName] = []); + handlers.push(handler); + + return { + cancel: function() { + var index = handlers.indexOf(handler); + if (~index) handlers.splice(index, 1); + } + }; +} + +function set(newState) { + this._set(assign({}, newState)); + if (this._root._lock) return; + this._root._lock = true; + callAll(this._root._beforecreate); + callAll(this._root._oncreate); + callAll(this._root._aftercreate); + this._root._lock = false; +} + +function _set(newState) { + var oldState = this._state, + changed = {}, + dirty = false; + + for (var key in newState) { + if (differs(newState[key], oldState[key])) changed[key] = dirty = true; + } + if (!dirty) return; + + this._state = assign({}, oldState, newState); + this._recompute(changed, this._state); + if (this._bind) this._bind(changed, this._state); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } +} + +function callAll(fns) { + while (fns && fns.length) fns.pop()(); +} + +function _mount(target, anchor) { + this._fragment.m(target, anchor); +} + +function _unmount() { + this._fragment.u(); +} + +var proto = { + destroy: destroy, + get: get, + fire: fire, + observe: observe, + on: on, + set: set, + teardown: destroy, + _recompute: noop, + _set: _set, + _mount: _mount, + _unmount: _unmount +}; + +/* generated by Svelte vX.Y.Z */ +function create_main_fragment(state, component) { + var div, text, div_1; + + return { + c: function create() { + div = createElement("div"); + text = createText("\n"); + div_1 = createElement("div"); + this.h(); + }, + + h: function hydrate() { + setAttribute(div, "data-foo", "bar"); + setAttribute(div_1, "data-foo", state.bar); + }, + + m: function mount(target, anchor) { + insertNode(div, target, anchor); + insertNode(text, target, anchor); + insertNode(div_1, target, anchor); + }, + + p: function update(changed, state) { + if (changed.bar) { + setAttribute(div_1, "data-foo", state.bar); + } + }, + + u: function unmount() { + detachNode(div); + detachNode(text); + detachNode(div_1); + }, + + d: noop + }; +} + +function SvelteComponent(options) { + init(this, options); + this._state = assign({}, options.data); + + this._fragment = create_main_fragment(this._state, this); + + if (options.target) { + this._fragment.c(); + this._fragment.m(options.target, options.anchor || null); + } +} + +assign(SvelteComponent.prototype, proto); + +export default SvelteComponent; diff --git a/test/js/samples/dont-use-dataset-in-legacy/expected.js b/test/js/samples/dont-use-dataset-in-legacy/expected.js new file mode 100644 index 0000000000..03eef26ced --- /dev/null +++ b/test/js/samples/dont-use-dataset-in-legacy/expected.js @@ -0,0 +1,55 @@ +/* generated by Svelte vX.Y.Z */ +import { assign, createElement, createText, detachNode, init, insertNode, noop, proto, setAttribute } from "svelte/shared.js"; + +function create_main_fragment(state, component) { + var div, text, div_1; + + return { + c: function create() { + div = createElement("div"); + text = createText("\n"); + div_1 = createElement("div"); + this.h(); + }, + + h: function hydrate() { + setAttribute(div, "data-foo", "bar"); + setAttribute(div_1, "data-foo", state.bar); + }, + + m: function mount(target, anchor) { + insertNode(div, target, anchor); + insertNode(text, target, anchor); + insertNode(div_1, target, anchor); + }, + + p: function update(changed, state) { + if (changed.bar) { + setAttribute(div_1, "data-foo", state.bar); + } + }, + + u: function unmount() { + detachNode(div); + detachNode(text); + detachNode(div_1); + }, + + d: noop + }; +} + +function SvelteComponent(options) { + init(this, options); + this._state = assign({}, options.data); + + this._fragment = create_main_fragment(this._state, this); + + if (options.target) { + this._fragment.c(); + this._fragment.m(options.target, options.anchor || null); + } +} + +assign(SvelteComponent.prototype, proto); +export default SvelteComponent; \ No newline at end of file diff --git a/test/js/samples/dont-use-dataset-in-legacy/input.html b/test/js/samples/dont-use-dataset-in-legacy/input.html new file mode 100644 index 0000000000..acd9d623b4 --- /dev/null +++ b/test/js/samples/dont-use-dataset-in-legacy/input.html @@ -0,0 +1,2 @@ + + diff --git a/test/js/samples/each-block-changed-check/expected-bundle.js b/test/js/samples/each-block-changed-check/expected-bundle.js index c73357a79d..c9723db616 100644 --- a/test/js/samples/each-block-changed-check/expected-bundle.js +++ b/test/js/samples/each-block-changed-check/expected-bundle.js @@ -165,9 +165,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { @@ -291,8 +294,8 @@ function create_each_block(state, comments, comment, i, component) { }, h: function hydrate() { - div.className = "comment"; span.className = "meta"; + div.className = "comment"; }, m: function mount(target, anchor) { diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js index be8a4943b5..29a57322e0 100644 --- a/test/js/samples/each-block-changed-check/expected.js +++ b/test/js/samples/each-block-changed-check/expected.js @@ -95,8 +95,8 @@ function create_each_block(state, comments, comment, i, component) { }, h: function hydrate() { - div.className = "comment"; span.className = "meta"; + div.className = "comment"; }, m: function mount(target, anchor) { diff --git a/test/js/samples/event-handlers-custom/expected-bundle.js b/test/js/samples/event-handlers-custom/expected-bundle.js index 24b32d9b1a..46c109100a 100644 --- a/test/js/samples/event-handlers-custom/expected-bundle.js +++ b/test/js/samples/event-handlers-custom/expected-bundle.js @@ -145,9 +145,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/if-block-no-update/expected-bundle.js b/test/js/samples/if-block-no-update/expected-bundle.js index aea386c29a..0277095a83 100644 --- a/test/js/samples/if-block-no-update/expected-bundle.js +++ b/test/js/samples/if-block-no-update/expected-bundle.js @@ -149,9 +149,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/if-block-simple/expected-bundle.js b/test/js/samples/if-block-simple/expected-bundle.js index be77d10110..40acbe3959 100644 --- a/test/js/samples/if-block-simple/expected-bundle.js +++ b/test/js/samples/if-block-simple/expected-bundle.js @@ -149,9 +149,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/inline-style-optimized-multiple/expected-bundle.js b/test/js/samples/inline-style-optimized-multiple/expected-bundle.js index e36eec0994..eff6f53cfa 100644 --- a/test/js/samples/inline-style-optimized-multiple/expected-bundle.js +++ b/test/js/samples/inline-style-optimized-multiple/expected-bundle.js @@ -149,9 +149,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/inline-style-optimized-url/expected-bundle.js b/test/js/samples/inline-style-optimized-url/expected-bundle.js index cbd51d1473..3a126946ad 100644 --- a/test/js/samples/inline-style-optimized-url/expected-bundle.js +++ b/test/js/samples/inline-style-optimized-url/expected-bundle.js @@ -149,9 +149,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/inline-style-optimized/expected-bundle.js b/test/js/samples/inline-style-optimized/expected-bundle.js index 7dd9e42578..0c127a6391 100644 --- a/test/js/samples/inline-style-optimized/expected-bundle.js +++ b/test/js/samples/inline-style-optimized/expected-bundle.js @@ -149,9 +149,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/inline-style-unoptimized/expected-bundle.js b/test/js/samples/inline-style-unoptimized/expected-bundle.js index 8d383eb38b..d6f545111a 100644 --- a/test/js/samples/inline-style-unoptimized/expected-bundle.js +++ b/test/js/samples/inline-style-unoptimized/expected-bundle.js @@ -149,9 +149,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/input-without-blowback-guard/expected-bundle.js b/test/js/samples/input-without-blowback-guard/expected-bundle.js index 4b42f9e572..1918f9f6d1 100644 --- a/test/js/samples/input-without-blowback-guard/expected-bundle.js +++ b/test/js/samples/input-without-blowback-guard/expected-bundle.js @@ -153,9 +153,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { @@ -199,8 +202,8 @@ function create_main_fragment(state, component) { }, h: function hydrate() { - input.type = "checkbox"; addListener(input, "change", input_change_handler); + input.type = "checkbox"; }, m: function mount(target, anchor) { diff --git a/test/js/samples/input-without-blowback-guard/expected.js b/test/js/samples/input-without-blowback-guard/expected.js index 247935ffbe..03f27ab6d9 100644 --- a/test/js/samples/input-without-blowback-guard/expected.js +++ b/test/js/samples/input-without-blowback-guard/expected.js @@ -15,8 +15,8 @@ function create_main_fragment(state, component) { }, h: function hydrate() { - input.type = "checkbox"; addListener(input, "change", input_change_handler); + input.type = "checkbox"; }, m: function mount(target, anchor) { diff --git a/test/js/samples/legacy-input-type/expected-bundle.js b/test/js/samples/legacy-input-type/expected-bundle.js index 19c61b0d19..7211738f45 100644 --- a/test/js/samples/legacy-input-type/expected-bundle.js +++ b/test/js/samples/legacy-input-type/expected-bundle.js @@ -151,9 +151,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/legacy-quote-class/expected-bundle.js b/test/js/samples/legacy-quote-class/expected-bundle.js index a88e843b6f..1575bf4064 100644 --- a/test/js/samples/legacy-quote-class/expected-bundle.js +++ b/test/js/samples/legacy-quote-class/expected-bundle.js @@ -168,9 +168,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/media-bindings/expected-bundle.js b/test/js/samples/media-bindings/expected-bundle.js index 19b7247c74..80545b9402 100644 --- a/test/js/samples/media-bindings/expected-bundle.js +++ b/test/js/samples/media-bindings/expected-bundle.js @@ -161,9 +161,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { @@ -194,78 +197,51 @@ var proto = { /* generated by Svelte vX.Y.Z */ function create_main_fragment(state, component) { - var audio, audio_updating = false, audio_animationframe, audio_paused_value = true; - - function audio_progress_loadedmetadata_handler() { - audio_updating = true; - component.set({ buffered: timeRangesToArray(audio.buffered) }); - audio_updating = false; - } - - function audio_loadedmetadata_handler() { - audio_updating = true; - component.set({ seekable: timeRangesToArray(audio.seekable) }); - audio_updating = false; - } + var audio, audio_is_paused = true, audio_updating = false, audio_animationframe; function audio_timeupdate_handler() { - audio_updating = true; - component.set({ played: timeRangesToArray(audio.played) }); - audio_updating = false; - } - - function audio_timeupdate_handler_1() { - audio_updating = true; cancelAnimationFrame(audio_animationframe); - if (!audio.paused) audio_animationframe = requestAnimationFrame(audio_timeupdate_handler_1); - component.set({ currentTime: audio.currentTime }); + if (!audio.paused) audio_animationframe = requestAnimationFrame(audio_timeupdate_handler); + audio_updating = true; + component.set({ played: timeRangesToArray(audio.played), currentTime: audio.currentTime }); audio_updating = false; } function audio_durationchange_handler() { - audio_updating = true; component.set({ duration: audio.duration }); - audio_updating = false; } - function audio_pause_handler() { + function audio_play_pause_handler() { audio_updating = true; component.set({ paused: audio.paused }); audio_updating = false; } + function audio_progress_handler() { + component.set({ buffered: timeRangesToArray(audio.buffered) }); + } + + function audio_loadedmetadata_handler() { + component.set({ buffered: timeRangesToArray(audio.buffered), seekable: timeRangesToArray(audio.seekable) }); + } + return { c: function create() { audio = createElement("audio"); - addListener(audio, "play", audio_pause_handler); this.h(); }, h: function hydrate() { - component._root._beforecreate.push(audio_progress_loadedmetadata_handler); - - addListener(audio, "progress", audio_progress_loadedmetadata_handler); - addListener(audio, "loadedmetadata", audio_progress_loadedmetadata_handler); - - component._root._beforecreate.push(audio_loadedmetadata_handler); - - addListener(audio, "loadedmetadata", audio_loadedmetadata_handler); - - component._root._beforecreate.push(audio_timeupdate_handler); - addListener(audio, "timeupdate", audio_timeupdate_handler); - - component._root._beforecreate.push(audio_timeupdate_handler_1); - - addListener(audio, "timeupdate", audio_timeupdate_handler_1); - - component._root._beforecreate.push(audio_durationchange_handler); - + if (!('played' in state && 'currentTime' in state)) component._root._beforecreate.push(audio_timeupdate_handler); addListener(audio, "durationchange", audio_durationchange_handler); - - component._root._beforecreate.push(audio_pause_handler); - - addListener(audio, "pause", audio_pause_handler); + if (!('duration' in state)) component._root._beforecreate.push(audio_durationchange_handler); + addListener(audio, "play", audio_play_pause_handler); + addListener(audio, "pause", audio_play_pause_handler); + addListener(audio, "progress", audio_progress_handler); + if (!('buffered' in state)) component._root._beforecreate.push(audio_progress_handler); + addListener(audio, "loadedmetadata", audio_loadedmetadata_handler); + if (!('buffered' in state && 'seekable' in state)) component._root._beforecreate.push(audio_loadedmetadata_handler); }, m: function mount(target, anchor) { @@ -273,13 +249,8 @@ function create_main_fragment(state, component) { }, p: function update(changed, state) { - if (!audio_updating && !isNaN(state.currentTime )) { - audio.currentTime = state.currentTime ; - } - - if (audio_paused_value !== (audio_paused_value = state.paused)) { - audio[audio_paused_value ? "pause" : "play"](); - } + if (!audio_updating && !isNaN(state.currentTime )) audio.currentTime = state.currentTime ; + if (!audio_updating && audio_is_paused !== (audio_is_paused = state.paused)) audio[audio_is_paused ? "pause" : "play"](); }, u: function unmount() { @@ -287,14 +258,12 @@ function create_main_fragment(state, component) { }, d: function destroy$$1() { - removeListener(audio, "progress", audio_progress_loadedmetadata_handler); - removeListener(audio, "loadedmetadata", audio_progress_loadedmetadata_handler); - removeListener(audio, "loadedmetadata", audio_loadedmetadata_handler); removeListener(audio, "timeupdate", audio_timeupdate_handler); - removeListener(audio, "timeupdate", audio_timeupdate_handler_1); removeListener(audio, "durationchange", audio_durationchange_handler); - removeListener(audio, "pause", audio_pause_handler); - removeListener(audio, "play", audio_pause_handler); + removeListener(audio, "play", audio_play_pause_handler); + removeListener(audio, "pause", audio_play_pause_handler); + removeListener(audio, "progress", audio_progress_handler); + removeListener(audio, "loadedmetadata", audio_loadedmetadata_handler); } }; } diff --git a/test/js/samples/media-bindings/expected.js b/test/js/samples/media-bindings/expected.js index bcab29fb75..c14eb2aa83 100644 --- a/test/js/samples/media-bindings/expected.js +++ b/test/js/samples/media-bindings/expected.js @@ -2,78 +2,51 @@ import { addListener, assign, callAll, createElement, detachNode, init, insertNode, proto, removeListener, timeRangesToArray } from "svelte/shared.js"; function create_main_fragment(state, component) { - var audio, audio_updating = false, audio_animationframe, audio_paused_value = true; - - function audio_progress_loadedmetadata_handler() { - audio_updating = true; - component.set({ buffered: timeRangesToArray(audio.buffered) }); - audio_updating = false; - } - - function audio_loadedmetadata_handler() { - audio_updating = true; - component.set({ seekable: timeRangesToArray(audio.seekable) }); - audio_updating = false; - } + var audio, audio_is_paused = true, audio_updating = false, audio_animationframe; function audio_timeupdate_handler() { - audio_updating = true; - component.set({ played: timeRangesToArray(audio.played) }); - audio_updating = false; - } - - function audio_timeupdate_handler_1() { - audio_updating = true; cancelAnimationFrame(audio_animationframe); - if (!audio.paused) audio_animationframe = requestAnimationFrame(audio_timeupdate_handler_1); - component.set({ currentTime: audio.currentTime }); + if (!audio.paused) audio_animationframe = requestAnimationFrame(audio_timeupdate_handler); + audio_updating = true; + component.set({ played: timeRangesToArray(audio.played), currentTime: audio.currentTime }); audio_updating = false; } function audio_durationchange_handler() { - audio_updating = true; component.set({ duration: audio.duration }); - audio_updating = false; } - function audio_pause_handler() { + function audio_play_pause_handler() { audio_updating = true; component.set({ paused: audio.paused }); audio_updating = false; } + function audio_progress_handler() { + component.set({ buffered: timeRangesToArray(audio.buffered) }); + } + + function audio_loadedmetadata_handler() { + component.set({ buffered: timeRangesToArray(audio.buffered), seekable: timeRangesToArray(audio.seekable) }); + } + return { c: function create() { audio = createElement("audio"); - addListener(audio, "play", audio_pause_handler); this.h(); }, h: function hydrate() { - component._root._beforecreate.push(audio_progress_loadedmetadata_handler); - - addListener(audio, "progress", audio_progress_loadedmetadata_handler); - addListener(audio, "loadedmetadata", audio_progress_loadedmetadata_handler); - - component._root._beforecreate.push(audio_loadedmetadata_handler); - - addListener(audio, "loadedmetadata", audio_loadedmetadata_handler); - - component._root._beforecreate.push(audio_timeupdate_handler); - addListener(audio, "timeupdate", audio_timeupdate_handler); - - component._root._beforecreate.push(audio_timeupdate_handler_1); - - addListener(audio, "timeupdate", audio_timeupdate_handler_1); - - component._root._beforecreate.push(audio_durationchange_handler); - + if (!('played' in state && 'currentTime' in state)) component._root._beforecreate.push(audio_timeupdate_handler); addListener(audio, "durationchange", audio_durationchange_handler); - - component._root._beforecreate.push(audio_pause_handler); - - addListener(audio, "pause", audio_pause_handler); + if (!('duration' in state)) component._root._beforecreate.push(audio_durationchange_handler); + addListener(audio, "play", audio_play_pause_handler); + addListener(audio, "pause", audio_play_pause_handler); + addListener(audio, "progress", audio_progress_handler); + if (!('buffered' in state)) component._root._beforecreate.push(audio_progress_handler); + addListener(audio, "loadedmetadata", audio_loadedmetadata_handler); + if (!('buffered' in state && 'seekable' in state)) component._root._beforecreate.push(audio_loadedmetadata_handler); }, m: function mount(target, anchor) { @@ -81,13 +54,8 @@ function create_main_fragment(state, component) { }, p: function update(changed, state) { - if (!audio_updating && !isNaN(state.currentTime )) { - audio.currentTime = state.currentTime ; - } - - if (audio_paused_value !== (audio_paused_value = state.paused)) { - audio[audio_paused_value ? "pause" : "play"](); - } + if (!audio_updating && !isNaN(state.currentTime )) audio.currentTime = state.currentTime ; + if (!audio_updating && audio_is_paused !== (audio_is_paused = state.paused)) audio[audio_is_paused ? "pause" : "play"](); }, u: function unmount() { @@ -95,14 +63,12 @@ function create_main_fragment(state, component) { }, d: function destroy() { - removeListener(audio, "progress", audio_progress_loadedmetadata_handler); - removeListener(audio, "loadedmetadata", audio_progress_loadedmetadata_handler); - removeListener(audio, "loadedmetadata", audio_loadedmetadata_handler); removeListener(audio, "timeupdate", audio_timeupdate_handler); - removeListener(audio, "timeupdate", audio_timeupdate_handler_1); removeListener(audio, "durationchange", audio_durationchange_handler); - removeListener(audio, "pause", audio_pause_handler); - removeListener(audio, "play", audio_pause_handler); + removeListener(audio, "play", audio_play_pause_handler); + removeListener(audio, "pause", audio_play_pause_handler); + removeListener(audio, "progress", audio_progress_handler); + removeListener(audio, "loadedmetadata", audio_loadedmetadata_handler); } }; } diff --git a/test/js/samples/non-imported-component/expected-bundle.js b/test/js/samples/non-imported-component/expected-bundle.js index 549c018e7e..575be613fa 100644 --- a/test/js/samples/non-imported-component/expected-bundle.js +++ b/test/js/samples/non-imported-component/expected-bundle.js @@ -147,9 +147,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js index 6199e5061f..f1e701bd04 100644 --- a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js +++ b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js @@ -133,9 +133,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/setup-method/expected-bundle.js b/test/js/samples/setup-method/expected-bundle.js index 3ec6edbb1b..0c545ea70c 100644 --- a/test/js/samples/setup-method/expected-bundle.js +++ b/test/js/samples/setup-method/expected-bundle.js @@ -133,9 +133,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/js/samples/use-elements-as-anchors/expected-bundle.js b/test/js/samples/use-elements-as-anchors/expected-bundle.js index 0529108287..8805cc3847 100644 --- a/test/js/samples/use-elements-as-anchors/expected-bundle.js +++ b/test/js/samples/use-elements-as-anchors/expected-bundle.js @@ -157,9 +157,12 @@ function _set(newState) { this._state = assign({}, oldState, newState); this._recompute(changed, this._state); if (this._bind) this._bind(changed, this._state); - dispatchObservers(this, this._observers.pre, changed, this._state, oldState); - this._fragment.p(changed, this._state); - dispatchObservers(this, this._observers.post, changed, this._state, oldState); + + if (this._fragment) { + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + this._fragment.p(changed, this._state); + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } } function callAll(fns) { diff --git a/test/runtime/samples/attribute-boolean-indeterminate/_config.js b/test/runtime/samples/attribute-boolean-indeterminate/_config.js new file mode 100644 index 0000000000..33acd07948 --- /dev/null +++ b/test/runtime/samples/attribute-boolean-indeterminate/_config.js @@ -0,0 +1,21 @@ +export default { + // This is a bit of a funny one — there's no equivalent attribute, + // so it can't be server-rendered + 'skip-ssr': true, + + data: { + indeterminate: true + }, + + html: ` + + `, + + test(assert, component, target) { + const input = target.querySelector('input'); + + assert.ok(input.indeterminate); + component.set({ indeterminate: false }); + assert.ok(!input.indeterminate); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/attribute-boolean-indeterminate/main.html b/test/runtime/samples/attribute-boolean-indeterminate/main.html new file mode 100644 index 0000000000..e7ab75dcc1 --- /dev/null +++ b/test/runtime/samples/attribute-boolean-indeterminate/main.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/runtime/samples/binding-input-checkbox-group/_config.js b/test/runtime/samples/binding-input-checkbox-group/_config.js index fac1795eca..bff66c903b 100644 --- a/test/runtime/samples/binding-input-checkbox-group/_config.js +++ b/test/runtime/samples/binding-input-checkbox-group/_config.js @@ -16,15 +16,15 @@ export default { Alpha - + Beta - + Gamma - + Beta`, test ( assert, component, target, window ) { @@ -42,15 +42,15 @@ export default { Alpha - + Beta - + Gamma - + Alpha, Beta ` ); @@ -63,15 +63,15 @@ export default { Alpha - + Beta - + Gamma - + Beta, Gamma ` ); } diff --git a/test/runtime/samples/binding-input-checkbox-indeterminate/_config.js b/test/runtime/samples/binding-input-checkbox-indeterminate/_config.js new file mode 100644 index 0000000000..3d905e5a9e --- /dev/null +++ b/test/runtime/samples/binding-input-checkbox-indeterminate/_config.js @@ -0,0 +1,42 @@ +export default { + 'skip-ssr': true, + + data: { + indeterminate: true, + }, + + html: ` + + checked? false + indeterminate? true + `, + + test(assert, component, target, window) { + const input = target.querySelector('input'); + assert.equal(input.checked, false); + assert.equal(input.indeterminate, true); + + const event = new window.Event('change'); + + input.checked = true; + input.indeterminate = false; + input.dispatchEvent(event); + + assert.equal(component.get('indeterminate'), false); + assert.equal(component.get('checked'), true); + assert.htmlEqual(target.innerHTML, ` + + checked? true + indeterminate? false + `); + + component.set({ indeterminate: true }); + assert.equal(input.indeterminate, true); + assert.equal(input.checked, true); + assert.htmlEqual(target.innerHTML, ` + + checked? true + indeterminate? true + `); + }, +}; diff --git a/test/runtime/samples/binding-input-checkbox-indeterminate/main.html b/test/runtime/samples/binding-input-checkbox-indeterminate/main.html new file mode 100644 index 0000000000..e4c83224f9 --- /dev/null +++ b/test/runtime/samples/binding-input-checkbox-indeterminate/main.html @@ -0,0 +1,3 @@ + +checked? {{checked}} +indeterminate? {{indeterminate}} diff --git a/test/runtime/samples/binding-select-implicit-option-value/_config.js b/test/runtime/samples/binding-select-implicit-option-value/_config.js new file mode 100644 index 0000000000..40a7364797 --- /dev/null +++ b/test/runtime/samples/binding-select-implicit-option-value/_config.js @@ -0,0 +1,40 @@ +export default { + data: { + values: [1, 2, 3], + foo: 2 + }, + + html: ` + + 1 + 2 + 3 + + + foo: 2 + `, + + test(assert, component, target, window) { + const select = target.querySelector('select'); + const options = [...target.querySelectorAll('option')]; + + assert.ok(options[1].selected); + assert.equal(component.get('foo'), 2); + + const change = new window.Event('change'); + + options[2].selected = true; + select.dispatchEvent(change); + + assert.equal(component.get('foo'), 3); + assert.htmlEqual( target.innerHTML, ` + + 1 + 2 + 3 + + + foo: 3 + ` ); + } +}; diff --git a/test/runtime/samples/binding-select-implicit-option-value/main.html b/test/runtime/samples/binding-select-implicit-option-value/main.html new file mode 100644 index 0000000000..ec5cc84a8d --- /dev/null +++ b/test/runtime/samples/binding-select-implicit-option-value/main.html @@ -0,0 +1,7 @@ + + {{#each values as v}} + {{v}} + {{/each}} + + +foo: {{foo}} \ No newline at end of file diff --git a/test/runtime/samples/binding-select-initial-value-undefined/_config.js b/test/runtime/samples/binding-select-initial-value-undefined/_config.js index d5944ae815..f625000edb 100644 --- a/test/runtime/samples/binding-select-initial-value-undefined/_config.js +++ b/test/runtime/samples/binding-select-initial-value-undefined/_config.js @@ -5,9 +5,9 @@ export default { selected: a - a - b - c + a + b + c selected: a diff --git a/test/runtime/samples/binding-select-initial-value/_config.js b/test/runtime/samples/binding-select-initial-value/_config.js index 99a7129262..7338af165f 100644 --- a/test/runtime/samples/binding-select-initial-value/_config.js +++ b/test/runtime/samples/binding-select-initial-value/_config.js @@ -3,9 +3,9 @@ export default { selected: b - a - b - c + a + b + c selected: b diff --git a/test/runtime/samples/binding-select-late/_config.js b/test/runtime/samples/binding-select-late/_config.js index 2fffa0e4ed..04b94613fc 100644 --- a/test/runtime/samples/binding-select-late/_config.js +++ b/test/runtime/samples/binding-select-late/_config.js @@ -22,9 +22,9 @@ export default { assert.htmlEqual( target.innerHTML, ` - one - two - three + one + two + three selected: two ` ); diff --git a/test/runtime/samples/binding-select-optgroup/_config.js b/test/runtime/samples/binding-select-optgroup/_config.js new file mode 100644 index 0000000000..0d651b6654 --- /dev/null +++ b/test/runtime/samples/binding-select-optgroup/_config.js @@ -0,0 +1,39 @@ +export default { + skip: true, // JSDOM + + html: ` + Hello Harry! + + + Harry + + World + + + `, + + test(assert, component, target, window) { + const select = target.querySelector('select'); + const options = [...target.querySelectorAll('option')]; + + assert.deepEqual(options, select.options); + assert.equal(component.get('name'), 'Harry'); + + const change = new window.Event('change'); + + options[1].selected = true; + select.dispatchEvent(change); + + assert.equal(component.get('name'), 'World'); + assert.htmlEqual(target.innerHTML, ` + Hello World! + + + Harry + + World + + + `); + }, +}; diff --git a/test/runtime/samples/binding-select-optgroup/main.html b/test/runtime/samples/binding-select-optgroup/main.html new file mode 100644 index 0000000000..2160d0e2a8 --- /dev/null +++ b/test/runtime/samples/binding-select-optgroup/main.html @@ -0,0 +1,8 @@ +Hello {{name}}! + + + Harry + + World + + \ No newline at end of file diff --git a/test/runtime/samples/binding-select/_config.js b/test/runtime/samples/binding-select/_config.js index 7fce00f327..8c8eced7bc 100644 --- a/test/runtime/samples/binding-select/_config.js +++ b/test/runtime/samples/binding-select/_config.js @@ -3,9 +3,9 @@ export default { selected: one - one - two - three + one + two + three selected: one @@ -32,9 +32,9 @@ export default { selected: two - one - two - three + one + two + three selected: two diff --git a/test/runtime/samples/component-binding-self-destroying/Nested.html b/test/runtime/samples/component-binding-self-destroying/Nested.html new file mode 100644 index 0000000000..3fb5ca4da3 --- /dev/null +++ b/test/runtime/samples/component-binding-self-destroying/Nested.html @@ -0,0 +1 @@ +Hide \ No newline at end of file diff --git a/test/runtime/samples/component-binding-self-destroying/_config.js b/test/runtime/samples/component-binding-self-destroying/_config.js new file mode 100644 index 0000000000..27a7ab108e --- /dev/null +++ b/test/runtime/samples/component-binding-self-destroying/_config.js @@ -0,0 +1,27 @@ +export default { + data: { + show: true + }, + + html: ` + Hide + `, + + test(assert, component, target, window) { + const click = new window.MouseEvent('click'); + + target.querySelector('button').dispatchEvent(click); + + assert.equal(component.get('show'), false); + assert.htmlEqual(target.innerHTML, ` + Show + `); + + target.querySelector('button').dispatchEvent(click); + + assert.equal(component.get('show'), true); + assert.htmlEqual(target.innerHTML, ` + Hide + `); + } +}; diff --git a/test/runtime/samples/component-binding-self-destroying/main.html b/test/runtime/samples/component-binding-self-destroying/main.html new file mode 100644 index 0000000000..74fa144d02 --- /dev/null +++ b/test/runtime/samples/component-binding-self-destroying/main.html @@ -0,0 +1,14 @@ +{{#if show}} + +{{else}} + Show +{{/if}} + + \ No newline at end of file diff --git a/test/sourcemaps/index.js b/test/sourcemaps/index.js index f2bc509748..939ae38d86 100644 --- a/test/sourcemaps/index.js +++ b/test/sourcemaps/index.js @@ -34,9 +34,11 @@ describe("sourcemaps", () => { cascade: config.cascade }); + const _code = code.replace(/Svelte v\d+\.\d+\.\d+/, match => match.replace(/\d/g, 'x')); + fs.writeFileSync( `${outputFilename}.js`, - `${code}\n//# sourceMappingURL=output.js.map` + `${_code}\n//# sourceMappingURL=output.js.map` ); fs.writeFileSync( `${outputFilename}.js.map`, @@ -62,12 +64,12 @@ describe("sourcemaps", () => { const locateInSource = getLocator(input); const smc = new SourceMapConsumer(map); - const locateInGenerated = getLocator(code); + const locateInGenerated = getLocator(_code); const smcCss = cssMap && new SourceMapConsumer(cssMap); const locateInGeneratedCss = getLocator(css || ''); - test({ assert, code, map, smc, smcCss, locateInSource, locateInGenerated, locateInGeneratedCss }); + test({ assert, code: _code, map, smc, smcCss, locateInSource, locateInGenerated, locateInGeneratedCss }); }); }); }); diff --git a/test/validator/samples/method-quoted/errors.json b/test/validator/samples/method-quoted/errors.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/test/validator/samples/method-quoted/errors.json @@ -0,0 +1 @@ +[] diff --git a/test/validator/samples/method-quoted/input.html b/test/validator/samples/method-quoted/input.html new file mode 100644 index 0000000000..790276eebc --- /dev/null +++ b/test/validator/samples/method-quoted/input.html @@ -0,0 +1,10 @@ + + + \ No newline at end of file
Beta
Alpha, Beta
Beta, Gamma
checked? false
indeterminate? true
checked? true
indeterminate? false
checked? {{checked}}
indeterminate? {{indeterminate}}
foo: 2
foo: 3
foo: {{foo}}
selected: a
selected: b
selected: two
selected: one