Last active
October 5, 2020 23:45
-
-
Save laughinghan/4c0a773266ff885e4a559d4dd4b89612 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
https://jsbin.com/licibaj/edit?js,output | |
// The app consists of two input boxes, one representing | |
// Celsius, and the other Fahrenheit. | |
%View "#app": | |
<div> | |
<input @celsius value="0" />ºC | |
= | |
<input @fahrenheit value="32" />ºF | |
</div> | |
// When either input box is modified, and it’s value is a | |
// valid number, the other input box is updated to reflect | |
// the corresponding temperature in the other units. | |
When celsius.Edit emits {value}: | |
Let degC = value.as_number() | |
If degC isnt #error _: | |
Let degF = (degC * 9) / 5 + 32 | |
Change fahrenheit.value to degF.as_string() | |
When fahrenheit.Edit emits {value}: | |
Let degF = value.as_number() | |
If degF isnt #error _: | |
Let degC = (degF - 32) / 1.8 | |
Change celsius.value to degC.as_string() | |
=== After expansion of %View macro/compiler plugin === | |
Get celsius_field = Do create_element("input", { value: "0" }) | |
Get fahrenheit_field = Do create_element("input", { value: "32" }) | |
Do render_view( | |
'#app', | |
create_element("div", attr: %map{}, content: | |
celsius_field, | |
"ºC =", | |
fahrenheit_field, | |
"ºF")) | |
Init celsius with celsius_field | |
Init fahrenheit with fahrenheit_field | |
Module celsius: | |
Export Edit, <getter, setter> as value | |
Preinit: celsius_field | |
Edit = Do edit_stream(celsius_field) | |
getter = Do get_form_field_value(celsius_field) | |
setter = value => cmd: Do set_form_field_value(celsius_field, value) | |
Module fahrenheit: | |
Export Edit, <getter, setter> as value | |
Preinit: fahrenheit_field | |
Edit = Do edit_stream(fahrenheit_field) | |
getter = Do get_form_field_value(fahrenheit_field) | |
setter = value => cmd: Do set_form_field_value(fahrenheit_field, value) | |
// View library functions: | |
Import render_view, create_element from "libview.js" | |
Declare create_element as | |
(String, attr?: Map<String, String>, | |
content?: ...List<*Element>) => Command<*Element> | |
Declare render_view as | |
(String, *Element) => Command<Void> | |
//*/ | |
// The app consists of two input boxes, one representing | |
// Celsius, and the other Fahrenheit. | |
const celsius_field = create_element("input", { value: "0" }) | |
const fahrenheit_field = create_element("input", { value: "32" }) | |
render_view( | |
'#app', | |
create_element("div", {}, | |
celsius_field, | |
"ºC =", | |
fahrenheit_field, | |
"ºF")) | |
namespace celsius { | |
export const Edit = edit_stream(celsius_field) | |
export const value = { | |
get: get_form_field_value(celsius_field), | |
set: (value: string) => set_form_field_value(celsius_field, value), | |
} | |
} | |
namespace fahrenheit { | |
export const Edit = edit_stream(fahrenheit_field) | |
export const value = { | |
get: get_form_field_value(fahrenheit_field), | |
set: (value: string) => set_form_field_value(fahrenheit_field, value), | |
} | |
} | |
// When either input box is modified, and it’s value is a | |
// valid number, the other input box is updated to reflect | |
// the corresponding temperature in the other units. | |
celsius.Edit.subscribe(({value}) => { | |
const degC = str_as_num(value) | |
if (!(degC instanceof Error)) { | |
const degF = (degC * 9) / 5 + 32 | |
fahrenheit.value.set(String(degF)) | |
} | |
}) | |
fahrenheit.Edit.subscribe(({value}) => { | |
const degF = str_as_num(value) | |
if (!(degF instanceof Error)) { | |
const degC = (degF - 32) / 1.8 | |
celsius.value.set(String(degC)) | |
} | |
}) | |
// view core | |
function create_element(tagName: string, attrs: {[k: string]: string}, ...children: Array<string | Element>): Element { | |
const el = document.createElement(tagName) | |
for (const name in attrs) { | |
el.setAttribute(name, attrs[name]) | |
} | |
for (const child of children) { | |
if (typeof child === 'string') { | |
el.appendChild(document.createTextNode(child)) | |
} else { | |
el.appendChild(child) | |
} | |
} | |
return el | |
} | |
function render_view(selector: string, el: Element) { | |
const mountEl = getElement(selector) | |
mountEl.innerHTML = '' | |
mountEl.appendChild(el) | |
} | |
function getElement(selector: string): Element { | |
if (selector.charAt(0) === '#') { | |
const matching = document.getElementById(selector.slice(1)) | |
if (matching === null) { | |
throw `Found no element with id: '${selector.slice(1)}'` | |
} | |
return matching | |
} else if (selector.charAt(0) === '.') { | |
const matching = document.getElementsByClassName(selector.slice(1)) | |
if (matching.length === 0) { | |
throw `Found no element with class name: '${selector.slice(1)}'` | |
} | |
if (matching.length > 1) { | |
throw `Found ${matching.length} elements with class name '${selector}', need exactly one only` | |
} | |
return matching[0] | |
} | |
throw `Selector for mount element to render at must start with either '#' or '.', got: '${selector.charAt(0)}'` | |
} | |
function edit_stream(el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement): Stream<{ value: string }> { | |
let event_name: string | |
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') { | |
event_name = 'input' | |
} else if (el.tagName === 'SELECT') { | |
event_name = 'change' | |
} else { | |
throw `'Edit' handlers only allowed on <input/>, <select/>, and <textarea/> elements, but got a <${el.tagName.toLowerCase()}/>` | |
} | |
return { | |
subscribe(handler: (ev: { value: string }) => void) { | |
let listener: (ev: Event) => void | |
el.addEventListener(event_name, listener = ev => { | |
handler({ value: (ev.target as HTMLInputElement).value }) | |
}) | |
return { | |
unsubscribe() { | |
el.removeEventListener(event_name, listener) | |
} | |
} | |
} | |
} | |
} | |
function get_form_field_value(..._: any) { | |
return () => { throw 'Not Implemented' } | |
} | |
function set_form_field_value(el: Element, value: string) { | |
if (!(el instanceof HTMLInputElement)) { | |
throw `set_form_field_value() only allowed on <input/>s, but got a <${el.tagName.toLowerCase()}/>` | |
} | |
if (el.value !== value) { | |
el.value = value | |
} | |
} | |
function str_as_num(str: string) { | |
const num = +str | |
return isNaN(num) | |
? new Error(`String cannot be converted to number: ${str}`) | |
: num | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
https://jsbin.com/xafelaj/edit?js,output | |
// A list of counters. | |
State counters = [0] | |
%View '#app': | |
<div> | |
<ol @list> | |
{ For Each item In counters: | |
<li> | |
<button @dec title="Decrement">-</button> | |
<input @counter value={item.as_string()} /> | |
<button @inc title="Increment">+</button> | |
<button @dupe title="Duplicate">⎘</button> | |
<button @delete title="Delete">x</button> | |
</li> | |
} | |
</ol> | |
<button @add_counter>Add Counter</button> | |
</div> | |
// There's a button to add more counters. | |
When add_counter.Click: | |
Change counters To [...counters, 0] | |
// Counters can be edited, but only to an integer. | |
When list.counter.Edit Emits {value, context: i}: | |
Let new_count = value.as_number() | |
If new_count isnt #error _: | |
Change counters[i] To new_count.round() | |
// Each counter has buttons that can decrement, increment, duplicate, or delete it. | |
When list.inc.Click Emits {context: i}: | |
Change counters[i] By _ + 1 | |
When list.dec.Click Emits {context: i}: | |
Change counters[i] By _ - 1 | |
When list.dupe.Click Emits {context: i}: | |
Change counters By _.insert(at: i, value: counters[i]) | |
When list.delete.Click Emits {context: i}: | |
Change counters By _.remove(from: i, to: i+1) | |
=== Browser Prelude === | |
Bootstrap create_element From "viewlib.js" | |
Init _viewlib(create_element) | |
=== After expansion of %View macro/compiler plugin === | |
make_list_items = counters => cmd: | |
Get looped_elements = counters.map(item => ({ | |
dec: Do create_element("button", attr: %map{ title: "Decrement" }, content: "-"), | |
counter: Do create_element("input", attr: %map{ value: item.as_string() }), | |
inc: Do create_element("button", attr: %map{ title: "Increment" }, content: "+"), | |
dupe: Do create_element("button", attr: %map{ title: "Duplicate" }, content: "⎘"), | |
delete: Do create_element("button", attr: %map{ title: "Delete" }, content: "x"), | |
})) | |
Get list_items = looped_elements.map(els => | |
Do create_element("li", attr: #none, content: | |
els.dec, | |
els.counter, | |
els.inc, | |
els.dupe, | |
els.delete)) | |
Get events = looped_elements.map(els => ({ | |
dec: Do event_stream(els.dec, 'click'), | |
counter: Do edit_stream(els.counter), | |
inc: Do event_stream(els.inc, 'click'), | |
dupe: Do event_stream(els.dupe, 'click'), | |
delete: Do event_stream(els.delete, 'click'), | |
})) | |
Let updaters = looped_elements.map(els => ({ | |
counter_value: updated_item => cmd: | |
Let item_as_string = updated_item.as_string() | |
If item_as_string !== els.counter.get_value(): | |
Do els.set_value(item_as_string) | |
})) | |
Return { list_items, events, updaters } | |
Get { list_items, events: list_events, updaters: list_updaters } = | |
Do make_list_items(Initial counters) | |
Get list_el = Do create_element("ol", attr: #none, content: ...list_items) | |
Get add_counter_button = Do create_element("button", %map{}, "Add Counter") | |
Do render_view( | |
'#app', | |
Do create_element("div", attr: #none, content: | |
list_el, | |
add_counter_button)) | |
Init list(list_el, events: list_events, updaters: list_updaters) | |
Init add_counter(button: add_counter_button) | |
Module list: | |
Export make_list, dec, counter, inc, dupe, delete | |
Init Params = (list_el, events: initial_events, updaters: initial_updaters) | |
Derive State view From counters: | |
Initial: { | |
events: initial_events, | |
updaters: initial_updaters, | |
} | |
Update(counters: ops): | |
Do ops.reduce(from: 0, update: (soFar: i, next: op) => | |
match op: | |
#skip n -> | |
i + n | |
#remove n -> Do cmd: | |
Do list_el.remove_looped_elements(at: i, n) | |
Change view.events By _.remove(at: i, n) | |
Change view.updaters By _.remove(at: i, n) | |
Return i | |
#insert counters_items -> Do cmd: | |
Let n = counters_items.length() | |
Get { list_items, events: new_events, updaters: new_updaters } = | |
Do make_list_items(counters_items) | |
Do list_el.insert_looped_elements(at: i, items: list_items) | |
Change view.events By _.insert(at: i, items: new_events) | |
Change view.updaters By _.insert(at: i, items: new_updaters) | |
Return i + n | |
#replace counters_items -> Do cmd: | |
Let n = counters_items.length() | |
Do counters_items.map_i((item, j) => | |
Do updaters[i + j].counter_value(item)) | |
Return i + n | |
) | |
Otherwise When counter.Edit Emits { context: i }: | |
// if `counters` *wasn't* updated, make sure the corresponding | |
// @counter <input/> value is still properly synced | |
Do updaters[i].counter_value(counters[i]) | |
contextualized_events = view.events.map_i((r, i) => | |
r.map_values(_.map(_ & { context: i }))) | |
Module dec: | |
Export Click | |
Import contextualized_events | |
Click = contextualized_events.map(.dec).any() | |
Module counter: | |
Export Edit | |
Import contextualized_events | |
Edit = contextualized_events.map(.counter).any() | |
Module inc: | |
Export Click | |
Import contextualized_events | |
Click = contextualized_events.map(.inc).any() | |
Module dupe: | |
Export Click | |
Import contextualized_events | |
Click = contextualized_events.map(.dupe).any() | |
Module delete: | |
Export Click | |
Import contextualized_events | |
Click = contextualized_events.map(.delete).any() | |
Module add_counter: | |
Export Click | |
Init Params = (button) | |
Click = Do event_stream(button, 'click') | |
// View library functions: | |
Import render_view, create_element from "libview.js" | |
Declare create_element as | |
(String, attr?: Map<String, String>, | |
content?: ...List<*Element>) => Command<*Element> | |
Declare render_view as | |
(String, *Element) => Command<Void> | |
*/ | |
// A list of counters. | |
const counters = [0] | |
const make_list_items = (counters: number[]) => { | |
const looped_elements = counters.map(item => ({ | |
dec: create_element("button", { title: "Decrement" }, "-"), | |
counter: create_element("input", { value: String(item) }), | |
inc: create_element("button", { title: "Increment" }, "+"), | |
dupe: create_element("button", { title: "Duplicate" }, "⎘"), | |
delete_: create_element("button", { title: "Delete" }, "x"), | |
})) | |
const list_items = looped_elements.map(els => | |
create_element("li", {}, | |
els.dec, | |
els.counter, | |
els.inc, | |
els.dupe, | |
els.delete_)) | |
const events = looped_elements.map(els => ({ | |
dec: event_stream(els.dec, 'click'), | |
counter: edit_stream(els.counter), | |
inc: event_stream(els.inc, 'click'), | |
dupe: event_stream(els.dupe, 'click'), | |
delete_: event_stream(els.delete_, 'click'), | |
})) | |
const updaters = looped_elements.map(els => ({ | |
counter_value: (updated_item: number) => { | |
const item_as_string = String(updated_item) | |
if (item_as_string !== get_form_field_value(els.counter)) { | |
set_form_field_value(els.counter, item_as_string) | |
} | |
}, | |
})) | |
return { list_items, events, updaters } | |
} | |
const { list_items, events: list_events, updaters: list_updaters } = | |
make_list_items(counters) | |
const list_el = create_element("ol", {}, ...list_items) | |
const add_counter_button = create_element('button', {}, 'Add Counter') | |
render_view( | |
'#app', | |
create_element('div', {}, | |
list_el, | |
add_counter_button)) | |
namespace list { | |
const initial_events = list_events | |
const initial_updaters = list_updaters | |
const view = { | |
events: initial_events, | |
updaters: initial_updaters, | |
} | |
export const update_view = (ops: ArrayDelta<typeof counters[number]>) => { | |
ops.reduce((i, op) => { | |
switch(op.$type) { | |
case 'skip': { | |
const n = op.$value | |
return i + n | |
} | |
case 'remove': { | |
const n = op.$value | |
remove_looped_elements(list_el, i, n) | |
view.events .splice(i, n) | |
view.updaters.splice(i, n) | |
dec .Click.remove(i, n) | |
counter.Edit .remove(i, n) | |
inc .Click.remove(i, n) | |
dupe .Click.remove(i, n) | |
delete_.Click.remove(i, n) | |
return i | |
} | |
case 'insert': { | |
const counters_items = op.$value | |
const n = counters_items.length | |
const { list_items, events: new_events, updaters: new_updaters } = make_list_items(counters_items) | |
insert_looped_elements(list_el, i, list_items) | |
view.events.splice(i, 0, ...new_events) | |
view.updaters.splice(i, 0, ...new_updaters) | |
const new_ctxd_evts = new_events.map(obj => { | |
const new_obj: any = {} | |
for (const key in obj) { | |
const stream: Stream<{ value: Event | string }> = obj[key as keyof typeof obj] | |
new_obj[key] = map_stream(stream, e => | |
({ ...e, context: view.events.indexOf(obj) })) | |
} | |
return new_obj as { | |
[K in keyof typeof obj]: typeof obj[K] extends Stream<infer V> | |
? Stream<V & { context: number }> | |
: never | |
} | |
}) | |
dec .Click.insert(i, new_ctxd_evts.map(e => e.dec)) | |
counter.Edit .insert(i, new_ctxd_evts.map(e => e.counter)) | |
inc .Click.insert(i, new_ctxd_evts.map(e => e.inc)) | |
dupe .Click.insert(i, new_ctxd_evts.map(e => e.dupe)) | |
delete_.Click.insert(i, new_ctxd_evts.map(e => e.delete_)) | |
return i + n | |
} | |
case 'replace': { | |
const counters_items = op.$value | |
const n = counters_items.length | |
for (let j = 0; j < n; j += 1) { | |
view.updaters[i+j].counter_value(counters_items[j]) | |
} | |
return i + n | |
} | |
} | |
}, 0) | |
} | |
type ListenerFor<S> = S extends { subscribe: (l: infer L) => any } ? L : never | |
export const sync_view: ListenerFor<typeof counter.Edit> = ({ context: i }) => { | |
view.updaters[i].counter_value(counters[i]) | |
} | |
const contextualized_events = view.events.map(obj => { | |
const new_obj: any = {} | |
for (const key in obj) { | |
const stream: Stream<{ value: Event | string }> = obj[key as keyof typeof obj] | |
new_obj[key] = map_stream(stream, e => | |
({ ...e, context: view.events.indexOf(obj) })) | |
} | |
return new_obj as { | |
[K in keyof typeof obj]: typeof obj[K] extends Stream<infer V> | |
? Stream<V & { context: number }> | |
: never | |
} | |
}) | |
export namespace dec { | |
export const Click = any_stream(contextualized_events.map(e => e.dec)) | |
} | |
export namespace counter { | |
export const Edit = any_stream(contextualized_events.map(e => e.counter)) | |
} | |
export namespace inc { | |
export const Click = any_stream(contextualized_events.map(e => e.inc)) | |
} | |
export namespace dupe { | |
export const Click = any_stream(contextualized_events.map(e => e.dupe)) | |
} | |
export namespace delete_ { | |
export const Click = any_stream(contextualized_events.map(e => e.delete_)) | |
} | |
} | |
namespace add_counter { | |
export const Click = event_stream(add_counter_button, 'click') | |
} | |
// There's a button to add more counters. | |
add_counter.Click.subscribe(() => { | |
list.update_view([ | |
{ $type: 'skip', $value: counters.length }, | |
{ $type: 'insert', $value: [0] }, | |
]) | |
counters.push(0) | |
}) | |
// Counters can be edited, but only to an integer. | |
list.counter.Edit.subscribe(e => { | |
const {value, context: i} = e | |
const new_count = str_as_num(value) | |
if (!(new_count instanceof Error)) { | |
list.update_view([ | |
{ $type: 'skip', $value: i }, | |
{ $type: 'replace', $value: [new_count] }, | |
]) | |
counters[i] = new_count | |
} | |
list.sync_view(e) | |
}) | |
// Each counter has buttons that can increment, decrement, or delete it. | |
list.inc.Click.subscribe(({context: i}) => { | |
list.update_view([ | |
{ $type: 'skip', $value: i }, | |
{ $type: 'replace', $value: [counters[i] + 1] }, | |
]) | |
counters[i] += 1 | |
}) | |
list.dec.Click.subscribe(({context: i}) => { | |
list.update_view([ | |
{ $type: 'skip', $value: i }, | |
{ $type: 'replace', $value: [counters[i] - 1] }, | |
]) | |
counters[i] -= 1 | |
}) | |
list.dupe.Click.subscribe(({context: i}) => { | |
list.update_view([ | |
{ $type: 'skip', $value: i }, | |
{ $type: 'insert', $value: [counters[i]] }, | |
]) | |
counters.splice(i, 0, counters[i]) | |
}) | |
list.delete_.Click.subscribe(({context: i}) => { | |
list.update_view([ | |
{ $type: 'skip', $value: i }, | |
{ $type: 'remove', $value: 1 }, | |
]) | |
counters.splice(i, 1) | |
}) | |
// utils for built-in types | |
type ArrayDelta<T> = ArrayDeltaItem<T>[] | |
type ArrayDeltaItem<T> = | |
{ $type: 'skip', $value: number } | |
| { $type: 'remove', $value: number } | |
| { $type: 'replace', $value: T[] } | |
| { $type: 'insert', $value: T[] } | |
function str_as_num(str: string) { | |
const num = +str | |
return isNaN(num) | |
? new Error(`String cannot be converted to number: ${str}`) | |
: num | |
} | |
type TaggedUnion<T = string, V = any> = { $type: T, $value: V } | |
type TagOf<U extends TaggedUnion> = | |
U extends TaggedUnion<infer T, any> ? T : never | |
type ValOf<U extends TaggedUnion> = | |
U extends TaggedUnion<string, infer V> ? V : never | |
function map_inner<U extends TaggedUnion, V>(x: U, fn: (v: ValOf<U>) => V): TaggedUnion<TagOf<U>, V> { | |
return { | |
$type: x.$type as TagOf<U>, | |
$value: fn(x.$value), | |
} | |
} | |
function unwrap<U extends TaggedUnion>(x: U): ValOf<U> { | |
return x.$value | |
} | |
// view core | |
type ElementFromTag<T> = | |
T extends 'div' ? HTMLDivElement : | |
T extends 'span' ? HTMLSpanElement : | |
T extends 'ol' ? HTMLOListElement : | |
T extends 'li' ? HTMLLIElement : | |
T extends 'button' ? HTMLButtonElement : | |
T extends 'input' ? HTMLInputElement : | |
T extends 'textarea' ? HTMLTextAreaElement : | |
T extends 'select' ? HTMLSelectElement : | |
Element | |
function create_element<T extends string>( | |
tagName: T, attrs: {[k: string]: string}, ...children: Array<string | Element> | |
) { | |
const el = document.createElement(tagName) as ElementFromTag<T> | |
for (const name in attrs) { | |
el.setAttribute(name, attrs[name]) | |
} | |
for (const child of children) { | |
if (typeof child === 'string') { | |
el.appendChild(document.createTextNode(child)) | |
} else { | |
el.appendChild(child) | |
} | |
} | |
return el | |
} | |
function render_view(selector: string, el: Element) { | |
const mountEl = getElement(selector) | |
mountEl.innerHTML = '' | |
mountEl.appendChild(el) | |
} | |
function getElement(selector: string): Element { | |
if (selector.charAt(0) === '#') { | |
const matching = document.getElementById(selector.slice(1)) | |
if (matching === null) { | |
throw `Found no element with id: '${selector.slice(1)}'` | |
} | |
return matching | |
} else if (selector.charAt(0) === '.') { | |
const matching = document.getElementsByClassName(selector.slice(1)) | |
if (matching.length === 0) { | |
throw `Found no element with class name: '${selector.slice(1)}'` | |
} | |
if (matching.length > 1) { | |
throw `Found ${matching.length} elements with class name '${selector}', need exactly one only` | |
} | |
return matching[0] | |
} | |
throw `Selector for mount element to render at must start with either '#' or '.', got: '${selector.charAt(0)}'` | |
} | |
// view form field utils | |
function get_form_field_value(el: HTMLInputElement): string { | |
return el.value | |
} | |
function set_form_field_value(el: HTMLInputElement, value: string): void { | |
if (el.value !== value) { | |
el.value = value | |
} | |
} | |
// view events | |
function event_stream(el: Element, event_name: string): Stream<{ value: Event }> { | |
return { | |
subscribe(handler: (ev: { value: Event }) => void) { | |
let listener: (ev: Event) => void | |
el.addEventListener(event_name, listener = ev => { | |
handler({ value: ev }) | |
}) | |
return { | |
unsubscribe() { | |
el.removeEventListener(event_name, listener) | |
} | |
} | |
} | |
} | |
} | |
function edit_stream(el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement): Stream<{ value: string }> { | |
let event_name: string | |
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') { | |
event_name = 'input' | |
} else if (el.tagName === 'SELECT') { | |
event_name = 'change' | |
} else { | |
throw `'Edit' handlers only allowed on <input/>, <select/>, and <textarea/> elements, but got a <${el.tagName.toLowerCase()}/>` | |
} | |
return map_stream(event_stream(el, event_name), ({ value: ev }) => { | |
return { value: (ev.target as HTMLInputElement).value } | |
}) | |
} | |
// view for-loop updates | |
function insert_looped_elements(el: Element, i: number, items: Element[]) { | |
const child = el.children[i] // in IE 5.5-8, includes comment nodes https://www.quirksmode.org/dom/core/#t81 | |
for (const item of items) { | |
el.insertBefore(item, child || null) // IE 5.5+ | |
} | |
} | |
function remove_looped_elements(el: Element, i: number, n: number) { | |
for (let j = 0; j < n; j += 1) { | |
el.removeChild(el.children[i]) // removeChild is IE 5.5+ | |
} | |
} | |
function replace_looped_elements(el: Element, i: number, items: Element[]) { | |
for (let j = 0; j < items.length; j += 1) { | |
el.replaceChild(items[j], el.children[i+j]) // replaceChild is IE 5.5+ | |
} | |
} | |
// stream utils | |
interface Stream<T> { | |
subscribe: (listener: (x: T) => void) => { | |
unsubscribe: () => void | |
} | |
} | |
function map_stream<T, U>(stream: Stream<T>, fn: (x: T) => U): Stream<U> { | |
return { | |
subscribe(listener) { | |
return stream.subscribe(x => listener(fn(x))) | |
} | |
} | |
} | |
function any_stream<T>(streams: Stream<T>[]) { | |
// array of subscriptions per stream | |
const subs: { unsubscribe: () => void }[][] = streams.map(_ => []) | |
const listeners: Array<(x: T) => void> = [] | |
return { | |
subscribe(listener: typeof listeners[number]) { | |
listeners.push(listener) | |
for (let i = 0; i < streams.length; i += 1) { | |
subs[i].push(streams[i].subscribe(listener)) | |
} | |
return { | |
unsubscribe() { | |
const i = listeners.indexOf(listener) | |
listeners.splice(i, 1) | |
for (const subsForStream of subs) { | |
subsForStream.splice(i, 1) | |
} | |
} | |
} | |
}, | |
insert(i: number, new_streams: Stream<T>[]) { | |
streams.splice(i, 0, ...new_streams) | |
subs.splice(i, 0, ...new_streams.map( | |
s => listeners.map(l => s.subscribe(l)))) | |
}, | |
remove(i: number, n: number) { | |
streams.splice(i, n) | |
const removed = subs.splice(i, n) | |
for (const subsForStream of removed) { | |
for (const sub of subsForStream) sub.unsubscribe() | |
} | |
}, | |
} | |
} | |
function filter_stream<T, U extends T>(stream: Stream<T>, predicate: (x: T) => x is U): Stream<U> { | |
return { | |
subscribe(listener) { | |
return stream.subscribe(x => { | |
if (predicate(x)) { | |
listener(x) | |
} | |
}) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment