docs: add topic for linkedSignal (#58647)

Adds a topic to the Signals guide for `linkedSignal`. Also fixes a few broken links.

PR Close #58647
This commit is contained in:
Jeremy Elbourn
2024-11-13 21:58:56 -08:00
committed by Jessica Janiuk
parent acca96c3e1
commit 506413cf23
5 changed files with 125 additions and 6 deletions
+12 -2
View File
@@ -83,8 +83,18 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
children: [
{
label: 'Signals',
path: 'guide/signals',
contentPath: 'guide/signals/overview',
children: [
{
label: 'Overview',
path: 'guide/signals',
contentPath: 'guide/signals/overview',
},
{
label: 'linkedSignal',
path: 'guide/signals/linked-signal',
contentPath: 'guide/signals/linked-signal',
},
],
},
{
label: 'Components',
+1 -1
View File
@@ -2,7 +2,7 @@
"DI" is a design pattern and mechanism for creating and delivering some parts of an app to other parts of an app that require them.
</docs-decorative-header>
Tip: Check out Angular's [Essentials](essentials/sharing-logic) before diving into this comprehensive guide.
Tip: Check out Angular's [Essentials](essentials/dependency-injection) before diving into this comprehensive guide.
When you develop a smaller part of your system, like a module or a class, you may need to use features from other classes. For example, you may need an HTTP service to make backend calls. Dependency Injection, or DI, is a design pattern and mechanism for creating and delivering some parts of an application to other parts of an application that require them. Angular supports this design pattern and you can use it in your applications to increase flexibility and modularity.
@@ -0,0 +1,109 @@
# `linkedSignal`
IMPORTANT: `linkedSignal` is [experimental](reference/releases#experimental). It's ready for you to try, but it might change before it is stable.
You can use the `signal` function to hold some state in your Angular code. Sometimes, this state depends on some _other_ state. For example, imagine a component that lets the user select a shipping method for an order:
```typescript
@Component({/* ... */})
export class ShippingMethodPicker {
shippingOptions: Signal<ShippingMethod[]> = getShippingOptions();
// Select the first shipping option by default.
selectedOption = signal(this.shippingOptions()[0]);
changeShipping(newOptionIndex: number) {
this.selectedOption.set(this.shippingOptions()[newOptionIndex]);
}
}
```
In this example, the `selectedOption` defaults to the first option, but changes if the user selects another option. But `shippingOptions` is a signal— its value may change! If `shippingOptions` changes, `selectedOption` may contain a value that is no longer a valid option.
**The `linkedSignal` function lets you create a signal to hold some state that is intrinsically _linked_ to some other state.** Revisiting the example above, `linkedSignal` can replace `signal`:
```typescript
@Component({/* ... */})
export class ShippingMethodPicker {
shippingOptions: Signal<ShippingMethod[]> = getShippingOptions();
// Initialize selectedOption to the first shipping option.
selectedOption = linkedSignal(() => this.shippingOptions()[0]);
changeShipping(index: number) {
this.selectedOption.set(this.shippingOptions()[index]);
}
}
```
`linkedSignal` works similarly to `signal` with one key difference— instead of passing a default value, you pass a _computation function_, just like `computed`. When the value of the computation changes, the value of the `linkedSignal` changes to the computation result. This helps ensure that the `linkedSignal` always has a valid value.
The following example shows how the value of a `linkedSignal` can change based on its linked state:
```typescript
const shippingOptions = signal(['Ground', 'Air', 'Sea']);
const selectedOption = linkedSignal(() => shippingOptions()[0]);
console.log(selectedOption()); // 'Ground'
selectedOption.set(shippingOptions[2]);
console.log(selectedOption()); // 'Sea'
shippingOptions.set(['Email', 'Will Call', 'Postal service']);
console.log(selectedOption()); // 'Email'
```
## Accounting for previous state
In some cases, the computation for a `linkedSignal` needs to account for the previous value of the `linkedSignal`.
In the example above, `selectedOption` always updates back to the first option when `shippingOptions` changes. You may, however, want to preserve the user's selection if their selected option is still somewhere in the list. To accomplish this, you can create a `linkedSignal` with a separate _source_ and _computation_:
```typescript
@Component({/* ... */})
export class ShippingMethodPicker {
shippingOptions: Signal<ShippingMethod[]> = getShippingOptions();
selectedOption = linkedSignal({
// `selectedOption` is set to the `computation` result whenever this `source` changes.
source: shippingOptions,
computation: (newOptions, previous) => {
// If the newOptions contain the previously selected option, preserve that selection.
// Otherwise, default to the first option.
return newOptions.find(opt => opt.id === previous?.value) ?? newOptions[0];
}
});
changeShipping(newOptionIndex: number) {
this.selectedOption.set(this.shippingOptions()[newOptionIndex]);
}
}
```
When you create a `linkedSignal`, you can pass an object with separate `source` and `computation` properties instead of providing just a computation.
The `source` can be any signal, such as a `computed` or component `input`. When the value of `source` changes, `linkedSignal` updates its value to the result of the provided `computation`.
The `computation` is a function that receives the new value of `source` and a `previous` object. The `previous` object has two properties— `previous.source` is the previous value of `source`, and `previous.value` is the previous result of the `computation`. You can use these previous values to decide the new result of the computation.
## Custom equality comparison
`linkedSignal` updates to the result of the computation every time its linked state changes. By default, Angular uses referential equality to determine if the linked state has changed. You can alternatively provide a custom equality function.
```typescript
const activeUser = signal({id: 123, name: 'Morgan'});
const email = linkedSignal(() => `${activeUser().name}@example.com`, {
// Consider the user as the same if it's the same `id`.
equal: (a, b) => a.id === b.id,
});
// Or, if separating `source` and `computation`
const alternateEmail = linkedSignal({
source: activeUser,
computation: user => `${user.name}@example.com`,
equal: (a, b) => a.id === b.id,
});
// This update to `activeUser` does not cause `email` or `alternateEmail`
// to update because the `id` is the same.
activeUser.set({id: 123, name: 'Morgan', isAdmin: false});
```
+2 -2
View File
@@ -2,7 +2,7 @@
Angular Signals is a system that granularly tracks how and where your state is used throughout an application, allowing the framework to optimize rendering updates.
</docs-decorative-header>
Tip: Check out Angular's [Essentials](essentials/managing-dynamic-data) before diving into this comprehensive guide.
Tip: Check out Angular's [Essentials](essentials/signals) before diving into this comprehensive guide.
## What are signals?
@@ -154,7 +154,7 @@ export class EffectiveCounterComponent {
}
```
To create an effect outside of the constructor, you can pass an `Injector` to `effect` via its options:
To create an effect outside the constructor, you can pass an `Injector` to `effect` via its options:
```ts
@Component({...})
+1 -1
View File
@@ -3,7 +3,7 @@ In Angular, a template is a chunk of HTML.
Use special syntax within a template to leverage many of Angular's features.
</docs-decorative-header>
Tip: Check out Angular's [Essentials](essentials/rendering-dynamic-templates) before diving into this comprehensive guide.
Tip: Check out Angular's [Essentials](essentials/templates) before diving into this comprehensive guide.
Every Angular component has a **template** that defines the [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) that the component renders onto the page. By using templates, Angular is able to automatically keep your page up-to-date as data changes.