Merge pull request #954 from sveltejs/gh-930-computed

computed store properties
pull/965/head
Rich Harris 7 years ago committed by GitHub
commit a3b4eea3d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,6 @@
src/shared
shared.js
store.js
test/test.js
test/setup.js
**/_actual.js

@ -7,6 +7,7 @@
"compiler",
"ssr",
"shared.js",
"store.js",
"README.md"
],
"scripts": {

@ -536,6 +536,9 @@ export default class Generator {
(param: Node) =>
param.type === 'AssignmentPattern' ? param.left.name : param.name
);
deps.forEach(dep => {
this.expectedProperties.add(dep);
});
dependencies.set(key, deps);
});

@ -184,15 +184,23 @@ export default function dom(
const debugName = `<${generator.customElement ? generator.tag : name}>`;
// generate initial state object
const globals = Array.from(generator.expectedProperties).filter(prop => globalWhitelist.has(prop));
const expectedProperties = Array.from(generator.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = options.store ? expectedProperties.filter(prop => prop[0] === '$') : [];
const initialState = [];
if (globals.length > 0) {
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
}
if (storeProps.length > 0) {
initialState.push(`this.store._init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`);
}
if (templateProperties.data) {
initialState.push(`%data()`);
} else if (globals.length === 0) {
} else if (globals.length === 0 && storeProps.length === 0) {
initialState.push('{}');
}
@ -205,6 +213,7 @@ export default function dom(
@init(this, options);
${generator.usesRefs && `this.refs = {};`}
this._state = @assign(${initialState.join(', ')});
${storeProps.length > 0 && `this.store._add(this, [${storeProps.map(prop => `"${prop.slice(1)}"`)}]);`}
${generator.metaBindings}
${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`}
${options.dev &&
@ -215,7 +224,11 @@ export default function dom(
${generator.bindingGroups.length &&
`this._bindingGroups = [${Array(generator.bindingGroups.length).fill('[]').join(', ')}];`}
${templateProperties.ondestroy && `this._handlers.destroy = [%ondestroy]`}
${(templateProperties.ondestroy || storeProps.length) && (
`this._handlers.destroy = [${
[templateProperties.ondestroy && `%ondestroy`, storeProps.length && `@removeFromStore`].filter(Boolean).join(', ')
}];`
)}
${generator.slots.size && `this._slotted = options.slots || {};`}

@ -195,13 +195,19 @@ export default function addBindings(
const usesContext = group.bindings.some(binding => binding.handler.usesContext);
const usesState = group.bindings.some(binding => binding.handler.usesState);
const usesStore = group.bindings.some(binding => binding.handler.usesStore);
const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');
const props = new Set();
const storeProps = new Set();
group.bindings.forEach(binding => {
binding.handler.props.forEach(prop => {
props.add(prop);
});
binding.handler.storeProps.forEach(prop => {
storeProps.add(prop);
});
}); // TODO use stringifyProps here, once indenting is fixed
// media bindings — awkward special case. The native timeupdate events
@ -222,9 +228,11 @@ export default function addBindings(
}
${usesContext && `var context = ${node.var}._svelte;`}
${usesState && `var state = #component.get();`}
${usesStore && `var $ = #component.store.get();`}
${needsLock && `${lock} = true;`}
${mutations.length > 0 && mutations}
#component.set({ ${Array.from(props).join(', ')} });
${props.size > 0 && `#component.set({ ${Array.from(props).join(', ')} });`}
${storeProps.size > 0 && `#component.store.set({ ${Array.from(storeProps).join(', ')} });`}
${needsLock && `${lock} = false;`}
}
`);
@ -307,6 +315,13 @@ function getEventHandler(
dependencies: string[],
value: string,
) {
let storeDependencies = [];
if (generator.options.store) {
storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1));
dependencies = dependencies.filter(prop => prop[0] !== '$');
}
if (block.contexts.has(name)) {
const tail = attribute.value.type === 'MemberExpression'
? getTailSnippet(attribute.value)
@ -318,8 +333,10 @@ function getEventHandler(
return {
usesContext: true,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${list}[${index}]${tail} = ${value};`,
props: dependencies.map(prop => `${prop}: state.${prop}`)
props: dependencies.map(prop => `${prop}: state.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
};
}
@ -336,16 +353,31 @@ function getEventHandler(
return {
usesContext: false,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${snippet} = ${value}`,
props: dependencies.map((prop: string) => `${prop}: state.${prop}`)
props: dependencies.map((prop: string) => `${prop}: state.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
};
}
let props;
let storeProps;
if (generator.options.store && name[0] === '$') {
props = [];
storeProps = [`${name.slice(1)}: ${value}`];
} else {
props = [`${name}: ${value}`];
storeProps = [];
}
return {
usesContext: false,
usesState: false,
usesStore: false,
mutation: null,
props: [`${name}: ${value}`]
props,
storeProps
};
}
@ -393,4 +425,4 @@ function isComputed(node: Node) {
}
return false;
}
}

@ -73,16 +73,22 @@ export default function ssr(
generator.stylesheet.render(options.filename, true);
// generate initial state object
// TODO this doesn't work, because expectedProperties isn't populated
const globals = Array.from(generator.expectedProperties).filter(prop => globalWhitelist.has(prop));
const expectedProperties = Array.from(generator.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = options.store ? expectedProperties.filter(prop => prop[0] === '$') : [];
const initialState = [];
if (globals.length > 0) {
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
}
if (storeProps.length > 0) {
initialState.push(`options.store._init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`);
}
if (templateProperties.data) {
initialState.push(`%data()`);
} else if (globals.length === 0) {
} else if (globals.length === 0 && storeProps.length === 0) {
initialState.push('{}');
}
@ -99,7 +105,7 @@ export default function ssr(
return ${templateProperties.data ? `%data()` : `{}`};
};
${name}.render = function(state, options) {
${name}.render = function(state, options = {}) {
state = Object.assign(${initialState.join(', ')});
${computations.map(

@ -79,6 +79,11 @@ export default function visitComponent(
let open = `\${${expression}.render({${props}}`;
const options = [];
if (generator.options.store) {
options.push(`store: options.store`);
}
if (node.children.length) {
const appendTarget: AppendTarget = {
slots: { default: '' },
@ -95,11 +100,15 @@ export default function visitComponent(
.map(name => `${name}: () => \`${appendTarget.slots[name]}\``)
.join(', ');
open += `, { slotted: { ${slotted} } }`;
options.push(`slotted: { ${slotted} }`);
generator.appendTargets.pop();
}
if (options.length) {
open += `, { ${options.join(', ')} }`;
}
generator.append(open);
generator.append(')}');
}

@ -56,6 +56,7 @@ export interface CompileOptions {
legacy?: boolean;
customElement?: CustomElementOptions | true;
css?: boolean;
store?: boolean;
onerror?: (error: Error) => void;
onwarn?: (warning: Warning) => void;

@ -2,16 +2,22 @@ import * as fs from 'fs';
import * as path from 'path';
import { compile } from '../index.ts';
const compileOptions = {};
function capitalise(name) {
return name[0].toUpperCase() + name.slice(1);
}
export default function register(options) {
const { extensions } = options;
if (extensions) {
_deregister('.html');
extensions.forEach(_register);
}
// TODO make this the default and remove in v2
if ('store' in options) compileOptions.store = options.store;
}
function _deregister(extension) {
@ -20,13 +26,15 @@ function _deregister(extension) {
function _register(extension) {
require.extensions[extension] = function(module, filename) {
const {code} = compile(fs.readFileSync(filename, 'utf-8'), {
const options = Object.assign({}, compileOptions, {
filename,
name: capitalise(path.basename(filename)
.replace(new RegExp(`${extension.replace('.', '\\.')}$`), '')),
generate: 'ssr',
generate: 'ssr'
});
const {code} = compile(fs.readFileSync(filename, 'utf-8'), options);
return module._compile(code, filename);
};
}

@ -65,12 +65,13 @@ export function get(key) {
}
export 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;
component.options = options;
component.store = component._root.options.store;
}
export function observe(key, callback, options) {
@ -187,6 +188,10 @@ export function _unmount() {
this._fragment.u();
}
export function removeFromStore() {
this.store._remove(this);
}
export var proto = {
destroy: destroy,
get: get,

@ -1,6 +1,6 @@
import flattenReference from '../../utils/flattenReference';
import list from '../../utils/list';
import { Validator } from '../index';
import validate, { Validator } from '../index';
import validCalleeObjects from '../../utils/validCalleeObjects';
import { Node } from '../../interfaces';
@ -28,6 +28,13 @@ export default function validateEventHandlerCallee(
return;
}
if (name === 'store' && attribute.expression.callee.type === 'MemberExpression') {
if (!validator.options.store) {
validator.warn('compile with `store: true` in order to call store methods', attribute.expression.start);
}
return;
}
if (
(callee.type === 'Identifier' && validBuiltins.has(callee.name)) ||
validator.methods.has(callee.name)
@ -35,6 +42,7 @@ export default function validateEventHandlerCallee(
return;
const validCallees = ['this.*', 'event.*', 'options.*', 'console.*'].concat(
validator.options.store ? 'store.*' : [],
Array.from(validBuiltins),
Array.from(validator.methods.keys())
);

@ -22,6 +22,7 @@ export class Validator {
readonly source: string;
readonly filename: string;
options: CompileOptions;
onwarn: ({}) => void;
locator?: (pos: number) => Location;
@ -37,8 +38,8 @@ export class Validator {
constructor(parsed: Parsed, source: string, options: CompileOptions) {
this.source = source;
this.filename = options.filename;
this.onwarn = options.onwarn;
this.options = options;
this.namespace = null;
this.defaultExport = null;
@ -78,7 +79,7 @@ export default function validate(
stylesheet: Stylesheet,
options: CompileOptions
) {
const { onwarn, onerror, name, filename } = options;
const { onwarn, onerror, name, filename, store } = options;
try {
if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) {
@ -99,6 +100,7 @@ export default function validate(
onwarn,
name,
filename,
store
});
if (parsed.js) {

@ -0,0 +1,153 @@
import {
assign,
blankObject,
differs,
dispatchObservers,
get,
observe
} from './shared.js';
function Store(state) {
this._observers = { pre: blankObject(), post: blankObject() };
this._changeHandlers = [];
this._dependents = [];
this._computed = blankObject();
this._sortedComputedProperties = [];
this._state = assign({}, state);
}
assign(Store.prototype, {
_add: function(component, props) {
this._dependents.push({
component: component,
props: props
});
},
_init: function(props) {
var state = {};
for (var i = 0; i < props.length; i += 1) {
var prop = props[i];
state['$' + prop] = this._state[prop];
}
return state;
},
_remove: function(component) {
var i = this._dependents.length;
while (i--) {
if (this._dependents[i].component === component) {
this._dependents.splice(i, 1);
return;
}
}
},
_sortComputedProperties: function() {
var computed = this._computed;
var sorted = this._sortedComputedProperties = [];
var visited = blankObject();
function visit(key) {
if (visited[key]) return;
var c = computed[key];
if (c) {
c.deps.forEach(visit);
sorted.push(c);
}
}
for (var key in this._computed) visit(key);
},
compute: function(key, deps, fn) {
var store = this;
var value;
var c = {
deps: deps,
update: function(state, changed, dirty) {
var values = deps.map(function(dep) {
if (dep in changed) dirty = true;
return state[dep];
});
if (dirty) {
var newValue = fn.apply(null, values);
if (differs(newValue, value)) {
value = newValue;
changed[key] = true;
state[key] = value;
}
}
}
};
c.update(this._state, {}, true);
this._computed[key] = c;
this._sortComputedProperties();
},
get: get,
observe: observe,
onchange: function(callback) {
this._changeHandlers.push(callback);
return {
cancel: function() {
var index = this._changeHandlers.indexOf(callback);
if (~index) this._changeHandlers.splice(index, 1);
}
};
},
set: function(newState) {
var oldState = this._state,
changed = this._changed = {},
dirty = false;
for (var key in newState) {
if (this._computed[key]) throw new Error("'" + key + "' is a read-only property");
if (differs(newState[key], oldState[key])) changed[key] = dirty = true;
}
if (!dirty) return;
this._state = assign({}, oldState, newState);
for (var i = 0; i < this._sortedComputedProperties.length; i += 1) {
this._sortedComputedProperties[i].update(this._state, changed);
}
for (var i = 0; i < this._changeHandlers.length; i += 1) {
this._changeHandlers[i](this._state, changed);
}
dispatchObservers(this, this._observers.pre, changed, this._state, oldState);
var dependents = this._dependents.slice(); // guard against mutations
for (var i = 0; i < dependents.length; i += 1) {
var dependent = dependents[i];
var componentState = {};
dirty = false;
for (var j = 0; j < dependent.props.length; j += 1) {
var prop = dependent.props[j];
if (prop in changed) {
componentState['$' + prop] = this._state[prop];
dirty = true;
}
}
if (dirty) dependent.component.set(componentState);
}
dispatchObservers(this, this._observers.post, changed, this._state, oldState);
}
});
export { Store };

@ -91,12 +91,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -67,12 +67,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -67,12 +67,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -87,12 +87,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -79,12 +79,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -83,12 +83,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -87,12 +87,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -99,12 +99,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -79,12 +79,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -83,12 +83,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -83,12 +83,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -83,12 +83,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -83,12 +83,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -83,12 +83,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -83,12 +83,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -87,12 +87,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -85,12 +85,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -102,12 +102,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -95,12 +95,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -81,12 +81,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -67,12 +67,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -24,7 +24,7 @@ function SvelteComponent(options) {
init(this, options);
this._state = assign({}, options.data);
this._handlers.destroy = [ondestroy]
this._handlers.destroy = [ondestroy];
var _oncreate = oncreate.bind(this);

@ -67,12 +67,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -4,7 +4,7 @@ SvelteComponent.data = function() {
return {};
};
SvelteComponent.render = function(state, options) {
SvelteComponent.render = function(state, options = {}) {
state = Object.assign({}, state);
return ``.trim();

@ -6,7 +6,7 @@ SvelteComponent.data = function() {
return {};
};
SvelteComponent.render = function(state, options) {
SvelteComponent.render = function(state, options = {}) {
state = Object.assign({}, state);
return ``.trim();

@ -91,12 +91,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -87,12 +87,13 @@ function get(key) {
}
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;
component.options = options;
component.store = component._root.options.store;
}
function observe(key, callback, options) {

@ -63,6 +63,7 @@ describe("runtime", () => {
compileOptions.shared = shared;
compileOptions.hydratable = hydrate;
compileOptions.dev = config.dev;
compileOptions.store = !!config.store;
// check that no ES2015+ syntax slipped in
if (!config.allowES2015) {
@ -88,7 +89,7 @@ describe("runtime", () => {
}
} catch (err) {
failed.add(dir);
showOutput(cwd, { shared }, svelte); // eslint-disable-line no-console
showOutput(cwd, { shared, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console
throw err;
}
}
@ -134,7 +135,7 @@ describe("runtime", () => {
try {
SvelteComponent = require(`./samples/${dir}/main.html`);
} catch (err) {
showOutput(cwd, { shared, hydratable: hydrate }, svelte); // eslint-disable-line no-console
showOutput(cwd, { shared, hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console
throw err;
}
@ -154,7 +155,8 @@ describe("runtime", () => {
const options = Object.assign({}, {
target,
hydrate,
data: config.data
data: config.data,
store: config.store
}, config.options || {});
const component = new SvelteComponent(options);
@ -188,12 +190,12 @@ describe("runtime", () => {
config.error(assert, err);
} else {
failed.add(dir);
showOutput(cwd, { shared, hydratable: hydrate }, svelte); // eslint-disable-line no-console
showOutput(cwd, { shared, hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console
throw err;
}
}
if (config.show) showOutput(cwd, { shared, hydratable: hydrate }, svelte);
if (config.show) showOutput(cwd, { shared, hydratable: hydrate, store: !!compileOptions.store }, svelte);
});
}

@ -1,6 +1,10 @@
export default {
dev: true,
data: {
a: 42
},
test ( assert, component ) {
try {
component.set({ foo: 1 });

@ -1,10 +1,14 @@
export default {
dev: true,
data: {
a: 42
},
test ( assert, component ) {
const obj = { a: 1 };
component.set( obj );
component.set( obj ); // will fail if the object is not cloned
component.destroy();
}
}
};

@ -0,0 +1,28 @@
import { Store } from '../../../../store.js';
const store = new Store({
name: 'world'
});
export default {
store,
html: `
<h1>Hello world!</h1>
<input>
`,
test(assert, component, target, window) {
const input = target.querySelector('input');
const event = new window.Event('input');
input.value = 'everybody';
input.dispatchEvent(event);
assert.equal(store.get('name'), 'everybody');
assert.htmlEqual(target.innerHTML, `
<h1>Hello everybody!</h1>
<input>
`);
}
};

@ -0,0 +1,10 @@
<h1>Hello {{$name}}!</h1>
<NameInput/>
<script>
import NameInput from './NameInput.html';
export default {
components: { NameInput }
};
</script>

@ -0,0 +1,15 @@
{{#if isVisible}}
<div class='{{todo.done ? "done": "pending"}}'>{{todo.description}}</div>
{{/if}}
<script>
export default {
computed: {
isVisible: ($filter, todo) => {
if ($filter === 'all') return true;
if ($filter === 'done') return todo.done;
if ($filter === 'pending') return !todo.done;
}
}
};
</script>

@ -0,0 +1,63 @@
import { Store } from '../../../../store.js';
class MyStore extends Store {
setFilter(filter) {
this.set({ filter });
}
toggleTodo(todo) {
todo.done = !todo.done;
this.set({ todos: this.get('todos') });
}
}
const todos = [
{
description: 'Buy some milk',
done: true,
},
{
description: 'Do the laundry',
done: true,
},
{
description: "Find life's true purpose",
done: false,
}
];
const store = new MyStore({
filter: 'all',
todos
});
export default {
store,
html: `
<div class='done'>Buy some milk</div>
<div class='done'>Do the laundry</div>
<div class='pending'>Find life's true purpose</div>
`,
test(assert, component, target) {
store.setFilter('pending');
assert.htmlEqual(target.innerHTML, `
<div class='pending'>Find life's true purpose</div>
`);
store.toggleTodo(todos[1]);
assert.htmlEqual(target.innerHTML, `
<div class='pending'>Do the laundry</div>
<div class='pending'>Find life's true purpose</div>
`);
store.setFilter('done');
assert.htmlEqual(target.innerHTML, `
<div class='done'>Buy some milk</div>
`);
}
};

@ -0,0 +1,11 @@
{{#each $todos as todo}}
<Todo :todo/>
{{/each}}
<script>
import Todo from './Todo.html';
export default {
components: { Todo }
};
</script>

@ -0,0 +1 @@
<input on:input='store.setName(this.value)'>

@ -0,0 +1,34 @@
import { Store } from '../../../../store.js';
class MyStore extends Store {
setName(name) {
this.set({ name });
}
}
const store = new MyStore({
name: 'world'
});
export default {
store,
html: `
<h1>Hello world!</h1>
<input>
`,
test(assert, component, target, window) {
const input = target.querySelector('input');
const event = new window.Event('input');
input.value = 'everybody';
input.dispatchEvent(event);
assert.equal(store.get('name'), 'everybody');
assert.htmlEqual(target.innerHTML, `
<h1>Hello everybody!</h1>
<input>
`);
}
};

@ -0,0 +1,10 @@
<h1>Hello {{$name}}!</h1>
<NameInput/>
<script>
import NameInput from './NameInput.html';
export default {
components: { NameInput }
};
</script>

@ -0,0 +1,17 @@
import { Store } from '../../../../store.js';
const store = new Store({
name: 'world'
});
export default {
store,
html: `<h1>Hello world!</h1>`,
test(assert, component, target) {
store.set({ name: 'everybody' });
assert.htmlEqual(target.innerHTML, `<h1>Hello everybody!</h1>`);
}
};

@ -0,0 +1 @@
<h1>Hello {{$name}}!</h1>

@ -22,7 +22,8 @@ function tryToReadFile(file) {
describe("ssr", () => {
before(() => {
require("../../ssr/register")({
extensions: ['.svelte', '.html']
extensions: ['.svelte', '.html'],
store: true
});
return setupHtmlEqual();
@ -98,9 +99,15 @@ describe("ssr", () => {
delete require.cache[resolved];
});
require("../../ssr/register")({
store: !!config.store
});
try {
const component = require(`../runtime/samples/${dir}/main.html`);
const html = component.render(config.data);
const html = component.render(config.data, {
store: config.store
});
if (config.html) {
assert.htmlEqual(html, config.html);

@ -0,0 +1,146 @@
import assert from 'assert';
import { Store, combineStores } from '../../store.js';
describe('store', () => {
describe('get', () => {
it('gets a specific key', () => {
const store = new Store({
foo: 'bar'
});
assert.equal(store.get('foo'), 'bar');
});
it('gets the entire state object', () => {
const store = new Store({
foo: 'bar'
});
assert.deepEqual(store.get(), { foo: 'bar' });
});
});
describe('set', () => {
it('sets state', () => {
const store = new Store();
store.set({
foo: 'bar'
});
assert.equal(store.get('foo'), 'bar');
});
});
describe('observe', () => {
it('observes state', () => {
let newFoo;
let oldFoo;
const store = new Store({
foo: 'bar'
});
store.observe('foo', (n, o) => {
newFoo = n;
oldFoo = o;
});
assert.equal(newFoo, 'bar');
assert.equal(oldFoo, undefined);
store.set({
foo: 'baz'
});
assert.equal(newFoo, 'baz');
assert.equal(oldFoo, 'bar');
});
});
describe('onchange', () => {
it('fires a callback when state changes', () => {
const store = new Store();
let count = 0;
let args;
store.onchange((state, changed) => {
count += 1;
args = { state, changed };
});
store.set({ foo: 'bar' });
assert.equal(count, 1);
assert.deepEqual(args, {
state: { foo: 'bar' },
changed: { foo: true }
});
// this should be a noop
store.set({ foo: 'bar' });
assert.equal(count, 1);
// this shouldn't
store.set({ foo: 'baz' });
assert.equal(count, 2);
assert.deepEqual(args, {
state: { foo: 'baz' },
changed: { foo: true }
});
});
});
describe('computed', () => {
it('computes a property based on data', () => {
const store = new Store({
foo: 1
});
store.compute('bar', ['foo'], foo => foo * 2);
assert.equal(store.get('bar'), 2);
const values = [];
store.observe('bar', bar => {
values.push(bar);
});
store.set({ foo: 2 });
assert.deepEqual(values, [2, 4]);
});
it('computes a property based on another computed property', () => {
const store = new Store({
foo: 1
});
store.compute('bar', ['foo'], foo => foo * 2);
store.compute('baz', ['bar'], bar => bar * 2);
assert.equal(store.get('baz'), 4);
const values = [];
store.observe('baz', baz => {
values.push(baz);
});
store.set({ foo: 2 });
assert.deepEqual(values, [4, 8]);
});
it('prevents computed properties from being set', () => {
const store = new Store({
foo: 1
});
store.compute('bar', ['foo'], foo => foo * 2);
assert.throws(() => {
store.set({ bar: 'whatever' });
}, /'bar' is a read-only property/);
});
});
});

@ -0,0 +1 @@
<button on:click='store.foo()'>foo</button>

@ -0,0 +1,8 @@
[{
"message": "compile with `store: true` in order to call store methods",
"loc": {
"line": 1,
"column": 18
},
"pos": 18
}]
Loading…
Cancel
Save