On Github calmm-js / training
Got empty project from Matti Lankinen.
Matti had just noticed perf problems with megablob.
I had just used Reagent in production.
Refactoring Matti's project (createStore) created Atom.
And with Matti developed ways to embed Bacon into JSX.
Later started using Bacon Model and, thanks to it, lenses.
Realized Bacon Model creates cycles => Bacon Atom.
And then developed Partial Lenses library.
Avoid boilerplate and glue
Avoid all-or-nothing / lock-in
Prefer declarative
Avoid unnecessary encoding
Structural programming
Components plug-and-play
There is actually nothing new here.
We are just combining a few old things.
const oncePerSec = Kefir.constant().merge(Kefir.interval(1000)) export default () => <K.div>{K(oncePerSec, () => new Date().toString())}</K.div>
export default ({elems = Atom([]), entry = Atom("")}) => <div> <div> <K.input type="text" {...bind({value: entry})}/> <button onClick={() => {const elem = entry.get().trim() if (elem) { elems.modify(R.append(elem)) entry.set("")}}}> Add </button> </div> <K.ul> {K(elems, es => es.map((e, i) => <li key={i}>{e}</li>))} </K.ul> </div>
const Slider = ({title, units, value, ...props}) => <div> <K.div>{title}: {value}{units}</K.div> <K.input type="range" {...bind({value})} {...props}/> </div> const BMI = ({bmi}) => <K.div {...classes("bmi", K(bmi, M.BMI.classification))}> <Slider title="Weight" units="kg" min={40} max={140} value={bmi.lens(M.BMI.weight)}/> <Slider title="Height" units="cm" min={140} max={210} value={bmi.lens(M.BMI.height)}/> <K.div>BMI: <K.span className="bmi-value">{K(bmi, M.BMI.bmi)}</K.span></K.div> </K.div> export default ({bmi}) => <BMI bmi={bmi.lens(M.BMI.augment)}/>
export const BMI = { augment: L.augment({ bmi: ({height, weight}) => Math.round(weight/(height * height * 0.0001))}), bmi: R.prop("bmi"), height: "height", weight: "weight", classification: ({bmi}) => bmi < 15 ? "bmi-underweight bmi-underweight-severely" : bmi < 18.5 ? "bmi-underweight" : bmi < 25 ? "bmi-normal" : bmi < 30 ? "bmi-overweight" : bmi < 35 ? "bmi-obese" : bmi < 40 ? "bmi-obese bmi-obese-severely" : "bmi-obese bmi-obese-very" }
const Contact = ({contact}) => <div> <TextInput value={contact.lens(M.Contact.name)}/> <TextInput value={contact.lens(M.Contact.number)}/> <button onClick={() => contact.modify(M.Contact.remove)}>Remove</button> </div> const Contacts = ({contacts}) => <K.div> {fromIds(K(contacts, M.Contacts.indices), i => <Contact key={i} contact={contacts.lens(i)}/>)} </K.div> export default ({phonebook = Atom(M.mock)}) => <div> <button onClick={() => phonebook.modify(M.Phonebook.addContact())}> New </button> <Contacts contacts={phonebook.lens(M.Phonebook.contacts)}/> </div>
export default ({value = Atom("")}) => { const editing = Atom(false) const exit = () => editing.set(false) const save = e => {value.set(e.target.value); exit(e)} return fromKefir(K(editing, e => e ? <K.input key="1" type="text" autoFocus defaultValue={value} onKeyDown={e => e.which === 13 && save(e) || e.which === 27 && exit(e)} onBlur={save}/> : <K.input key="0" type="text" disabled {...{value}} onDoubleClick={() => editing.set(true)}/>)) }
export const mock = [{name: "Mr Digits", number: "1-23-456789"}] export const Contact = { create: ({name = "", number = ""} = {}) => ({name, number}), remove: () => {}, id: "id", name: "name", number: "number" } export const Contacts = { indices: R.pipe(R.length, iota) } export const Phonebook = { contacts: L.define([]), addContact: contact => R.append(Contact.create(contact)) }
Maintaining consistent state in the face of async inputs.
In order of importance:
Specify dependent computations as observables. Embed observables directly into JSX. Store state in mutable observable Atoms. Use lenses to access state in Atoms.All optional!
And can mix with other React components.
Atom(vInitial)
For example:
const counter = Atom(0)
atom.get()
For example:
counter.get() === 0
atom.modify(vOld => vNew)
For example:
counter.modify(c => c + 1)
Now:
counter.get() === 1
Also for convenience:
atom.set(vNew) === atom.modify(() => vNew)
Can store in data structures, pass to and return from functions.
Want multiple things to always share the same value?
Put the value in an Atom and share it.
Avoid get.
Use FRP combinators to express dependent computations.
Stream
A B CD E F G HIJ K L M N O P QR Se.g. key down events, mouse clicks, ... filter, skip, merge, combine, scan, toProperty, ...
Property
AABBCDDDDDDDDEEFFFFFFFFFFFFFFGGGGGGGHIJJKKLKKKMMMNNNNOOOPPPPPPQRRRRSe.g. text accumulated, position of pointer, ... combine, sample, changes, toEventStream, ...
Both are used. We are mostly concerned with properties.
Bacon.combineWith(p1, ..., pN, (v1, ..., vN) => result) Kefir.combine(p1, ..., pN, (v1, ..., vN) => result)
We abbreviate:
B(p1, ..., pN, (v1, ..., vN) => result) K(p1, ..., pN, (v1, ..., vN) => result)
For example:
const input = Atom("This is an example.") const words = K(input, R.split(" ")) const numWords = K(words, R.length) const unique = K(words, R.uniqBy(R.toUpper)) const numUnique = K(unique, R.length)
Properties being observed are kept in sync.
We just decide which properties to observe.
const greetingsTarget = Atom("world") ... <div>Hello, {greetingsTarget}!</div>
It crashes. React cannot render observables.
const greetingsTarget = Atom("world") ... <K.div>Hello, {greetingsTarget}!</K.div>
<K.input type="text" value={greetingsTarget} onChange={e => greetingsTarget.set(e.target.value)}/>
JSX, e.g.
<div someProp="a value"><p>Child</p></div>
evaluates into a tree of objects, roughly
{ "type": "div", "props": { "someProp": "a value", "children": { "type": "p", "props": { "children": "Child" } } } }
and the props are passed to the React class.
componentWillMount() { ... subscribe ... } componentWillUnmount() { ... dispose ... } render() { return this.state.rendered }
Subscribe creates a stream to update rendered state.
Everything can be made to work incrementally.
Mount and unmount take a little extra.
But then you only recompute changed VDOM.
Asymptotically better than recomputing all VDOM.
Never write another React class or shouldComponentUpdate.
Properties and streams everywhere.
Possibly thousands upon thousands.
Bacon is neither space nor time optimal. :(
Kefir uses significantly less (~5x).
With Kefir performance seems very competitive.
Let you declare a path to an element.
const data = {items: [{id: "a", value: 20}, {id: "b", value: 10}]} const itemWith = id => L("items", L.find(R.whereEq({id}))) L.view(itemWith("a"), data) L.set(L(itemWith("a"), "value"), 15, data) L.delete(itemWith("a"), data)
atom.lens(partial-lens)
For example:
import L from "partial.lenses" const names = Atom(["first"]) const first = names.lens(L.index(0))
Now you can, e.g.
first.set("Still first")
Don't overuse Atoms: leads to imperative spaghetti.
Use them for simple data-binding.
Setting the value of an atom in response to a change of an atom is a smell.
Remember: You can use other kinds of wiring!
But more complex wiring seems to be rarely needed.
Atoms and Observable embedding make it easy to have: component = 1 function.
But you really dont want to clump everything together.
Separate the model, meta-model, control.
It makes code conceptually clearer.
It makes the models more easily testable and usable.
It is easy to go overboard with components.
Wrapping basic HTML elements, e.g. textarea or select, as components.
You end up making them difficult to customize.
Your components should do something substantial.
Does it have a non-trivial model?
Is it a combination of elements you use in lots of places?
Issues raised in AngularJS: The Bad Parts:
Dynamic scoping and Parameter name based DI We use ordinary JavaScript with lexical scoping. Data is explicitly routed using lenses and properties. The digest loop Property updates are done incrementally by the underlying "FRP" library. Redefining terminology All concepts have been around for a long time (Observable, Property, Atom, Lens).The meta model is just a bunch of operations on model.
Add e.g. mocha to the project and tycittele a few tests.
Make editable link component.
Look at TextInput.
When not editing, render as a link.
Select some React datepicker component.
Bind it to the model.
Limit the number of viewed items.
Create them as components.
That lets you navigate a list (prev, next, ...).
(I'll give the code for Undo.)
You will likely need to add Node (Express) to the project.