Skip to content

Commit

Permalink
Merge pull request #4 from cesarParra/more-examples
Browse files Browse the repository at this point in the history
More examples
  • Loading branch information
cesarParra committed Jun 1, 2024
2 parents 95b199e + 261ed7f commit d33f184
Show file tree
Hide file tree
Showing 289 changed files with 7,820 additions and 186 deletions.
11 changes: 8 additions & 3 deletions .forceignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ package.xml
**/__tests__/signals.test.ts

appMenus/
dashboards/
reports/
reportTypes/
# Reports and Report Types
**/reports/**
**/reportTypes/**

# Dashboards
**/dashboards/**

**/tw/input.css
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
node-version: "20"
cache: "npm"
- run: npm ci
- run: npm run build --if-present
Expand Down
1 change: 0 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"trailingComma": "none",
"plugins": [
"prettier-plugin-apex",
"@prettier/plugin-xml"
],
"overrides": [
Expand Down
149 changes: 135 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A simple yet powerful reactive state management solution for Lightning Web Compo

Inspired by the Signals technology behind SolidJs, Preact, Svelte 5 Runes and the Vue 3 Composition API, LWC Signals is
a
reactive signals for Lightning Web Components that allows you to create reactive data signalss
reactive signals for Lightning Web Components that allows you to create reactive data signals
that can be used to share state between components.

It features:
Expand All @@ -19,6 +19,22 @@ It features:
- 🔬️ **Small Surface** The API does not offer more than what is needed, keeping the learning curve and bloat to a
minimum

The goal is to allow you to create beautiful and complex user experiences, while achieving clean code that
separates concerns and is easy to maintain!

Easily implement:

- Reactive data stores
- Undo
- Optimistic updates
- Data caching through storage (localStorage, cookies, etc)

<p align="center">
<img width="500" src="./doc-assets/full-example.gif" alt="Kitchen Sink Example" />
</p>

> To see the code for the example above, check the `examples/shopping-cart` folder.
# Getting Started

Copy the `force-app/lwc/signals` folder to your project.
Expand Down Expand Up @@ -124,9 +140,9 @@ export default class Display extends LightningElement {
> 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.
<div style="text-align: center;">
<p align="center">
<img src="./doc-assets/counter-example.gif" alt="Counter Example" />
</div>
</p>

### Stacking computed values

Expand Down Expand Up @@ -203,7 +219,7 @@ export default class ContactInfoForm extends LightningElement {
}
```

**You can create a computed value that depends on both signalss**
**You can create a computed value that depends on both signals**

```html
<!-- businessCard.html -->
Expand All @@ -221,7 +237,7 @@ export default class ContactInfoForm extends LightningElement {
// businessCard.js
import { LightningElement } from "lwc";
import { $computed } from "c/signals";
import { accountName, contactName } from "c/demoSignalss";
import { accountName, contactName } from "c/demoSignals";

export default class BusinessCard extends LightningElement {
contactInfo = $computed(
Expand All @@ -234,9 +250,9 @@ export default class BusinessCard extends LightningElement {
}
```

<div style="text-align: center;">
<p align="center">
<img src="./doc-assets/business-card-example.gif" alt="Counter Example" />
</div>
</p>

> ❗ Notice that we are using a property instead of a getter in the `$computed` callback function, because
> we need to reassign the value to `this.contactInfo` to trigger the reactivity, as it is a complex object.
Expand Down Expand Up @@ -346,9 +362,9 @@ type AsyncData<T> = {
> 🍪 One benefit of using the `$resource` over declarative Apex or `@wire` is that it keeps track of the loading
> state for you, which saves you the effort of having to calculate it yourself.
<div style="text-align: center;">
<p align="center">
<img src="./doc-assets/apex-fetch.gif" alt="Fetching From Apex" />
</div>
</p>

---

Expand Down Expand Up @@ -421,7 +437,7 @@ Let's now create our picklist component that allows the user to select an accoun
// accountPicker.js
import { LightningElement, track, wire } from "lwc";
import getAccounts from "@salesforce/apex/ResourceController.getAccounts";
import { selectedAccountId } from "c/demoSignalss";
import { selectedAccountId } from "c/demoSignals";

export default class AccountPicker extends LightningElement {
@track accounts = [];
Expand Down Expand Up @@ -486,16 +502,16 @@ Now, let's create the component that displays the details of the selected accoun
// accountDetails.js
import { LightningElement } from "lwc";
import { $computed } from "c/signals";
import { getAccount } from "c/demoSignalss";
import { getAccount } from "c/demoSignals";

export default class AccountDetails extends LightningElement {
account = $computed(() => (this.account = getAccount.value)).value;
}
```

<div style="text-align: center;">
<p align="center">
<img src="./doc-assets/account-picker.gif" alt="Account Picker Example" />
</div>
</p>

> 🍪 One extra feature of the data returned by the `$resource` function is that when it is reloading the data, the
> previous data is still available in the `data` property. This allows you to keep the old value while the new value is
Expand Down Expand Up @@ -550,6 +566,93 @@ export default class ContactList extends LightningElement {
}
```

### Mutating `$resource` data

Besides `refetch`, the `$resource` function also returns a `mutate` function that allows you to mutate the data.

`mutate` is useful when you want to update the data without refetching it (and avoid a trip to the server).

It receives a single value, which will be set as the new value of the data. The `resource` value will be updated
immediately, the `.loading` property will be set to `false`, and the `.error` property will be set to `null`.

```javascript
import { $resource } from "c/signals";

const { data, mutate } = $resource(asyncFunction);

mutate("new value");
```

#### Reacting to mutated values

When using the `mutate` function, you might want to react to the changes in the data. For example, you might now
want to call an Apex function to save the new value to the server, and make sure the data is synced.

For this, you can provide a function through the options object's `onMutate`.

The function you provide can receive 3 arguments:

- The new value
- The old value
- A `mutate` function that you can use the update the data again. This can be used for when you want to update the data
based on what was returned from the server, but you don't want to refetch the data. You SHOULD use this mutate
function over the one returned when creating the `$resource` because this will not trigger `onMutate` once again.

```javascript
import { $resource } from "c/signals";
import getContacts from "@salesforce/apex/ContactController.getContacts";
import saveContacts from "@salesforce/apex/ContactController.saveContacts";

const { data, mutate } = $resource(
getContacts,
{},
{
onMutate: async (newValue, oldValue, mutate) => {
await saveContacts({ contacts: newValue });
mutate(newValue);
}
}
);
```

In the case an error occurs on your server call, the `mutate` you can pass an error object as the second argument to
the `mutate` function. This will set the `.error` property of the `resource` to the error object.

```javascript
import { $resource } from "c/signals";

const { data, mutate } = $resource(asyncFunction);

try {
await saveContacts({ contacts: newValue });
mutate(newValue);
} catch (error) {
mutate(null, error);
}
```

#### Optimistic updating

When you mutate a `resource` as exemplified above, you can achieve the concept of optimistic updating. This is when you
update the value immediately before the server responds, and then update the value again when the server responds.

Optimistically updating the value can provide a better user experience by making the UI feel more responsive, but it
can also lead to inconsistencies if the server responds with an error. So if you wish to turn this off, and
manage updating the value yourself, either by `refetching` or by using an `onMutate` function, you can set the
`optimisticMutate` option to `false`.

```javascript
import { $resource } from "c/signals";

const { data, refetch, mutate } = $resource(
asyncFunction,
{},
{
optimisticMutate: false
}
);
```

## Storage

By default, any created signal is stored in memory and will be lost when the component is destroyed. This behavior
Expand All @@ -568,7 +671,7 @@ The following storage helpers are available by default:
- `useCookies(key: string, expires?: Date)`: Stores the signal in a cookie with the given key. You can also pass an
optional `expires` parameter to set the expiration date of the cookie

### Creating custom storage
### Creating a custom storage

The `storage` option receives a function that defines the behavior for where the data should be stored.
This means you can create your own custom storage solution by passing a function with the following
Expand Down Expand Up @@ -633,6 +736,24 @@ storage, and the setter should set the value in the storage.
Notice that any additional properties you add to the object returned by `createStorage` will be available in the
returned object. That is how we can add the `undo` function to the `counter` signal and use it to undo the changes.

## Examples

You can find full working examples in the `examples` folder.

For a full kitchen sink example that combines all the concepts, you can check the `shopping-cart` example.

It includes:

- Getting data from the server
- Optimistic updates by updating the local value on change
- Re-updating the value when the server responds
- Undo functionality by storing the state history in a custom signal
- Caching the data in the `localStorage` for a fast first load.

<p align="center">
<img width="500" src="./doc-assets/kitchen-sink.gif" alt="Kitchen Sink Example" />
</p>

# Contributing

Contributions are welcome! Please read the [Contributing Guide](CONTRIBUTING.md) for more information.
Binary file added doc-assets/full-example.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc-assets/kitchen-sink.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
<div>Contact Name: {contactInfo.contactName}</div>
</div>
</div>
</template>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ export default class BusinessCard extends LightningElement {
contactName: contactName.value
})
).value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
<target>lightningCommunity__Default</target>
<target>lightningCommunity__Page</target>
</targets>
</LightningComponentBundle>
</LightningComponentBundle>
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
value={contactName}
onchange={handleContactNameChange}
></lightning-input>
</template>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ export default class ContactInfoForm extends LightningElement {
handleContactNameChange(event) {
contactName.value = event.target.value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
<target>lightningCommunity__Default</target>
<target>lightningCommunity__Page</target>
</targets>
</LightningComponentBundle>
</LightningComponentBundle>
2 changes: 1 addition & 1 deletion examples/counter/lwc/countChanger/countChanger.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
<button onclick={decrementCount}>Decrement</button>
<button onclick={incrementCount}>Increment</button>
</div>
</template>
</template>
2 changes: 1 addition & 1 deletion examples/counter/lwc/countChanger/countChanger.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ export default class CountChanger extends LightningElement {
decrementCount() {
counter.value--;
}
}
}
2 changes: 1 addition & 1 deletion examples/counter/lwc/countChanger/countChanger.js-meta.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
<target>lightningCommunity__Default</target>
<target>lightningCommunity__Page</target>
</targets>
</LightningComponentBundle>
</LightningComponentBundle>
2 changes: 1 addition & 1 deletion examples/counter/lwc/countTracker/countTracker.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<template>
The current count is ($computed reactive property): {reactiveProperty} <br />
The counter plus two value is (nested computed): {counterPlusTwo}
</template>
</template>
2 changes: 1 addition & 1 deletion examples/counter/lwc/countTracker/countTracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export default class CountTracker extends LightningElement {

counterPlusTwo = $computed(() => (this.counterPlusTwo = counterPlusTwo.value))
.value;
}
}
2 changes: 1 addition & 1 deletion examples/counter/lwc/countTracker/countTracker.js-meta.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
<target>lightningCommunity__Default</target>
<target>lightningCommunity__Page</target>
</targets>
</LightningComponentBundle>
</LightningComponentBundle>
2 changes: 1 addition & 1 deletion examples/demo-signals/lwc/demoSignals/apex-fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const { data: fetchContacts } = $resource(getContacts);

export const selectedAccountId = $signal(null);

$effect(() => console.log(selectedAccountId.value));
$effect(() => console.log('selected Account Id', selectedAccountId.value));

export const { data: getAccount } = $resource(getAccountDetails, () => ({
accountId: selectedAccountId.value
Expand Down
2 changes: 1 addition & 1 deletion examples/demo-signals/lwc/demoSignals/contact-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import { $signal } from "c/signals";

export const accountName = $signal("ACME");

export const contactName = $signal("John Doe");
export const contactName = $signal("John Doe");
Loading

0 comments on commit d33f184

Please sign in to comment.