mirror of https://github.com/sveltejs/svelte
commit
82fc0f2713
@ -1,5 +1,6 @@
|
||||
src/shared
|
||||
shared.js
|
||||
store.js
|
||||
test/test.js
|
||||
test/setup.js
|
||||
**/_actual.js
|
||||
|
@ -0,0 +1,164 @@
|
||||
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 cycles;
|
||||
var visited = blankObject();
|
||||
|
||||
function visit(key) {
|
||||
if (cycles[key]) {
|
||||
throw new Error('Cyclical dependency detected');
|
||||
}
|
||||
|
||||
if (visited[key]) return;
|
||||
visited[key] = true;
|
||||
|
||||
var c = computed[key];
|
||||
|
||||
if (c) {
|
||||
cycles[key] = true;
|
||||
c.deps.forEach(visit);
|
||||
sorted.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
for (var key in this._computed) {
|
||||
cycles = blankObject();
|
||||
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 };
|
@ -0,0 +1,148 @@
|
||||
import assert from 'assert';
|
||||
import {svelte} from '../helpers.js';
|
||||
|
||||
describe('preprocess', () => {
|
||||
it('preprocesses entire component', () => {
|
||||
const source = `
|
||||
<h1>Hello __NAME__!</h1>
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
<h1>Hello world!</h1>
|
||||
`;
|
||||
|
||||
return svelte.preprocess(source, {
|
||||
markup: ({ content }) => {
|
||||
return {
|
||||
code: content.replace('__NAME__', 'world')
|
||||
};
|
||||
}
|
||||
}).then(processed => {
|
||||
assert.equal(processed.toString(), expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('preprocesses style', () => {
|
||||
const source = `
|
||||
<div class='brand-color'>$brand</div>
|
||||
|
||||
<style>
|
||||
.brand-color {
|
||||
color: $brand;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
<div class='brand-color'>$brand</div>
|
||||
|
||||
<style>
|
||||
.brand-color {
|
||||
color: purple;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
return svelte.preprocess(source, {
|
||||
style: ({ content }) => {
|
||||
return {
|
||||
code: content.replace('$brand', 'purple')
|
||||
};
|
||||
}
|
||||
}).then(processed => {
|
||||
assert.equal(processed.toString(), expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('preprocesses style asynchronously', () => {
|
||||
const source = `
|
||||
<div class='brand-color'>$brand</div>
|
||||
|
||||
<style>
|
||||
.brand-color {
|
||||
color: $brand;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
<div class='brand-color'>$brand</div>
|
||||
|
||||
<style>
|
||||
.brand-color {
|
||||
color: purple;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
return svelte.preprocess(source, {
|
||||
style: ({ content }) => {
|
||||
return Promise.resolve({
|
||||
code: content.replace('$brand', 'purple')
|
||||
});
|
||||
}
|
||||
}).then(processed => {
|
||||
assert.equal(processed.toString(), expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('preprocesses script', () => {
|
||||
const source = `
|
||||
<script>
|
||||
console.log(__THE_ANSWER__);
|
||||
</script>
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
<script>
|
||||
console.log(42);
|
||||
</script>
|
||||
`;
|
||||
|
||||
return svelte.preprocess(source, {
|
||||
script: ({ content }) => {
|
||||
return {
|
||||
code: content.replace('__THE_ANSWER__', '42')
|
||||
};
|
||||
}
|
||||
}).then(processed => {
|
||||
assert.equal(processed.toString(), expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('parses attributes', () => {
|
||||
const source = `
|
||||
<style type='text/scss' data-foo="bar" bool></style>
|
||||
`;
|
||||
|
||||
return svelte.preprocess(source, {
|
||||
style: ({ attributes }) => {
|
||||
assert.deepEqual(attributes, {
|
||||
type: 'text/scss',
|
||||
'data-foo': 'bar',
|
||||
bool: true
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores null/undefined returned from preprocessor', () => {
|
||||
const source = `
|
||||
<script>
|
||||
console.log('ignore me');
|
||||
</script>
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
<script>
|
||||
console.log('ignore me');
|
||||
</script>
|
||||
`;
|
||||
|
||||
return svelte.preprocess(source, {
|
||||
script: () => null
|
||||
}).then(processed => {
|
||||
assert.equal(processed.toString(), expected);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1 @@
|
||||
<a href={{href}}>link</a>
|
@ -0,0 +1,3 @@
|
||||
export default {
|
||||
html: `<a href='/cool'>link</a>`
|
||||
};
|
@ -0,0 +1,10 @@
|
||||
<Link x href="/cool"/>
|
||||
|
||||
<script>
|
||||
import Link from './Link.html';
|
||||
export default {
|
||||
components: {
|
||||
Link
|
||||
}
|
||||
};
|
||||
</script>
|
@ -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 @@
|
||||
<input bind:value=$name>
|
@ -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>
|
@ -0,0 +1,17 @@
|
||||
export default {
|
||||
test(assert, component, target, window, raf) {
|
||||
component.set({ visible: true });
|
||||
const div = target.querySelector('div');
|
||||
assert.equal(div.foo, 0);
|
||||
|
||||
raf.tick(50);
|
||||
assert.equal(div.foo, 100);
|
||||
|
||||
raf.tick(100);
|
||||
assert.equal(div.foo, 200);
|
||||
|
||||
raf.tick(101);
|
||||
|
||||
component.destroy();
|
||||
},
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
{{#if visible}}
|
||||
<div in:foo='{k: 200}'>fades in</div>
|
||||
{{/if}}
|
||||
|
||||
<script>
|
||||
export default {
|
||||
transitions: {
|
||||
foo: function(node, params) {
|
||||
return {
|
||||
duration: 100,
|
||||
tick: t => {
|
||||
node.foo = t * params.k;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -0,0 +1,190 @@
|
||||
import fs from 'fs';
|
||||
import assert from 'assert';
|
||||
import MagicString from 'magic-string';
|
||||
import { parse } from 'acorn';
|
||||
import { Store } from '../../store.js';
|
||||
|
||||
describe('store', () => {
|
||||
it('is written in ES5', () => {
|
||||
const source = fs.readFileSync('store.js', 'utf-8');
|
||||
|
||||
const ast = parse(source, {
|
||||
sourceType: 'module'
|
||||
});
|
||||
|
||||
const magicString = new MagicString(source);
|
||||
ast.body.forEach(node => {
|
||||
if (/^(Im|Ex)port/.test(node.type)) magicString.remove(node.start, node.end);
|
||||
});
|
||||
|
||||
parse(magicString.toString(), {
|
||||
ecmaVersion: 5
|
||||
});
|
||||
});
|
||||
|
||||
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/);
|
||||
});
|
||||
|
||||
it('allows multiple dependents to depend on the same computed property', () => {
|
||||
const store = new Store({
|
||||
a: 1
|
||||
});
|
||||
|
||||
store.compute('b', ['a'], a => a * 2);
|
||||
store.compute('c', ['b'], b => b * 3);
|
||||
store.compute('d', ['b'], b => b * 4);
|
||||
|
||||
assert.deepEqual(store.get(), { a: 1, b: 2, c: 6, d: 8 });
|
||||
|
||||
// bit cheeky, testing a private property, but whatever
|
||||
assert.equal(store._sortedComputedProperties.length, 3);
|
||||
});
|
||||
|
||||
it('prevents cyclical dependencies', () => {
|
||||
const store = new Store();
|
||||
|
||||
assert.throws(() => {
|
||||
store.compute('a', ['b'], b => b + 1);
|
||||
store.compute('b', ['a'], a => a + 1);
|
||||
}, /Cyclical dependency detected/);
|
||||
});
|
||||
});
|
||||
});
|
@ -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…
Reference in new issue