Skip to content

Commit 91d758e

Browse files
authored
introduce $$restProps (#4489)
1 parent 4872152 commit 91d758e

File tree

19 files changed

+272
-17
lines changed

19 files changed

+272
-17
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Svelte changelog
22

3+
## Unreleased
4+
5+
* Expose object of unknown props in `$$restProps` ([#2930](https://github.com/sveltejs/svelte/issues/2930))
6+
37
## 3.19.2
48

59
* 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))

site/content/docs/02-template-syntax.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,15 @@ An element or component can have multiple spread attributes, interspersed with r
113113
<Widget {...$$props}/>
114114
```
115115

116+
---
117+
118+
*`$$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.
119+
120+
```html
121+
<input {...$$restProps}>
122+
```
123+
124+
---
116125

117126
### Text expressions
118127

src/compiler/compile/Component.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { Node, ImportDeclaration, Identifier, Program, ExpressionStatement, Assi
2828
import add_to_set from './utils/add_to_set';
2929
import check_graph_for_cycles from './utils/check_graph_for_cycles';
3030
import { print, x, b } from 'code-red';
31+
import { is_reserved_keyword } from './utils/reserved_keywords';
3132

3233
interface ComponentOptions {
3334
namespace?: string;
@@ -185,7 +186,7 @@ export default class Component {
185186

186187
if (variable) {
187188
variable.referenced = true;
188-
} else if (name === '$$props') {
189+
} else if (is_reserved_keyword(name)) {
189190
this.add_var({
190191
name,
191192
injected: true,
@@ -649,7 +650,7 @@ export default class Component {
649650
reassigned: true,
650651
initialised: true,
651652
});
652-
} else if (name === '$$props') {
653+
} else if (is_reserved_keyword(name)) {
653654
this.add_var({
654655
name,
655656
injected: true,
@@ -1276,7 +1277,7 @@ export default class Component {
12761277

12771278
warn_if_undefined(name: string, node, template_scope: TemplateScope) {
12781279
if (name[0] === '$') {
1279-
if (name === '$' || name[1] === '$' && name !== '$$props') {
1280+
if (name === '$' || name[1] === '$' && !is_reserved_keyword(name)) {
12801281
this.error(node, {
12811282
code: 'illegal-global',
12821283
message: `${name} is an illegal variable name`
@@ -1285,7 +1286,7 @@ export default class Component {
12851286

12861287
this.has_reactive_assignments = true; // TODO does this belong here?
12871288

1288-
if (name === '$$props') return;
1289+
if (is_reserved_keyword(name)) return;
12891290

12901291
name = name.slice(1);
12911292
}

src/compiler/compile/nodes/shared/Expression.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { b } from 'code-red';
1313
import { invalidate } from '../../render_dom/invalidate';
1414
import { Node, FunctionExpression, Identifier } from 'estree';
1515
import { TemplateNode } from '../../../interfaces';
16+
import { is_reserved_keyword } from '../../utils/reserved_keywords';
1617

1718
type Owner = Wrapper | TemplateNode;
1819

@@ -158,7 +159,7 @@ export default class Expression {
158159
dynamic_dependencies() {
159160
return Array.from(this.dependencies).filter(name => {
160161
if (this.template_scope.is_let(name)) return true;
161-
if (name === '$$props') return true;
162+
if (is_reserved_keyword(name)) return true;
162163

163164
const variable = this.component.var_lookup.get(name);
164165
return is_dynamic(variable);
@@ -355,7 +356,7 @@ function get_function_name(_node, parent) {
355356
}
356357

357358
function is_contextual(component: Component, scope: TemplateScope, name: string) {
358-
if (name === '$$props') return true;
359+
if (is_reserved_keyword(name)) return true;
359360

360361
// if it's a name below root scope, it's contextual
361362
if (!scope.is_top_level(name)) return true;

src/compiler/compile/render_dom/Renderer.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import FragmentWrapper from './wrappers/Fragment';
55
import { x } from 'code-red';
66
import { Node, Identifier, MemberExpression, Literal, Expression, BinaryExpression } from 'estree';
77
import flatten_reference from '../utils/flatten_reference';
8+
import { reserved_keywords } from '../utils/reserved_keywords';
89

910
interface ContextMember {
1011
name: string;
@@ -50,9 +51,11 @@ export default class Renderer {
5051
// ensure store values are included in context
5152
component.vars.filter(v => v.subscribable).forEach(v => this.add_to_context(`$${v.name}`));
5253

53-
if (component.var_lookup.has('$$props')) {
54-
this.add_to_context('$$props');
55-
}
54+
reserved_keywords.forEach(keyword => {
55+
if (component.var_lookup.has(keyword)) {
56+
this.add_to_context(keyword);
57+
}
58+
});
5659

5760
if (component.slots.size > 0) {
5861
this.add_to_context('$$scope');

src/compiler/compile/render_dom/index.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,24 @@ export default function dom(
7171
}
7272

7373
const uses_props = component.var_lookup.has('$$props');
74-
const $$props = uses_props ? `$$new_props` : `$$props`;
74+
const uses_rest = component.var_lookup.has('$$restProps');
75+
const $$props = uses_props || uses_rest ? `$$new_props` : `$$props`;
7576
const props = component.vars.filter(variable => !variable.module && variable.export_name);
7677
const writable_props = props.filter(variable => variable.writable);
7778

78-
const set = (uses_props || writable_props.length > 0 || component.slots.size > 0)
79+
const omit_props_names = component.get_unique_name('omit_props_names');
80+
const compute_rest = x`@compute_rest_props($$props, ${omit_props_names.name})`;
81+
const rest = uses_rest ? b`
82+
const ${omit_props_names.name} = [${props.map(prop => `"${prop.export_name}"`).join(',')}];
83+
let $$restProps = ${compute_rest};
84+
` : null;
85+
86+
const set = (uses_props || uses_rest || writable_props.length > 0 || component.slots.size > 0)
7987
? x`
8088
${$$props} => {
8189
${uses_props && renderer.invalidate('$$props', x`$$props = @assign(@assign({}, $$props), @exclude_internal_props($$new_props))`)}
90+
${uses_rest && !uses_props && x`$$props = @assign(@assign({}, $$props), @exclude_internal_props($$new_props))`}
91+
${uses_rest && renderer.invalidate('$$restProps', x`$$restProps = ${compute_rest}`)}
8292
${writable_props.map(prop =>
8393
b`if ('${prop.export_name}' in ${$$props}) ${renderer.invalidate(prop.name, x`${prop.name} = ${$$props}.${prop.export_name}`)};`
8494
)}
@@ -341,20 +351,20 @@ export default function dom(
341351

342352
component.reactive_declarations.forEach(d => {
343353
const dependencies = Array.from(d.dependencies);
344-
const uses_props = !!dependencies.find(n => n === '$$props');
354+
const uses_rest_or_props = !!dependencies.find(n => n === '$$props' || n === '$$restProps');
345355

346356
const writable = dependencies.filter(n => {
347357
const variable = component.var_lookup.get(n);
348358
return variable && (variable.export_name || variable.mutated || variable.reassigned);
349359
});
350360

351-
const condition = !uses_props && writable.length > 0 && renderer.dirty(writable, true);
361+
const condition = !uses_rest_or_props && writable.length > 0 && renderer.dirty(writable, true);
352362

353363
let statement = d.node; // TODO remove label (use d.node.body) if it's not referenced
354364

355365
if (condition) statement = b`if (${condition}) { ${statement} }`[0] as Statement;
356366

357-
if (condition || uses_props) {
367+
if (condition || uses_rest_or_props) {
358368
reactive_declarations.push(statement);
359369
} else {
360370
fixed_reactive_declarations.push(statement);
@@ -383,7 +393,7 @@ export default function dom(
383393
});
384394

385395
let unknown_props_check;
386-
if (component.compile_options.dev && !component.var_lookup.has('$$props')) {
396+
if (component.compile_options.dev && !(uses_props || uses_rest)) {
387397
unknown_props_check = b`
388398
const writable_props = [${writable_props.map(prop => x`'${prop.export_name}'`)}];
389399
@_Object.keys($$props).forEach(key => {
@@ -402,6 +412,8 @@ export default function dom(
402412

403413
body.push(b`
404414
function ${definition}(${args}) {
415+
${rest}
416+
405417
${reactive_store_declarations}
406418
407419
${reactive_store_subscriptions}
@@ -473,7 +485,7 @@ export default function dom(
473485
@insert(options.target, this, options.anchor);
474486
}
475487
476-
${(props.length > 0 || uses_props) && b`
488+
${(props.length > 0 || uses_props || uses_rest) && b`
477489
if (options.props) {
478490
this.$set(options.props);
479491
@flush();

src/compiler/compile/render_ssr/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export default function ssr(
3131
{ code: null, map: null } :
3232
component.stylesheet.render(options.filename, true);
3333

34+
const uses_rest = component.var_lookup.has('$$restProps');
35+
const props = component.vars.filter(variable => !variable.module && variable.export_name);
36+
const rest = uses_rest ? b`let $$restProps = @compute_rest_props($$props, [${props.map(prop => `"${prop.export_name}"`).join(',')}]);` : null;
37+
3438
const reactive_stores = component.vars.filter(variable => variable.name[0] === '$' && variable.name[1] !== '$');
3539
const reactive_store_values = reactive_stores
3640
.map(({ name }) => {
@@ -130,6 +134,7 @@ export default function ssr(
130134
return ${literal};`;
131135

132136
const blocks = [
137+
rest,
133138
...reactive_stores.map(({ name }) => {
134139
const store_name = name.slice(1);
135140
const store = component.var_lookup.get(store_name);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const reserved_keywords = new Set(["$$props", "$$restProps"]);
2+
3+
export function is_reserved_keyword(name) {
4+
return reserved_keywords.has(name);
5+
}

src/runtime/internal/utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ export function exclude_internal_props(props) {
109109
return result;
110110
}
111111

112+
export function compute_rest_props(props, keys) {
113+
const rest = {};
114+
keys = new Set(keys);
115+
for (const k in props) if (!keys.has(k) && k[0] !== '$') rest[k] = props[k];
116+
return rest;
117+
}
118+
112119
export function once(fn) {
113120
let ran = false;
114121
return function(this: any, ...args) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
export let a;
3+
export function b() {}
4+
export let c = 1;
5+
6+
$: length = Object.keys($$restProps).length;
7+
$: values = Object.values($$restProps);
8+
</script>
9+
<div>Length: {length}</div>
10+
<div>Values: {values.join(',')}</div>
11+
12+
<div {...$$restProps} />
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
export default {
2+
props: {
3+
a: 3,
4+
b: 4,
5+
c: 5,
6+
d: 6
7+
},
8+
html: `
9+
<div>Length: 3</div>
10+
<div>Values: 4,5,1</div>
11+
<div d="4" e="5" foo="1"></div>
12+
<button></button><button></button><button></button><button></button>
13+
`,
14+
async test({ assert, target, window, }) {
15+
const [btn1, btn2, btn3, btn4] = target.querySelectorAll('button');
16+
const clickEvent = new window.MouseEvent('click');
17+
18+
await btn1.dispatchEvent(clickEvent);
19+
20+
assert.htmlEqual(target.innerHTML, `
21+
<div>Length: 3</div>
22+
<div>Values: 4,5,1</div>
23+
<div d="4" e="5" foo="1"></div>
24+
<button></button><button></button><button></button><button></button>
25+
`);
26+
27+
await btn2.dispatchEvent(clickEvent);
28+
29+
assert.htmlEqual(target.innerHTML, `
30+
<div>Length: 3</div>
31+
<div>Values: 34,5,1</div>
32+
<div d="34" e="5" foo="1"></div>
33+
<button></button><button></button><button></button><button></button>
34+
`);
35+
36+
await btn3.dispatchEvent(clickEvent);
37+
38+
assert.htmlEqual(target.innerHTML, `
39+
<div>Length: 3</div>
40+
<div>Values: 34,5,31</div>
41+
<div d="34" e="5" foo="31"></div>
42+
<button></button><button></button><button></button><button></button>
43+
`);
44+
45+
await btn4.dispatchEvent(clickEvent);
46+
47+
assert.htmlEqual(target.innerHTML, `
48+
<div>Length: 4</div>
49+
<div>Values: 34,5,31,2</div>
50+
<div d="34" e="5" foo="31" bar="2"></div>
51+
<button></button><button></button><button></button><button></button>
52+
`);
53+
}
54+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script>
2+
import App from './App.svelte';
3+
let a = 1, b = 2, c = 3, d = 4, e = 5;
4+
let f = { foo: 1 };
5+
6+
function updateProps() {
7+
a = 31;
8+
b = 32;
9+
}
10+
function updateRest() {
11+
d = 34;
12+
}
13+
function updateSpread() {
14+
f.foo = 31;
15+
}
16+
function updateSpread2() {
17+
f.bar = 2;
18+
}
19+
</script>
20+
21+
<App {a} {b} {c} {d} {e} {...f} />
22+
<button on:click={updateProps}></button>
23+
<button on:click={updateRest}></button>
24+
<button on:click={updateSpread}></button>
25+
<button on:click={updateSpread2}></button>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
export let a;
3+
export function b() {}
4+
export let c = 1;
5+
6+
$: length = Object.keys($$restProps).length;
7+
$: values = Object.values($$restProps);
8+
</script>
9+
<div>Length: {length}</div>
10+
<div>Values: {values.join(',')}</div>
11+
12+
<div {...$$restProps} />
13+
<div {...$$props} />

0 commit comments

Comments
 (0)