-
Notifications
You must be signed in to change notification settings - Fork 0
Expr
function expr<R>(fn: ($: TrackFunc, _: TrackFunc) => R | typeof SKIP): Source<R>
type TrackFunc = {
<T>(source: Source<T>): T
n<T>(source: Source<T>): T | undefined
on(...sources: Source<unknown>[]): void
}
a: -----1------2-----3-|
b: -2-------0-----3------6--|
expr($ => $(a) + $(b)): -----3---1--2--5--6---9--|
Creates an expression of other sources. The expression is first computed when all sources have emitted at least once, and then is re-calculated whenever any of them emits a new value. For example, in the following code, the div will display the sum of the values of the inputs, when both inputs have a value:
HTML Code
<input type="number" id="a" />
<input type="number" id="b" />
<div></div>
import { pipe, event, expr, observe, tap, map } from 'streamlets'
const $a = document.querySelector('#a')
const $b = document.querySelector('#b')
const $div = document.querySelector('div')
const a = map(event($a, 'input'), () => parseInt($a.value))
const b = map(event($b, 'input'), () => parseInt($b.value))
pipe(
expr($ => $(a) + $(b)),
tap(x => $div.textContent = x),
observe
)
Expressions can also be conditionals (or any function really):
import { pipe, interval, event, expr, observe, tap, map } from 'streamlets'
const $a = document.querySelector('#a')
const $b = document.querySelector('#b')
const $div = document.querySelector('div')
const a = map(event($a, 'input'), () => parseInt($a.value))
const b = map(event($b, 'input'), () => parseInt($b.value))
const timer = interval(1000)
pipe(
expr($ => {
// alternate every second between displaying
// the sum or the difference between the two
// input values.
//
if ($(timer) % 2 === 0) {
return $(a) + $(b)
} else {
return $(a) - $(b)
}
}),
tap(x => $div.textContent = x),
observe
)
💡 A source will only be connected to when it is accessed in at least one run of the expression.
Return SKIP
if you don't want to emit any values:
import { interval, pipe, expr, tap, observe, SKIP } from 'streamlets'
const timer = interval(1000)
pipe(
expr($ => $(timer) % 2 === 0 ? $(timer) : SKIP),
tap(console.log),
observe
)
// > 0
// > 2
// > 4
// > ...
expr()
to
track values of each! Instead, create your sources outside of expr()
and just combined them inside:
// ❌❌ THIS IS WRONG!! ❌❌
expr($ => $(interval(1000)) + $(event(btn, 'click')).clientX))
// ✅✅ This is CORRECT! ✅✅
const timer = interval(1000)
const click = event(btn, 'click')
expr($ => $(timer) + $(click).clientX)
Expressions can be asynchronous functions:
const inp = event(input, 'input')
const response = expr(async $ => {
const query = $(inp).value.toLowerCase()
const resp = await fetch('https://my.api/?q=' + query)
const json = await resp.json()
return json
})
💡 Async expressions are cancelled mid-flight: if a source emits before the previous execution is finished, the previous execution will be cancelled: its result being ignored. Note that the execution itself is NOT stopped: i.e. if you make a request in your async expression, the request will definitely be sent even if the expression function is re-executed.
Track sources passively with the second argument passed to the expression function. If a passively tracked source emits, the expression won't be re-computed, but its emitted value will be used for the next run of the expression:
import { pipe, event, expr, observe, tap, map } from 'streamlets'
const $a = document.querySelector('#a')
const $b = document.querySelector('#b')
const $div = document.querySelector('div')
const a = map(event($a, 'input'), () => parseInt($a.value))
const b = map(event($b, 'input'), () => parseInt($b.value))
pipe(
// now changing value of the second input won't
// trigger an update in the displayed result.
//
expr(($, _) => $(a) + _(b)),
tap(x => $div.textContent = x),
observe
)
💡 If no sources are actively tracked (considering conditionals), then the expression will end.
Flatten higher-level streams with chain applying of the $
function:
HTML Code
<button>(Re)Start Timer</button>
<div></div>
import { pipe, interval, event, expr, observe, tap, map } from 'streamlets'
const $btn = document.querySelector('button')
const $div = document.querySelector('div')
const click = event($btn, 'click')
const timers = expr($ => ($(click), interval(100)))
const timer = expr($ => $($(timers)) / 10)
pipe(
timer,
tap(x => $div.textContent = x),
observe
)
Use $.on()
to explicitly specify all sources the expression should be dependent on:
HTML Code
<input type="number" />
<button>(Re)Start Timer</button>
<div></div>
import { pipe, interval, event, expr, observe, tap, map, prepend, of } from 'streamlets'
const $btn = document.querySelector('button')
const $input = document.querySelector('input')
const $div = document.querySelector('div')
const click = prepend(event($btn, 'click'), of({}))
const rate = map(event($input, 'input'), () => parseInt($input.value))
const timers = expr($ => {
$.on(click)
return interval($(rate) * 200)
})
pipe(
expr($ => $($(timers)) / 5),
tap(x => $div.textContent = x),
observe
)
💡 After explicitly outlining dependencies, all other sources will be passively tracked.
Expressions don't emit until all of the sources they track (either actively or passively) emit at least once. In some cases, it is preferable to assume some default value for a source before its first emission. Use $.n()
and/or _.n()
instead of the usual tracking functions $()
and _()
to get undefined
as the value for sources who have not emitted yet:
import { pipe, event, expr, observe, tap, map } from 'streamlets'
const $a = document.querySelector('#a')
const $b = document.querySelector('#b')
const $div = document.querySelector('div')
const a = map(event($a, 'input'), () => parseInt($a.value))
const b = map(event($b, 'input'), () => parseInt($b.value))
pipe(
// now will start with an initial value of 0
// before any user input.
expr($ =>
($.n(a) ?? 0) // default value for a (0)
+ ($.n(b) ?? 0) // default value for b (also 0)
),
tap(x => $div.textContent = x),
observe
)