introduce $$restProps (#4489)

pull/4539/head
Tan Li Hau 5 years ago committed by GitHub
parent 48721520bd
commit 91d758e35b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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))

@ -113,6 +113,15 @@ An element or component can have multiple spread attributes, interspersed with r
<Widget {...$$props}/>
```
---
*`$$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
<input {...$$restProps}>
```
---
### Text expressions

@ -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);
}

@ -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;

@ -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');

@ -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();

@ -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);

@ -0,0 +1,5 @@
export const reserved_keywords = new Set(["$$props", "$$restProps"]);
export function is_reserved_keyword(name) {
return reserved_keywords.has(name);
}

@ -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) {

@ -0,0 +1,12 @@
<script>
export let a;
export function b() {}
export let c = 1;
$: length = Object.keys($$restProps).length;
$: values = Object.values($$restProps);
</script>
<div>Length: {length}</div>
<div>Values: {values.join(',')}</div>
<div {...$$restProps} />

@ -0,0 +1,54 @@
export default {
props: {
a: 3,
b: 4,
c: 5,
d: 6
},
html: `
<div>Length: 3</div>
<div>Values: 4,5,1</div>
<div d="4" e="5" foo="1"></div>
<button></button><button></button><button></button><button></button>
`,
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, `
<div>Length: 3</div>
<div>Values: 4,5,1</div>
<div d="4" e="5" foo="1"></div>
<button></button><button></button><button></button><button></button>
`);
await btn2.dispatchEvent(clickEvent);
assert.htmlEqual(target.innerHTML, `
<div>Length: 3</div>
<div>Values: 34,5,1</div>
<div d="34" e="5" foo="1"></div>
<button></button><button></button><button></button><button></button>
`);
await btn3.dispatchEvent(clickEvent);
assert.htmlEqual(target.innerHTML, `
<div>Length: 3</div>
<div>Values: 34,5,31</div>
<div d="34" e="5" foo="31"></div>
<button></button><button></button><button></button><button></button>
`);
await btn4.dispatchEvent(clickEvent);
assert.htmlEqual(target.innerHTML, `
<div>Length: 4</div>
<div>Values: 34,5,31,2</div>
<div d="34" e="5" foo="31" bar="2"></div>
<button></button><button></button><button></button><button></button>
`);
}
};

@ -0,0 +1,25 @@
<script>
import App from './App.svelte';
let a = 1, b = 2, c = 3, d = 4, e = 5;
let f = { foo: 1 };
function updateProps() {
a = 31;
b = 32;
}
function updateRest() {
d = 34;
}
function updateSpread() {
f.foo = 31;
}
function updateSpread2() {
f.bar = 2;
}
</script>
<App {a} {b} {c} {d} {e} {...f} />
<button on:click={updateProps}></button>
<button on:click={updateRest}></button>
<button on:click={updateSpread}></button>
<button on:click={updateSpread2}></button>

@ -0,0 +1,13 @@
<script>
export let a;
export function b() {}
export let c = 1;
$: length = Object.keys($$restProps).length;
$: values = Object.values($$restProps);
</script>
<div>Length: {length}</div>
<div>Values: {values.join(',')}</div>
<div {...$$restProps} />
<div {...$$props} />

@ -0,0 +1,60 @@
export default {
props: {
a: 3,
b: 4,
c: 5,
d: 6
},
html: `
<div>Length: 3</div>
<div>Values: 4,5,1</div>
<div d="4" e="5" foo="1"></div>
<div a="1" b="2" c="3" d="4" e="5" foo="1"></div>
<button></button><button></button><button></button><button></button>
`,
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, `
<div>Length: 3</div>
<div>Values: 4,5,1</div>
<div d="4" e="5" foo="1"></div>
<div a="31" b="32" c="3" d="4" e="5" foo="1"></div>
<button></button><button></button><button></button><button></button>
`);
await btn2.dispatchEvent(clickEvent);
assert.htmlEqual(target.innerHTML, `
<div>Length: 3</div>
<div>Values: 34,5,1</div>
<div d="34" e="5" foo="1"></div>
<div a="31" b="32" c="3" d="34" e="5" foo="1"></div>
<button></button><button></button><button></button><button></button>
`);
await btn3.dispatchEvent(clickEvent);
assert.htmlEqual(target.innerHTML, `
<div>Length: 3</div>
<div>Values: 34,5,31</div>
<div d="34" e="5" foo="31"></div>
<div a="31" b="32" c="3" d="34" e="5" foo="31"></div>
<button></button><button></button><button></button><button></button>
`);
await btn4.dispatchEvent(clickEvent);
assert.htmlEqual(target.innerHTML, `
<div>Length: 4</div>
<div>Values: 34,5,31,2</div>
<div d="34" e="5" foo="31" bar="2"></div>
<div a="31" b="32" c="3" d="34" e="5" foo="31" bar="2"></div>
<button></button><button></button><button></button><button></button>
`);
}
};

@ -0,0 +1,25 @@
<script>
import App from './App.svelte';
let a = 1, b = 2, c = 3, d = 4, e = 5;
let f = { foo: 1 };
function updateProps() {
a = 31;
b = 32;
}
function updateRest() {
d = 34;
}
function updateSpread() {
f.foo = 31;
}
function updateSpread2() {
f.bar = 2;
}
</script>
<App {a} {b} {c} {d} {e} {...f} />
<button on:click={updateProps}></button>
<button on:click={updateRest}></button>
<button on:click={updateSpread}></button>
<button on:click={updateSpread2}></button>

@ -4,4 +4,4 @@
</script>
<div>{foo}</div>
<div>{JSON.stringify($$props)}</div>
<div>{JSON.stringify($$restProps)}</div>

@ -0,0 +1,7 @@
<script>
export let foo = undefined;
</script>
<div>{foo}</div>
<div>{JSON.stringify($$props)}</div>

@ -0,0 +1,7 @@
export default {
compileOptions: {
dev: true
},
warnings: []
};

@ -0,0 +1,5 @@
<script>
import Foo from './Foo.svelte';
</script>
<Foo fo="sho"/>
Loading…
Cancel
Save