|
| 1 | +--- |
| 2 | +chapter: 15 |
| 3 | +subtitle: The Provider pattern |
| 4 | +hidden: true |
| 5 | +--- |
| 6 | + |
| 7 | +The [Provider pattern](https://www.patterns.dev/posts/provider-pattern/) allows for deeply nested children to ask ancestors for values. This can be useful for decoupling state inside a component, centralising it higher up in the DOM heirarchy. A top level container component might store values, and many children can consume those values, without having logic duplicated across the app. It's quite an abstract pattern so is better explained with examples... |
| 8 | + |
| 9 | +Say for example a set of your components are built to perform actions on a user, but need a User ID. One way to handle this is to set the User ID as an attribute on each element, but this can lead to a lot of duplication. Instead these actions can request the ID from a parent component, which can provide the User ID without creating an explicit relationship (which can lead to brittle code). |
| 10 | + |
| 11 | +The `@providable` ability allows a Catalyst controller to become a provider or consumer (or both) of one or many properties. To provide a property to nested controllers that ask for it, mark a property as `@provide`. To consume a property from a parent, mark a property as `@consume`. Let's try implementing the user actions using `@providable`: |
| 12 | + |
| 13 | +```typescript |
| 14 | +import {providable, consume, provide, controller} from '@github/catalyst' |
| 15 | + |
| 16 | +@providable |
| 17 | +@controller |
| 18 | +class BlockUser extends HTMLElement { |
| 19 | + // This will request `userId`, and default to '' if not provided. |
| 20 | + @consume userId = '' |
| 21 | + // This will request `userName`, and default to '' if not provided. |
| 22 | + @consume userName = '' |
| 23 | + |
| 24 | + async handleEvent() { |
| 25 | + if (confirm(`Would you like to block ${this.userName}?`)) { |
| 26 | + await fetch(`/users/${userId}/delete`) |
| 27 | + } |
| 28 | + } |
| 29 | +} |
| 30 | + |
| 31 | +@providable |
| 32 | +@controller |
| 33 | +class FollowUser extends HTMLElement { |
| 34 | + // This will request `userId`, and default to '' if not provided. |
| 35 | + @consume userId = '' |
| 36 | + // This will request `userName`, and default to '' if not provided. |
| 37 | + @consume userName = '' |
| 38 | + |
| 39 | + async handleEvent() { |
| 40 | + if (confirm(`Would you like to follow ${this.userName}?`)) { |
| 41 | + await fetch(`/users/${userId}/delete`) |
| 42 | + } |
| 43 | + } |
| 44 | +} |
| 45 | + |
| 46 | +@providable |
| 47 | +@controller |
| 48 | +class UserRow extends HTMLElement { |
| 49 | + // This will provide `userId` as '123' to any nested children that request it. |
| 50 | + @provide userId = '123' |
| 51 | + // This will provide `userName` as 'Alex' to any nested children that request it. |
| 52 | + @provide userId = 'Alex' |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +```html |
| 57 | +<user-row> |
| 58 | + <follow-user><button data-action="click:follow-user"></follow-user> |
| 59 | + <block-user><button data-action="click:block-user"></block-user> |
| 60 | +</user-row> |
| 61 | +``` |
| 62 | + |
| 63 | +This shows how the basic pattern works, but `UserRow` having fixed strings isn't very useful. The `@provide` decorator can be combined with other decorators to make it more powerful, for example `@attr`: |
| 64 | + |
| 65 | +```typescript |
| 66 | +import {providable, consume, provide, @attr, controller} from '@github/catalyst' |
| 67 | + |
| 68 | +@providable |
| 69 | +@controller |
| 70 | +class UserRow extends HTMLElement { |
| 71 | + @provide @attr userId = '' |
| 72 | + @provide @attr userName = '' |
| 73 | +} |
| 74 | +``` |
| 75 | +```html |
| 76 | +<user-row user-id="123" user-name="Alex"> |
| 77 | + <follow-user><button data-action="click:follow-user"></follow-user> |
| 78 | + <block-user><button data-action="click:block-user"></block-user> |
| 79 | +</user-row> |
| 80 | +<user-row user-id="864" user-name="Riley"> |
| 81 | + <follow-user><button data-action="click:follow-user"></follow-user> |
| 82 | + <block-user><button data-action="click:block-user"></block-user> |
| 83 | +</user-row> |
| 84 | +``` |
| 85 | + |
| 86 | +Values aren't just limited to strings, they can be any type; for example functions, classes, or even other controllers! We could implement a custom dialog component which exists as a sibling and invoke it using providers and `@target`: |
| 87 | + |
| 88 | + |
| 89 | +```typescript |
| 90 | +import {providable, consume, provide, target, attr, controller} from '@github/catalyst' |
| 91 | + |
| 92 | +@providable |
| 93 | +@controller |
| 94 | +class UserList extends HTMLElement { |
| 95 | + @provide @target dialog: UserDialogElement |
| 96 | +} |
| 97 | + |
| 98 | +@controller |
| 99 | +class UserDialog extends HTMLElement { |
| 100 | + setTitle(title: string) { |
| 101 | + this.title.textContent = title |
| 102 | + } |
| 103 | + confirm() { |
| 104 | + this.show() |
| 105 | + return this.untilClosed() |
| 106 | + } |
| 107 | + //... |
| 108 | +} |
| 109 | + |
| 110 | +@providable |
| 111 | +@controller |
| 112 | +class FollowUser extends HTMLElement { |
| 113 | + // This will request `userId`, and default to '' if not provided. |
| 114 | + @consume userId = '' |
| 115 | + // This will request `userName`, and default to '' if not provided. |
| 116 | + @consume userName = '' |
| 117 | + // This will request `dialog`, defaulting it to `null` if not provided: |
| 118 | + @consume dialog: UserDialog | null = null |
| 119 | + |
| 120 | + async handleEvent() { |
| 121 | + if (!this.dialog) return |
| 122 | + this.dialog.setTitle(`Would you like to follow ${this.userName}?`) |
| 123 | + if (await this.dialog.confirm()) { |
| 124 | + await fetch(`/users/${this.userId}/delete`) |
| 125 | + } |
| 126 | + } |
| 127 | +} |
| 128 | +``` |
| 129 | +```html |
| 130 | +<user-list> |
| 131 | + <user-row user-id="123" user-name="Alex"> |
| 132 | + <follow-user><button data-action="click:follow-user"></follow-user> |
| 133 | + <block-user><button data-action="click:block-user"></block-user> |
| 134 | + </user-row> |
| 135 | + <user-row user-id="864" user-name="Riley"> |
| 136 | + <follow-user><button data-action="click:follow-user"></follow-user> |
| 137 | + <block-user><button data-action="click:block-user"></block-user> |
| 138 | + </user-row> |
| 139 | + |
| 140 | + <user-dialog data-target="user-list.dialog"><!-- ... --></user-dialog> |
| 141 | + |
| 142 | +</user-list> |
| 143 | +``` |
| 144 | + |
| 145 | +If you're interested to find out how the Provider pattern works, you can look at the [context community-protocol as part of webcomponents-cg](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md). |
0 commit comments