Comparing version 0.2.0 to 0.3.0
# Changelog | ||
## 0.3.0 - 2018-12-25 | ||
- New setState API inspired by Falcor paths to handle ranges. | ||
- Reduction in API to remove State object functions and change to React-like Hooks API syntax. | ||
- Expose reconcile method to do deep differences against immutable data sources (previously automatically done for selectors). | ||
- Removed 'from' operators as limited usefulness with new patterns. | ||
## 0.2.0 - 2018-11-13 | ||
@@ -4,0 +10,0 @@ - Large simplifications to remove inconsistency around wrapping and unwrapping values. State values are always wrapped get, and fully unwrapped on set. |
@@ -58,6 +58,6 @@ import S from 's-js'; | ||
function unwrap(item, deep) { | ||
function unwrap(item) { | ||
let result, unwrapped, v; | ||
if (result = item != null ? item._state : void 0) return result; | ||
if (!deep || !isObject(item) || (typeof item === 'function') || (item instanceof Element)) return item; | ||
if (!isObject(item) || (typeof item === 'function') || (item instanceof Element)) return item; | ||
if (Object.isFrozen(item)) item = clone(item); | ||
@@ -68,3 +68,3 @@ | ||
v = item[i]; | ||
if ((unwrapped = unwrap(v, deep)) !== v) item[i] = unwrapped; | ||
if ((unwrapped = unwrap(v)) !== v) item[i] = unwrapped; | ||
} | ||
@@ -75,3 +75,3 @@ } else { | ||
v = item[keys[i]]; | ||
if ((unwrapped = unwrap(v, deep)) !== v) item[keys[i]] = unwrapped; | ||
if ((unwrapped = unwrap(v)) !== v) item[keys[i]] = unwrapped; | ||
} | ||
@@ -85,2 +85,17 @@ } | ||
const proxyTraps = { | ||
get(target, property) { | ||
if (property === '_state') return target; | ||
const value = target[property]; | ||
if (S.isListening() && typeof value !== 'function') track(target, property, value); | ||
return (!isObject(value) || typeof value === 'function' || value instanceof Element) ? value : wrap(value); | ||
}, | ||
set() { return true; }, | ||
deleteProperty() { return true; } | ||
}; | ||
function wrap(value) { return value[SPROXY] || (value[SPROXY] = new Proxy(value, proxyTraps)); } | ||
function getDataNode(target) { | ||
@@ -92,7 +107,2 @@ let node = target[SNODE]; | ||
function trigger(node, property, notify) { | ||
if (node[property]) node[property].next(); | ||
if (notify && node._self) node._self.next(); | ||
} | ||
function track(target, property, value) { | ||
@@ -111,177 +121,82 @@ let node; | ||
function setNested(item, changes) { | ||
let node = getDataNode(item), | ||
isArray = Array.isArray(item), | ||
value, notify, keys; | ||
if (arguments.length === 3) { | ||
notify = isArray || !(arguments[1] in item); | ||
value = unwrap(arguments[2], true); | ||
if (item[arguments[1]] === value) return; | ||
if (value === void 0) { | ||
delete item[arguments[1]]; | ||
if (isArray) item.length--; | ||
} else item[arguments[1]] = value; | ||
trigger(node, arguments[1], notify); | ||
return; | ||
} | ||
keys = Object.keys(changes); | ||
for (let i = 0, l = keys.length; i < l; i++) { | ||
const property = keys[i]; | ||
notify = isArray || !(property in item); | ||
value = unwrap(changes[property], true); | ||
if (value === void 0) delete item[property]; | ||
else item[property] = value; | ||
trigger(node, property, notify); | ||
} | ||
function trigger(node, property, notify) { | ||
if (node[property]) node[property].next(); | ||
if (notify && node._self) node._self.next(); | ||
} | ||
function resolvePath(current, path, length) { | ||
let i = 0, temp; | ||
while (i < length && (temp = current[path[i]]) != null) { | ||
current = temp; | ||
i++; | ||
} | ||
return current; | ||
function setProperty(state, property, value) { | ||
value = unwrap(value); | ||
if (state[property] === value) return; | ||
const notify = Array.isArray(state) || !(property in state); | ||
if (value === void 0) delete state[property]; | ||
else state[property] = value; | ||
trigger(getDataNode(state), property, notify); | ||
} | ||
function wrap(value) { return value[SPROXY] || (value[SPROXY] = new Proxy(value, proxyTraps)); } | ||
function resolveAsync(value, fn) { | ||
if (!isObject(value)) return fn(value); | ||
if ('subscribe' in value) { | ||
const dispose = value.subscribe(fn); | ||
S.cleanup(function disposer() { dispose.unsubscribe(); }); | ||
return; | ||
function mergeState(state, value) { | ||
const keys = Object.keys(value) || []; | ||
for (let i = 0; i < keys.length; i += 1) { | ||
const key = keys[i]; | ||
setProperty(state, key, value[key]); | ||
} | ||
if ('then' in value) { | ||
value.then(fn); | ||
return; | ||
} | ||
fn(value); | ||
} | ||
const proxyTraps = { | ||
get(target, property) { | ||
if (property === '_state') return target; | ||
const value = target[property]; | ||
if (S.isListening() && typeof value !== 'function') track(target, property, value); | ||
return (!isObject(value) || typeof value === 'function' || value instanceof Element) ? value : wrap(value); | ||
}, | ||
set() { return true; }, | ||
deleteProperty() { return true; } | ||
}; | ||
class State { | ||
constructor(state = {}) { | ||
Object.defineProperties(this, { | ||
_state: { value: unwrap(state, true), writable: true } | ||
}); | ||
const keys = Object.keys(this._state); | ||
for (let i = 0, l = keys.length; i < l; i++) this._defineProperty(keys[i]); | ||
} | ||
set() { | ||
const args = arguments; | ||
S.freeze(() => { | ||
if (args.length === 1) { | ||
if (Array.isArray(args[0])) { | ||
for (let i = 0; i < args[0].length; i++) this.set.apply(this, args[0][i]); | ||
} else { | ||
const keys = Object.keys(args[0]); | ||
for (let i = 0, l = keys.length; i < l; i++) { | ||
const property = keys[i]; | ||
this._setProperty(property, args[0][property]); | ||
} | ||
} | ||
function updatePath(current, path, traversed = []) { | ||
if (path.length === 1) { | ||
let value = path[0]; | ||
if (typeof value === 'function') { | ||
value = value(wrap(current), traversed); | ||
// deep map | ||
if (Array.isArray(value)) { | ||
for (let i = 0; i < value.length; i += 1) | ||
updatePath(current, value[i], traversed); | ||
return; | ||
} | ||
setNested(resolvePath(this._state, args, args.length - 1), args[args.length - 1]); | ||
}); | ||
return this; | ||
} | ||
replace() { | ||
if (arguments.length === 1) { | ||
if (!(arguments[0] instanceof Object)) { | ||
console.log('replace must be provided a replacement state'); | ||
return this; | ||
} | ||
let changes = arguments[0]; | ||
S.freeze(() => { | ||
if (!(Array.isArray(changes))) changes = diff(changes, this._state); | ||
for (let i = 0; i < changes.length; i++) this.replace.apply(this, changes[i]); | ||
}); | ||
return this; | ||
} | ||
if (arguments.length === 2) { | ||
this._setProperty.apply(this, arguments); | ||
return this; | ||
} | ||
const value = arguments[arguments.length - 1], | ||
property = arguments[arguments.length - 2]; | ||
setNested(resolvePath(this._state, arguments, arguments.length - 2), property, value); | ||
return this; | ||
return mergeState(current, value); | ||
} | ||
select() { | ||
const mapFn1 = selection => () => { | ||
const unwrapped = unwrap(selection(), true), | ||
results = []; | ||
resolveAsync(unwrapped, (value) => { | ||
if (value === void 0) return; | ||
for (let key in value || {}) { | ||
results.push(diff(value[key], this._state[key], [key])); | ||
} | ||
this.replace([].concat(...results)); | ||
}); | ||
}; | ||
const part = path.shift(), | ||
partType = typeof part, | ||
isArray = Array.isArray(current); | ||
const mapFn2 = (key, selector) => () => { | ||
const unwrapped = unwrap(selector(), true); | ||
resolveAsync(unwrapped, (value) => { | ||
if (value === void 0) return; | ||
this.replace(diff(value, this._state[key], [key])); | ||
}); | ||
}; | ||
if (Array.isArray(part)) { | ||
// Ex. update('data', [2, 23], 'label', l => l + ' !!!'); | ||
for (let i = 0; i < part.length; i++) | ||
updatePath(current, [part[i]].concat(path), [].concat(traversed, [part[i]])); | ||
} else if (isArray && partType === 'function') { | ||
// Ex. update('data', i => i.id === 42, 'label', l => l + ' !!!'); | ||
for (let i = 0; i < current.length; i++) | ||
if (part(current[i], i)) updatePath(current[i], path.slice(0), [].concat(traversed, [i])); | ||
} else if (isArray && partType === 'object') { | ||
// Ex. update('data', { from: 3, to: 12, by: 2 }, 'label', l => l + ' !!!'); | ||
const {from = 0, to = current.length - 1, by = 1} = part; | ||
for (let i = from; i <= to; i += by) | ||
updatePath(current[i], path.slice(0), [].concat(traversed, [i])); | ||
} else if (isArray && part === '*') { | ||
// Ex. update('data', '*', 'label', l => l + ' !!!'); | ||
for (let i = 0; i < current.length; i++) | ||
updatePath(current, [i].concat(path), [].concat(traversed, [i])); | ||
} else if (path.length === 1) { | ||
let value = path[0]; | ||
if (typeof value === 'function') | ||
value = value(typeof current[part] === 'object' ? wrap(current[part]) : current[part], traversed.concat([part])); | ||
if (current[part] != null && typeof current[part] === 'object' && value !== null && typeof value === 'object' && !Array.isArray(value)) | ||
return mergeState(current[part], value); | ||
return setProperty(current, part, value); | ||
} else updatePath(current[part], path); | ||
} | ||
for (let i = 0; i < arguments.length; i++) { | ||
const selection = arguments[i]; | ||
if (typeof selection === 'function') { | ||
S.makeComputationNode(mapFn1(selection)); | ||
continue; | ||
} | ||
for (let key in selection) { | ||
if (!(key in this)) this._defineProperty(key); | ||
S.makeComputationNode(mapFn2(key, selection[key])); | ||
} | ||
} | ||
return this; | ||
} | ||
function useState(state) { | ||
state = unwrap(state); | ||
const wrappedState = wrap(state); | ||
return [wrappedState, setState]; | ||
_setProperty(property, value) { | ||
if (!(property in this)) this._defineProperty(property); | ||
value = unwrap(value, true); | ||
if (this._state[property] === value) return; | ||
if (value === void 0) delete this._state[property]; | ||
else this._state[property] = value; | ||
trigger(getDataNode(this._state), property); | ||
} | ||
_defineProperty(property) { | ||
Object.defineProperty(this, property, { | ||
get() { | ||
const value = this._state[property]; | ||
if (S.isListening()) track(this._state, property, value); | ||
return (!isObject(value) || typeof value === 'function' || value instanceof Element) ? value : wrap(value); | ||
}, | ||
enumerable: true | ||
function setState() { | ||
const args = arguments; | ||
S.freeze(() => { | ||
if (Array.isArray(args[0])) { | ||
for (let i = 0; i < args.length; i += 1) | ||
updatePath(state, args[i]); | ||
} else updatePath(state, Array.prototype.slice.call(args)); | ||
}); | ||
@@ -291,33 +206,19 @@ } | ||
function fromPromise(promise, seed) { | ||
let s = S.makeDataNode(seed), | ||
complete = false; | ||
promise | ||
.then((value) => { | ||
if (complete) return; | ||
s.next(value); | ||
}).catch(err => console.error(err)); | ||
S.cleanup(function dispose() { complete = true; }); | ||
return () => s.current(); | ||
function reconcile() { | ||
const path = Array.prototype.slice.call(arguments, 0, -1), | ||
value = arguments[arguments.length - 1]; | ||
return state => { | ||
state = unwrap(state); | ||
for (let i = 0; i < path.length; i += 1) state = state[path[i]]; | ||
return diff(value, state, path); | ||
} | ||
} | ||
function fromObservable(observable, seed) { | ||
let s = S.makeDataNode(seed), | ||
disposable = observable.subscribe(v => s.next(v), err => console.error(err)); | ||
function useMemo(fn, seed) { return S(fn, seed); } | ||
S.cleanup(function dispose() { | ||
disposable.unsubscribe(); | ||
disposable = null; | ||
}); | ||
return () => s.current(); | ||
} | ||
function useSignal(value) { return S.data(value); } | ||
function from(input, seed) { | ||
if (isObject(input)) { | ||
if (typeof input === 'function') return input; | ||
if (Symbol.observable in input) return fromObservable(input[Symbol.observable](), seed); | ||
if ('then' in input) return fromPromise(input, seed); | ||
} | ||
throw new Error('from() input must be a function, Promise, or Observable'); | ||
function useEffect(fn, deps, defer) { | ||
if (!deps) return S.effect(fn); | ||
S.on(deps, fn, undefined, defer); | ||
} | ||
@@ -329,2 +230,8 @@ | ||
function compose(...fns) { | ||
if (!fns) return i => i; | ||
if (fns.length === 1) return fns[0]; | ||
return input => fns.reduce(((prev, fn) => fn(prev)), input); | ||
} | ||
function map(fn) { | ||
@@ -338,6 +245,8 @@ return input => () => { | ||
function compose(...fns) { | ||
if (!fns) return i => i; | ||
if (fns.length === 1) return fns[0]; | ||
return input => fns.reduce(((prev, fn) => fn(prev)), input); | ||
function tap(fn) { | ||
return input => () => { | ||
const value = input(); | ||
if (value !== void 0) S.sample(() => fn(value)); | ||
return; | ||
} | ||
} | ||
@@ -482,4 +391,4 @@ | ||
const { root, cleanup, sample, data, effect } = S; | ||
const { root, cleanup: useCleanup, sample, freeze } = S; | ||
export { root, cleanup, sample, data, effect, State, unwrap, from, pipe, map, compose, when, each, observable }; | ||
export { root, useCleanup, sample, freeze, unwrap, useState, reconcile, useMemo, useSignal, useEffect, pipe, compose, map, tap, when, each, observable }; |
@@ -8,8 +8,9 @@ # Components | ||
```jsx | ||
import { State, root } from 'solid-js' | ||
import { useState, root } from 'solid-js' | ||
class Component { | ||
constructor () { | ||
this.state = new State({}) | ||
this.props = new State({}); | ||
const [state, setState] = useState({}), | ||
[props, setProps] = useState({}); | ||
Object.assign(this, { state, setState, props, _setProps: setProps }); | ||
} | ||
@@ -23,3 +24,3 @@ | ||
attributeChangedCallback(attr, oldVal, newVal) { | ||
this.props.replace(attr, newVal); | ||
this._setProps({[attr]: newVal}); | ||
} | ||
@@ -30,3 +31,3 @@ } | ||
constuctor () { | ||
this.state.set({greeting: 'World'}); | ||
this.setState({greeting: 'World'}); | ||
} | ||
@@ -42,12 +43,12 @@ render() { | ||
```jsx | ||
import { State, root } from 'solid-js' | ||
import { useState, root } from 'solid-js' | ||
function Component(fn) { | ||
state = new State({}); | ||
props = new State({}); | ||
return fn({state, props}); | ||
const [state, setState] = useState({}), | ||
[props] = useState({}); | ||
return fn({state, setState, props}); | ||
} | ||
function MyComponent({state}) { | ||
state.set({greeting: 'World'}); | ||
function MyComponent({state, setState}) { | ||
setState({greeting: 'World'}); | ||
return <div>Hello {(state.greeting)}</div>; | ||
@@ -54,0 +55,0 @@ } |
@@ -14,14 +14,16 @@ # Operators | ||
```js | ||
import { State, map } from 'solid-js' | ||
import { useState, useEffect, map } from 'solid-js' | ||
state = new State({name: 'Heather', count: 1}); | ||
const [state, setState] = useState({name: 'Heather', count: 1}); | ||
// single expression | ||
upperName = map((name) => name.toUpperCase())(() => state.name) | ||
const upperName = map(name => name.toUpperCase())(() => state.name); | ||
// in steps | ||
reverseName = map((name) => name.reverse()) | ||
reverseUpperName = reverseName(upperName) | ||
const reverseName = map(name => name.reverse()); | ||
const reverseUpperName = reverseName(upperName); | ||
state.select({ upperName, reverseUpperName }); | ||
useEffect(() => | ||
setState({ upperName: upperName(), reverseUpperName: reverseUpperName() }) | ||
) | ||
``` | ||
@@ -36,5 +38,2 @@ | ||
### from(observable | promise, seed) | ||
Makes promises/observables trackable by Solid. | ||
Current operators: | ||
@@ -60,3 +59,3 @@ | ||
```js | ||
import { State, pipe, map } from 'solid-js'; | ||
import { useState, useEffect, pipe, map, tap } from 'solid-js'; | ||
@@ -72,12 +71,11 @@ function someOperator(...someArguments) { | ||
// now you can use it in a pipe | ||
let state = new State({data: ....}), | ||
let [state, setState] = useState({data: ....}), | ||
someArguments = .... | ||
state.select({ | ||
derived: pipe( | ||
() => state.data, | ||
map(i => i), | ||
someOperator(...someArguments) | ||
) | ||
}); | ||
useEffect(pipe( | ||
() => state.data, | ||
map(i => i), | ||
someOperator(...someArguments) | ||
tap(derived => setState({ derived })) | ||
)); | ||
``` |
# Signals | ||
Signals are the glue that hold the library together. They often are invisible but interact in very powerful ways that you get more familiar with Solid they unlock a lot of potential. So consider this an intermediate to advanced topic. | ||
Signals are the glue that hold the library together. They often are invisible but interact in very powerful ways that you get more familiar with Solid they unlock a lot of potential. So consider this an intermediate topic. | ||
At it's core Solid uses [S.js](https://github.com/adamhaile/S) to propagate it's change detection. Signals are a simple primitive that contain values that change over time. With Signals you can track sorts of changes from various sources in your applications. You can create a Signal manually or from any Async source. | ||
At it's core Solid uses [S.js](https://github.com/adamhaile/S) to propagate it's change detection. Signals are a simple primitive that contain values that change over time. With Signals you can track sorts of changes from various sources in your applications. Solid's State object is built from a Proxy over a tree of Signals. You can update a Signal manually or from any Async source. | ||
```js | ||
import S from 's-js'; | ||
import { useSignal, useCleanup } from 'solid-js'; | ||
function fromInterval(delay) { | ||
var s = S.data(0); | ||
handle = setInterval(() => s(s() + 1), delay); | ||
S.cleanup(() => clearInterval(handle)); | ||
var s = useSignal(0); | ||
handle = setInterval(() => s(s() + 1), delay); | ||
useCleanup(() => clearInterval(handle)); | ||
return s; | ||
} | ||
``` | ||
Solid comes with a from operator that automatically handles creating Signals from Promises, and Observables. As a convenience several S methods including root and cleanup are exposed through Solid as exports as well. | ||
### Computation | ||
### Accessors & Context | ||
A computation is calculation over a function execution that automatically dynamically tracks it's dependencies. A computation goes through a cycle on execution where it releases its previous execution's dependencies, then executes grabbing the current dependencies. You can create a computation by passing a function into state.select and any of Solid's operators. | ||
Signals are special functions that when executed return their value. Accessors are just functions that "access", or read a value from one or more Signals. At the time of reading the Signal the current execution context (a computation) has the ability to track Signals that have been read, building out a dependency tree that can automatically trigger recalculations as their values are updated. This can be as nested as desired and each new nested context tracks it's own dependencies. Since Accessors by nature of being composed of Signal reads are too reactive we don't need to wrap Signals at every level just at the top level where they are used and around any place that is computationally expensive where you may want to memoize or store intermediate values. | ||
### Computations | ||
An computation is calculation over a function execution that automatically dynamically tracks any dependent signals. A computation goes through a cycle on execution where it releases its previous execution's dependencies, then executes grabbing the current dependencies. | ||
There are 2 main computations used by Solid: Effects which produce side effects, and Memos which are pure and return a read-only Signal. | ||
```js | ||
import S from 's-js'; | ||
import { State } from 'solid-js'; | ||
import { useState, useEffect } from 'solid-js'; | ||
state = new State({count: 1}); | ||
const [state, setState] = useState({count: 1}); | ||
S(() => console.log(state.count)); | ||
state.set({count: state.count + 1}); | ||
useEffect(() => console.log(state.count)); | ||
setState({count: state.count + 1}); | ||
@@ -36,3 +40,3 @@ // 1 | ||
They also pass the previous value on each execution. This is useful for reducing operations (obligatory Redux in a couple lines example): | ||
Memos also pass the previous value on each execution. This is useful for reducing operations (obligatory Redux in a couple lines example): | ||
@@ -50,23 +54,23 @@ ```js | ||
// redux | ||
const action = S.data(), | ||
store = S(state => reducer(state, action()), {list: []}); | ||
const dispatch = useSignal(), | ||
getStore = useMemo(state => reducer(state, dispatch()), {list: []}); | ||
// subscribe and dispatch | ||
S(() => console.log(store().list)); | ||
action({type: 'LIST/ADD', payload: {id: 1, title: 'New Value'}}); | ||
useEffect(() => console.log(getStore().list)); | ||
dispatch({type: 'LIST/ADD', payload: {id: 1, title: 'New Value'}}); | ||
``` | ||
That being said there are plenty of reasons to use actual Redux. And since a Redux Store exports an Observable it's just a map function away from passing into a State Selector in Solid to use in your components. | ||
That being said there are plenty of reasons to use actual Redux. | ||
### Rendering | ||
You can also use S.js `S.data` or `S.value` signals directly. As an example, the following will show a count of ticking seconds: | ||
You can also use signals directly. As an example, the following will show a count of ticking seconds: | ||
```jsx | ||
import S from 's-js' | ||
import { useSignal } from 'solid-js' | ||
const seconds = S.data(0); | ||
const div = <div>Number of seconds elapsed: {(seconds())}</div> | ||
const seconds = useSignal(0); | ||
const div = <div>Number of seconds elapsed: {( seconds() )}</div> | ||
setInterval(() => seconds(seconds() + 1), 1000) | ||
S.root(() => document.body.appendChild(div)) | ||
root(() => document.body.appendChild(div)) | ||
``` | ||
@@ -73,0 +77,0 @@ |
# State | ||
State is the core work horse of Solid. It represents the local data, the output all the asynchronous interaction as a simple to read javascript object. While fine grained observable itself it is has a minimal API footprint and in most cases be treated like a normal object when reading, supporting destructuring and native methods. However, when under a computation, ie under the function context of a Sync or Selector, you are dealing with proxy objects that automatically tracked as dependencies of the computation and upon changing will force evaluation. In fact, Solid can be written that dependency tracking is handled automatically by the library. | ||
State is the core work horse of Solid. It represents the local data, the output all the asynchronous interaction as a simple to read javascript object. While fine grained observable itself it is has a minimal API footprint and in most cases be treated like a normal object when reading, supporting destructuring and native methods. However you are dealing with proxy objects that automatically tracked as dependencies of memoization and effects and upon changing will force evaluation. | ||
While this state concept is heavily borrowed from React and it's API from ImmutableJS, there is a key difference in the role it plays here. In React you keep things simple in your state and the whole library is about reconciling DOM rendering. Here you can almost view the State object as the target, the thing that is diffed and maintained. The DOM rendering is actually quite simple to the point the compiled source exposes the vast majority of the DOM manipulations, where you can easily drop a breakpoint. So change detection being nested and focusing on interaction with other change mechanisms are key. | ||
### constructor(object) | ||
### useState(object) | ||
Initializes with object value. | ||
Initializes with object value and returns an array where the first index is the state object and the second is the setState method. | ||
### set(...path, changes) | ||
### setState(changes) | ||
### setState(...path, changes) | ||
### setState([...path, changes], [...path, changes]) | ||
@@ -17,10 +19,12 @@ This merges the changes into the path on the state object. All changes in set operation are applied at the same time so it is often more optimal than replace. | ||
### replace(...path, value) | ||
Path can be string keys, array of keys, wildcards ('*'), iterating objects ({from, to, by}), or filter functions. This gives incredible expressive power to describe state changes. | ||
This replaces the value at the path on the state object. Sometimes changes need to be made in several locations and this is the easiest way to swap out a specific value. When there is no path it will replace the current state object and notify via diff. This is useful when replacing the state object from the outside like integrating with Time Travel or Redux Dev Tools. | ||
All changes made in a single setState command are applied syncronously (ie all changes see each other at the same time). | ||
Alternatively if you can do multiple replaces in a single call by passing an array of paths and values. | ||
### reconcile(...path, value) | ||
### select(...(object|fn)) | ||
This can be used to do deep diffs by producing the list of changes to apply from a new State value. This is useful when pulling in immutable data trees from stores to ensure the least amount of mutations to your state. It can also be used to replace the all keys on the base state object if no path is provided as it does both positive and negative diff. | ||
This takes either Observable, Selector, Function or an object that maps keys to an Observable, Selector, or Function. The Object is the most common form but supports a straight function to be able to map multiple values from a single selector. | ||
```js | ||
setState(reconcile('users', store.get('users'))) | ||
``` |
326
lib/solid.js
@@ -64,6 +64,6 @@ 'use strict'; | ||
function unwrap(item, deep) { | ||
function unwrap(item) { | ||
let result, unwrapped, v; | ||
if (result = item != null ? item._state : void 0) return result; | ||
if (!deep || !isObject(item) || (typeof item === 'function') || (item instanceof Element)) return item; | ||
if (!isObject(item) || (typeof item === 'function') || (item instanceof Element)) return item; | ||
if (Object.isFrozen(item)) item = clone(item); | ||
@@ -74,3 +74,3 @@ | ||
v = item[i]; | ||
if ((unwrapped = unwrap(v, deep)) !== v) item[i] = unwrapped; | ||
if ((unwrapped = unwrap(v)) !== v) item[i] = unwrapped; | ||
} | ||
@@ -81,3 +81,3 @@ } else { | ||
v = item[keys[i]]; | ||
if ((unwrapped = unwrap(v, deep)) !== v) item[keys[i]] = unwrapped; | ||
if ((unwrapped = unwrap(v)) !== v) item[keys[i]] = unwrapped; | ||
} | ||
@@ -91,2 +91,17 @@ } | ||
const proxyTraps = { | ||
get(target, property) { | ||
if (property === '_state') return target; | ||
const value = target[property]; | ||
if (S.isListening() && typeof value !== 'function') track(target, property, value); | ||
return (!isObject(value) || typeof value === 'function' || value instanceof Element) ? value : wrap(value); | ||
}, | ||
set() { return true; }, | ||
deleteProperty() { return true; } | ||
}; | ||
function wrap(value) { return value[SPROXY] || (value[SPROXY] = new Proxy(value, proxyTraps)); } | ||
function getDataNode(target) { | ||
@@ -98,7 +113,2 @@ let node = target[SNODE]; | ||
function trigger(node, property, notify) { | ||
if (node[property]) node[property].next(); | ||
if (notify && node._self) node._self.next(); | ||
} | ||
function track(target, property, value) { | ||
@@ -117,177 +127,82 @@ let node; | ||
function setNested(item, changes) { | ||
let node = getDataNode(item), | ||
isArray = Array.isArray(item), | ||
value, notify, keys; | ||
if (arguments.length === 3) { | ||
notify = isArray || !(arguments[1] in item); | ||
value = unwrap(arguments[2], true); | ||
if (item[arguments[1]] === value) return; | ||
if (value === void 0) { | ||
delete item[arguments[1]]; | ||
if (isArray) item.length--; | ||
} else item[arguments[1]] = value; | ||
trigger(node, arguments[1], notify); | ||
return; | ||
} | ||
keys = Object.keys(changes); | ||
for (let i = 0, l = keys.length; i < l; i++) { | ||
const property = keys[i]; | ||
notify = isArray || !(property in item); | ||
value = unwrap(changes[property], true); | ||
if (value === void 0) delete item[property]; | ||
else item[property] = value; | ||
trigger(node, property, notify); | ||
} | ||
function trigger(node, property, notify) { | ||
if (node[property]) node[property].next(); | ||
if (notify && node._self) node._self.next(); | ||
} | ||
function resolvePath(current, path, length) { | ||
let i = 0, temp; | ||
while (i < length && (temp = current[path[i]]) != null) { | ||
current = temp; | ||
i++; | ||
} | ||
return current; | ||
function setProperty(state, property, value) { | ||
value = unwrap(value); | ||
if (state[property] === value) return; | ||
const notify = Array.isArray(state) || !(property in state); | ||
if (value === void 0) delete state[property]; | ||
else state[property] = value; | ||
trigger(getDataNode(state), property, notify); | ||
} | ||
function wrap(value) { return value[SPROXY] || (value[SPROXY] = new Proxy(value, proxyTraps)); } | ||
function resolveAsync(value, fn) { | ||
if (!isObject(value)) return fn(value); | ||
if ('subscribe' in value) { | ||
const dispose = value.subscribe(fn); | ||
S.cleanup(function disposer() { dispose.unsubscribe(); }); | ||
return; | ||
function mergeState(state, value) { | ||
const keys = Object.keys(value) || []; | ||
for (let i = 0; i < keys.length; i += 1) { | ||
const key = keys[i]; | ||
setProperty(state, key, value[key]); | ||
} | ||
if ('then' in value) { | ||
value.then(fn); | ||
return; | ||
} | ||
fn(value); | ||
} | ||
const proxyTraps = { | ||
get(target, property) { | ||
if (property === '_state') return target; | ||
const value = target[property]; | ||
if (S.isListening() && typeof value !== 'function') track(target, property, value); | ||
return (!isObject(value) || typeof value === 'function' || value instanceof Element) ? value : wrap(value); | ||
}, | ||
set() { return true; }, | ||
deleteProperty() { return true; } | ||
}; | ||
class State { | ||
constructor(state = {}) { | ||
Object.defineProperties(this, { | ||
_state: { value: unwrap(state, true), writable: true } | ||
}); | ||
const keys = Object.keys(this._state); | ||
for (let i = 0, l = keys.length; i < l; i++) this._defineProperty(keys[i]); | ||
} | ||
set() { | ||
const args = arguments; | ||
S.freeze(() => { | ||
if (args.length === 1) { | ||
if (Array.isArray(args[0])) { | ||
for (let i = 0; i < args[0].length; i++) this.set.apply(this, args[0][i]); | ||
} else { | ||
const keys = Object.keys(args[0]); | ||
for (let i = 0, l = keys.length; i < l; i++) { | ||
const property = keys[i]; | ||
this._setProperty(property, args[0][property]); | ||
} | ||
} | ||
function updatePath(current, path, traversed = []) { | ||
if (path.length === 1) { | ||
let value = path[0]; | ||
if (typeof value === 'function') { | ||
value = value(wrap(current), traversed); | ||
// deep map | ||
if (Array.isArray(value)) { | ||
for (let i = 0; i < value.length; i += 1) | ||
updatePath(current, value[i], traversed); | ||
return; | ||
} | ||
setNested(resolvePath(this._state, args, args.length - 1), args[args.length - 1]); | ||
}); | ||
return this; | ||
} | ||
replace() { | ||
if (arguments.length === 1) { | ||
if (!(arguments[0] instanceof Object)) { | ||
console.log('replace must be provided a replacement state'); | ||
return this; | ||
} | ||
let changes = arguments[0]; | ||
S.freeze(() => { | ||
if (!(Array.isArray(changes))) changes = diff(changes, this._state); | ||
for (let i = 0; i < changes.length; i++) this.replace.apply(this, changes[i]); | ||
}); | ||
return this; | ||
} | ||
if (arguments.length === 2) { | ||
this._setProperty.apply(this, arguments); | ||
return this; | ||
} | ||
const value = arguments[arguments.length - 1], | ||
property = arguments[arguments.length - 2]; | ||
setNested(resolvePath(this._state, arguments, arguments.length - 2), property, value); | ||
return this; | ||
return mergeState(current, value); | ||
} | ||
select() { | ||
const mapFn1 = selection => () => { | ||
const unwrapped = unwrap(selection(), true), | ||
results = []; | ||
resolveAsync(unwrapped, (value) => { | ||
if (value === void 0) return; | ||
for (let key in value || {}) { | ||
results.push(diff(value[key], this._state[key], [key])); | ||
} | ||
this.replace([].concat(...results)); | ||
}); | ||
}; | ||
const part = path.shift(), | ||
partType = typeof part, | ||
isArray = Array.isArray(current); | ||
const mapFn2 = (key, selector) => () => { | ||
const unwrapped = unwrap(selector(), true); | ||
resolveAsync(unwrapped, (value) => { | ||
if (value === void 0) return; | ||
this.replace(diff(value, this._state[key], [key])); | ||
}); | ||
}; | ||
if (Array.isArray(part)) { | ||
// Ex. update('data', [2, 23], 'label', l => l + ' !!!'); | ||
for (let i = 0; i < part.length; i++) | ||
updatePath(current, [part[i]].concat(path), [].concat(traversed, [part[i]])); | ||
} else if (isArray && partType === 'function') { | ||
// Ex. update('data', i => i.id === 42, 'label', l => l + ' !!!'); | ||
for (let i = 0; i < current.length; i++) | ||
if (part(current[i], i)) updatePath(current[i], path.slice(0), [].concat(traversed, [i])); | ||
} else if (isArray && partType === 'object') { | ||
// Ex. update('data', { from: 3, to: 12, by: 2 }, 'label', l => l + ' !!!'); | ||
const {from = 0, to = current.length - 1, by = 1} = part; | ||
for (let i = from; i <= to; i += by) | ||
updatePath(current[i], path.slice(0), [].concat(traversed, [i])); | ||
} else if (isArray && part === '*') { | ||
// Ex. update('data', '*', 'label', l => l + ' !!!'); | ||
for (let i = 0; i < current.length; i++) | ||
updatePath(current, [i].concat(path), [].concat(traversed, [i])); | ||
} else if (path.length === 1) { | ||
let value = path[0]; | ||
if (typeof value === 'function') | ||
value = value(typeof current[part] === 'object' ? wrap(current[part]) : current[part], traversed.concat([part])); | ||
if (current[part] != null && typeof current[part] === 'object' && value !== null && typeof value === 'object' && !Array.isArray(value)) | ||
return mergeState(current[part], value); | ||
return setProperty(current, part, value); | ||
} else updatePath(current[part], path); | ||
} | ||
for (let i = 0; i < arguments.length; i++) { | ||
const selection = arguments[i]; | ||
if (typeof selection === 'function') { | ||
S.makeComputationNode(mapFn1(selection)); | ||
continue; | ||
} | ||
for (let key in selection) { | ||
if (!(key in this)) this._defineProperty(key); | ||
S.makeComputationNode(mapFn2(key, selection[key])); | ||
} | ||
} | ||
return this; | ||
} | ||
function useState(state) { | ||
state = unwrap(state); | ||
const wrappedState = wrap(state); | ||
return [wrappedState, setState]; | ||
_setProperty(property, value) { | ||
if (!(property in this)) this._defineProperty(property); | ||
value = unwrap(value, true); | ||
if (this._state[property] === value) return; | ||
if (value === void 0) delete this._state[property]; | ||
else this._state[property] = value; | ||
trigger(getDataNode(this._state), property); | ||
} | ||
_defineProperty(property) { | ||
Object.defineProperty(this, property, { | ||
get() { | ||
const value = this._state[property]; | ||
if (S.isListening()) track(this._state, property, value); | ||
return (!isObject(value) || typeof value === 'function' || value instanceof Element) ? value : wrap(value); | ||
}, | ||
enumerable: true | ||
function setState() { | ||
const args = arguments; | ||
S.freeze(() => { | ||
if (Array.isArray(args[0])) { | ||
for (let i = 0; i < args.length; i += 1) | ||
updatePath(state, args[i]); | ||
} else updatePath(state, Array.prototype.slice.call(args)); | ||
}); | ||
@@ -297,33 +212,19 @@ } | ||
function fromPromise(promise, seed) { | ||
let s = S.makeDataNode(seed), | ||
complete = false; | ||
promise | ||
.then((value) => { | ||
if (complete) return; | ||
s.next(value); | ||
}).catch(err => console.error(err)); | ||
S.cleanup(function dispose() { complete = true; }); | ||
return () => s.current(); | ||
function reconcile() { | ||
const path = Array.prototype.slice.call(arguments, 0, -1), | ||
value = arguments[arguments.length - 1]; | ||
return state => { | ||
state = unwrap(state); | ||
for (let i = 0; i < path.length; i += 1) state = state[path[i]]; | ||
return diff(value, state, path); | ||
} | ||
} | ||
function fromObservable(observable, seed) { | ||
let s = S.makeDataNode(seed), | ||
disposable = observable.subscribe(v => s.next(v), err => console.error(err)); | ||
function useMemo(fn, seed) { return S(fn, seed); } | ||
S.cleanup(function dispose() { | ||
disposable.unsubscribe(); | ||
disposable = null; | ||
}); | ||
return () => s.current(); | ||
} | ||
function useSignal(value) { return S.data(value); } | ||
function from(input, seed) { | ||
if (isObject(input)) { | ||
if (typeof input === 'function') return input; | ||
if (Symbol.observable in input) return fromObservable(input[Symbol.observable](), seed); | ||
if ('then' in input) return fromPromise(input, seed); | ||
} | ||
throw new Error('from() input must be a function, Promise, or Observable'); | ||
function useEffect(fn, deps, defer) { | ||
if (!deps) return S.effect(fn); | ||
S.on(deps, fn, undefined, defer); | ||
} | ||
@@ -335,2 +236,8 @@ | ||
function compose(...fns) { | ||
if (!fns) return i => i; | ||
if (fns.length === 1) return fns[0]; | ||
return input => fns.reduce(((prev, fn) => fn(prev)), input); | ||
} | ||
function map(fn) { | ||
@@ -344,6 +251,8 @@ return input => () => { | ||
function compose(...fns) { | ||
if (!fns) return i => i; | ||
if (fns.length === 1) return fns[0]; | ||
return input => fns.reduce(((prev, fn) => fn(prev)), input); | ||
function tap(fn) { | ||
return input => () => { | ||
const value = input(); | ||
if (value !== void 0) S.sample(() => fn(value)); | ||
return; | ||
} | ||
} | ||
@@ -488,17 +397,20 @@ | ||
const { root, cleanup, sample, data, effect } = S; | ||
const { root, cleanup: useCleanup, sample, freeze } = S; | ||
exports.root = root; | ||
exports.cleanup = cleanup; | ||
exports.useCleanup = useCleanup; | ||
exports.sample = sample; | ||
exports.data = data; | ||
exports.effect = effect; | ||
exports.State = State; | ||
exports.freeze = freeze; | ||
exports.unwrap = unwrap; | ||
exports.from = from; | ||
exports.useState = useState; | ||
exports.reconcile = reconcile; | ||
exports.useMemo = useMemo; | ||
exports.useSignal = useSignal; | ||
exports.useEffect = useEffect; | ||
exports.pipe = pipe; | ||
exports.compose = compose; | ||
exports.map = map; | ||
exports.compose = compose; | ||
exports.tap = tap; | ||
exports.when = when; | ||
exports.each = each; | ||
exports.observable = observable; |
{ | ||
"name": "solid-js", | ||
"description": "A declarative JavaScript library for building user interfaces.", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"author": "Ryan Carniato", | ||
@@ -6,0 +6,0 @@ "license": "MIT", |
@@ -14,5 +14,2 @@ # Solid.js | ||
* Easy interopt with existing libraries that manage services and state. | ||
* All Selectors when resolved to these automatically update asynchronously allowing for 100% pure data declarations. | ||
* Async Functions are first class citizens and Selectors can be written using them. | ||
* from operator converts Promise or Observable to a Signal. | ||
* Expandable custom operators and binding directives. | ||
@@ -29,6 +26,6 @@ * Truly just a render library | ||
```jsx | ||
import { State, root } from 'solid-js' | ||
import { useState, root } from 'solid-js' | ||
function MyComponent() { | ||
const state = new State({ | ||
const [state, setState] = useState({ | ||
users: [{ | ||
@@ -44,3 +41,3 @@ id: 1, firstName: 'John', lastName: 'Smith' | ||
<ul>{ | ||
state.users.map(user => <li>{(user.firstName)} {(user.lastName)}</li>) | ||
state.users.map(user => <li>{( user.firstName )} {( user.lastName )}</li>) | ||
}</ul> | ||
@@ -61,7 +58,7 @@ </>); | ||
It all starts with a State object. These objects can represent the local state or the props in your components. State objects look like plain javascript options except to control change detection you call their set method. They give the control of an immutable interface and the performance of a mutable one. | ||
It all starts with a State object. These objects can represent the local state or the props in your components. State objects look like plain javascript options except to control change detection you call their setter method. They give the control of an immutable interface and the performance of a mutable one. | ||
```js | ||
const state = new State({counter: 0}); | ||
state.set({ | ||
const [state, setState] = useState({counter: 0}); | ||
setState({ | ||
counter: state.counter + 1 | ||
@@ -74,3 +71,3 @@ }); | ||
```js | ||
const state = new State({ | ||
const [state, setState] = useState({ | ||
user: { | ||
@@ -82,13 +79,19 @@ firstName: 'John' | ||
state.set('user', {firstName: 'Jake', middleName: 'Reese'}); | ||
setState('user', {firstName: 'Jake', middleName: 'Reese'}); | ||
``` | ||
You can also use functions: | ||
```js | ||
const [state, setState] = useState({counter: 0}); | ||
setState('counter', c => c + 1); | ||
``` | ||
This takes the form similar to ImmutableJS for set and setIn leaving all mutation control at the top level state object. | ||
But where the magic happens is with making Selectors. This done through State's select method which takes an Observable, a Promise, or a Function and maps it to a state property. The simplest form of passing a function will wrap it in an Observable which will automatically tracks dependencies. | ||
But where the magic happens is with computations(effects and memos) which automatically track dependencies. | ||
```js | ||
state.select({ | ||
displayName: () => `${state.user.firstName} ${state.user.lastName}`; | ||
}) | ||
useEffect(() => setState({ | ||
displayName: `${state.user.firstName} ${state.user.lastName}` | ||
})); | ||
@@ -98,18 +101,12 @@ console.log(state.displayName); // Jake Smith | ||
Whenever any dependency changes the State value will immediately update. Internally all JSX expressions also get wrapped in computations so for something as trivial as a display name you could just inline the expression in the template and have it update automatically. | ||
Whenever any dependency changes the State value will immediately update. Internally all JSX expressions also get wrapped in effects so for something as trivial as a display name you could just inline the expression in the template and have it update automatically. | ||
This is also primary mechanism to interopt with store technologies like Redux, Apollo, RxJS which expose themselves as Observables or Promises. When you hook up these Selectors you can use standard methods to map the properties you want and the State object will automatically diff the changes to only affect the minimal amount. | ||
This is also primary mechanism to interopt with store technologies like Redux, Apollo, RxJS which expose themselves as Observables or Promises. When you hook up these effects you can use standard methods to map the properties you want and the reconcile method will diff the changes to only affect the minimal amount. | ||
```js | ||
props.select({ | ||
myCounter: map(({counter}) => counter)(from(store.observable())) | ||
}) | ||
// or if you prefer | ||
props.select({ | ||
myCounter: pipe( | ||
from(store.observable()), | ||
map(({counter}) => counter) | ||
) | ||
}) | ||
useEffect(() => { | ||
const disposable = store.observable() | ||
.subscribe(({ todos }) => setState(reconcile('todos', todos))); | ||
useCleanup(() => disposable.unsubscribe()); | ||
}); | ||
``` | ||
@@ -137,6 +134,6 @@ | ||
This project started as trying to find a small performant library to work with Web Components, that had easy interopt with existing standards. It is very inspired by fine grain change detection libraries like Knockout.js and the radical approach taken by Cycle.js. The idea here is to ease users into the world of Observable programming by keeping it transparent and starting simple. The Virtual DOM as seen in React for all it's advances has some signifigant trade offs: | ||
This project started as trying to find a small performant library to work with Web Components, that had easy interopt with existing standards. It is very inspired by fine grain change detection libraries like Knockout.js and RxJS. The idea here is to ease users into the world of Observable programming by keeping it transparent and starting simple. Classically the Virtual DOM as seen in React for all it's advances has some signifigant trade offs: | ||
* The VDOM render while performant is still conceptually a constant re-render | ||
* It feels much more imperative as variable declarations and iterative methods for constructing the tree are constantly re-evaluating | ||
* It feels much more imperative as variable declarations and iterative methods for constructing the tree are constantly re-evaluating. | ||
* Reintroduced lifecycle function hell that break apart the declarative nature of the data. Ex. relying on blacklisting changes across the tree with shouldComponentUpdate. | ||
@@ -156,12 +153,4 @@ * Homogenous promise of Components and the overly simplistic local state in practice: | ||
Admittedly it takes a strong reason to not go with the general consensus of best, and most supported libraries and frameworks. But I believe there is a better way out there than how we do it today. | ||
Admittedly it takes a strong reason to not go with the general consensus of best, and most supported libraries and frameworks. And React's Hooks API addresses the majority of what I once considered it's most untenable faults. But I believe there is a better way out there than how we do it today. | ||
Moreover, working in Web Components has changed my perspective on where the natural boundaries are for the parts of modern web application building. In a sense a library like React does too much and doesn't allow separating key technical approaches. From a Web Component perspective the base pieces are: | ||
1. Renderer (JSX vs String Templates vs DOM Crawling, Virtual DOM vs Fine Grained vs Dirty Checking) | ||
2. Change Management (Declarative Data vs Lifecycle, Mutable vs Immutable) | ||
3. Container (JS Objects vs Web Components, Inheritance vs Function Composition) | ||
React takes care of all 3 and doesn't let you swap your solutions for each. Each these areas have different approaches and tradeoffs. Solid.js focuses on 2, just touching on the first as needed. Containers are not a concern here making it a great candidate for Web Components. | ||
## Documentation | ||
@@ -171,5 +160,6 @@ | ||
* [Components](../master/documentation/components.md) | ||
* [Signals](../master/documentation/signals.md) | ||
* [Operators](../master/documentation/operators.md) | ||
* [Rendering](../master/documentation/rendering.md) | ||
* [Signals](../master/documentation/signals.md) | ||
* [API](../master/documentation/api.md) | ||
@@ -195,2 +185,2 @@ ## Examples | ||
This project is still a work in progress. Although I've been working on it for the past 2 years it's been evolving considerably. I've decided to open source this at this point to share the concept. | ||
This project is still a work in progress. Do not use in production. I am still refining the API. |
@@ -1,67 +0,9 @@ | ||
const S = require('s-js'); | ||
const { from, pipe, map } = require('../lib/solid'); | ||
const { useSignal, root, pipe, map } = require('../lib/solid'); | ||
const Observable = require('zen-observable'); | ||
describe('from operator', () => { | ||
test('Signal passthrough', () => { | ||
S.root(() => { | ||
var data = S.data(5), | ||
out = from(data); | ||
expect(out).toBe(data); | ||
}); | ||
}); | ||
test('Signal from an async Signal', (done) => { | ||
s = S.data('init') | ||
setTimeout(s, 20, 'started'); | ||
S.root(() => { | ||
var out = from(s); | ||
expect(out()).toBe('init'); | ||
S.on(out, () => { | ||
expect(out()).toBe('started'); | ||
done(); | ||
}, null, true); | ||
}); | ||
}); | ||
test('Signal from a promise', (done) => { | ||
S.root(() => { | ||
var p = new Promise(resolve => { setTimeout(resolve, 20, 'promised'); }), | ||
out = from(p, 'init'); | ||
expect(out()).toBe('init'); | ||
S.on(out, () => { | ||
expect(out()).toBe('promised'); | ||
done(); | ||
}, null, true); | ||
}); | ||
}); | ||
test('Signal from an observable', (done) => { | ||
S.root(() => { | ||
var o = new Observable(observer => { | ||
let timer = setTimeout(() => { | ||
observer.next('hello'); | ||
observer.complete(); | ||
}, 20); | ||
return () => clearTimeout(timer); | ||
}), out = from(o, 'init'); | ||
expect(out()).toBe('init'); | ||
S.on(out, () => { | ||
expect(out()).toBe('hello'); | ||
done(); | ||
}, null, true); | ||
}); | ||
}); | ||
}); | ||
describe('pipe operator', () => { | ||
test('Signal passthrough', () => { | ||
S.root(() => { | ||
var data = S.data(5), | ||
root(() => { | ||
var data = useSignal(5), | ||
out = pipe(data); | ||
@@ -74,4 +16,4 @@ | ||
test('pipe map', () => { | ||
S.root(() => { | ||
var data = S.data(5), | ||
root(() => { | ||
var data = useSignal(5), | ||
out = pipe(data, map(i => i * 2)); | ||
@@ -78,0 +20,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
18
56439
906
176