{"id":19967238,"url":"https://github.com/lochrist/spui","last_synced_at":"2025-06-12T15:03:01.418Z","repository":{"id":57368094,"uuid":"106590320","full_name":"lochrist/spui","owner":"lochrist","description":"SPUI is a library to create html that upates automatically","archived":false,"fork":false,"pushed_at":"2020-02-26T15:21:13.000Z","size":688,"stargazers_count":5,"open_issues_count":2,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-06-12T15:02:30.360Z","etag":null,"topics":["dom","examples","framework","small","ui","web"],"latest_commit_sha":null,"homepage":"https://lochrist.github.io/spui/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lochrist.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-10-11T18:01:56.000Z","updated_at":"2023-03-01T19:58:11.000Z","dependencies_parsed_at":"2022-09-05T19:21:24.572Z","dependency_job_id":null,"html_url":"https://github.com/lochrist/spui","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/lochrist/spui","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lochrist%2Fspui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lochrist%2Fspui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lochrist%2Fspui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lochrist%2Fspui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lochrist","download_url":"https://codeload.github.com/lochrist/spui/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lochrist%2Fspui/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259490389,"owners_count":22865760,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dom","examples","framework","small","ui","web"],"created_at":"2024-11-13T02:40:38.337Z","updated_at":"2025-06-12T15:03:01.394Z","avatar_url":"https://github.com/lochrist.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SPUI - SimPle UI\n\nSPUI is a library using an hyperscript syntax that helps create dom ui and automatically update when model changes. SPUI is fully DOM based (NOT vdom). \n\n### Buzzwords and hype galore\n\nSPUI is simple, small (about 600 LOC) and blazing fast (tm). And it is yet another UI framework. This is my rite of passage as a web developer.\n\n## Introduction\n\nThe goal of SPUI is to make it easy to create web application without relying on external templating library (mustache), or template compiler (jsx). All code is pure Javascript and uses the Document Object Model (DOM). This is not a VirtualDom library. I found that working closely with the DOM without having an intermediary abstraction like a VDOM helps get things done more easily. \n\nSPUI comes with a Stream module that makes it easy to define two way data bindings that will update the DOM automatically when model values are modified.\n\nThe SPUI streams enable that kind of workflow:\n\n```javascript\n// Label that updates automatically when a user types in an input field.\nconst model = valueStream('this is my initial value');\nh('div', {}, [\n    h('input', { value: model, oninput: selectTargetAttr('value', model) }),\n    h('label', {}, model)\n]);\n```\n\n![label update](docs/label_update.gif)\n\n## Installation\n\n```\nnpm install spui --save-dev\n```\n\n`dist/spui.js` contains the whole library. It is published as a UMD module.\n\n## Getting Started\n\nSPUI is basically an hyperscript function named `h` and a stream api to notify the DOM when model changes. The best way to dive into SPUI is to look at a dead simple TODO application example:\n\nFirst we create a stream that will store whatever the user types in an `\u003cinput\u003e` field:\n\n```javascript\nconst newTitle = sp.valueStream('');\n```\n\nThen we create an `ObservableArray` that will store our list of `Todo` models. When this list is mutated (if we add or remove from it) the DOM will be notified and updated accordingly:\n\n```javascript\nconst todos = new sp.ObservableArray();\n```\n\nNow let's add a function to add new todos to our list:\n\n```javascript\nfunction addTodo() {\n    if (newTitle()) {\n        todos.push(createTodo(newTitle()));\n        newTitle('');\n    }\n};\n```\n\nNotice how our stream `newTitle` is a getter/setter function. When called without argument it returns its backing value. When called with a single argument, it updates its backing value and notify all listeners about it.\n\nWe then add an helper function to create new todo:\n\n```javascript\nfunction createTodo (title: string, done = false) {\n    return {\n        title: sp.valueStream(title),\n        done: sp.valueStream(done)\n    };\n}\n```\n\nNotice how `title` and `done` are implemented as streams: this means modifying thoses values will trigger a notification that will keep the DOM up to date.\n\nIt is now time to create a `view` using the hyperscript `h` function:\n\n```javascript\nconst view = h('div', { id: 'todoapp'}, [\n    h('div', {class: 'header'}, [\n        h('h3', {}, 'todo express'),\n        h('input', { type: 'text', \n                    // `newTitle` is used as a getter here. Each time it will be change\n                    // the 'value' property will be updated as well.\n                    value: newTitle, \n                    placeholder: 'what is up?', \n                    // `newTitle` is used as a setter each time a new character is typed. \n                    // `targetAttr` is just an helper to extract the 'value` property\n                    // from the event target.\n                    oninput: sp.targetAttr('value', newTitle) \n        }),\n        // You can hook on to any dom events by adding an attribute prefixed with `on`\n        h('span', { class: 'addBtn', onclick: addTodo }, 'Add'),\n    ]),\n    // This is how SPUI handles list of elements: we bind the ObservableArray to an Element list:\n    sp.elementList('ul', {}, todos, (listNode: HTMLElement, todo: Todo) =\u003e {\n        // This is the function that is called any time a new DOM element needs to be constructed \n        // for a new Todo object.\n        return h('li', { class: { checked: todo.done }, \n                         onclick: () =\u003e todo.done(!todo.done()) }, [\n            todo.title,\n            // ObservableArray has a nifty remove function that will remove the Todo from the list\n            // and notify the DOM about it:\n            h('span', {class: 'close', onclick: () =\u003e todos.remove(todo)}, 'x')\n        ]);\n    })\n]);\n```\n\nAll that is left is to hook the view on to the body of the Document. Since the result of `h` is an `HTMLElement` it is as easy as this:\n\n```javascript\ndocument.body.appendChild(view);\n```\n\nAnd that's it! We now have a todo application (like a million other ones):\n\n![spui](docs/spui.gif)\n\nCheck [here](https://github.com/lochrist/spui/blob/master/examples/mini-todo/index.ts) to see the complete source of the examples.\n\n## More Examples\n\nMore complete examples can be found below:\n\n- [SPUI TODO MVC](examples/todomvc): the official [TODO MVC](http://todomvc.com/) has been implemented for SPUI will all kinds of nifty TODO workflows.\n- [Table benchmarks](examples/table-tests) and implementation of the world famous [JS Framework Benchmarks](https://github.com/krausest/js-framework-benchmark) using SPUI. This implementation is deceptively small and the performance is really good. It will be submitted to the official site soon(ish).\n- [Markdown Editor](examples/editor): really simple example on how to listen to changes in a `\u003ctextarea\u003e` and live convert those changes to markdown.\n- [Basic API usages](examples/basic-usages) this is a repository of snippets that showcases all the different functions of the SPUI Api. Some of these examples are used in the official Api documentation in the next section.\n\n# API\n\nThis section covers all the public Api for SPUI. If it becomes out of date (how could that happen?) you can always look at the typescript declaration files [here](https://github.com/lochrist/spui/tree/master/dist/types/spui) to have access to the actual Api.\n\n## DOM Manipulation\n\nThese are all the functions that helps create an HTML view and manipulate DOM elements. All of those functions can be used with the stream Api to benefit from automatic update. But you can use the hyperscript `h` function alone just to quickly create a new static HTML view.\n\n## h(tagName, attrs?, children?) -\u003e HTMLElement\n\nArgument    | Type                 | Required | Description\n----------- | -------------------- | -------- | ---\n`tagname`   | `string`            | Yes      | The HTML Element tagname (div, label, button, ...)\n`attrs`    | `Object` | No      | An Object where all keys are attributes or properties to specify  on the HTMLElement\n`children`    | `Array| string | HTMLElement | Function` | No      | Children to append to the HTMLElement\n**returns** |                      |          | Returns the newly created [HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement)\n\n#### Description\n\n`h` is the one stop shop to create a DOM view. This function has a lot of flexibility and intricacies like all hyperscript function can be. At its simplest, `h` can be seen as a blueprint on how the HTML to build would look:\n\nWhen you want to produce the following HTML:\n```html\n\u003cdiv id='todoapp'\u003e\n    \u003cinput type='text' placeholder='what is up?' input=myHandler\u003e\n    \u003cbutton click=clickHandler\u003eAdd\u003c/button\u003e\n\u003c/div\u003e\n```\n\nyou use `h` like this:\n\n```javascript\nh('div', {id: 'todoapp'}, [\n    h('input', {type: 'text', placeholder: 'what is up?', onpinput=myHandler),\n    h('button', {onclick: clickHandler}, 'Add')\n])\n```\n\nAll the intricacies are about specifying the different attributes and children. Keep in mind that the result of `h` is an [HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). You can use that element directly and manipulate it as you see fit.\n\n#### General attributes\n\nAttributes are specified as a javascript object where each `key` must be a `string`. Each of these `key` will be used to set an attribute (or property) of the created `HTMLElement`. The value of each attribute can be either a `number`, `string`, `boolean` or a `function`. If you specify a `function`, SPUI will evaluate it to see if it involves Stream evaluation and if so, it will keep a binding on that Stream so when it changes, the DOM attribute will be updated as well. Here is an example of an element attribute:\n\n```javascript\nconst model = valueStream('this is my initial value');\nh('div', {}, [\n    // when the user types into the \u003cinput\u003e we update model...\n    h('input', { id: 'myInput', placeholder: 'Enter a value' value: model, oninput: targetAttr('value', model) }),\n    // ...when model changes, label content updates automatically.\n    h('label', {}, model)\n]);\n```\n\n#### class attribute\n\nThe `class` attribute can be specified in multiple ways. Either as a `string`:\n\n```javascript\nconst label = {info: true, danger: false};\nh('div', { class: 'alert alert-info' }, 'This is an alert label');\n```\n\nOr as an object where each key is a `class` to add if the corresponding value is truthy:\n\n```javascript\nconst label = {info: true, danger: false};\n\n// This creates the same class as above =\u003e 'alert alert-info'\nh('div', { class: { alert: true,\n                    'alert-info': label.info,\n                    'alert-danger': label.danger \n        } \n    }, 'This is an alert label');\n```\n\nYou can speficy a stream either for the whole class value:\n\n```javascript\nconst labelClass = valueStream('alert alert-info');\nh('div', { class: labelClass }, 'This is an alert label');\n\n// This will update the class attribute to danger!\nlabelClass('alert alert-danger'); \n```\n\nOr for any value of the `Object` specifying the class:\n```javascript\nconst isInfo = valueStream(true);\nconst isDanger = valueStream(false);\nh('div', { class: { alert: true,\n                    'alert-info': isInfo,\n                    'alert-danger': isDanger \n        } \n    }, 'This is an alert label');\n\nisInfo(false); // class becomes 'alert'\nisDanger(true); // class becomes 'alert alert-danger'\n```\n#### style attribute\n\nStyle attribute is similar to `class` attribute in that you can specify it with multiples ways using Stream (or not). It can be specified as a `string`:\n\n```javascript\nh('div', { style: 'color: black; background-color: grey;padding: 10px;' }, 'dark label #1');\n```\n\nOr as an object where each key is a `style attribute`:\n\n```javascript\nh('div', { style: { color: 'grey', backgroundColor: 'black', padding: '10px' } }, 'darker label #2');\n```\n\nYou can use Stream either for the whole `style` value or for any `style` attributes:\n\n```javascript\n// Attribute with a boolean value, are setup specially in the DOM\nconst colors = ['blue', 'green', 'red', 'black', 'pink'];\nconst randomColor = () =\u003e randomElement(colors);\nconst color1 = valueStream(randomColor());\nconst color2 = valueStream(randomColor());\nh('div', { class: 'child-container' }, [\n    h('button', { onclick: () =\u003e {\n            color1(randomColor());\n            color2(randomColor());\n        } }, 'random color'),\n    // This is a computed whole style value:\n    h('div', { class: 'color-display', style: () =\u003e 'background-color: ' + color1() }),\n    // pass the Stream directly for the backgroundColor\n    h('div', { class: 'color-display', style: { backgroundColor: color2 } })\n]);\n```\n\n#### boolean attributes\n\nBoolean attributes are special in HTML: they need to be specified in the `HTMLElement`when it when they are `true` and need to be removed from the HTMLElement when `false`.\n\n```HTML\n\u003cbutton\u003eEnabled\u003c/button\u003e\n\u003cbutton disabled\u003eDisabled\u003c/button\u003e\n```\n\nSPUI will ensure all boolean attributes are properly added (or removed) from owning `HTMLElement`. If you use a Stream to specify the value of the attribute this makes it easy to have it be updated automatically:\n\n```javascript\n// Attribute with a boolean value, are setup specially in the DOM\nconst readonlyInput = valueStream(true);\nconst disabledInput = valueStream(true);\n\nh('div', { class: 'child-container' }, [\n    // Toggle if `i2` is disabled or not\n    h('button', { onclick: () =\u003e disabledInput(!disabledInput()) }, 'Toggle'),\n    h('input', { id: 'i1', readonly: readonlyInput }),\n    h('input', { id: 'i2', disabled: disabledInput }),\n]);\n\n// Makes `i1` input writable\nreadonlyInput(true);\n```\n\n#### events\n\nAny attribute names beginning with `on` is assumed to be an handler that will be added as a *event listene*r to the HTMLElement:\n\n```javascript\nlet input, button, buttonText = 'Roll D6';\nh('div', { class: 'child-container' }, [\n    input = h('input', { placeholder: 'this is readonly', readonly: true, value: 1 }),\n    // Listener to an event is adding an attribute of name : on\u003ceventName\u003e\n    button = h('button', {\n        onclick: () =\u003e {\n            // h() returns an HTMLElement. So it is really easy changing the value of input:\n            input.value = randomNumber(6) + 1;\n        },\n        onmouseenter: () =\u003e {\n            button.innerText = 'Click to roll';\n        },\n        onmouseleave: () =\u003e {\n            button.innerText = buttonText;\n        }\n    }, buttonText),\n]);\n```\n\n#### children\n\nYou can specify children in multiple ways with `h`. Children can be a single value(`string`, `number`, `boolean`, `HTMLElement`):\n\n```javascript\nh('div', {}, 'this is a text children');\nh('div', {}, 42);\nh('div', {}, \n    h('span', {}, 'hello world')\n);\n```\n\nYou can also specify a `function` that will receive the newly created element as a parameter and that must output an `HTMLElement`:\n\n```javascript\nlet parent = h('div', {}, p =\u003e {\n    // Here p === parent:\n    return h('span', {}, 'I am a children');\n});\n```\n\nYou can also specify a Stream that will update the children value:\n\n```javascript\nconst title = valueStream('This is my title');\nh('div', {}, title);\n\ntitle('I have changed'); // Updates the text children node of the HTMLElement\n```\n\nA children value in `h` can also be an `array` of any of the above:\n\n```javascript\nconst title = valueStream('This is my title');\nlet parent = h('div', { class: 'child-container' }, [\n    'this is a text children',\n    title\n    p =\u003e {\n        // Here p === parent:\n        return h('span', {}, 'I am a children');\n    }\n    h('button', {}, 'Button child')\n]);\n```\n\nFor information on specifying a dynamic list of children see `elementList` below.\n\n---\n\n## elementList(tagname, attrs, models, elementCreator) -\u003e HTMLElement\n\nArgument    | Type                 | Required | Description\n----------- | -------------------- | -------- | ---\n`tagname`   | `string`            | Yes      | The HTML List Element tagname (div, ul, ...)\n`attrs`    | `Object` | Yes      | An Object where all keys are attributes or properties to specify  on the list Element\n`models`    | `ObservableArray\u003cT\u003e` | Yes      | An Array containing all the model objects.\n`elementCreator`    | `Function` | Yes      | Function called to create HTMLElement when a model is added to `models`\n**returns** |                      |          | Returns the newly created list as an [HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement)\n\n#### Description\n\n`elementList` will create an HTMLElement that listens to `models` mutating notifications to keep the HTML list up to date. When new model are added to `models`, `elementCreator` is invoked to create a new HTML child element add it at the corresponding position in the HTML list. When a model is removed from `models` the corresponding HTML children is removed as well.\n\n\n`elementCreator` has the following signature: `(listRootElement: HTMLElement, model: any, index: number) =\u003e HTMLElement`\n- `listRootElement`: this is the parent of the HTMLElement child to create.\n- `model`: This is the model used to populate the child HTMLElement\n- `index`: index of the model in the `models` ObservableArray\n\n```javascript\nconst models = new ObservableArray();\nlet count = 0;\nh('div', {}, [\n    // Add a new value to the models: this will add a new HTML children in the DOM\n    h('button', { onclick: () =\u003e models.push(valueStream(count++)) }, 'Add'),\n    // Remove a random element from models. This will remove the corresponding\n    // HTMLElement from the DOM\n    h('button', { onclick: () =\u003e models.splice(randomIndex(models.array), 1) }, 'Remove random'),\n    // elementList will update the \u003cul\u003e element when new elements are added or removed.\n    elementList('ul', {}, models, (listNode, model, index) =\u003e {\n        // Simple \u003cli\u003e sowing the model value:\n        return h('li', {}, model);\n    })\n]);\n```\n\n---\n\n## targetAttr(eventAttrName, handler) -\u003e (event) =\u003e void\n\nArgument    | Type                 | Required | Description\n----------- | -------------------- | -------- | ---\n`eventAttrName`   | `string`            | Yes      | Name of an attribute on `event.target`.\n`handler`    | `(value) =\u003e void` | Yes      | A callback that takes the selected attribute as parameters\n**returns** |                      |          | Returns an event handler that can be hooked on to a [DOM event](https://developer.mozilla.org/en-US/docs/Web/Events).\n\n#### Description\n\nThis is a helper method used to bind to HTML event in a simpler way. If you use `targetAttr` when registering a DOM event it extracts from `event.target` a specific attribute and forward it to a custom `handler`.\n\nHere is what extracting the `checked` value would look like *without* `targetAttr`:\n\n```javascript\nh('input', { type: 'checkbox', checked: isChecked, onclick: event =\u003e isChecked(event.target.checked) })\n```\n\nThis is how it looks with `targetAttr`:\n\n```javascript\nh('input', { type: 'checkbox', checked: isChecked, onclick: targetAttr('checked', isChecked) })\n```\n\nThis makes it easier to forward events to Stream directly.\n\n---\n\n## Stream\n\nThe stream API is based on the concept of getter/setter functions. These functions can be combined using helper method to create a stream of events that will trigger and transform the resulting values.\n\nFor more information on reactive programming you can look [here](https://www.google.ca/url?sa=t\u0026rct=j\u0026q=\u0026esrc=s\u0026source=web\u0026cd=13\u0026ved=0ahUKEwjmyufywZvXAhWs34MKHYgEBL8QFghjMAw\u0026url=https%3A%2F%2Fgist.github.com%2Fstaltz%2F868e7e9bc2a7b8c1f754\u0026usg=AOvVaw1AIbUOGhFWsI1yyAON-2FL).\n\nSPUI streams are similar to [flyd](https://github.com/paldepind/flyd) and [mithril.js streams](https://mithril.js.org/stream.html). They are just less powerful and have less features :)\n\n---\n\n## valueStream(initialValue, transformer?) -\u003e Stream\n\nArgument    | Type                 | Required | Description\n----------- | -------------------- | -------- | ---\n`initialValue`   | any | Yes      | Initial value of the stream\n`transformer`    | value =\u003e any | No      | Function that will convert the the `value` each time the stream is invoked as a setter.\n**returns** |                      |          | Returns a Stream wrapping `value`\n\n#### Description\n\n`valueStream` creates a Stream. A Stream is a `function` that act as a getter when invoked with no parameter. It act as a setter when invoked with a single parameter and it notifies it listeners that its value has changed.\n\nIf `transformer` is specified, each time the stream is invoked as a setter, it will apply the `transformer` function before setting the sream backing value.\n\n```javascript\nconst model = valueStream(42);\n// stream called with no param: getter\nconsole.log(model());\n// stream called with a param: setter\nmodel(71);\n// Prints 71\nconsole.log(model());\n\nconst doublingModel = valueStream(42, value =\u003e value * 2);\nconsole.log(doublingModel()); // Prints 84\n\ndoublingModel(21);\nconsole.log(doublingModel()); // Prints 42\n```\n\n---\n\n## addListener(stream, listener) -\u003e FunctorToStopListening\n\nArgument    | Type                 | Required | Description\n----------- | -------------------- | -------- | ---\n`stream`   | `Stream` | Yes      | The stream to listen for value changes\n`listener`    | `value =\u003e void` | Yes      | callback to be invoked when the stream value is updated\n**returns** | `() =\u003e void`|          | Returns a `function` that will stop listening for value changes when invoked\n\n#### Description\n\nAdd a listener to be notified when the stream value changes:\n\n```javascript\nconst model = valueStream(42);\nconst stopListening = addListener(model, value =\u003e {\n    console.log('Model has changed: ', value);\n});\n// prints: Model has changed: 11\nmodel(11);\nstopListening();\n// This won't print anything.\nmodel(22);\n```\n\n---\n\n## removeListener(stream, listener) -\u003e void\n\nArgument    | Type                 | Required | Description\n----------- | -------------------- | -------- | ---\n`stream`   | `Stream` | Yes      | Stream to stop listening to\n`listener`    |`() =\u003e void` | Yes      | Original listener\n\n#### Description\n\nStops listening to value changes of a particular stream.\n\n---\n\n## addTransform(stream, transformer) -\u003e void\n\nArgument    | Type                 | Required | Description\n----------- | -------------------- | -------- | ---\n`stream`   | `Stream` | Yes      | Stream that will be added a `transformer` function\n`transformer`    | `value =\u003e any` | Yes      | Function that will transformed the value before setting it in the stream.\n\n#### Description\n\nModifies a Stream and add a `transformer` function to it. This `transformer` will be invoked each time the stream is used as a setter.\n\n```javascript\nconst model = valueStream(42);\n// Prints 42\nconsole.log(model());\n// Modify the stream itself by adding a transformer:\naddTransform(model, value =\u003e value * 2);\n// Prints 84\nconsole.log(model());\n```\n---\n\n## computeStream(functor) -\u003e Stream\n\nArgument    | Type                 | Required | Description\n----------- | -------------------- | -------- | ---\n`functor`   | `() =\u003e any` | Yes      | A Function that can contains any number of stream usages and that returns any value.\n**returns** | `Stream`|          | Returns a computed stream that updates when any of its dependencies are modified.\n\n#### Description\n\n`compute` creates a new computed stream that is the result of evaluating a `functor` containing stream usages. Each time any of the dependency stream changes, the value of the computed stream is updated.\n\n```javascript\nconst firstName = valueStream('Donald');\nconst lastName = valueStream('Knuth');\nconst fullName = computeStream(() =\u003e {\n    return firstName() + ' ' + lastName();\n});\n// Prints: Donald Knight\nconsole.log(fullName());\nlastName('Duck');\n// Prints Donald Duck. And loses all respect.\nconsole.log(fullName());\n```\n\nNotice that the computed stream **cannot** be used as a setter. Its resulting value is ALWAYS the result of the computation.\n\n```javascript\nfullName('Donald the Mighty');\n\n// Still prints Donald Duck.\nconsole.log(fullName());\n```\n\n---\n\n## compute(functor) -\u003e Computation\n\nArgument    | Type                 | Required | Description\n----------- | -------------------- | -------- | ---\n`functor`   | '() =\u003e any' | Yes      | A Function that can contains any number of stream usage and that returns any value.\n**returns** | `Computation`|          | Returns a `Computation` that contains a computed stream and its dependencies.\n\n#### Description\n\nSimilar to `computeStream` above but also returns the lists of all dependencies used to evaluate the `computedStream`.\n\n### Computation declaration\n\n```javascript\nexport interface Computation {\n    computedStream: Stream;\n    dependencies: Stream[];\n}\n```\n---\n\n## map(stream, transformer) -\u003e Stream\n\n`stream`   | `Stream` | Yes      | Stream we will react to in order to apply a `transformer`\n`transformer`    | `value =\u003e any` | Yes      | Function that will transform the value before setting it in the stream.\n**returns** | `Stream`|          | Returns a new stream \n\n#### Description\n\nCreates a new Stream that will invoke `transformer` function each time the original `stream` is changed.\n\n```javascript\nconst model = valueStream(42);\n// Create a new stream that maps the original model value:\nconst mappedModel = map(model, value =\u003e value * 2);\n// Prints 42\nconsole.log(model());\n// Prints 84\nconsole.log(mappedModel());\nmodel(11);\n// Prints 22\nconsole.log(mappedModel());\n```\n---\n\n## ObservableArray\n\nObservableArray is a wrapper over builtin javascript array. It reimplements all the mutators functions (`push`, `pop`, `splice`, `shift`, `unshift`, `sort`, `reverse`) and broadcasts events to listeners when any of those mutators are called. As an example of usage, SPUI `elementList` listens to changes happening in an ObservableArray to update the DOM by adding or removing new HTML Element.\n\n### ObservableArray declaration\n\n```javascript\nclass ObservableArray\u003cT\u003e {\n    array: T[];\n    listeners: ArrayListener[];\n    constructor(array?: T[]);\n    readonly length: number;\n    \n    push(...args: any[]): any;\n    pop(...args: any[]): any;\n    reverse(...args: any[]): any;\n    shift(...args: any[]): any;\n    splice(...args: any[]): any;\n    sort(...args: any[]): any;\n    unshift(...args: any[]): any;\n    remove(value: T): void;\n\n    applyChanges(changeFunctor: () =\u003e any): any;\n    addListener(callback: ArrayListener): () =\u003e any[];\n    removeListener(callback: ArrayListener): any[];\n}\n```\n\n## addListener(arrayChangeListener) -\u003e RemoveListenerFunctor\n\nArgument    | Type                 | Required | Description\n----------- | -------------------- | -------- | ---\n`arrayChangeListener`   | see below | Yes      | A function that gets called when any of the ObservableArray mutators gets called.\n**returns** | `() =\u003e void`|          | Returns a `function` that when invoked will stop listening for array changes.\n\n#### Description\n\n### Array change listener declaration\n\n`(op: string, args: any[], opReturnValue: any) =\u003e void`\n\n- `op`: name of the mutator that was invoked\n- `args`: arguments passed to the mutators\n- `opReturnValue`: result of the mutator invocation\n\n`addListener` allows to receive notifications when the ObservableArray is modified. It returns a `function` that when invoked will stop listening for changes.\n\nSee [Filter.srcChanged](https://github.com/lochrist/spui/blob/3be7a9bfb0af88c897641e76e485b6fcf64371b8/spui/observable-array.ts#L178) or [elementList.onModelChange](https://github.com/lochrist/spui/blob/3be7a9bfb0af88c897641e76e485b6fcf64371b8/spui/dom.ts#L164) to have examples on how to react to ObservableArray changes.\n\n---\n\n## removeListener(arrayChangeListener)\n\nArgument    | Type                 | Required | Description\n----------- | -------------------- | -------- | ---\n`arrayChangeListener`   | see below | Yes      | Original array listener to stop listening on.\n\n#### Description\n\nStops listening to the ObservableArray changes.\n\n---\n\n## applyChanges(changeFunctor) -\u003e any\n\nArgument    | Type                 | Required | Description\n----------- | -------------------- | -------- | ---\n`changeFunctor`   | () =\u003e any | Yes      | A `function` invoking multiple mutators.\n**returns** | any|          | Returns whatever the `changeFunctor` returned.\n\n#### Description\n\n`applyChanges` will invoke `changeFunctor` and during that invocation all mutators caled on the ObservableArray will have their notification batched in a single event called `changes` allowing a listener to process all notifications at once.\n\n```javascript\nconst obsArray = new sp.ObservableArray\u003cnumber\u003e();\n\nobsArray.addListener((op, args, returnValue) =\u003e {\n    console.log(op, args, returnValue);\n});\n\nconst finalValue = obsArray.applyChanges(() =\u003e {\n    obsArray.push(1);\n    obsArray.splice(0, 1, 42);\n\n    return obsArray.length;\n});\n\n// When this resolve the listener will print this:\n// changes [['push', [1], 1], ['splice' [0, 1, 42] [1] ]]\n\n// Notice that finalValue === obsArray.length\n```\n\n---\n\n## Filter\n\n### Filter declaration\n\n```javascript\nclass Filter\u003cT\u003e {\n    src: ObservableArray\u003cT\u003e;\n    filtered: ObservableArray\u003cT\u003e;\n    predicate: FilterPredicate\u003cT\u003e;\n    constructor(src: ObservableArray\u003cT\u003e, predicate: FilterPredicate\u003cT\u003e);\n    applyFilter(predicate?: FilterPredicate\u003cT\u003e, reset?: boolean): Changes;\n}\n```\n\n## constructor(src, predicate)\n\nArgument    | Type                 | Required | Description\n----------- | -------------------- | -------- | ---\n`src`   | `ObservableArray` | Yes      | ObservableArray that will be filtered.\n`predicate`    | `value =\u003e boolean` | Yes      | Predicate invoked to filter each value of `src`.\n\n#### Description\n\nCreates a new `Filter` object from an `src` ObservableArray. `Filter` contains a `filtered` ObservableArray that is the result of applying the `predicate` function to all of the values of `src`. Each time any of the mutators function of `src` is invoked, `filtered` is kept up to date.\n\nHere is how you would update a list of names as a user types a pattern in a `input`: \n\n```javascript\nconst models = new ObservableArray();\nfor (let i = 0; i \u003c 1000; ++i)\n    models.push(generateName());\nconst match = valueStream('');\nconst filter = new Filter(models, (model) =\u003e {\n    return match() ? model.indexOf(match()) \u003e -1 : true;\n});\nconst triggerFilter = map(match, () =\u003e filter.applyFilter());\nh('div', {}, [\n    h('input', { oninput: targetAttr('value', match) }),\n    // elementList will update \u003cul\u003e when new elements are added or removed due to filtering\n    elementList('ul', { style: 'height: 300px;overflow: auto' }, filter.filtered, (listNode, model, index) =\u003e {\n        return h('li', {}, model);\n    })\n]);\n```\n\nIn the above example, notice we use `filter.filtered` as the source ObservableArray to build the `elementList`. This means each time `filtered` is udpated due to filtering, the `elementList` is kept up to date.\n\n![](docs/list_filter_update.gif)\n\n---\n\n## applyFilter(predicate?, reset?) : Changes\n\nArgument    | Type                 | Required | Description\n----------- | -------------------- | -------- | ---\n`predicate`   | value =\u003e boolean | No      | New predicate to set in the Filter.\n`reset`    | `boolean` | No      | Will empty the current array and filter it from scratch (this creates less notifications).\n\n#### Description\n\nReapply the `predicate` over the values of the `src` array of the filter. This needs to be called if the `predicate` has changed. You can also use this function to change the `predicate` altogether.\n\n--- \n\n## SPUI Inspiration\n\nSPUI owes a lot to [Mithril JS](https://mithril.js.org/). This is the first vdom framework I used and this is where I discovered that I like to use hyperscript. When creating SPUI, I wanted to have a similar syntax but to work directly on the DOM.\n\n[Redom](https://redom.js.org/) is another source of inspiration. This is a tight framework that is totally DOM based. I modeled SPUI `elementList` API over Redom `list`. Redom `list` have to be refreshed manually though. This makes it simpler to reconcile the DOM but makes the update less magical :)\n\n[Surplus-js](https://github.com/adamhaile/surplus) has been another inspiration. This is a lightning fast library that is also DOM based and that updates automatically.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flochrist%2Fspui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flochrist%2Fspui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flochrist%2Fspui/lists"}