UNPKG

8.98 kBMarkdownView Raw
1# mutant
2
3Create observables and map them to DOM elements. Massively inspired by [hyperscript](https://github.com/dominictarr/hyperscript) and [`observ-*`](https://github.com/Raynos/observ).
4
5No virtual DOM, just direct observable bindings. Unnecessary garbage collection is avoided by using mutable objects instead of blasting immutable junk all over the place.
6
7## Current Status: Experimental / Maintained
8
9Expect breaking changes.
10
11## Used By
12
13- **[Loop Drop](https://github.com/mmckegg/loop-drop-app)**: Live electronic music performance app. MIDI looper, modular synth and sampler app built around Novation Launchpad controller. Powered by Web Audio, Web MIDI, and [electron](https://electron.atom.io).
14- **[Ferment](https://github.com/mmckegg/ferment)**: Peer-to-peer audio publishing and streaming application. Like SoundCloud but decentralized. A mashup of [ssb](https://scuttlebot.io/), [webtorrent](https://webtorrent.io/) and [electron](https://electron.atom.io).
15
16## Install
17
18```bash
19npm install mutant --save
20```
21
22## Compatibility
23
24Requires an environment that supports:
25 - `setImmediate(fn)`
26 - `requestIdleCallback(fn)` (optional, only when using `{idle: true}`, `mutant/once-idle` or `mutant/idle-proxy`)
27 - `Map` and `WeakMap`
28 - `element.classList`
29 - `MutationObserver` (optional, only for root `html-element` binding support)
30 - ES5 arrays (`Array.prototype.forEach`, etc)
31 - `Array.prototype.includes`
32
33## Example
34
35```js
36var h = require('mutant/html-element')
37var Struct = require('mutant/struct')
38var send = require('mutant/send')
39var computed = require('mutant/computed')
40var when = require('mutant/when')
41
42var state = Struct({
43 text: 'Test',
44 color: 'red',
45 value: 0
46})
47
48var isBlue = computed([state.color], color => color === 'blue')
49
50var element = h('div.cool', {
51 classList: ['cool', state.text],
52 style: {
53 'background-color': state.color
54 }
55}, [
56 h('div', [
57 state.text, ' ', state.value, ' ', h('strong', 'test')
58 ]),
59 h('div', [
60 when(isBlue,
61 h('button', {
62 'ev-click': send(state.color.set, 'red')
63 }, 'Change color to red'),
64 h('button', {
65 'ev-click': send(state.color.set, 'blue')
66 }, 'Change color to blue')
67 )
68
69 ])
70])
71
72setTimeout(function () {
73 state.text.set('Another value')
74}, 5000)
75
76setInterval(function () {
77 state.value.set(state.value() + 1)
78}, 1000)
79
80setInterval(function () {
81 // bulk update state
82 state.set({
83 text: 'Retrieved from server (not really)',
84 color: '#FFEECC',
85 value: 1337
86 })
87}, 10000)
88
89document.body.appendChild(element)
90```
91
92---
93
94## Types
95
96Observables that store data
97
98- Array
99- Dict
100- Set
101- Struct
102- Value
103- MappedArray
104- MappedDict
105
106### Array
107
108Like [observ-array](https://github.com/raynos/observ-array) but as with struct, emits the same object. No constant shallow cloning on every change. You can push observables (or ordinary values) and it will emit whenever any of them change. Works well with mutant/map.
109
110There's also `mutant/set` which is similar but only allows values to exist once.
111
112
113### Dict
114
115...
116
117
118### Set
119
120...
121
122
123### Struct
124
125Mostly the same as [observ-struct](https://github.com/raynos/observ-struct) except that it always emits the same object (with the properties changed). This means it violates immutability, but the trade-off is less garbage collection. The rest of the mutant helpers can handle this case pretty well.
126
127They accept a set list of keys that specify types. For example:
128
129```js
130var struct = MutantStruct({
131 description: Value(),
132 tags: Set(),
133 likes: Value(0, {defaultValue: 0}),
134 props: MutantArray(),
135 attrs: MutantDict()
136})
137```
138
139You can use these as your primary state atoms. I often use them like classes, extending them with additional methods to help with a given role. Another nice side effect is they work great for serializing/deserializing state. You can call them with `JSON.stringify(struct())` to get their entire tree state, then call them again later with `struct.set(JSON.parse(data))` to put it back. This is how state and file persistence works in [Loop Drop](https://github.com/mmckegg/loop-drop-app).
140
141
142### Value
143
144This is almost the same as [observable](https://github.com/dominictarr/observable) and [observ](https://github.com/raynos/observ). There's only a couple of small differences: you can specify a default value (fallback when null) and it will throw if you try and add a non-function as a listener (this one always got me)
145
146
147### MappedArray
148
149...
150
151
152### MappedDict
153
154...
155
156
157---
158
159## ProxyType
160
161A more advanced feature - allow you to create observable slots which allow you to hot-swap observables in/ out of.
162
163- ProxyCollection
164- ProxyDictionary
165- Proxy
166
167
168### ProxyCollection
169
170...
171
172### ProxyDictionary
173
174...
175
176### Proxy
177
178...
179
180
181---
182
183## Transforms
184
185Take one or more observables and transform them into an observable
186
187- computed
188- concat
189- dictToCollection
190- idleProxy
191- keys
192- lookup
193- map
194- merge
195- throttle
196- when
197
198### computed
199
200Once again, similar to the observ and observable implementations. It has a few key differences though.
201
202- It will try to avoid computing if its inputs have not changed.
203- It also won't emit a change if the new computed value is the same as the old one. This helps to prevent additional work duplication and render noise downstream.
204- There is an optional "nextTick" mode that queues up change events until nextTick before computing. But if you call it (`value()`) in the current tick, it will compute immediately.
205- It acts like a pull through stream: if it doesn't have a sink, no code is run. It won't bind and resolve until it gets a listener itself.
206- It accepts non-observable values too. This makes it possible to pass all state to a shared computed function (no need to waste more memory on those extra closures)
207- If the value returned by the compute function is an observable, it will bind to this and emit the resolve values. Crazy nested computes FTW!
208- These extra features do take up a bit of extra memory so I use an internal prototype (not visible to api) to reduce the footprint below that of observable and observ/computed
209
210### concat
211
212...
213
214
215### dictToCollection
216
217...
218
219
220### idleProxy
221
222...
223
224
225### keys
226
227...
228
229
230### lookup
231
232...
233
234
235### map
236
237A `through` transform. It won't do any work and won't listen to its parents unless it has a listener. Calls your function with the original observable object (not the resolve value). You can then return an additional observable value as its result. It has methods on it that make it behave like an array.
238
239One of the most interesting features is its `maxTime` option. This is a ms value that specifies the max time to spend in a tight loop before emit the changes so far. This makes rendering large datasets to DOM elements much more responsive - a lot more like how the browser does it when it parses html. Things load in little chunks down the page. This for me has made it much easier to build apps that feel responsive and leave the main thread available for more important things (like playing sound).
240
241
242### merge
243
244...
245
246
247### throttle
248
249...
250
251
252### when
253
254...
255
256
257---
258
259## Sinks
260
261Stuff that are exit hatches / sinks / make changes in the real world.
262
263- HtmlElement
264- watchAll
265- watchThrottle
266- watch
267
268
269### HtmlElement / h
270
271A fancy wrapper around `document.createElement()` that allows you to create DOM elements (entire trees if needed) without setting lots of properties or writing html. It just returns plain old DOM elements that can be added directly to the DOM.
272
273This is basically just [hyperscript](https://github.com/dominictarr/hyperscript) with a bunch of small tweaks that make it a lot more memory friendly. I've also enhanced the binding ability.
274
275In hyperscript you can add [observables](https://github.com/dominictarr/observable) as properties and when the observable value changes, the DOM magically updates. You can also return a DOM element. But in mutant, I've gone an extra step further and allow observables to return **multiple DOM elements**. I've also made "cleanup" (unbinding from events to free memory) automatic. It's a lot like pull streams: the DOM acts as a sink. **If an element created by mutant is not in the DOM, it doesn't listen to its observable properties.** It only resolves them once it is added, and if it is removed unlistens again.
276
277
278### watchAll
279
280...
281
282
283### watchThrottle
284
285...
286
287
288### watch
289
290- This is a generic sink. Almost the same as listening to a value using `value(function (v) { })` except that it emits the initial value too.
291- It also accepts non-observable objects and will just emit their value once and then never all again. Kind of like Promise.resolve(). (I think, never used promises)
292
293
294---
295
296## Helpers
297
298A grab bag of useful things for dealing with mutant stuff.
299A lot of these are used internally, but are useful more generally
300
301- forEachPair
302- forEach
303- isObservable
304- onceIdle
305- resolve
306- send
307
308
309### forEachPair
310
311...
312
313
314### forEach
315
316...
317
318
319### isObservable
320
321...
322
323
324### onceIdle
325
326...
327
328
329### resolve
330
331...
332
333
334### send
335
336...
337
338
339
340---
341
342## License
343
344MIT