SkillAgentSearch skills...

Mutant

Create observables and map them to DOM elements. Massively inspired by hyperscript and observ-*, but avoids GC thrashing.

Install / Use

/learn @mmckegg/Mutant
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

mutant

Create observables and map them to DOM elements. Massively inspired by hyperscript and observ-*.

No virtual DOM, just direct observable bindings. Unnecessary garbage collection is avoided by using mutable objects instead of blasting immutable junk all over the place.

Current Status: Experimental / Maintained

Expect breaking changes.

Used By

  • Loop Drop: 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.
  • Patchwork: A decentralized messaging and sharing app built on top of Secure Scuttlebutt (SSB).
  • Ferment: Peer-to-peer audio publishing and streaming application. Like SoundCloud but decentralized. A mashup of ssb, webtorrent and electron.

Install

npm install mutant --save

Compatibility

Requires an environment that supports:

  • setImmediate(fn) (only available in node/electron by default so in browser need to use setImmediate shim)
  • requestIdleCallback(fn) (optional, only when using {idle: true}, mutant/once-idle or mutant/idle-proxy)
  • Map, WeakMap, and WeakSet
  • element.classList
  • MutationObserver (optional, only for root html-element binding support)
  • ES5 arrays (Array.prototype.forEach, etc)
  • IntersectionObserver (optional, only when using intersectionBindingViewport attribute on elements)
  • Array.prototype.includes

Example

var h = require('mutant/html-element')
var Struct = require('mutant/struct')
var send = require('mutant/send')
var computed = require('mutant/computed')
var when = require('mutant/when')

var state = Struct({
  text: 'Test',
  color: 'red',
  value: 0
})

var isBlue = computed([state.color], color => color === 'blue')

var element = h('div.cool', {
  classList: ['cool', state.text],
  style: {
    'background-color': state.color
  }
}, [
  h('div', [
    state.text, ' ', state.value, ' ', h('strong', 'test')
  ]),
  h('div', [
    when(isBlue,
      h('button', {
        'ev-click': send(state.color.set, 'red')
      }, 'Change color to red'),
      h('button', {
        'ev-click': send(state.color.set, 'blue')
      }, 'Change color to blue')
    )

  ])
])

setTimeout(function () {
  state.text.set('Another value')
}, 5000)

setInterval(function () {
  state.value.set(state.value() + 1)
}, 1000)

setInterval(function () {
  // bulk update state
  state.set({
    text: 'Retrieved from server (not really)',
    color: '#FFEECC',
    value: 1337
  })
}, 10000)

document.body.appendChild(element)

Types

Observables that store data

Value

The classic observable - stores a single value, updates listeners when the values changes.

var Value = require('mutant/value')

var obs = Value()
obs.set(true)
//set listener
obs(value => { 
  // called with resolved value whenever the observable changes
})

This is almost the same as observable and 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)

Array

An observable with additional array like methods, which update the observable. The array items can be ordinary values or observables.

Like 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.

There's also mutant/set which is similar but only allows values to exist once.

additional methods:

  • array.get(index) get the value at index
  • array.getLength() get the length of the array
  • array.put(index, value) set item at index to value
  • array.push(value) append value to end of array.
  • array.pop() remove item from end.
  • array.shift() remove item from start.
  • array.insert(value, index) equivalent to [].splice(index, 0, value) on a standard js array.
  • array.delete(value) remove the first occurance of value from the array.
  • array.deleteAt(index) remove item at index.
  • array.transaction(fn) apply a series of changes to the array and then update listeners in one go.
  • array.includes(item) check if the array includes item
  • array.indexOf(item) find the index of item in the array
  • array.find(fn) return the first item array for which fn(item) == true
  • array.forEach(fn) iterate over all raw items in the array
  • array.set(array) overwrite the contents of the mutant array with array
  • array.clear() remove all items.

Dict

The complement to Struct - but instead of representing a fixed set of sub-observables, it's a single observable which you can add sub-keys to.

var Dict = require('mutant/dict')
var d = Dict()
d.put('key', 1)
d(function (v) {
  // => {key: 1}
})

additional methods:

  • dict.put(key, value) set property key to value
  • dict.delete(key) remove property key
  • dict.has(key) returns true if key is present.
  • dict.keys() return array of keys.

Set

Represents a collection like Array except without ordering or duplicate values.

additional methods:

  • set.add(value) add value to the set.
  • set.clear() remove all items.
  • set.has() check if item is in the set.
  • set.get(index) get the item at index in the underlying array
  • set.getLength() get the number of items in the set.

Struct

Take a fixed set of observables (or values) and return a single observable of the observed values, which updates whenever the inner values update. Subobservables can by any observable type.

They also have a set function which can be used to push a json object into the nested observables. Any additional set keys will be preserved if you resolve it.

Mostly the same as 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.

They accept a set list of keys that specify types. For example:

var struct = MutantStruct({
  description: Value(),
  tags: MutantSet(),
  likes: Value(0, {defaultValue: 0}),
  props: MutantArray(),
  attrs: MutantDict()
})

You 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.

MappedArray

...

MappedDict

...

TypedCollection

This is similar to MappedArray, except with key checking. You can specify a matcher option to define how to decide that two objects are the same (defaults to (rawValue) => rawValue.id), and now any time set is called, instances with the same matcher result will be updated instead of recreated even if the order has changed. This is really useful when turning a collection into DOM elements, and reordering can happen remotely and you are just syncing the entire state every time.

var state = Struct({
  models: TypedCollection(YourModel, {
    matcher: (value) => value.id
  })
})

function YourModel () {
  return Struct({
    id: Value(),
    tags: MutantSet(),
    options: Dict()
  }) 
}

The constructor is called with the rawValue of the object, however you don't need to use this to populate the object as it will also be called with .set. This is just to allow polymorphic typing checks:

var types = {
  Cat () {
    return Struct({
      id: Value(),
      age: Value(),
      ...props
    }) 
  },
  Dog () {
    return Struct({
      id: Value(),
      age: Value(),
      ...props
    }) 
  }
}

var state = Struct({
  pets: TypedCollection((value) => types[value.type](), {
    matcher: (value) => value.id,
    invalidator: (current, newValue) => current.type != newValue.type,
    onAdd: (obj) => console.log('added', resolve(obj)),
    onRemove: (obj) => console.log('removed', resolve(obj)),
  })
})

state.set({
  pets: [
    {id: 1, age: 2, type: 'Dog'},
    {id: 2, age: 9, type: 'Cat'}
  ]
})
// => added {id: 1, type: 'Dog'}
// => added {id: 2, type: 'Cat'}

state.set({
  pets: [
    {id: 2, age: 9, type: 'Cat'},
    {id: 1, age: 3, type: 'Dog'}
  ]
})
// the inner models are updated and order changed, but no new cats/dogs are created

state.set({
  pets: [
    // somehow our dog has turned into a cat!
    // even though the cat has the same ID as the dog, the original object is discarded, and a new one constructed as invalidator returns true
    {id: 1, age: 3, type: 'Cat'},
    {id: 2, age: 9, type: 'Cat'}
  ]
})
// => removed {id: 1, type: 'Dog'}
// => added {id: 1, type: 'Cat'}

ProxyType

A more advanced feature - allow you to create observable slots which allow you to hot-swap observables in/ out of.

  • ProxyCollection
  • ProxyDictionary
  • Proxy

ProxyCollection

...

ProxyDictionary

..

View on GitHub
GitHub Stars124
CategoryDevelopment
Updated9mo ago
Forks11

Languages

JavaScript

Security Score

87/100

Audited on Jun 10, 2025

No findings