Skip to content

Commit a3b4eea

Browse files
authored
Merge pull request #954 from sveltejs/gh-930-computed
computed store properties
2 parents ede70be + d5d1ecc commit a3b4eea

File tree

58 files changed

+708
-79
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+708
-79
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
src/shared
22
shared.js
3+
store.js
34
test/test.js
45
test/setup.js
56
**/_actual.js

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"compiler",
88
"ssr",
99
"shared.js",
10+
"store.js",
1011
"README.md"
1112
],
1213
"scripts": {

src/generators/Generator.ts

+3
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,9 @@ export default class Generator {
536536
(param: Node) =>
537537
param.type === 'AssignmentPattern' ? param.left.name : param.name
538538
);
539+
deps.forEach(dep => {
540+
this.expectedProperties.add(dep);
541+
});
539542
dependencies.set(key, deps);
540543
});
541544

src/generators/dom/index.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -184,15 +184,23 @@ export default function dom(
184184
const debugName = `<${generator.customElement ? generator.tag : name}>`;
185185

186186
// generate initial state object
187-
const globals = Array.from(generator.expectedProperties).filter(prop => globalWhitelist.has(prop));
187+
const expectedProperties = Array.from(generator.expectedProperties);
188+
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
189+
const storeProps = options.store ? expectedProperties.filter(prop => prop[0] === '$') : [];
190+
188191
const initialState = [];
192+
189193
if (globals.length > 0) {
190194
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
191195
}
192196

197+
if (storeProps.length > 0) {
198+
initialState.push(`this.store._init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`);
199+
}
200+
193201
if (templateProperties.data) {
194202
initialState.push(`%data()`);
195-
} else if (globals.length === 0) {
203+
} else if (globals.length === 0 && storeProps.length === 0) {
196204
initialState.push('{}');
197205
}
198206

@@ -205,6 +213,7 @@ export default function dom(
205213
@init(this, options);
206214
${generator.usesRefs && `this.refs = {};`}
207215
this._state = @assign(${initialState.join(', ')});
216+
${storeProps.length > 0 && `this.store._add(this, [${storeProps.map(prop => `"${prop.slice(1)}"`)}]);`}
208217
${generator.metaBindings}
209218
${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`}
210219
${options.dev &&
@@ -215,7 +224,11 @@ export default function dom(
215224
${generator.bindingGroups.length &&
216225
`this._bindingGroups = [${Array(generator.bindingGroups.length).fill('[]').join(', ')}];`}
217226
218-
${templateProperties.ondestroy && `this._handlers.destroy = [%ondestroy]`}
227+
${(templateProperties.ondestroy || storeProps.length) && (
228+
`this._handlers.destroy = [${
229+
[templateProperties.ondestroy && `%ondestroy`, storeProps.length && `@removeFromStore`].filter(Boolean).join(', ')
230+
}];`
231+
)}
219232
220233
${generator.slots.size && `this._slotted = options.slots || {};`}
221234

src/generators/dom/visitors/Element/addBindings.ts

+37-5
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,19 @@ export default function addBindings(
195195

196196
const usesContext = group.bindings.some(binding => binding.handler.usesContext);
197197
const usesState = group.bindings.some(binding => binding.handler.usesState);
198+
const usesStore = group.bindings.some(binding => binding.handler.usesStore);
198199
const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');
199200

200201
const props = new Set();
202+
const storeProps = new Set();
201203
group.bindings.forEach(binding => {
202204
binding.handler.props.forEach(prop => {
203205
props.add(prop);
204206
});
207+
208+
binding.handler.storeProps.forEach(prop => {
209+
storeProps.add(prop);
210+
});
205211
}); // TODO use stringifyProps here, once indenting is fixed
206212

207213
// media bindings — awkward special case. The native timeupdate events
@@ -222,9 +228,11 @@ export default function addBindings(
222228
}
223229
${usesContext && `var context = ${node.var}._svelte;`}
224230
${usesState && `var state = #component.get();`}
231+
${usesStore && `var $ = #component.store.get();`}
225232
${needsLock && `${lock} = true;`}
226233
${mutations.length > 0 && mutations}
227-
#component.set({ ${Array.from(props).join(', ')} });
234+
${props.size > 0 && `#component.set({ ${Array.from(props).join(', ')} });`}
235+
${storeProps.size > 0 && `#component.store.set({ ${Array.from(storeProps).join(', ')} });`}
228236
${needsLock && `${lock} = false;`}
229237
}
230238
`);
@@ -307,6 +315,13 @@ function getEventHandler(
307315
dependencies: string[],
308316
value: string,
309317
) {
318+
let storeDependencies = [];
319+
320+
if (generator.options.store) {
321+
storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1));
322+
dependencies = dependencies.filter(prop => prop[0] !== '$');
323+
}
324+
310325
if (block.contexts.has(name)) {
311326
const tail = attribute.value.type === 'MemberExpression'
312327
? getTailSnippet(attribute.value)
@@ -318,8 +333,10 @@ function getEventHandler(
318333
return {
319334
usesContext: true,
320335
usesState: true,
336+
usesStore: storeDependencies.length > 0,
321337
mutation: `${list}[${index}]${tail} = ${value};`,
322-
props: dependencies.map(prop => `${prop}: state.${prop}`)
338+
props: dependencies.map(prop => `${prop}: state.${prop}`),
339+
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
323340
};
324341
}
325342

@@ -336,16 +353,31 @@ function getEventHandler(
336353
return {
337354
usesContext: false,
338355
usesState: true,
356+
usesStore: storeDependencies.length > 0,
339357
mutation: `${snippet} = ${value}`,
340-
props: dependencies.map((prop: string) => `${prop}: state.${prop}`)
358+
props: dependencies.map((prop: string) => `${prop}: state.${prop}`),
359+
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
341360
};
342361
}
343362

363+
let props;
364+
let storeProps;
365+
366+
if (generator.options.store && name[0] === '$') {
367+
props = [];
368+
storeProps = [`${name.slice(1)}: ${value}`];
369+
} else {
370+
props = [`${name}: ${value}`];
371+
storeProps = [];
372+
}
373+
344374
return {
345375
usesContext: false,
346376
usesState: false,
377+
usesStore: false,
347378
mutation: null,
348-
props: [`${name}: ${value}`]
379+
props,
380+
storeProps
349381
};
350382
}
351383

@@ -393,4 +425,4 @@ function isComputed(node: Node) {
393425
}
394426

395427
return false;
396-
}
428+
}

src/generators/server-side-rendering/index.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,22 @@ export default function ssr(
7373
generator.stylesheet.render(options.filename, true);
7474

7575
// generate initial state object
76-
// TODO this doesn't work, because expectedProperties isn't populated
77-
const globals = Array.from(generator.expectedProperties).filter(prop => globalWhitelist.has(prop));
76+
const expectedProperties = Array.from(generator.expectedProperties);
77+
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
78+
const storeProps = options.store ? expectedProperties.filter(prop => prop[0] === '$') : [];
79+
7880
const initialState = [];
7981
if (globals.length > 0) {
8082
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
8183
}
8284

85+
if (storeProps.length > 0) {
86+
initialState.push(`options.store._init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`);
87+
}
88+
8389
if (templateProperties.data) {
8490
initialState.push(`%data()`);
85-
} else if (globals.length === 0) {
91+
} else if (globals.length === 0 && storeProps.length === 0) {
8692
initialState.push('{}');
8793
}
8894

@@ -99,7 +105,7 @@ export default function ssr(
99105
return ${templateProperties.data ? `%data()` : `{}`};
100106
};
101107
102-
${name}.render = function(state, options) {
108+
${name}.render = function(state, options = {}) {
103109
state = Object.assign(${initialState.join(', ')});
104110
105111
${computations.map(

src/generators/server-side-rendering/visitors/Component.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ export default function visitComponent(
7979

8080
let open = `\${${expression}.render({${props}}`;
8181

82+
const options = [];
83+
if (generator.options.store) {
84+
options.push(`store: options.store`);
85+
}
86+
8287
if (node.children.length) {
8388
const appendTarget: AppendTarget = {
8489
slots: { default: '' },
@@ -95,11 +100,15 @@ export default function visitComponent(
95100
.map(name => `${name}: () => \`${appendTarget.slots[name]}\``)
96101
.join(', ');
97102

98-
open += `, { slotted: { ${slotted} } }`;
103+
options.push(`slotted: { ${slotted} }`);
99104

100105
generator.appendTargets.pop();
101106
}
102107

108+
if (options.length) {
109+
open += `, { ${options.join(', ')} }`;
110+
}
111+
103112
generator.append(open);
104113
generator.append(')}');
105114
}

src/interfaces.ts

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface CompileOptions {
5656
legacy?: boolean;
5757
customElement?: CustomElementOptions | true;
5858
css?: boolean;
59+
store?: boolean;
5960

6061
onerror?: (error: Error) => void;
6162
onwarn?: (warning: Warning) => void;

src/server-side-rendering/register.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@ import * as fs from 'fs';
22
import * as path from 'path';
33
import { compile } from '../index.ts';
44

5+
const compileOptions = {};
6+
57
function capitalise(name) {
68
return name[0].toUpperCase() + name.slice(1);
79
}
810

911
export default function register(options) {
1012
const { extensions } = options;
13+
1114
if (extensions) {
1215
_deregister('.html');
1316
extensions.forEach(_register);
1417
}
18+
19+
// TODO make this the default and remove in v2
20+
if ('store' in options) compileOptions.store = options.store;
1521
}
1622

1723
function _deregister(extension) {
@@ -20,13 +26,15 @@ function _deregister(extension) {
2026

2127
function _register(extension) {
2228
require.extensions[extension] = function(module, filename) {
23-
const {code} = compile(fs.readFileSync(filename, 'utf-8'), {
29+
const options = Object.assign({}, compileOptions, {
2430
filename,
2531
name: capitalise(path.basename(filename)
2632
.replace(new RegExp(`${extension.replace('.', '\\.')}$`), '')),
27-
generate: 'ssr',
33+
generate: 'ssr'
2834
});
2935

36+
const {code} = compile(fs.readFileSync(filename, 'utf-8'), options);
37+
3038
return module._compile(code, filename);
3139
};
3240
}

src/shared/index.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,13 @@ export function get(key) {
6565
}
6666

6767
export function init(component, options) {
68-
component.options = options;
69-
7068
component._observers = { pre: blankObject(), post: blankObject() };
7169
component._handlers = blankObject();
7270
component._root = options._root || component;
7371
component._bind = options._bind;
72+
73+
component.options = options;
74+
component.store = component._root.options.store;
7475
}
7576

7677
export function observe(key, callback, options) {
@@ -187,6 +188,10 @@ export function _unmount() {
187188
this._fragment.u();
188189
}
189190

191+
export function removeFromStore() {
192+
this.store._remove(this);
193+
}
194+
190195
export var proto = {
191196
destroy: destroy,
192197
get: get,

src/validate/html/validateEventHandler.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import flattenReference from '../../utils/flattenReference';
22
import list from '../../utils/list';
3-
import { Validator } from '../index';
3+
import validate, { Validator } from '../index';
44
import validCalleeObjects from '../../utils/validCalleeObjects';
55
import { Node } from '../../interfaces';
66

@@ -28,13 +28,21 @@ export default function validateEventHandlerCallee(
2828
return;
2929
}
3030

31+
if (name === 'store' && attribute.expression.callee.type === 'MemberExpression') {
32+
if (!validator.options.store) {
33+
validator.warn('compile with `store: true` in order to call store methods', attribute.expression.start);
34+
}
35+
return;
36+
}
37+
3138
if (
3239
(callee.type === 'Identifier' && validBuiltins.has(callee.name)) ||
3340
validator.methods.has(callee.name)
3441
)
3542
return;
3643

3744
const validCallees = ['this.*', 'event.*', 'options.*', 'console.*'].concat(
45+
validator.options.store ? 'store.*' : [],
3846
Array.from(validBuiltins),
3947
Array.from(validator.methods.keys())
4048
);

src/validate/index.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class Validator {
2222
readonly source: string;
2323
readonly filename: string;
2424

25+
options: CompileOptions;
2526
onwarn: ({}) => void;
2627
locator?: (pos: number) => Location;
2728

@@ -37,8 +38,8 @@ export class Validator {
3738
constructor(parsed: Parsed, source: string, options: CompileOptions) {
3839
this.source = source;
3940
this.filename = options.filename;
40-
4141
this.onwarn = options.onwarn;
42+
this.options = options;
4243

4344
this.namespace = null;
4445
this.defaultExport = null;
@@ -78,7 +79,7 @@ export default function validate(
7879
stylesheet: Stylesheet,
7980
options: CompileOptions
8081
) {
81-
const { onwarn, onerror, name, filename } = options;
82+
const { onwarn, onerror, name, filename, store } = options;
8283

8384
try {
8485
if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) {
@@ -99,6 +100,7 @@ export default function validate(
99100
onwarn,
100101
name,
101102
filename,
103+
store
102104
});
103105

104106
if (parsed.js) {

0 commit comments

Comments
 (0)