From 0d0d2bb3c2a8dd007cfc554edb9fbd24379dcccc Mon Sep 17 00:00:00 2001 From: fynyky Date: Sun, 31 Aug 2025 03:20:26 +0800 Subject: [PATCH 1/8] Removed redundant docker dev environment config --- .docker/config.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .docker/config.json diff --git a/.docker/config.json b/.docker/config.json deleted file mode 100644 index b60591f..0000000 --- a/.docker/config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "image": "docker/dev-environments-javascript:stable-1" -} \ No newline at end of file From d10f3f96293aaea78788b8f0bce9cb34a5f0f04b Mon Sep 17 00:00:00 2001 From: fynyky Date: Sun, 31 Aug 2025 03:29:29 +0800 Subject: [PATCH 2/8] Removed JSFiddle since it's obsolete --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index a2df94a..894f9d6 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,6 @@ Elemental is meant to be unobtrusive and unopinionated. - There is no need to manually declare listeners or bindings. Elemental automatically keeps track of all that for you. - Use it for the whole front-end or just a few components. Elements created by Elemental are just normal DOM elements, and any variable can be easily replaced with a reactive one without changing the rest of your codebase. -You try it yourself in a JSFiddle [here](https://jsfiddle.net/8wrqLy9h/) - Elemental is a reactive UI layer built on top of [Reactor.js](https://github.com/fynyky/reactor.js) Installation From c486c7b32b93bd17f924fa43eba310897d8f4027 Mon Sep 17 00:00:00 2001 From: fynyky Date: Sun, 31 Aug 2025 05:28:21 +0800 Subject: [PATCH 3/8] Cleaned up README opening --- README.md | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 894f9d6..bed9a19 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,16 @@ Elemental ========== -Elemental is a simple reactive front-end library. It provides -- `Reactor` objects that store reactive variables -- `Observer` functions that automatically track the reactive variables that they use and retrigger if any of these variables are updated. The function `ob` is shorthand for `new Observer` -- A function `el` that allows declarative element creation in javascript +Elemental is a simple front-end library that lets you build reactive UIs declaratively using plain JavaScript. No special syntax to learn or complex frameworks to build around, just normal nested functions. Here's a quick example of what Elemental does: ```javascript import { Reactor, ob, el } from '@fynyky/elemental' -const rx = new Reactor({ - name: 'Anakin' -}) +const rx = new Reactor({ name: 'Anakin' }) el(document.body, - el('main', + el('main', el('h1', 'Hello World!'), el('h2', (x) => { x.id ='foo' }, () => 'returned text'), el('defaults to div', ['this', 'is', 'an', 'array']), @@ -34,20 +29,20 @@ rx.name = 'Darth' // Changes to //

My name is Darth

``` -- `Reactor` objects work like normal objects that you can set and get properties on -- `Observer` functions work like normal functions that you can define and call -- When an `Observer` reads a `Reactor` it registers itself as a dependent -- When a `Reactor` is updated it automatically retriggers the dependent `Observer` functions -- `el` creates a DOM element - - The first argument is the type of element it creates +- `el` is a function that creates elements then attaches children to them + - The first argument is the type of element to create (or an existing element to reuse) - Subsequent arguments are appended as children - - Function children are run with the context of the parent element - - Function return values are appended as children - - `Observer` functions automatically replace their child nodes when retriggered + - Functions are run given their parent, and their return values are appended + - `Observer` functions do the same, but their children get replaced when updated +- `ob` is shorthand for `new Observer` +- An `Observer` is a function that automatically tracks reactive variables that it uses, and retriggers if they get updated. +- A `Reactor` is an object that stores reactive variables +- When a `Reactor` is updated it automatically retriggers the dependent `Observer` functions -Elemental is meant to be unobtrusive and unopinionated. -- No special syntax to learn. Everything is just plain javascript -- There is no need to manually declare listeners or bindings. Elemental automatically keeps track of all that for you. +Elemental is designed to be unobtrusive and unopinionated. +- No special syntax to learn. Everything is just plain JavaScript +- No need to manually declare listeners. Elemental automatically keeps track of all that +- No complex framework internals to debug. Its just appending elements and rerunning observers when needed - Use it for the whole front-end or just a few components. Elements created by Elemental are just normal DOM elements, and any variable can be easily replaced with a reactive one without changing the rest of your codebase. Elemental is a reactive UI layer built on top of [Reactor.js](https://github.com/fynyky/reactor.js) From 7396552655c3126f6e0d77d28f1e496e1426654f Mon Sep 17 00:00:00 2001 From: fynyky Date: Sun, 31 Aug 2025 11:31:39 +0800 Subject: [PATCH 4/8] Simplified the auto query selector stuff --- src/elemental.js | 4 ++-- src/utils.js | 24 ++++-------------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/elemental.js b/src/elemental.js index d3ec766..73625d7 100644 --- a/src/elemental.js +++ b/src/elemental.js @@ -16,7 +16,7 @@ // This enables automatic UI updates when the underlying data changes. import { Observer, shuck, hide } from 'reactorjs' -import { getAllComments, getNodesBetween, isQuerySelector, VALID_HTML_TAGS } from './utils.js' +import { getAllComments, getNodesBetween, isTreatedAsQuerySelector, VALID_HTML_TAGS } from './utils.js' // Automatically start/stop observers when elements are added/removed from the DOM. // This prevents "orphan" observers from staying alive and updating nodes that are no longer relevant. @@ -90,7 +90,7 @@ export const el = (descriptor, ...children) => { if (descriptor instanceof Element) { self = descriptor // If it looks like a selector try to find the existing element - } else if (isQuerySelector(descriptor)) { + } else if (isTreatedAsQuerySelector(descriptor)) { self = document.querySelector(descriptor) if (!self) { throw new Error(`el descriptor selector "${descriptor}" not found`) diff --git a/src/utils.js b/src/utils.js index 96a93d0..81bc208 100644 --- a/src/utils.js +++ b/src/utils.js @@ -68,27 +68,11 @@ export function getNodesBetween (startNode, endNode) { return result } -// Helper function to check if a string is a valid css query selector but not a single word html tag -// Need to distinguish because html tags are also valid css selectors -export function isQuerySelector (testString) { +// Helper function to check if a string should be used as a css query selector +export function isTreatedAsQuerySelector (testString) { if (typeof testString !== 'string' || testString.length === 0) { return false } - - // Common CSS selector patterns - const selectorPatterns = [ - /^[.#[]/, // Starts with . # [ - /^[a-zA-Z-]+\[/, // Attribute selectors like div[attr] - /^[a-zA-Z-]+:/, // Pseudo-selectors like div:hover - /^[a-zA-Z-]+::/, // Pseudo-elements like div::before - /^[a-zA-Z-]+\.[a-zA-Z-]/, // Compound selectors like div.class - /^[a-zA-Z-]+#[a-zA-Z-]/, // Compound selectors like div#id - /^[>+~]/, // Starts with > + ~ - /^[a-zA-Z-]+\s/, // Tag followed by space (descendant selector) - /^[a-zA-Z-]+>/, // Tag followed by > (child selector) - /^[a-zA-Z-]+\+/, // Tag followed by + (adjacent sibling) - /^[a-zA-Z-]+~/ // Tag followed by ~ (general sibling) - ] - - return selectorPatterns.some(pattern => pattern.test(testString)) + // Simple test: starts with # or . + return /^[.#]/.test(testString) } From 39fe4380b8066f1b7548d2b60e01b2ac873beb9f Mon Sep 17 00:00:00 2001 From: fynyky Date: Sun, 31 Aug 2025 11:58:27 +0800 Subject: [PATCH 5/8] README cleanup --- README.md | 133 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index bed9a19..48856e1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ const rx = new Reactor({ name: 'Anakin' }) el(document.body, el('main', el('h1', 'Hello World!'), - el('h2', (x) => { x.id ='foo' }, () => 'returned text'), + el('h2', (x) => { x.id = 'foo' }, () => 'returned text'), el('defaults to div', ['this', 'is', 'an', 'array']), el('p more class names', ob(() => ('My name is ' + rx.name))) ) @@ -42,10 +42,10 @@ rx.name = 'Darth' Elemental is designed to be unobtrusive and unopinionated. - No special syntax to learn. Everything is just plain JavaScript - No need to manually declare listeners. Elemental automatically keeps track of all that -- No complex framework internals to debug. Its just appending elements and rerunning observers when needed +- No complex framework internals to debug. It just appends elements and runs functions when needed - Use it for the whole front-end or just a few components. Elements created by Elemental are just normal DOM elements, and any variable can be easily replaced with a reactive one without changing the rest of your codebase. -Elemental is a reactive UI layer built on top of [Reactor.js](https://github.com/fynyky/reactor.js) +Elemental is built on top of [Reactor.js](https://github.com/fynyky/reactor.js) Installation ------------ @@ -57,7 +57,7 @@ $ npm install @fynyky/elemental Import it using: ```javascript -import { +import { el, ob, attr, @@ -67,10 +67,11 @@ import { hide, batch, shuck -} from '@fynyky/elemental' +} from '@fynyky/elemental' +``` ``` -It is also available directly from [unpkg](unpkg.com). You can import it in javascript using +It is also available directly from [unpkg](https://unpkg.com). You can import it in JavaScript using ```javascript import { el, attr, bind, ob, Reactor, Observer, hide, batch, shuck } from 'https://unpkg.com/@fynyky/elemental' ``` @@ -99,9 +100,11 @@ el('foo bar') ``` If given an existing `Element` it does nothing on its own but uses the provided element as a target for applying the `children` arguments. For example you can append things to the document body by doing + ```javascript el(document.body, 'hello world') ``` + ```html hello world @@ -124,19 +127,25 @@ el(document.querySelector('some query')) -------------------------------------------------------------------------------- `String` arguments provided as `children` are appended as text nodes + ```javascript -el(h1, 'hello world') +el('h1', 'hello world') ``` + ```html

hello world

``` + `Element` arguments are just appended directly + ```javascript -el(h1, document.createElement('div')) +el('h1', document.createElement('div')) ``` + ```html

``` + Since `el` itself returns elements, this allows nesting of `el` calls to declaratively create the DOM ```javascript @@ -158,7 +167,7 @@ el(document.body, -------------------------------------------------------------------------------- -`Function` arguments are run in the context of the parent. This allows arbitrary manipulation of the parent such as attaching listeners, setting styles, etc +`Function` arguments are run in the context of the parent. This allows arbitrary manipulation of the parent such as attaching listeners, setting styles, etc. ```javascript el('h1', function() { @@ -167,11 +176,12 @@ el('h1', function() { this.style.color = 'red' }) ``` + ```html

``` -The parent is also provided as the first argument to the function when it is called. This allows [arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) to work +The parent is also provided as the first argument to the function when it is called. This allows [arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) to work. ```javascript el('h1', x => { @@ -180,10 +190,12 @@ el('h1', x => { x.style.color = 'red' }) ``` + ```html

``` -If the function returns a value, that value is appended as a child + +If the function returns a value, that value is appended as a child. ```javascript el('h1', x => { @@ -196,26 +208,32 @@ el('h1',

some text more text

``` -The `attr(attribute, value)` function is provided as a shorthand for +The `attr(attribute, value)` function is provided as a shorthand for + ```javascript $ => { $.setAttribute(attribute, value) } ``` -This allows easy setting of attributes like this + +This allows easy setting of attributes like this + ```javascript el('h1', attr('id', 'foo')) ``` + ```html

``` -Similarly the `bind(reactor, value)` function is provided as a shorthand for +Similarly the `bind(reactor, value)` function is provided as a shorthand for + ```javascript $ => { $.oninput = () => { reactor[key] = $.value } return new Observer(() => { $.value = reactor[key] }) } ``` -This allows for easy 2-way binding for input fields and `Reactor` objects + +This allows for easy 2-way binding for input fields and `Reactor` objects. ```javascript const rx = new Reactor({ name: 'foo' }) @@ -225,15 +243,16 @@ el('input', bind(rx, 'name')) -------------------------------------------------------------------------------- -`Array` arguments are flattened and its elements recursively appended +`Array` arguments are flattened and its elements recursively appended. ```javascript -el('h1',[ +el('h1', [ 'some text', document.createElement('div'), x => 'boop' ]) ``` + ```html

some text
boop

``` @@ -241,17 +260,22 @@ el('h1',[ -------------------------------------------------------------------------------- `Promise` arguments create a comment placeholder. + ```javascript let somePromise = new Promise() el('h1', somePromise) ``` + ```html

``` -When the promise resolves this placeholder is replaced with the resolved value + +When the promise resolves this placeholder is replaced with the resolved value. + ```javascript somePromise.resolve('resolved!') ``` + ```html

resolved!

``` @@ -261,8 +285,9 @@ somePromise.resolve('resolved!') ```javascript const rx = new Reactor({ name: 'foo' }) -el('h1', ob(() => rx.name )) +el('h1', ob(() => rx.name)) ``` + ```html

@@ -271,12 +296,12 @@ el('h1', ob(() => rx.name ))

``` -When the observer is retriggered, everything between the bookmarks is removed and replaced with the new output - +When the observer is retriggered, everything between the bookmarks is removed and replaced with the new output. ```javascript rx.name = 'bar' ``` + ```html

@@ -285,7 +310,7 @@ rx.name = 'bar'

``` -Read below for more details on how observers work +Read below for more details on how observers work. Note: for observers attached as children via `el` they are deactivated automatically when their parent is not attached to the document. This should not affect most use cases since the observer is reactivated when the parent is reattached to the document. But note that unattached elements just being held in memory will not be changing. @@ -295,18 +320,21 @@ Reactors A `Reactor` is an object wrapper which automatically tracks `Observer` functions that read its properties and notifies the observers when those properties are updated. You create a new reactor by just calling its constructor. + ```javascript const reactor = new Reactor() ``` You can also wrap an existing object with a reactor by passing it to the constructor. Changes to the reactor are passed through to the underlying object. + ```javascript const reactor = new Reactor({ foo: "bar" }) ``` -Reactors behave mostly like plain javascript objects. +Reactors behave mostly like plain JavaScript objects. + ```javascript const reactor = new Reactor({ foo: "bar" @@ -342,7 +370,8 @@ Object.defineProperty(reactor, "foo", { delete reactor.foo // prints "foo is undefined" ``` -Tracking is property specific so observers will not trigger if a different property is updated +Tracking is property specific so observers will not trigger if a different property is updated. + ```javascript const reactor = new Reactor({ foo: "bar", @@ -362,7 +391,8 @@ reactor.moo = "mar2" // prints "moo tracker is now mar2" reactor.goo = "goop" // does not trigger any observers ``` -If reading a reactor's property also returns an object, that object is recursively also wrapped in a reactor before being returned. This allows observers to tracks dependencies in nested objects easily. +If reading a reactor's property also returns an object, that object is recursively also wrapped in a reactor before being returned. This allows observers to track dependencies in nested objects easily. + ```javascript const reactor = new Reactor({ outer: { @@ -400,6 +430,7 @@ Observers An `Observer` is like a normal function that you can define and call. When an `Observer` reads from a `Reactor` it automatically tracks that dependency, and when that reactor's property is updated it automatically triggers the observer again. `Observer` functions are created by passing a function to its constructor. + ```javascript const observer = new Observer(() => { console.log("hello world") @@ -407,14 +438,16 @@ const observer = new Observer(() => { observer() // prints "hello world" and starts the observer ``` -For brevity observers can also be created and instantly executed like this +For brevity observers can also be created and instantly executed like this. + ```javascript new Observer(() => { console.log("hello world") })() // prints "hello world" and starts the observer ``` -For further simplicity the shorthand `ob` is also provided that is equivalent to `new Observer` +For further simplicity the shorthand `ob` is also provided that is equivalent to `new Observer`. + ```javascript ob(() => { console.log("hello world") @@ -422,6 +455,7 @@ ob(() => { ``` When an `Observer` reads a `Reactor` property it gets saved as a dependent. When that property is updated it notifies the observer which reruns its function. This happens automatically without any need to manually declare dependencies. + ```javascript const reactor = new Reactor() new Observer(() => { @@ -432,6 +466,7 @@ reactor.foo = "bar" // prints "reactor.foo is bar" ``` An observer's dependencies are dynamically determined. Only the dependencies actually read in the last execution of an observer can trigger it again. This means that Reactor reads that are only conditionally used will not trigger the observer unnecessarily. + ```javascript const reactor = new Reactor({ a: true, @@ -455,6 +490,7 @@ reactor.c = "cheese" // prints "reactor.c is cheese" ``` An observer's results are themselves observable via either the `value` property, or by triggering the observer via `observer()` and using the return value. This allows you to chain observers together. + ```javascript const reactor = new Reactor({ foo: 'bar' }) const capitalizer = new Observer(() => { @@ -467,13 +503,14 @@ reactor.foo = 'baz' // Prints 'BAZ' ``` This works too: + ```javascript const reactor = new Reactor({ foo: 'bar' }) const capitalizer = new Observer(() => { return reactor.foo.toUpperCase() }) // Did not start the observer here const printer = new Observer(() => { - // Manually calls capitalizer like a function which actives it + // Manually calls capitalizer like a function which activates it // As well as accesses its return value as a dependency console.log(capitalizer()) })() // Starts printer which starts capitalizer @@ -481,6 +518,7 @@ reactor.foo = 'baz' // Prints 'BAZ' ``` You can stop an observer by just calling `stop()` on the returned observer object. This clears any existing dependencies and prevents triggering. You can restart the observer by just calling `start()`. Starting is idempotent so calling `start()` on an already running observer will have no effect. + ```javascript const reactor = new Reactor() const observer = new Observer(() => { @@ -512,7 +550,7 @@ reactor.foo = "hi" // prints "hi" observer() // prints "hi" again observer.stop() -reactor.foo = "hola" // does not trigger the observer since its stopped +reactor.foo = "hola" // does not trigger the observer since it's stopped observer() // prints "hola" ``` @@ -526,12 +564,13 @@ parameterizedObserver('beep', 'bop') // Prints bazbeepbop reactor.foo = 'bla' // Prints blabeepbop ``` -Observers can also use and remember the last `this` context. Note that just like normal functions, for the `this` context to be bound to the holding object, it needs to be defined with the traditional `function` keyboard instead of es6 arrow functions. +Observers can also use and remember the last `this` context. Note that just like normal functions, for the `this` context to be bound to the holding object, it needs to be defined with the traditional `function` keyword instead of ES6 arrow functions. + ```javascript const holdingObject = { name: 'Mario', - greet: new Observer(function () { // Need to use of `function` - console.log("Hello " + reactor.foo + " itsa me " + this.name) + greet: new Observer(function () { // Need to use `function` + console.log("Hello " + reactor.foo + " itsa me " + this.name) }) } holdingObject.greet() // Prints "Hello bla itsa me Mario" @@ -543,6 +582,7 @@ holdingObject.name = 'Luigi' // Prints nothing since holdingObject is not a Reac ### Hide Sometimes you might want to read from a reactor without becoming dependent on it. A common case for this is when using array modification methods. These often also read from the array in order to do the modification. + ```javascript const taskList = new Reactor(["a", "b", "c", "d"]) @@ -558,18 +598,17 @@ new Observer(() => { ``` In these cases you can use "hide" to shield a block of code from creating dependencies. It takes a function and any reactor properties read inside that function will not be set as dependencies. `hide` also passes through the return value of its function for syntactic simplicity. + ```javascript const taskList = new Reactor(["a", "b", "c", "d"]) new Observer(() => { - console.log( - // Because we wrap pop() call in an hide block - // It is not create a depndency on the length property + // Because we wrap pop() call in a hide block + // It does not create a dependency on the length property // Unlike our previous example hide(() => taskList.pop()) ) - })() // prints "d" taskList.push("e") // does not trigger the observer @@ -578,6 +617,7 @@ taskList.push("e") // does not trigger the observer Note that only the reads inside the hide block are shielded from creating dependencies. The rest of the observe block still creates dependencies as normal. ### Overrides + If you need to access the raw function the observer is wrapping you do so with the `execute` property. ```javascript @@ -586,24 +626,25 @@ const observer = new Observer(myFunction) myFunction === observer.execute // true ``` -By setting this property you can change an observers internal logic. Doing so clears dependencies and retriggers the observer. Note that the previous `this` and arguments contexts will stay. +By setting this property you can change an observer's internal logic. Doing so clears dependencies and retriggers the observer. Note that the previous `this` and arguments contexts will stay. ```javascript const reactor = new Reactor({ foo: "bar" }) -let observerToBeOverriden = new Observer((arg) => { +let observerToBeOverridden = new Observer((arg) => { console.log(reactor.foo, 'and', arg) }) -observerToBeOverriden('blap') // prints "bar and blap" +observerToBeOverridden('blap') // prints "bar and blap" reactor.foo = "moo" // prints "moo and blap" // Setting the execute property replaces the old function -observerToBeOverriden.execute = (arg) => { +observerToBeOverridden.execute = (arg) => { console.log("I am saying", arg, reactor.foo) } // prints "I am saying blap moo" reactor.foo = "blep" // prints "I am saying blap blep" ``` ### Batching + One problem with automatic watchers is that you might end up with multiple repeated triggering when you're updating a whole lot of information all at once. The following code shows an example where you want to update multiple properties, but each property update prematurely triggers the observer since you are not done updating yet. ```javascript @@ -638,6 +679,7 @@ person.rank = "Lord" // prints "I am Darth Vader, Sith Lord" ``` The `batch` function is provided to allow you to batch multiple updates together and only trigger the appropriate observers once at the end of the batch block. So the last part of the previous example can be turned into: + ```javascript // batch postpones any observer triggers that originate from inside it // Triggers are deduplicated so any observer is triggered at most once @@ -652,10 +694,11 @@ batch(() => { This is useful when you are making multiple data updates and want to avoid showing an "incomplete" view of the data to observers. -Note that only the observer triggering is postponed till the end. The actual reactor propertes are updated in place as expected. This means that you can have other logic with read-what-you-write semantics within the observer block working just fine. +Note that only the observer triggering is postponed till the end. The actual reactor properties are updated in place as expected. This means that you can have other logic with read-what-you-write semantics within the observer block working just fine. Summary ------- + ```javascript import { el, attr, bind, ob, @@ -718,7 +761,7 @@ document.body.appendChild(reactiveEl) // Attached observers sleep when their // parent is out of the DOM // Need to attach it for reactivity -// When updated anything inbetween the bookmarks gets replaces +// When updated anything between the bookmarks gets replaced rx.foo = 'bar' // Updates to //

@@ -786,7 +829,7 @@ reactor.foo = 'bla' // Prints blabeepbop const holdingObject = { name: 'Mario', greet: new Observer(function () { // Need to use traditional functions instead of arrow functions - console.log("Hello " + reactor.foo + " itsa me " + this.name) + console.log("Hello " + reactor.foo + " itsa me " + this.name) }) } holdingObject.greet() // Prints "Hello bla itsa me Mario" @@ -824,7 +867,7 @@ batch(() => { }) // prints 'Look its Bruce Wayne' // shuck removes the Reactor layer and returns the base object -// This is necessary for some native objects which dont work with proxies +// This is necessary for some native objects which don't work with proxies const mapReactor = new Reactor(new Map()) Map.prototype.keys.call(mapReactor) // throws an Error Map.prototype.keys.call(shuck(mapReactor)) // works fine @@ -834,7 +877,7 @@ Development & Testing --------------------- Tests are stored in `test.js` to be run using Mocha. -Run `npm install` to install the the dev dependencies. +Run `npm install` to install the dev dependencies. To run the tests run `npm test`. From ea3b6546e8712d4df71776e29ae9b35c309d62ad Mon Sep 17 00:00:00 2001 From: fynyky Date: Sun, 31 Aug 2025 12:02:48 +0800 Subject: [PATCH 6/8] Fixed error in bind documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 48856e1..969043c 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ el('h1', attr('id', 'foo'))

``` -Similarly the `bind(reactor, value)` function is provided as a shorthand for +Similarly the `bind(reactor, key)` function is provided as a shorthand for ```javascript $ => { From 993975dceea1ff068c59a8e80b7aac0c8781196f Mon Sep 17 00:00:00 2001 From: fynyky Date: Sun, 31 Aug 2025 12:11:33 +0800 Subject: [PATCH 7/8] Updated README with the updated reactor.js documentation --- README.md | 160 ++++++++++++++++++++---------------------------------- 1 file changed, 58 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 969043c..3ddd128 100644 --- a/README.md +++ b/README.md @@ -317,24 +317,21 @@ Note: for observers attached as children via `el` they are deactivated automatic Reactors -------- -A `Reactor` is an object wrapper which automatically tracks `Observer` functions that read its properties and notifies the observers when those properties are updated. - -You create a new reactor by just calling its constructor. +A `Reactor` is an object wrapper that automatically tracks `Observer` functions that read its properties and notifies the observers when those properties are updated. +You create a new reactor by calling its constructor: ```javascript const reactor = new Reactor() ``` -You can also wrap an existing object with a reactor by passing it to the constructor. Changes to the reactor are passed through to the underlying object. - +You can also wrap an existing object with a reactor by passing it to the constructor. Changes to the reactor are passed through to the underlying object: ```javascript const reactor = new Reactor({ foo: "bar" }) ``` -Reactors behave mostly like plain JavaScript objects. - +Reactors behave mostly like plain JavaScript objects: ```javascript const reactor = new Reactor({ foo: "bar" @@ -352,7 +349,7 @@ delete reactor.foo reactor.foo // undefined ``` -The key difference of `Reactor` objects is that they track when one of their properties is read by an `Observer` function and will notify that observer when the property is updated. +The key difference of `Reactor` objects is that they track when one of their properties is read by an `Observer` function and will notify that observer when the property is updated: ```javascript const reactor = new Reactor({ foo: "bar" }) @@ -370,8 +367,7 @@ Object.defineProperty(reactor, "foo", { delete reactor.foo // prints "foo is undefined" ``` -Tracking is property specific so observers will not trigger if a different property is updated. - +Tracking is property-specific, so observers will not trigger if a different property is updated: ```javascript const reactor = new Reactor({ foo: "bar", @@ -391,8 +387,7 @@ reactor.moo = "mar2" // prints "moo tracker is now mar2" reactor.goo = "goop" // does not trigger any observers ``` -If reading a reactor's property also returns an object, that object is recursively also wrapped in a reactor before being returned. This allows observers to track dependencies in nested objects easily. - +If reading a reactor's property returns an object, that object is recursively wrapped in a reactor before being returned. This allows observers to track dependencies in nested objects easily: ```javascript const reactor = new Reactor({ outer: { @@ -405,7 +400,7 @@ new Observer(() => { })() // prints "inner value is cake" ``` -Reactors are implemented using [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) objects. This means reactors created from scratch typecheck as Reactors, but Reactors created from an existing object typecheck as the original object. +Reactors are implemented using [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) objects. This means reactors created from scratch typecheck as Reactors, but reactors created from an existing object typecheck as the original object: ```javascript const baseReactor = new Reactor() @@ -415,7 +410,7 @@ mapReactor instanceof Reactor // false mapReactor instanceof Map // true ``` -This also has implications for native objects or objects which use private properties. Since proxies can't access native or private properties, some methods will fail. To get around this, we provide the `shuck` function which returns a reactor's internal object. +This also has implications for native objects or objects that use private properties. Since proxies can't access native or private properties, some methods will fail. To work around this, we provide the `shuck` function which returns a reactor's internal object: ```javascript // Native object example @@ -427,10 +422,9 @@ Map.prototype.keys.apply(shuck(mapReactor)) // works fine Observers --------- -An `Observer` is like a normal function that you can define and call. When an `Observer` reads from a `Reactor` it automatically tracks that dependency, and when that reactor's property is updated it automatically triggers the observer again. - -`Observer` functions are created by passing a function to its constructor. +An `Observer` is like a normal function that you can define and call. When an `Observer` reads from a `Reactor`, it automatically tracks that dependency, and when that reactor's property is updated, it automatically triggers the observer again. +`Observer` functions are created by passing a function to its constructor: ```javascript const observer = new Observer(() => { console.log("hello world") @@ -438,24 +432,14 @@ const observer = new Observer(() => { observer() // prints "hello world" and starts the observer ``` -For brevity observers can also be created and instantly executed like this. - +For brevity, observers can also be created and instantly executed like this: ```javascript new Observer(() => { console.log("hello world") })() // prints "hello world" and starts the observer ``` -For further simplicity the shorthand `ob` is also provided that is equivalent to `new Observer`. - -```javascript -ob(() => { - console.log("hello world") -})() // prints "hello world" and starts the observer -``` - -When an `Observer` reads a `Reactor` property it gets saved as a dependent. When that property is updated it notifies the observer which reruns its function. This happens automatically without any need to manually declare dependencies. - +When an `Observer` reads a `Reactor` property, it gets saved as a dependent. When that property is updated, it notifies the observer which reruns its function. This happens automatically without any need to manually declare dependencies: ```javascript const reactor = new Reactor() new Observer(() => { @@ -465,8 +449,7 @@ new Observer(() => { reactor.foo = "bar" // prints "reactor.foo is bar" ``` -An observer's dependencies are dynamically determined. Only the dependencies actually read in the last execution of an observer can trigger it again. This means that Reactor reads that are only conditionally used will not trigger the observer unnecessarily. - +An observer's dependencies are dynamically determined. Only the dependencies actually read in the last execution of an observer can trigger it again. This means that reactor reads that are only conditionally used will not trigger the observer unnecessarily: ```javascript const reactor = new Reactor({ a: true, @@ -489,8 +472,7 @@ reactor.b = "blue" // does not trigger the observer reactor.c = "cheese" // prints "reactor.c is cheese" ``` -An observer's results are themselves observable via either the `value` property, or by triggering the observer via `observer()` and using the return value. This allows you to chain observers together. - +An observer's results are themselves observable via either the `value` property or by triggering the observer via `observer()` and using the return value. This allows you to chain observers together: ```javascript const reactor = new Reactor({ foo: 'bar' }) const capitalizer = new Observer(() => { @@ -499,11 +481,10 @@ const capitalizer = new Observer(() => { const printer = new Observer(() => { console.log(capitalizer.value) })() // prints 'BAR' -reactor.foo = 'baz' // Prints 'BAZ' +reactor.foo = 'baz' // prints 'BAZ' ``` -This works too: - +This also works: ```javascript const reactor = new Reactor({ foo: 'bar' }) const capitalizer = new Observer(() => { @@ -512,13 +493,12 @@ const capitalizer = new Observer(() => { const printer = new Observer(() => { // Manually calls capitalizer like a function which activates it // As well as accesses its return value as a dependency - console.log(capitalizer()) -})() // Starts printer which starts capitalizer -reactor.foo = 'baz' // Prints 'BAZ' + console.log(capitalizer()) +})() // starts printer which starts capitalizer +reactor.foo = 'baz' // prints 'BAZ' ``` -You can stop an observer by just calling `stop()` on the returned observer object. This clears any existing dependencies and prevents triggering. You can restart the observer by just calling `start()`. Starting is idempotent so calling `start()` on an already running observer will have no effect. - +You can stop an observer by calling `stop()` on the returned observer object. This clears any existing dependencies and prevents triggering. You can restart the observer by calling `start()`. Starting is idempotent, so calling `start()` on an already running observer will have no effect: ```javascript const reactor = new Reactor() const observer = new Observer(() => { @@ -539,7 +519,7 @@ observer.start() // No effect reactor.foo = "moo" // prints "moo" ``` -For convenience, you can call an observer to execute like a normal function. This works regardless of whether the observer is stopped. Doing so starts the observer up again. +For convenience, you can call an observer to execute like a normal function. This works regardless of whether the observer is stopped. Doing so starts the observer up again: ```javascript const reactor = new Reactor({ foo: "hello" }) @@ -554,17 +534,17 @@ reactor.foo = "hola" // does not trigger the observer since it's stopped observer() // prints "hola" ``` -Like normal functions, observers can expect and be called with arguments. They remember the arguments from the last time they were called and reuse them when automatically triggered. +Like normal functions, observers can expect and be called with arguments. They remember the arguments from the last time they were called and reuse them when automatically triggered: ```javascript const parameterizedObserver = new Observer((arg1, arg2) => { console.log(reactor.foo + arg1 + arg2) }) -parameterizedObserver('beep', 'bop') // Prints bazbeepbop -reactor.foo = 'bla' // Prints blabeepbop +parameterizedObserver('beep', 'bop') // prints bazbeepbop +reactor.foo = 'bla' // prints blabeepbop ``` -Observers can also use and remember the last `this` context. Note that just like normal functions, for the `this` context to be bound to the holding object, it needs to be defined with the traditional `function` keyword instead of ES6 arrow functions. +Observers can also use and remember the last `this` context. Note that just like normal functions, for the `this` context to be bound to the holding object, it needs to be defined with the traditional `function` keyword instead of ES6 arrow functions: ```javascript const holdingObject = { @@ -573,42 +553,47 @@ const holdingObject = { console.log("Hello " + reactor.foo + " itsa me " + this.name) }) } -holdingObject.greet() // Prints "Hello bla itsa me Mario" -reactor.foo = 'bonk' // Prints "Hello bonk itsa me Mario" -holdingObject.name = 'Luigi' // Prints nothing since holdingObject is not a Reactor +holdingObject.greet() // prints "Hello bla itsa me Mario" +reactor.foo = 'bonk' // prints "Hello bonk itsa me Mario" +holdingObject.name = 'Luigi' // prints nothing since holdingObject is not a Reactor ``` +If you ever need to access the raw function the observer is wrapping, you do so by using `shuck`: -### Hide +```javascript +const myFunction = () => {} +const observer = new Observer(myFunction) +myFunction === shuck(observer) // true +``` -Sometimes you might want to read from a reactor without becoming dependent on it. A common case for this is when using array modification methods. These often also read from the array in order to do the modification. +### Hide +Sometimes you might want to read from a reactor without becoming dependent on it. A common case for this is when using array modification methods. These often also read from the array in order to do the modification: ```javascript const taskList = new Reactor(["a", "b", "c", "d"]) -// Creating the following observer will throw a LoopError -// Because it both reads from and modifies the length property of taskList -// As a result it triggers itself in the middle of execution +// Creating the following observer will cause an infinite loop +// because it both reads from and modifies the length property of taskList +// As a result, it triggers itself in the middle of execution // This loop is detected and creates an exception new Observer(() => { // Even though we only want to modify the array // pop() also reads the length property of the array - console.log(taskList.pop()) + console.log(taskList.pop()) })() ``` -In these cases you can use "hide" to shield a block of code from creating dependencies. It takes a function and any reactor properties read inside that function will not be set as dependencies. `hide` also passes through the return value of its function for syntactic simplicity. - +In these cases, you can use "hide" to shield a block of code from creating dependencies. It takes a function and any reactor properties read inside that function will not be set as dependencies. `hide` also passes through the return value of its function for syntactic simplicity: ```javascript const taskList = new Reactor(["a", "b", "c", "d"]) new Observer(() => { console.log( - // Because we wrap pop() call in a hide block - // It does not create a dependency on the length property - // Unlike our previous example + // Because we wrap the pop() call in a hide block + // it does not create a dependency on the length property + // unlike our previous example hide(() => taskList.pop()) - ) + ) })() // prints "d" taskList.push("e") // does not trigger the observer @@ -616,61 +601,33 @@ taskList.push("e") // does not trigger the observer Note that only the reads inside the hide block are shielded from creating dependencies. The rest of the observe block still creates dependencies as normal. -### Overrides - -If you need to access the raw function the observer is wrapping you do so with the `execute` property. - -```javascript -const myFunction = () => {} -const observer = new Observer(myFunction) -myFunction === observer.execute // true -``` - -By setting this property you can change an observer's internal logic. Doing so clears dependencies and retriggers the observer. Note that the previous `this` and arguments contexts will stay. - -```javascript -const reactor = new Reactor({ foo: "bar" }) -let observerToBeOverridden = new Observer((arg) => { - console.log(reactor.foo, 'and', arg) -}) -observerToBeOverridden('blap') // prints "bar and blap" -reactor.foo = "moo" // prints "moo and blap" - -// Setting the execute property replaces the old function -observerToBeOverridden.execute = (arg) => { - console.log("I am saying", arg, reactor.foo) -} // prints "I am saying blap moo" -reactor.foo = "blep" // prints "I am saying blap blep" -``` - ### Batching - -One problem with automatic watchers is that you might end up with multiple repeated triggering when you're updating a whole lot of information all at once. The following code shows an example where you want to update multiple properties, but each property update prematurely triggers the observer since you are not done updating yet. +One problem with automatic watchers is that you might end up with multiple repeated triggering when you're updating a lot of information all at once. The following code shows an example where you want to update multiple properties, but each property update prematurely triggers the observer since you are not done updating yet: ```javascript -const person = new Reactor({ +const person = new Reactor({ firstName: "Anakin", lastName: "Skywalker", faction: "Jedi", rank: "Knight" }) -// This observer tracks multiple properties +// This observer tracks multiple properties // and so will be triggered when any of the properties get updated const observer = new Observer(() => { console.log( "I am " + - person.firstName + - " " + - person.lastName + - ", " + - person.faction + - " " + + person.firstName + + " " + + person.lastName + + ", " + + person.faction + + " " + person.rank ) })() // prints "I am Anakin Skywalker, Jedi Knight" -// The following updates will each trigger the observer even though we only +// The following updates will each trigger the observer even though we only // want to trigger the observer once all the updates are complete person.firstName = "Darth" // prints "I am Darth Skywalker, Jedi Knight" person.lastName = "Vader" // prints "I am Darth Vader, Jedi Knight" @@ -679,13 +636,12 @@ person.rank = "Lord" // prints "I am Darth Vader, Sith Lord" ``` The `batch` function is provided to allow you to batch multiple updates together and only trigger the appropriate observers once at the end of the batch block. So the last part of the previous example can be turned into: - ```javascript // batch postpones any observer triggers that originate from inside it // Triggers are deduplicated so any observer is triggered at most once batch(() => { // None of the following updates will trigger the observer yet - person.firstName = "Darth" + person.firstName = "Darth" person.lastName = "Vader" person.faction = "Sith" person.rank = "Lord" @@ -694,7 +650,7 @@ batch(() => { This is useful when you are making multiple data updates and want to avoid showing an "incomplete" view of the data to observers. -Note that only the observer triggering is postponed till the end. The actual reactor properties are updated in place as expected. This means that you can have other logic with read-what-you-write semantics within the observer block working just fine. +Note that only the observer triggering is postponed until the end. The actual reactor properties are updated in place as expected. This means that you can have other logic with read-what-you-write semantics within the observer block working just fine. Summary ------- From c3db4e9fa4e56621ab711cfa01f92b55bae0c6f1 Mon Sep 17 00:00:00 2001 From: fynyky Date: Sun, 31 Aug 2025 12:32:03 +0800 Subject: [PATCH 8/8] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d8cbe4f..e166c21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fynyky/elemental", - "version": "0.0.1", + "version": "0.0.2", "description": "Simple reactive ui building without frameworks", "type": "module", "main": "dist/main.js",