-
Notifications
You must be signed in to change notification settings - Fork 32
Add Reactive Patterns guide #4922
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
abdelaziz-mahdy
wants to merge
1
commit into
kgrgreer:development
Choose a base branch
from
abdelaziz-mahdy:docs/foam-reactive-patterns
base: development
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,387 @@ | ||
| # FOAM Reactive Patterns — Complete Reference | ||
|
|
||
| All variations of slots, expressions, dynamic functions, and bindings in FOAM3. | ||
|
|
||
| --- | ||
|
|
||
| ## 1. `obj.name$` — PropertySlot Accessor | ||
|
|
||
| **Source**: `Property.js:497` — getter installed on prototype via `Object.defineProperty` | ||
|
|
||
| ```javascript | ||
| // Returns a PropertySlot — a live, subscribable handle to a single property | ||
| var slot = person.fname$; | ||
| slot.get(); // → 'John' | ||
| slot.set('Steve'); // same as person.fname = 'Steve' | ||
| slot.sub(listener); // fires when fname changes | ||
| ``` | ||
|
|
||
| **Returns**: `PropertySlot` (cached per instance in `obj.getPrivate_('name$')`) | ||
|
|
||
| **Use case**: When you need a slot reference to a single, flat property — for linking, following, or passing to views. | ||
|
|
||
| --- | ||
|
|
||
| ## 2. `obj.slot('name')` — Axiom-Based Slot Lookup | ||
|
|
||
| **Source**: `FObject.js:763-805` | ||
|
|
||
| ```javascript | ||
| // Equivalent to obj.name$ for simple properties | ||
| var slot = person.slot('fname'); | ||
|
|
||
| // But also supports $ chaining (see #3) | ||
| var slot = person.slot('address$city'); | ||
|
|
||
| // And function expressions (see #6) | ||
| var slot = person.slot(function(fname, lname) { return fname + ' ' + lname; }); | ||
| ``` | ||
|
|
||
| **Returns**: `PropertySlot` for simple names, `SubSlot` chain for `$`-separated paths, `ExpressionSlot` for functions. | ||
|
|
||
| **Use case**: Programmatic access when the property name is a variable, or when you need the `$` chaining syntax. | ||
|
|
||
| --- | ||
|
|
||
| ## 3. `obj.slot('a$b$c')` or `obj.a$.dot('b').dot('c')` — Deep Slot Chain | ||
|
|
||
| **Source**: `FObject.js:792-802` splits on `$` then calls `slot.dot()`. `Slot.js:72-87` `dot()` creates `SubSlot` instances. | ||
|
|
||
| ```javascript | ||
| // These three are IDENTICAL: | ||
| var s1 = obj.slot('block$flowParent$value$currency'); | ||
| var s2 = obj.block$.dot('flowParent$value$currency'); | ||
| var s3 = obj.block$.dot('flowParent').dot('value').dot('currency'); | ||
| ``` | ||
|
|
||
| **Internal mechanics** (`SubSlot` in `Slot.js:319-405`): | ||
|
|
||
| ``` | ||
| PropertySlot(block) | ||
| └─ SubSlot(flowParent) ← watches block, re-subs when block changes | ||
| └─ SubSlot(value) ← watches flowParent, re-subs when it changes | ||
| └─ SubSlot(currency) ← watches value, re-subs when it changes | ||
| ``` | ||
|
|
||
| Each `SubSlot.parentChange` listener (line 384) does: | ||
|
|
||
| 1. Detaches the old subscription | ||
| 2. Gets the new parent value | ||
| 3. If the new value has the axiom, subscribes to `newValue.slot(name)` | ||
| 4. Fires `valueChange` to propagate | ||
|
|
||
| **Use case**: When you need to reactively track a deeply nested property, and any object along the chain might be swapped out. The chain automatically re-wires. | ||
|
|
||
| --- | ||
|
|
||
| ## 4. `expression: function(a, b)` — Declarative Computed Property | ||
|
|
||
| **Source**: `Property.js:652-690` (`exprFactory`) | ||
|
|
||
| ```javascript | ||
| properties: [ | ||
| { name: 'fname' }, | ||
| { name: 'lname' }, | ||
| { | ||
| name: 'fullName', | ||
| expression: function(fname, lname) { return fname + ' ' + lname; } | ||
| } | ||
| ] | ||
| ``` | ||
|
|
||
| **Internal mechanics**: | ||
|
|
||
| 1. `exprFactory` extracts arg names via `foam.Function.argNames(e)` → `['fname', 'lname']` | ||
| 2. For each arg, calls `this.slot(argName)` — which supports `$` chaining | ||
| 3. Subscribes to each slot; on change: clears the cached value (`clearPrivate_`) | ||
| 4. Next `get()` re-runs the expression | ||
|
|
||
| **Key behaviors**: | ||
|
|
||
| - **Lazy** — only recomputes when someone reads the value | ||
| - **Overridable** — setting `obj.fullName = 'Custom'` overrides the expression until `clearProperty('fullName')` | ||
| - **Receives resolved values**, not slots (arg `fname` = `'John'`, not a PropertySlot) | ||
|
|
||
| **Deep path in expression**: | ||
|
|
||
| ```javascript | ||
| expression: function(block$flowParent$value$currency) { | ||
| // Parameter name tells FOAM to call this.slot('block$flowParent$value$currency') | ||
| // The arg receives the resolved leaf VALUE: 'USD' | ||
| return block$flowParent$value$currency || 'USD'; | ||
| } | ||
| ``` | ||
|
|
||
| **Use case**: Derived properties that depend on other properties. The primary reactive mechanism in model definitions. | ||
|
|
||
| --- | ||
|
|
||
| ## 5. `this.dynamic(function(a, b) {...})` — Eager Side-Effect Runner | ||
|
|
||
| **Source**: `FObject.js:748-761` → creates `DynamicFunction` (`Slot.js:636-733`) | ||
|
|
||
| ```javascript | ||
| // In a view's render method: | ||
| var self = this; | ||
| this.data.dynamic(function(items, isLoading) { | ||
| // 'this' is the Element being built | ||
| this.start('div').addClass(self.myClass('list')); | ||
| for ( var i = 0 ; i < items.length ; i++ ) { | ||
| this.start('span').add(items[i]).end(); | ||
| } | ||
| this.end(); | ||
| }); | ||
| ``` | ||
|
|
||
| **How it differs from `expression`**: | ||
|
|
||
| | Aspect | `expression` | `dynamic()` | | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The second column should be expressions and slots. |
||
| |---|---|---| | ||
| | **Evaluation** | Lazy (pull) — only on `get()` | Eager (push) — runs immediately on change | | ||
| | **Invalidation** | `ExpressionSlot.invalidate` clears value | `DynamicFunction.invalidate` clears AND reads `this.value` | | ||
| | **Framing** | Not framed | `isFramed: true` — batches to animation frame | | ||
| | **Return value** | Used as property value | Ignored (side-effects only, returns seqNo) | | ||
| | **Async** | Stores promise, resolves later | Tracks `running`/`rerun` to prevent re-entrant calls | | ||
| | **`this` context** | The object owning the property | Configurable via `self` parameter | | ||
|
|
||
| **Use case**: UI rendering in views. The dynamic function rebuilds DOM whenever dependencies change. Not for computed values — for side effects. | ||
|
|
||
| --- | ||
|
|
||
| ## 6. `this.slot(function(a, b) {...})` — Lazy Computed Slot | ||
|
|
||
| **Source**: `FObject.js:767-775` → creates `ExpressionSlot` | ||
|
|
||
| ```javascript | ||
| // Returns an ExpressionSlot | ||
| var fullNameSlot = person.slot(function(fname, lname) { | ||
| return fname + ' ' + lname; | ||
| }); | ||
| fullNameSlot.get(); // → 'John Smith' | ||
| fullNameSlot.sub(l); // fires when fname or lname changes | ||
| ``` | ||
|
|
||
| **How it differs from `expression` property**: | ||
|
|
||
| - `expression:` is baked into a property definition (model time) | ||
| - `slot(fn)` creates an ad-hoc ExpressionSlot at runtime | ||
|
|
||
| **Use case**: When you need a reactive computed value outside of a model definition — in a method, view, or controller. | ||
|
|
||
| --- | ||
|
|
||
| ## 7. `slot.map(f)` — Transform a Slot's Value | ||
|
|
||
| **Source**: `Slot.js:222-224` | ||
|
|
||
| ```javascript | ||
| function map(f) { | ||
| return foam.lang.ExpressionSlot.create({code: f, args: [this]}); | ||
| } | ||
| ``` | ||
|
|
||
| ```javascript | ||
| var upperSlot = person.fname$.map(function(name) { | ||
| return name.toUpperCase(); | ||
| }); | ||
| upperSlot.get(); // → 'JOHN' | ||
| ``` | ||
|
|
||
| **Use case**: Deriving a new reactive value from a single slot. Common in views for formatting. | ||
|
|
||
| --- | ||
|
|
||
| ## 8. `slot.follow(other)` — One-Way Binding | ||
|
|
||
| **Source**: `Slot.js:183-193` | ||
|
|
||
| ```javascript | ||
| // slotA always copies slotB's value (one-directional) | ||
| slotA.follow(slotB); | ||
| ``` | ||
|
|
||
| **Use case**: When one slot should mirror another but not push changes back. | ||
|
|
||
| --- | ||
|
|
||
| ## 9. `slot.linkFrom(other)` — Two-Way Binding | ||
|
|
||
| **Source**: `Slot.js:124-173` | ||
|
|
||
| ```javascript | ||
| // Bidirectional sync with feedback protection | ||
| slotA.linkFrom(slotB); | ||
| ``` | ||
|
|
||
| Also triggered when you **set** a `$` slot: | ||
|
|
||
| ```javascript | ||
| // Property.js:501 — setting name$ links the two slots | ||
| person.fname$ = someOtherSlot; // calls toSlot(this).linkFrom(slot2) | ||
| ``` | ||
|
|
||
| **Use case**: Keeping two properties in sync across different objects (e.g., view ↔ model). | ||
|
|
||
| --- | ||
|
|
||
| ## 10. `sub('propertyChange', 'name', listener)` — Raw Subscription | ||
|
|
||
| **Source**: `FObject.js:700-730` (pub/sub system) | ||
|
|
||
| ```javascript | ||
| person.sub('propertyChange', 'fname', function(sub, topic, propName, slot) { | ||
| console.log('fname changed to', slot.get()); | ||
| }); | ||
| ``` | ||
|
|
||
| **Use case**: When you just need a callback, not a slot. Lowest-level mechanism. | ||
|
|
||
| --- | ||
|
|
||
| ## Decision Tree | ||
|
|
||
| ``` | ||
| Need to react to property changes? | ||
| │ | ||
| ├── In a MODEL DEFINITION (property depends on others)? | ||
| │ └── Use `expression: function(a, b$c) {...}` (#4) | ||
| │ | ||
| ├── In a VIEW (need to rebuild DOM)? | ||
| │ └── Use `this.data.dynamic(function(prop1, prop2) {...})` (#5) | ||
| │ | ||
| ├── Need a SLOT REFERENCE to pass around? | ||
| │ ├── Single property? | ||
| │ │ └── Use `obj.name$` (#1) | ||
| │ ├── Deep nested path? | ||
| │ │ └── Use `obj.slot('a$b$c')` or `obj.a$.dot('b')` (#3) | ||
| │ └── Computed from multiple props? | ||
| │ └── Use `obj.slot(function(a, b) {...})` (#6) | ||
| │ | ||
| ├── Need to TRANSFORM a slot? | ||
| │ └── Use `slot.map(fn)` (#7) | ||
| │ | ||
| ├── Need to SYNC two slots? | ||
| │ ├── One-way → `slot.follow(other)` (#8) | ||
| │ └── Two-way → `slot.linkFrom(other)` or `a$ = b$` (#9) | ||
| │ | ||
| └── Just need a CALLBACK? | ||
| └── Use `obj.sub('propertyChange', 'name', fn)` (#10) | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## The Trailing `$` — `test$testf$testf2$` vs `test$testf$testf2` | ||
|
|
||
| The trailing `$` is a critical distinction. These are NOT the same thing. | ||
|
|
||
| ### As JavaScript Property Access | ||
|
|
||
| In JavaScript, `$` is a valid identifier character. So these are completely different: | ||
|
|
||
| ```javascript | ||
| obj.test$ // FOAM getter → returns PropertySlot for 'test' | ||
| obj.test$testf$ // ONE JS identifier 'test$testf$' → likely undefined (no such getter) | ||
| ``` | ||
|
|
||
| FOAM only installs `name$` getters for each property (`Property.js:497`). It does NOT install multi-segment getters. So `obj.test$testf$testf2$` is a single JavaScript identifier — it returns `undefined` unless you happen to have a property literally named `test$testf$testf2`. | ||
|
|
||
| ### As a String in `slot()` or Expression Arg Name | ||
|
|
||
| When the `$` chain appears as a string (in `slot()`) or as a function parameter name (in `expression:`), FOAM parses the `$` as a separator: | ||
|
|
||
| ``` | ||
| 'test$testf$testf2' → 3 segments: test → testf → testf2 (CORRECT) | ||
| 'test$testf$testf2$' → 4 segments: test → testf → testf2 → '' (BROKEN) | ||
| ``` | ||
|
|
||
| **Trace of `'test$testf$testf2$'` through the code:** | ||
|
|
||
| **Step 1** — `FObject.slot('test$testf$testf2$')` (`FObject.js:792`): | ||
| ```javascript | ||
| var split = 'test$testf$testf2$'.indexOf('$'); // → 4 | ||
| var axiom = this.cls_.getAxiomByName('test'); // gets 'test' axiom | ||
| var slot = axiom.toSlot(this); // PropertySlot(test) | ||
| slot = slot.dot('testf$testf2$'); // chains the remainder | ||
| ``` | ||
|
|
||
| **Step 2** — `dot('testf$testf2$')` (`Slot.js:73`): | ||
| ```javascript | ||
| var i = 'testf$testf2$'.indexOf('$'); // → 5 | ||
| var left = 'testf'; // SubSlot(testf) | ||
| var right = 'testf2$'; // recurse | ||
| → SubSlot(testf).dot('testf2$') | ||
| ``` | ||
|
|
||
| **Step 3** — `dot('testf2$')` (`Slot.js:73`): | ||
| ```javascript | ||
| var i = 'testf2$'.indexOf('$'); // → 6 | ||
| var left = 'testf2'; // SubSlot(testf2) | ||
| var right = ''; // EMPTY STRING | ||
| → SubSlot(testf2).dot('') | ||
| ``` | ||
|
|
||
| **Step 4** — `dot('')` (`Slot.js:73`): | ||
| ```javascript | ||
| var i = ''.indexOf('$'); // → -1, no more $ | ||
| // Falls to else branch: | ||
| return SubSlot.create({ parent: this, name: '' }); // SubSlot with EMPTY name | ||
| ``` | ||
|
|
||
| **Result**: Creates this chain: | ||
| ``` | ||
| PropertySlot(test) | ||
| └─ SubSlot(testf) | ||
| └─ SubSlot(testf2) | ||
| └─ SubSlot('') ← BROKEN: name is empty string | ||
| ``` | ||
|
|
||
| The empty-name SubSlot does `parent.get()['']` (`Slot.js:343`) which returns `undefined`. Its `parentChange` listener (`Slot.js:390`) calls `o.cls_.getAxiomByName('')` which returns `null`, causing the SubSlot to **detach itself** — killing the entire reactive chain. | ||
|
|
||
| ### As an Expression Parameter | ||
|
|
||
| ```javascript | ||
| // BROKEN — trailing $ creates empty SubSlot, detaches, stops reacting | ||
| expression: function(test$testf$testf2$) { | ||
| return test$testf$testf2$; // gets undefined, then stops updating | ||
| } | ||
|
|
||
| // CORRECT — no trailing $ | ||
| expression: function(test$testf$testf2) { | ||
| return test$testf$testf2; // gets leaf value, stays reactive | ||
| } | ||
| ``` | ||
|
|
||
| ### Summary Table | ||
|
|
||
| | Syntax | Context | Result | | ||
| |---|---|---| | ||
| | `obj.test$` | JS property access | PropertySlot for `test` | | ||
| | `obj.test$testf$` | JS property access | `undefined` — single identifier, no such getter | | ||
| | `obj.slot('test$testf')` | String in `slot()` | PropertySlot(test) → SubSlot(testf) | | ||
| | `obj.slot('test$testf$')` | String in `slot()` | ...→ SubSlot('') — BROKEN, detaches | | ||
| | `function(test$testf)` | Expression arg | Watches test.testf, receives value | | ||
| | `function(test$testf$)` | Expression arg | Broken empty SubSlot, stops reacting | | ||
|
|
||
| ### Rule | ||
|
|
||
| **Never end a `$` chain with `$`.** The trailing `$` creates an empty-name SubSlot that detaches immediately. Use `test$testf$testf2` (no trailing `$`) for deep path watching. | ||
|
|
||
| The `$` suffix only has meaning as a **JavaScript property accessor** on the object itself (`obj.name$`), and even then only for single property names that FOAM installed getters for. | ||
|
|
||
| --- | ||
|
|
||
| ## Quick Equivalence Table | ||
|
|
||
| | Syntax | Creates | Evaluation | Depth | | ||
| |---|---|---|---| | ||
| | `obj.name$` | PropertySlot | — | Flat | | ||
| | `obj.slot('name')` | PropertySlot | — | Flat | | ||
| | `obj.slot('a$b$c')` | PropertySlot → SubSlot chain | — | Deep | | ||
| | `obj.a$.dot('b').dot('c')` | PropertySlot → SubSlot chain | — | Deep | | ||
| | `expression: function(a) {}` | exprFactory (inline) | Lazy | Flat or Deep via `$` args | | ||
| | `obj.slot(function(a) {})` | ExpressionSlot | Lazy | Flat or Deep via `$` args | | ||
| | `obj.dynamic(function(a) {})` | DynamicFunction | Eager + Framed | Flat or Deep via `$` args | | ||
| | `slot.map(fn)` | ExpressionSlot | Lazy | Single input | | ||
| | `slot.follow(other)` | Subscription | Push | — | | ||
| | `slot.linkFrom(other)` | Bidirectional sub | Push | — | | ||
| | `obj.sub(...)` | Raw listener | Push | — | | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should add that person.fname$ is a short-form for person.slot('fname')