Skip to content

Commit

Permalink
feat: Proposal bind shorthand (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarParra authored Jan 30, 2025
1 parent f5ecff2 commit a64ca63
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 12 deletions.
55 changes: 49 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ clicked.
</template>
```

To update the counter, you can simply change the `counter.value` property directly.
To update the counter, you can change the `counter.value` property directly.

```javascript
// counter.js
Expand All @@ -137,9 +137,35 @@ export default class Counter extends LightningElement {

## Reacting to changes

### `$computed`
### Through `$bind`

You can use the `$computed` function to create a reactive value that depends on the signal.
You can use the `$bind` function to create a reactive value that depends on the signal.

```javascript
// display.js
import { LightningElement } from "lwc";
import { $bind } from "c/signals";
import { counter } from "c/counter-signals";

export default class Display extends LightningElement {
counterProp = $bind(this, "counterProp").to(counter);
}
```

Note that the first argument to the `$bind` function is the `this` context of the component, and the second argument
is the name of the property that will be created on the component as a string. Then you call the `.to` function with
the signal you want to bind to.

<p align="center">
<img src="./doc-assets/counter-example.gif" alt="Counter Example" />
</p>

### Through `$computed`

One downside of using `$bind` is that the second argument is a string, which can lead to typos and errors if the
property name is changed but the string is not updated.

So, alternatively, you can use the `$computed` function to create a reactive value that depends on the signal.

```javascript
// display.js
Expand All @@ -152,13 +178,30 @@ export default class Display extends LightningElement {
}
```

But notice that this syntax is a lot more verbose than using `$bind`.

> ❗ Note that in the callback function we **need** to reassign the value to `this.counter`
> to trigger the reactivity. This is because we need the value to be reassigned so that
> LWC reactive system can detect the change and update the UI.
<p align="center">
<img src="./doc-assets/counter-example.gif" alt="Counter Example" />
</p>
### Through `$effect`

Finally, you can use the `$effect` function to create a side effect that depends on the signal.

```javascript
// display.js
import { LightningElement } from "lwc";
import { $effect } from "c/signals";
import { counter } from "c/counter-signals";

export default class Display extends LightningElement {
counter = 0;

constructor() {
$effect(() => (this.counter = counter.value));
}
}
```

#### Stacking computed values

Expand Down
3 changes: 2 additions & 1 deletion examples/counter/lwc/countTracker/countTracker.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<template>
The current count is ($computed reactive property): {reactiveProperty} <br />
Binded: {bindTest} The current count is ($computed reactive property):
{reactiveProperty} <br />
The counter plus two value is (nested computed): {counterPlusTwo}
</template>
4 changes: 3 additions & 1 deletion examples/counter/lwc/countTracker/countTracker.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { LightningElement } from "lwc";
import { $computed } from "c/signals";
import { $binded, $computed } from "c/signals";
import { counter, counterPlusTwo } from "c/demoSignals";

export default class CountTracker extends LightningElement {
bindTest = $binded(this, "bindTest").to(counter);

reactiveProperty = $computed(() => (this.reactiveProperty = counter.value))
.value;

Expand Down
26 changes: 25 additions & 1 deletion force-app/lwc/signals/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,4 +361,28 @@ function $resource(fn, source, options) {
function isSignal(anything) {
return !!anything && anything.brand === SIGNAL_OBJECT_BRAND;
}
export { $signal, $effect, $computed, $resource, isSignal };
class Binder {
constructor(component, propertyName) {
this.component = component;
this.propertyName = propertyName;
}
to(signal) {
$effect(() => {
// @ts-expect-error The property name will be found
this.component[this.propertyName] = signal.value;
});
return signal.value;
}
}
function bind(component, propertyName) {
return new Binder(component, propertyName);
}
export {
$signal,
$effect,
$computed,
$resource,
bind,
bind as $bind,
isSignal
};
39 changes: 36 additions & 3 deletions src/lwc/signals/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,13 @@ function $effect(fn: VoidFunction, options?: Partial<EffectOptions>): Effect {
};

const execute = () => {
if (effectNode.state === COMPUTING && effectNode.stackDepth >= MAX_STACK_DEPTH) {
throw new Error(`Circular dependency detected. Maximum stack depth of ${MAX_STACK_DEPTH} exceeded.`);
if (
effectNode.state === COMPUTING &&
effectNode.stackDepth >= MAX_STACK_DEPTH
) {
throw new Error(
`Circular dependency detected. Maximum stack depth of ${MAX_STACK_DEPTH} exceeded.`
);
}

context.push(execute);
Expand Down Expand Up @@ -589,4 +594,32 @@ function isSignal(anything: unknown): anything is Signal<unknown> {
);
}

export { $signal, $effect, $computed, $resource, isSignal };
class Binder {
constructor(
private component: Record<string, object>,
private propertyName: string
) {}

to(signal: Signal<unknown>) {
$effect(() => {
// @ts-expect-error The property name will be found
this.component[this.propertyName] = signal.value;
});

return signal.value;
}
}

function bind(component: Record<string, object>, propertyName: string) {
return new Binder(component, propertyName);
}

export {
$signal,
$effect,
$computed,
$resource,
bind,
bind as $bind,
isSignal
};

0 comments on commit a64ca63

Please sign in to comment.