From 91d758e35b2b2154512ddd11e6b6d9d65708a99e Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Tue, 10 Mar 2020 10:22:39 +0800 Subject: [PATCH] introduce $$restProps (#4489) --- CHANGELOG.md | 4 ++ site/content/docs/02-template-syntax.md | 9 +++ src/compiler/compile/Component.ts | 9 +-- .../compile/nodes/shared/Expression.ts | 5 +- src/compiler/compile/render_dom/Renderer.ts | 9 ++- src/compiler/compile/render_dom/index.ts | 26 +++++--- src/compiler/compile/render_ssr/index.ts | 5 ++ .../compile/utils/reserved_keywords.ts | 5 ++ src/runtime/internal/utils.ts | 7 +++ .../samples/$$rest-without-props/App.svelte | 12 ++++ .../samples/$$rest-without-props/_config.js | 54 +++++++++++++++++ .../samples/$$rest-without-props/main.svelte | 25 ++++++++ test/runtime/samples/$$rest/App.svelte | 13 ++++ test/runtime/samples/$$rest/_config.js | 60 +++++++++++++++++++ test/runtime/samples/$$rest/main.svelte | 25 ++++++++ .../Foo.svelte | 2 +- .../Foo.svelte | 7 +++ .../_config.js | 7 +++ .../main.svelte | 5 ++ 19 files changed, 272 insertions(+), 17 deletions(-) create mode 100644 src/compiler/compile/utils/reserved_keywords.ts create mode 100644 test/runtime/samples/$$rest-without-props/App.svelte create mode 100644 test/runtime/samples/$$rest-without-props/_config.js create mode 100644 test/runtime/samples/$$rest-without-props/main.svelte create mode 100644 test/runtime/samples/$$rest/App.svelte create mode 100644 test/runtime/samples/$$rest/_config.js create mode 100644 test/runtime/samples/$$rest/main.svelte create mode 100644 test/runtime/samples/dev-warning-unknown-props-with-$$rest/Foo.svelte create mode 100644 test/runtime/samples/dev-warning-unknown-props-with-$$rest/_config.js create mode 100644 test/runtime/samples/dev-warning-unknown-props-with-$$rest/main.svelte diff --git a/CHANGELOG.md b/CHANGELOG.md index f1564848b5..7848af35be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Svelte changelog +## Unreleased + +* Expose object of unknown props in `$$restProps` ([#2930](https://github.com/sveltejs/svelte/issues/2930)) + ## 3.19.2 * In `dev` mode, display a runtime warning when a component is passed an unexpected slot ([#1020](https://github.com/sveltejs/svelte/issues/1020), [#1447](https://github.com/sveltejs/svelte/issues/1447)) diff --git a/site/content/docs/02-template-syntax.md b/site/content/docs/02-template-syntax.md index d25b348254..ea36474bed 100644 --- a/site/content/docs/02-template-syntax.md +++ b/site/content/docs/02-template-syntax.md @@ -113,6 +113,15 @@ An element or component can have multiple spread attributes, interspersed with r ``` +--- + +*`$$restProps`* contains only the props which are *not* declared with `export`. It can be used to pass down other unknown attributes to an element in a component. + +```html + +``` + +--- ### Text expressions diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index a7fe2992d1..ae2c4b704a 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -28,6 +28,7 @@ import { Node, ImportDeclaration, Identifier, Program, ExpressionStatement, Assi import add_to_set from './utils/add_to_set'; import check_graph_for_cycles from './utils/check_graph_for_cycles'; import { print, x, b } from 'code-red'; +import { is_reserved_keyword } from './utils/reserved_keywords'; interface ComponentOptions { namespace?: string; @@ -185,7 +186,7 @@ export default class Component { if (variable) { variable.referenced = true; - } else if (name === '$$props') { + } else if (is_reserved_keyword(name)) { this.add_var({ name, injected: true, @@ -649,7 +650,7 @@ export default class Component { reassigned: true, initialised: true, }); - } else if (name === '$$props') { + } else if (is_reserved_keyword(name)) { this.add_var({ name, injected: true, @@ -1276,7 +1277,7 @@ export default class Component { warn_if_undefined(name: string, node, template_scope: TemplateScope) { if (name[0] === '$') { - if (name === '$' || name[1] === '$' && name !== '$$props') { + if (name === '$' || name[1] === '$' && !is_reserved_keyword(name)) { this.error(node, { code: 'illegal-global', message: `${name} is an illegal variable name` @@ -1285,7 +1286,7 @@ export default class Component { this.has_reactive_assignments = true; // TODO does this belong here? - if (name === '$$props') return; + if (is_reserved_keyword(name)) return; name = name.slice(1); } diff --git a/src/compiler/compile/nodes/shared/Expression.ts b/src/compiler/compile/nodes/shared/Expression.ts index db0908c3f4..2f2541c720 100644 --- a/src/compiler/compile/nodes/shared/Expression.ts +++ b/src/compiler/compile/nodes/shared/Expression.ts @@ -13,6 +13,7 @@ import { b } from 'code-red'; import { invalidate } from '../../render_dom/invalidate'; import { Node, FunctionExpression, Identifier } from 'estree'; import { TemplateNode } from '../../../interfaces'; +import { is_reserved_keyword } from '../../utils/reserved_keywords'; type Owner = Wrapper | TemplateNode; @@ -158,7 +159,7 @@ export default class Expression { dynamic_dependencies() { return Array.from(this.dependencies).filter(name => { if (this.template_scope.is_let(name)) return true; - if (name === '$$props') return true; + if (is_reserved_keyword(name)) return true; const variable = this.component.var_lookup.get(name); return is_dynamic(variable); @@ -355,7 +356,7 @@ function get_function_name(_node, parent) { } function is_contextual(component: Component, scope: TemplateScope, name: string) { - if (name === '$$props') return true; + if (is_reserved_keyword(name)) return true; // if it's a name below root scope, it's contextual if (!scope.is_top_level(name)) return true; diff --git a/src/compiler/compile/render_dom/Renderer.ts b/src/compiler/compile/render_dom/Renderer.ts index 2501279214..e933faf97a 100644 --- a/src/compiler/compile/render_dom/Renderer.ts +++ b/src/compiler/compile/render_dom/Renderer.ts @@ -5,6 +5,7 @@ import FragmentWrapper from './wrappers/Fragment'; import { x } from 'code-red'; import { Node, Identifier, MemberExpression, Literal, Expression, BinaryExpression } from 'estree'; import flatten_reference from '../utils/flatten_reference'; +import { reserved_keywords } from '../utils/reserved_keywords'; interface ContextMember { name: string; @@ -50,9 +51,11 @@ export default class Renderer { // ensure store values are included in context component.vars.filter(v => v.subscribable).forEach(v => this.add_to_context(`$${v.name}`)); - if (component.var_lookup.has('$$props')) { - this.add_to_context('$$props'); - } + reserved_keywords.forEach(keyword => { + if (component.var_lookup.has(keyword)) { + this.add_to_context(keyword); + } + }); if (component.slots.size > 0) { this.add_to_context('$$scope'); diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index fc3c94a2be..4009c6bddf 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -71,14 +71,24 @@ export default function dom( } const uses_props = component.var_lookup.has('$$props'); - const $$props = uses_props ? `$$new_props` : `$$props`; + const uses_rest = component.var_lookup.has('$$restProps'); + const $$props = uses_props || uses_rest ? `$$new_props` : `$$props`; const props = component.vars.filter(variable => !variable.module && variable.export_name); const writable_props = props.filter(variable => variable.writable); - const set = (uses_props || writable_props.length > 0 || component.slots.size > 0) + const omit_props_names = component.get_unique_name('omit_props_names'); + const compute_rest = x`@compute_rest_props($$props, ${omit_props_names.name})`; + const rest = uses_rest ? b` + const ${omit_props_names.name} = [${props.map(prop => `"${prop.export_name}"`).join(',')}]; + let $$restProps = ${compute_rest}; + ` : null; + + const set = (uses_props || uses_rest || writable_props.length > 0 || component.slots.size > 0) ? x` ${$$props} => { ${uses_props && renderer.invalidate('$$props', x`$$props = @assign(@assign({}, $$props), @exclude_internal_props($$new_props))`)} + ${uses_rest && !uses_props && x`$$props = @assign(@assign({}, $$props), @exclude_internal_props($$new_props))`} + ${uses_rest && renderer.invalidate('$$restProps', x`$$restProps = ${compute_rest}`)} ${writable_props.map(prop => b`if ('${prop.export_name}' in ${$$props}) ${renderer.invalidate(prop.name, x`${prop.name} = ${$$props}.${prop.export_name}`)};` )} @@ -341,20 +351,20 @@ export default function dom( component.reactive_declarations.forEach(d => { const dependencies = Array.from(d.dependencies); - const uses_props = !!dependencies.find(n => n === '$$props'); + const uses_rest_or_props = !!dependencies.find(n => n === '$$props' || n === '$$restProps'); const writable = dependencies.filter(n => { const variable = component.var_lookup.get(n); return variable && (variable.export_name || variable.mutated || variable.reassigned); }); - const condition = !uses_props && writable.length > 0 && renderer.dirty(writable, true); + const condition = !uses_rest_or_props && writable.length > 0 && renderer.dirty(writable, true); let statement = d.node; // TODO remove label (use d.node.body) if it's not referenced if (condition) statement = b`if (${condition}) { ${statement} }`[0] as Statement; - if (condition || uses_props) { + if (condition || uses_rest_or_props) { reactive_declarations.push(statement); } else { fixed_reactive_declarations.push(statement); @@ -383,7 +393,7 @@ export default function dom( }); let unknown_props_check; - if (component.compile_options.dev && !component.var_lookup.has('$$props')) { + if (component.compile_options.dev && !(uses_props || uses_rest)) { unknown_props_check = b` const writable_props = [${writable_props.map(prop => x`'${prop.export_name}'`)}]; @_Object.keys($$props).forEach(key => { @@ -402,6 +412,8 @@ export default function dom( body.push(b` function ${definition}(${args}) { + ${rest} + ${reactive_store_declarations} ${reactive_store_subscriptions} @@ -473,7 +485,7 @@ export default function dom( @insert(options.target, this, options.anchor); } - ${(props.length > 0 || uses_props) && b` + ${(props.length > 0 || uses_props || uses_rest) && b` if (options.props) { this.$set(options.props); @flush(); diff --git a/src/compiler/compile/render_ssr/index.ts b/src/compiler/compile/render_ssr/index.ts index ad22869d32..c87fe3bdd9 100644 --- a/src/compiler/compile/render_ssr/index.ts +++ b/src/compiler/compile/render_ssr/index.ts @@ -31,6 +31,10 @@ export default function ssr( { code: null, map: null } : component.stylesheet.render(options.filename, true); + const uses_rest = component.var_lookup.has('$$restProps'); + const props = component.vars.filter(variable => !variable.module && variable.export_name); + const rest = uses_rest ? b`let $$restProps = @compute_rest_props($$props, [${props.map(prop => `"${prop.export_name}"`).join(',')}]);` : null; + const reactive_stores = component.vars.filter(variable => variable.name[0] === '$' && variable.name[1] !== '$'); const reactive_store_values = reactive_stores .map(({ name }) => { @@ -130,6 +134,7 @@ export default function ssr( return ${literal};`; const blocks = [ + rest, ...reactive_stores.map(({ name }) => { const store_name = name.slice(1); const store = component.var_lookup.get(store_name); diff --git a/src/compiler/compile/utils/reserved_keywords.ts b/src/compiler/compile/utils/reserved_keywords.ts new file mode 100644 index 0000000000..75825c1719 --- /dev/null +++ b/src/compiler/compile/utils/reserved_keywords.ts @@ -0,0 +1,5 @@ +export const reserved_keywords = new Set(["$$props", "$$restProps"]); + +export function is_reserved_keyword(name) { + return reserved_keywords.has(name); +} \ No newline at end of file diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts index acb88c7669..487116b655 100644 --- a/src/runtime/internal/utils.ts +++ b/src/runtime/internal/utils.ts @@ -109,6 +109,13 @@ export function exclude_internal_props(props) { return result; } +export function compute_rest_props(props, keys) { + const rest = {}; + keys = new Set(keys); + for (const k in props) if (!keys.has(k) && k[0] !== '$') rest[k] = props[k]; + return rest; +} + export function once(fn) { let ran = false; return function(this: any, ...args) { diff --git a/test/runtime/samples/$$rest-without-props/App.svelte b/test/runtime/samples/$$rest-without-props/App.svelte new file mode 100644 index 0000000000..e98fec30d4 --- /dev/null +++ b/test/runtime/samples/$$rest-without-props/App.svelte @@ -0,0 +1,12 @@ + +
Length: {length}
+
Values: {values.join(',')}
+ +
\ No newline at end of file diff --git a/test/runtime/samples/$$rest-without-props/_config.js b/test/runtime/samples/$$rest-without-props/_config.js new file mode 100644 index 0000000000..017f9df561 --- /dev/null +++ b/test/runtime/samples/$$rest-without-props/_config.js @@ -0,0 +1,54 @@ +export default { + props: { + a: 3, + b: 4, + c: 5, + d: 6 + }, + html: ` +
Length: 3
+
Values: 4,5,1
+
+ + `, + async test({ assert, target, window, }) { + const [btn1, btn2, btn3, btn4] = target.querySelectorAll('button'); + const clickEvent = new window.MouseEvent('click'); + + await btn1.dispatchEvent(clickEvent); + + assert.htmlEqual(target.innerHTML, ` +
Length: 3
+
Values: 4,5,1
+
+ + `); + + await btn2.dispatchEvent(clickEvent); + + assert.htmlEqual(target.innerHTML, ` +
Length: 3
+
Values: 34,5,1
+
+ + `); + + await btn3.dispatchEvent(clickEvent); + + assert.htmlEqual(target.innerHTML, ` +
Length: 3
+
Values: 34,5,31
+
+ + `); + + await btn4.dispatchEvent(clickEvent); + + assert.htmlEqual(target.innerHTML, ` +
Length: 4
+
Values: 34,5,31,2
+
+ + `); + } +}; diff --git a/test/runtime/samples/$$rest-without-props/main.svelte b/test/runtime/samples/$$rest-without-props/main.svelte new file mode 100644 index 0000000000..21b2690584 --- /dev/null +++ b/test/runtime/samples/$$rest-without-props/main.svelte @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/test/runtime/samples/$$rest/App.svelte b/test/runtime/samples/$$rest/App.svelte new file mode 100644 index 0000000000..875372f670 --- /dev/null +++ b/test/runtime/samples/$$rest/App.svelte @@ -0,0 +1,13 @@ + +
Length: {length}
+
Values: {values.join(',')}
+ +
+
\ No newline at end of file diff --git a/test/runtime/samples/$$rest/_config.js b/test/runtime/samples/$$rest/_config.js new file mode 100644 index 0000000000..255927f354 --- /dev/null +++ b/test/runtime/samples/$$rest/_config.js @@ -0,0 +1,60 @@ +export default { + props: { + a: 3, + b: 4, + c: 5, + d: 6 + }, + html: ` +
Length: 3
+
Values: 4,5,1
+
+
+ + `, + + async test({ assert, target, window, }) { + const [btn1, btn2, btn3, btn4] = target.querySelectorAll('button'); + const clickEvent = new window.MouseEvent('click'); + + await btn1.dispatchEvent(clickEvent); + + assert.htmlEqual(target.innerHTML, ` +
Length: 3
+
Values: 4,5,1
+
+
+ + `); + + await btn2.dispatchEvent(clickEvent); + + assert.htmlEqual(target.innerHTML, ` +
Length: 3
+
Values: 34,5,1
+
+
+ + `); + + await btn3.dispatchEvent(clickEvent); + + assert.htmlEqual(target.innerHTML, ` +
Length: 3
+
Values: 34,5,31
+
+
+ + `); + + await btn4.dispatchEvent(clickEvent); + + assert.htmlEqual(target.innerHTML, ` +
Length: 4
+
Values: 34,5,31,2
+
+
+ + `); + } +}; diff --git a/test/runtime/samples/$$rest/main.svelte b/test/runtime/samples/$$rest/main.svelte new file mode 100644 index 0000000000..21b2690584 --- /dev/null +++ b/test/runtime/samples/$$rest/main.svelte @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/test/runtime/samples/dev-warning-unknown-props-with-$$props/Foo.svelte b/test/runtime/samples/dev-warning-unknown-props-with-$$props/Foo.svelte index 9e5c62339d..375b1a6a0a 100644 --- a/test/runtime/samples/dev-warning-unknown-props-with-$$props/Foo.svelte +++ b/test/runtime/samples/dev-warning-unknown-props-with-$$props/Foo.svelte @@ -4,4 +4,4 @@
{foo}
-
{JSON.stringify($$props)}
+
{JSON.stringify($$restProps)}
diff --git a/test/runtime/samples/dev-warning-unknown-props-with-$$rest/Foo.svelte b/test/runtime/samples/dev-warning-unknown-props-with-$$rest/Foo.svelte new file mode 100644 index 0000000000..9e5c62339d --- /dev/null +++ b/test/runtime/samples/dev-warning-unknown-props-with-$$rest/Foo.svelte @@ -0,0 +1,7 @@ + + +
{foo}
+
{JSON.stringify($$props)}
diff --git a/test/runtime/samples/dev-warning-unknown-props-with-$$rest/_config.js b/test/runtime/samples/dev-warning-unknown-props-with-$$rest/_config.js new file mode 100644 index 0000000000..62ad08624d --- /dev/null +++ b/test/runtime/samples/dev-warning-unknown-props-with-$$rest/_config.js @@ -0,0 +1,7 @@ +export default { + compileOptions: { + dev: true + }, + + warnings: [] +}; diff --git a/test/runtime/samples/dev-warning-unknown-props-with-$$rest/main.svelte b/test/runtime/samples/dev-warning-unknown-props-with-$$rest/main.svelte new file mode 100644 index 0000000000..1566cf3e41 --- /dev/null +++ b/test/runtime/samples/dev-warning-unknown-props-with-$$rest/main.svelte @@ -0,0 +1,5 @@ + + +