https://rekotiira.github.io/calmm-training/
Reko Tiira
import * as React from 'baret' // or karet for Kefir
const MyComponent = () => {
const time = Bacon.once()
.merge(Bacon.interval(1000))
.map(() => new Date().toString())
.toProperty()
return <div>The time is <strong>{time}</strong></div>
}
karet and baret hooks into the createElement API of React, which is the reason you need to import React from karet or baret library.
Bacon.combineTemplate({
todos: todosProperty,
counter: counterProperty
})
.takeUntil(componentUnmountStream)
.onValue(state => this.setState(state))
const mapStateToProps = (state, ownProps) => ({
todos: state.todos,
counter: state.counter
})
const mapDispatchToProps = ...
const WrappedComponent = connect(mapStateToProps, mapDispatchToProps)(TodoComponent)
const UselessComponent = ({ observable }) =>
<div>
{new Date().getFullYear() >= YEAR_OF_LINUX_DESKTOP
? observable
: 'The year of the desktop linux desktop is not yet here'}
</div>
observable is only subscribed to if the year of linux desktop has been reached.
const MyComponent = ({ observable }) =>
<div>
<ComplexComponent />
The value of observable is: <strong>{observable}</strong>
</div>
const MultiplyByTwo = ({ number }) => <div>{number * 2}</div>
Does not work with observables.
const MultiplyByTwo = ({ number }) => <div>{number.map(it => it * 2)}</div>
Does not work with plain values.
import * as U from 'karet.util'
import * as R from 'kefir.ramda'
const MultiplyByTwoA = ({ number }) => <div>{U.mapValue(it => it * 2, number)}</div>
const MultiplyByTwoB = ({ number }) => <div>{R.multiply(number, 2)}</div>
Works with both!
U.mapValue and R.multiply are lifted to work with both plain values and observables.
import * as U from 'karet.util'
const includes = U.lift((xs, x) => xs.includes(x))
const obsOfBooleans = includes(obsOfArrays, obsOfValues)
const PositiveAndLessThan100 = ({ number }) =>
<div>
Is {number} positive and less than 100:
{U.ifElse(
U.and(R.gte(number, 0), R.lt(number, 100)),
'yes',
'no')}
</div>
Also possible to write
const PositiveAndLessThan100 = ({ number }) => {
const test = U.lift(number => number >= 0 && number < 100)
return (
<div>
Is {number} positive and less than 100: {U.ifElse(test(number), 'yes', 'no')}
</div>
)
}
or
const PositiveAndLessThan100 = ({ number }) =>
<div>
Is {number} positive and less than 100:
{U.mapValue(number => (number >= 0 && number < 100) ? 'yes' : 'no', number)}
</div>
If you have complex conditions it is sometimes useful to break down the logic into a separate lifted function for readability.
const Search = ({ text = U.atom('') }) => {
const getSearchUrl = text => `https://my.api/search?text=${encodeURIComponent(text)}`
const results = U.thru(
text,
U.debounce(300),
U.flatMapLatest(text => U.thru(
U.fromPromise(() => fetch(getSearchUrl(text))),
U.startWith(undefined)
)),
U.startWith([])
)
return (
<div>
<U.Input value={text} />
{U.ifElse(
R.is(Array, results),
<ul>
{U.mapElems((result, i) => <li key={i}>{result}</li>, results)}
</ul>,
'Loading'
)}
</div>
)
}
const Counter = ({ count = Atom(0) }) =>
<span>
<button onClick={() => count.modify(R.add(-1))}>-</button>
{count}
<button onClick={() => count.modify(R.add(1))}>+</button>
</span>
Having the count atom as a parameter makes it easy to use the component with local or global (external) state, making the component more composeable and flexible.
<Counter />
const count = Atom(10)
<Counter count={count} />
const counters = Atom([0, 0, 0])
<Counter count={counters.view(0)} />
<Counter count={counters.view(1)} />
<Counter count={counters.view(2)} />
const todos = Atom([
{ title: 'Wash laundry', finished: false },
{ title: 'Buy groceries', finished: true },
{ title: 'Clean kitchen', finished: false }
])
const TodoList = ({ todos }) =>
<div>
<h2>Todo</h2>
{U.mapElems(
(todo, i) => <TodoItem key={i} todo={todo} />,
todos
)}
</div>
const UnfinishedTodos = ({ todos }) => {
const onlyUnfinishedLens = L.filter(todo => !todo.finished)
return <TodoList todos={todos.view(onlyUnfinishedLens)} />
}
const obj = Atom({
foo: {
bar: [1, 2, 3]
}
})
// focus the 2nd element of the foo.bar array
const slice = obj.view(['foo', 'bar', 1])
slice.set(20)
obj.get() // { foo: { bar: [ 1, 20, 3 ] }}
slice.modify(R.multiply(2))
obj.get() // { foo: { bar: [ 1, 40, 3 ] }}
const list = Atom({ items: ['x', 'y'] })
list.modify(L.set(['items', L.append], 'z'))
list.get() // { items: ['x', 'y', 'z'] }
const list = Atom({ items: ['x', 'y'] })
list.modify(L.set(['items'], 'z'))
list.get() // { items: 'z' }
const list = Atom(['x', 'y'])
list.modify(R.append('z')) // list.modify(L.set(L.append, 'z'))
list.get() // ['x', 'y', 'z']
const list = Atom(['x', 'y', 'z'])
list.modify(L.remove(1))
list.get() // ['x', 'z']
Lensed atoms can also remove themselves
const list = Atom(['x', 'y', 'z'])
const item = list.view(1)
item.remove()
list.get() // ['x', 'z']
This is a very useful pattern when you want to have removable UI components where the remove button is in the removable component itself.
Open https://codesandbox.io/s/4wyz1lo5y0 in your browser and fork it.
Re-factor the Contact component to a functional calmm-style component.
Re-factor the ContactList component so that it works even if it received contacts as an observable.
Create a state atom to hold the contacts and pass the contacts slice from the state atom to ContactList component.
<pre>{U.stringify(state, null, 2)}</pre>
Make the contacts actually editable.
Make the contacts deletable.
Make it possible to add new contacts.
Display the total contact count somewhere.
What was the first solution you came up with?Add "editing" class to the Contact component root element when editing.
Only allow one contact to be edited at one time.
import * as React from 'karet'
import { Map } from '3rd-party-map'
<Map position={currentPositionAtom} />
React will throw an error if you try to pass an observable to the Map component, since it doesn't use karet/baret.
Easy way to fix this is using karet-lift attribute. If the Map component is used a lot, you can also create a lifted version of the component and use that.
import * as React from 'karet'
import * as U from 'karet.util'
import { Map } from '3rd-party-map'
const LiftedMap = React.fromClass(Map)
const LiftedMap2 = U.toKaret(Map) // this does the same
<Map position={currentPositionAtom} karet-lift />
<LiftedMap position={currentPositionAtom} />
Now the Map component will receive the position as a plain value.
Calmm components should only render when they're mounted. If you're using calmm components as children of a non-calmm component this behavior is likely to break, because the parent container is being re-rendered which also causes the calmm components to re-render.
karet.util library has an useful utility function U.toReact which converts plain value props into observable props and stops re-rendering of the calmm component.
import * as U from 'karet.util'
import CalmmComponent from './components/calmm-component'
const CalmmComponentInterop = U.toReact(CalmmComponent)
class ReactComponent extends React.Component {
render() {
return <CalmmComponentInterop {...this.props} />
}
}
Especially useful with libraries such as react-router.