@servicetitan/docs-uikit 29.0.0 → 30.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/BREAKING_CHANGES.mdx +10 -0
- package/docs/ajax-handlers.mdx +1 -1
- package/docs/eslint-config.mdx +10 -8
- package/docs/folder-schema.mdx +16 -3
- package/docs/startup.mdx +49 -32
- package/docs/web-components/event-bus.mdx +515 -0
- package/docs/{web-components.mdx → web-components/web-components.mdx} +230 -271
- package/package.json +2 -2
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
title: BREAKING CHANGES
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## v30.0.0
|
|
6
|
+
|
|
7
|
+
### [@servicetitan/startup](./startup)
|
|
8
|
+
|
|
9
|
+
- Upgraded ESLint to v9.x and dropped support for legacy "eslintrc" configuration. See [Upgrading to ESLint v9.x](/docs/frontend/upgrading-to-eslint-v9) for guidance.
|
|
10
|
+
|
|
11
|
+
### [@servicetitan/eslint-config](./eslint-config)
|
|
12
|
+
|
|
13
|
+
- Dropped support for legacy "eslintrc" configuration
|
|
14
|
+
|
|
5
15
|
## v29.0.0
|
|
6
16
|
|
|
7
17
|
### [@servicetitan/startup](./startup)
|
package/docs/ajax-handlers.mdx
CHANGED
|
@@ -86,7 +86,7 @@ See [Authentication Adapters](#authentication-adapters) for more information.
|
|
|
86
86
|
|
|
87
87
|
The authentication endpoint for protected resources.
|
|
88
88
|
|
|
89
|
-
- For **Bearer** authentication, this is the endpoint that returns the bearer token to send in **Authorization** headers. Defaults to `${baseURL}/
|
|
89
|
+
- For **Bearer** authentication, this is the endpoint that returns the bearer token to send in **Authorization** headers. Defaults to `${baseURL}/auth`.
|
|
90
90
|
|
|
91
91
|
- For **Token server** authentication, this is the "silent login" endpoint. Defaults to `${baseURL}/bff/silent-login`.
|
|
92
92
|
|
package/docs/eslint-config.mdx
CHANGED
|
@@ -20,18 +20,20 @@ title: ESLint & Stylelint configurations
|
|
|
20
20
|
|
|
21
21
|
#### In the Lerna project
|
|
22
22
|
|
|
23
|
-
```
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
```js title="eslint.config.js"
|
|
24
|
+
import { defineConfig } from 'eslint/config';
|
|
25
|
+
import mono from '@servicetitan/eslint-config/mono';
|
|
26
|
+
|
|
27
|
+
export default defineConfig(mono);
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
#### Without Lerna
|
|
30
31
|
|
|
31
|
-
```
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
```js title="eslint.config.js"
|
|
33
|
+
import { defineConfig } from 'eslint/config';
|
|
34
|
+
import single from '@servicetitan/eslint-config/single';
|
|
35
|
+
|
|
36
|
+
export default defineConfig(single);
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
### Stylelint
|
package/docs/folder-schema.mdx
CHANGED
|
@@ -18,9 +18,22 @@ Folder schema is a flexible tool to configure a strict hierarchy of files in you
|
|
|
18
18
|
|
|
19
19
|
Contains `@servicetitan/folder-schema/check` rule with the next options:
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
| Name | Description |
|
|
22
|
+
| :--------- | :------------------------------------------------------------------------------------------------------------------------------ |
|
|
23
|
+
| `config` | Object with a configuration of files hierarchy |
|
|
24
|
+
| `root?` | Optional entry point to start recursive checking. Defaults to the current working directory. |
|
|
25
|
+
| `docLink?` | Optional link to a document describing a required files hierarchy. Defaults to https://docs.st.dev/docs/frontend/file-structure |
|
|
26
|
+
|
|
27
|
+
### Configuration
|
|
28
|
+
|
|
29
|
+
Use the `eslint.config.{js,cjs,mjs}` file to configure rules using the [flag config](https://eslint.org/docs/latest/use/configure/configuration-files) style.
|
|
30
|
+
|
|
31
|
+
```js title="eslint.config.js"
|
|
32
|
+
import folderSchemaPlugin from '@servicetitan/eslint-plugin-folder-schema';
|
|
33
|
+
|
|
34
|
+
// Call configs.recommended with desired options
|
|
35
|
+
export = [...folderSchemaPlugin.configs.recommended({ config: require.resolve('./config') })];
|
|
36
|
+
```
|
|
24
37
|
|
|
25
38
|
## @servicetitan/folder-lint
|
|
26
39
|
|
package/docs/startup.mdx
CHANGED
|
@@ -26,31 +26,22 @@ Updating build tooling is typically a daunting and time-consuming task. When new
|
|
|
26
26
|
Experimental flags don't follow semver. There might be breaking changes in minor versions of `@servicetitan/startup` when you opt-in to experimental flags.
|
|
27
27
|
:::
|
|
28
28
|
|
|
29
|
-
###
|
|
29
|
+
### convert-eslint-config
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Convert an ESLint v8 configuration to v9 format.
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
|
-
$ npx
|
|
34
|
+
$ npx startup convert-eslint-config
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Installs the package dependencies. This command should be run via `npx` before the build.
|
|
37
|
+
Use this command when upgrading from ESLint v8 to v9, to convert a v8 `.eslintrc.json` to the equivalent `eslint.config.mjs`. See [Upgrading to ESLint v9.x](/docs/frontend/upgrading-to-eslint-v9) for guidance on upgrading to ESLint v9.
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
The command takes no arguments. Simply run it from a directory with `.eslintrc.json` and it creates `eslint.config.mjs` in the same location.
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
#### Arguments
|
|
48
|
-
|
|
49
|
-
- `--scope <glob>` - Include only packages with names matching the given glob.
|
|
50
|
-
- `--ignore <glob>` - Exclude packages with names matching the given glob.
|
|
51
|
-
- `--esbuild` - Use [esbuild-loader](https://github.com/privatenumber/esbuild-loader) to process TypeScript files instead of ts-loader.
|
|
52
|
-
- `--experimental-bundlers` - Use experimental build optimizations (alternative loaders and bundlers)
|
|
53
|
-
- `--code-coverage` - Add [instrumentation](https://github.com/JS-DevTools/coverage-istanbul-loader) to bundled code in order to collect code coverage
|
|
41
|
+
:::caution
|
|
42
|
+
Comments are not copied from the source `.eslintrc.json` to the output file.
|
|
43
|
+
If the source file contains important comments, copy them manually to the output file.
|
|
44
|
+
:::
|
|
54
45
|
|
|
55
46
|
### build
|
|
56
47
|
|
|
@@ -66,14 +57,37 @@ Build packages for production to the `dist/bundle` folders. It bundles them in p
|
|
|
66
57
|
- `--experimental-bundlers` - Use experimental build optimizations (alternative loaders and bundlers)
|
|
67
58
|
- `--code-coverage` - Add [instrumentation](https://github.com/JS-DevTools/coverage-istanbul-loader) to bundled code in order to collect code coverage
|
|
68
59
|
|
|
69
|
-
###
|
|
60
|
+
### init
|
|
70
61
|
|
|
71
|
-
|
|
62
|
+
Generates initial project structure. This command should be run via `npx` in an empty folder.
|
|
72
63
|
|
|
73
|
-
|
|
64
|
+
```sh
|
|
65
|
+
$ npx -y @servicetitan/startup@latest init
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### install
|
|
69
|
+
|
|
70
|
+
Installs the package dependencies. This command should be run via `npx` before the build.
|
|
71
|
+
|
|
72
|
+
See [package.json in the example project](https://github.com/search?q=repo%3Aservicetitan%2Ffrontend-example+path%3A**%2Fpackage.json+%22startup+install%22&type=code).
|
|
73
|
+
|
|
74
|
+
### kendo-ui-license
|
|
75
|
+
|
|
76
|
+
Activates KendoReact components by installing the license key. The license key is only installed if the project depends on `@progress/kendo` components. Otherwise, this command has no effect.
|
|
77
|
+
|
|
78
|
+
**Note:** The [build](#build) command automatically detects when a project uses KendoRect components and runs this command.
|
|
79
|
+
|
|
80
|
+
Use it to install the license key separately from a build, or to override the default license key.
|
|
74
81
|
|
|
75
82
|
```sh
|
|
76
|
-
$ npx startup
|
|
83
|
+
$ npx startup kendo-ui-license
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
To install a different license, set the KENDO_UI_LICENSE environment variable to the key to use.
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
$ export KENDO_UI_LICENSE=<license-key>
|
|
90
|
+
$ npx startup kendo-ui-license
|
|
77
91
|
```
|
|
78
92
|
|
|
79
93
|
### lint
|
|
@@ -143,23 +157,26 @@ To customize the recognized branches for an MFE set `cli.web-component.branches`
|
|
|
143
157
|
|
|
144
158
|
- `publishTag: string` - Tag used when publishing package.
|
|
145
159
|
|
|
146
|
-
###
|
|
160
|
+
### start
|
|
147
161
|
|
|
148
|
-
|
|
162
|
+
Runs package in the development mode. Applications will be hosted on sequential free ports starting from 8080. Pages will automatically reload on changes to the code.
|
|
149
163
|
|
|
150
|
-
|
|
164
|
+
#### Arguments
|
|
151
165
|
|
|
152
|
-
|
|
166
|
+
- `--scope <glob>` - Include only packages with names matching the given glob.
|
|
167
|
+
- `--ignore <glob>` - Exclude packages with names matching the given glob.
|
|
168
|
+
- `--esbuild` - Use [esbuild-loader](https://github.com/privatenumber/esbuild-loader) to process TypeScript files instead of ts-loader.
|
|
169
|
+
- `--experimental-bundlers` - Use experimental build optimizations (alternative loaders and bundlers)
|
|
170
|
+
- `--code-coverage` - Add [instrumentation](https://github.com/JS-DevTools/coverage-istanbul-loader) to bundled code in order to collect code coverage
|
|
153
171
|
|
|
154
|
-
|
|
155
|
-
$ npx startup kendo-ui-license
|
|
156
|
-
```
|
|
172
|
+
### test
|
|
157
173
|
|
|
158
|
-
|
|
174
|
+
Runs all existing tests in all packages.
|
|
175
|
+
|
|
176
|
+
To run tests a subset of tests is possible to pass paths to specific directories or test files as positional parameters.
|
|
159
177
|
|
|
160
178
|
```sh
|
|
161
|
-
$
|
|
162
|
-
$ npx startup kendo-ui-license
|
|
179
|
+
$ npx startup test -- packages/desktop/app/modules/inventory/
|
|
163
180
|
```
|
|
164
181
|
|
|
165
182
|
## Build Steps
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: EventBus
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
import Tabs from '@theme/Tabs';
|
|
6
|
+
import TabItem from '@theme/TabItem';
|
|
7
|
+
|
|
8
|
+
Use EventBus to exchange messages (aka events) between a host and MFEs, or between concurrently mounted MFEs.
|
|
9
|
+
|
|
10
|
+
### Providing EventBus
|
|
11
|
+
|
|
12
|
+
To use EventBus, provide an instance via `EVENT_BUS_TOKEN`.
|
|
13
|
+
[Loader](#loader) automatically passes the EventBus through to MFEs.
|
|
14
|
+
For example,
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { Provider } from '@servicetitan/react-ioc';
|
|
18
|
+
import { EVENT_BUS_TOKEN, EventBus } from '@servicetitan/web-components';
|
|
19
|
+
|
|
20
|
+
export const App = () => {
|
|
21
|
+
return (
|
|
22
|
+
<Provider singletons={[{ provide: EVENT_BUS_TOKEN, useValue: new EventBus() }]}>
|
|
23
|
+
{/* EVENT_BUS_TOKEN is automatically provided in MFEs */}
|
|
24
|
+
</Provider>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Naming events
|
|
30
|
+
|
|
31
|
+
EventBus is a shared resource that can be used simultaneously by independently
|
|
32
|
+
developed modules and MFEs and it is important to avoid generic event names
|
|
33
|
+
that could be mistakenly handled by the wrong recipient.
|
|
34
|
+
|
|
35
|
+
To avoid name collisions, prefix events names with the name of the module that
|
|
36
|
+
owns them; that could be the module that sends the events, or the module that handles them.
|
|
37
|
+
|
|
38
|
+
The recommended format is `{module}:{name}`, for example **"fleet-mfe:refresh-token"** and **"navigation:tooltip"**.
|
|
39
|
+
|
|
40
|
+
### Sending events
|
|
41
|
+
|
|
42
|
+
To send an event, call [emit](#emit) with the event name.
|
|
43
|
+
|
|
44
|
+
The event is delivered to all active listeners regardless whether it originated within the host or an MFE.
|
|
45
|
+
|
|
46
|
+
For example, this component sends a **"example:click"** when the user clicks a button.
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { Button } from '@servicetitan/anvil2';
|
|
50
|
+
import { useDependencies } from '@servicetitan/react-ioc';
|
|
51
|
+
import { EVENT_BUS_TOKEN } from '@servicetitan/web-components';
|
|
52
|
+
import { useCallback } from 'react';
|
|
53
|
+
|
|
54
|
+
function EventSender() {
|
|
55
|
+
const [eventBus] = useDependencies(EVENT_BUS_TOKEN);
|
|
56
|
+
|
|
57
|
+
const handleClick = useCallback(() => eventBus.emit('example:click'), [eventBus]);
|
|
58
|
+
|
|
59
|
+
return <Button onClick={handleClick}>Click Here</Button>;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Handling events
|
|
64
|
+
|
|
65
|
+
To handle an event, call [on](#on) to register a handler that is called when the event occurs, and call [off](#off) to remove the handler.
|
|
66
|
+
|
|
67
|
+
All active handlers are called when the event occurs, regardless whether the handler is in the host or an MFE.
|
|
68
|
+
|
|
69
|
+
For example, this component renders the date and time when the **"example:click"** last occurred:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { useDependencies } from '@servicetitan/react-ioc';
|
|
73
|
+
import { EVENT_BUS_TOKEN } from '@servicetitan/web-components';
|
|
74
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
75
|
+
|
|
76
|
+
function EventHandler() {
|
|
77
|
+
const [eventBus] = useDependencies(EVENT_BUS_TOKEN);
|
|
78
|
+
const [lastOccurredAt, setLastOccurredAt] = useState<Date>();
|
|
79
|
+
|
|
80
|
+
const handleClickEvent = useCallback(() => setLastOccurredAt(new Date()), []);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
eventBus.on('example:click', handleClickEvent);
|
|
84
|
+
return () => {
|
|
85
|
+
eventBus.off('example:click', handleClickEvent);
|
|
86
|
+
};
|
|
87
|
+
}, [eventBus, handleClickEvent]);
|
|
88
|
+
|
|
89
|
+
return lastOccurredAt ? `Click last occurred at: ${lastOccurredAt.toISOString()}` : null;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Sending data with events
|
|
94
|
+
|
|
95
|
+
To send data with an event, call [emit](#emit) with the data to send.
|
|
96
|
+
|
|
97
|
+
This example sends an **"example:input"** with the value entered into an input field.
|
|
98
|
+
It also illustrates how to ensure event data is correctly typed (see [typedEventBusToken](#typedeventbustoken)).
|
|
99
|
+
|
|
100
|
+
<Tabs
|
|
101
|
+
defaultValue="sender"
|
|
102
|
+
values={[
|
|
103
|
+
{ label: 'Sender', value: 'sender' },
|
|
104
|
+
{ label: 'Handler', value: 'handler' },
|
|
105
|
+
{ label: 'Types', value: 'types' },
|
|
106
|
+
]}
|
|
107
|
+
>
|
|
108
|
+
<TabItem value="types">
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { EventBus, typedEventBusToken } from '@servicetitan/web-components';
|
|
112
|
+
|
|
113
|
+
export enum Events {
|
|
114
|
+
input = 'example:input',
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ExampleEvents {
|
|
118
|
+
[Events.input]: { value: string };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type ExampleEventBus = EventBus<ExampleEvents>;
|
|
122
|
+
|
|
123
|
+
export const EXAMPLE_EVENT_BUS_TOKEN = typedEventBusToken<ExampleEvents>();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
</TabItem>
|
|
127
|
+
<TabItem value="sender">
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { TextField } from '@servicetitan/anvil2';
|
|
131
|
+
import { useDependencies } from '@servicetitan/react-ioc';
|
|
132
|
+
import { useEffect, useState } from 'react';
|
|
133
|
+
|
|
134
|
+
import { Events, EXAMPLE_EVENT_BUS_TOKEN } from './types';
|
|
135
|
+
|
|
136
|
+
function EventSender() {
|
|
137
|
+
const [eventBus] = useDependencies(EXAMPLE_EVENT_BUS_TOKEN);
|
|
138
|
+
const [input, setInput] = useState('');
|
|
139
|
+
|
|
140
|
+
useEffect(() => eventBus.emit(Events.input, { value: input }), [eventBus, input]);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<TextField
|
|
144
|
+
label="Enter value:"
|
|
145
|
+
value={input}
|
|
146
|
+
onChange={event => setInput(event.target.value)}
|
|
147
|
+
/>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
</TabItem>
|
|
153
|
+
<TabItem value="handler">
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
import { useDependencies } from '@servicetitan/react-ioc';
|
|
157
|
+
import { useEffect, useState } from 'react';
|
|
158
|
+
|
|
159
|
+
import { Events, ExampleEvents, EXAMPLE_EVENT_BUS_TOKEN } from './types';
|
|
160
|
+
|
|
161
|
+
function EventHandler() {
|
|
162
|
+
const [eventBus] = useDependencies(EXAMPLE_EVENT_BUS_TOKEN);
|
|
163
|
+
const [data, setData] = useState<ExampleEvents[Events.input]>();
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
eventBus.on(Events.input, setData);
|
|
167
|
+
return () => {
|
|
168
|
+
eventBus.off(Events.input, setData);
|
|
169
|
+
};
|
|
170
|
+
}, [eventBus]);
|
|
171
|
+
|
|
172
|
+
return data ? `Received: "${data.value}"` : null;
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
</TabItem>
|
|
177
|
+
</Tabs>
|
|
178
|
+
|
|
179
|
+
### Returning results from handlers
|
|
180
|
+
|
|
181
|
+
EventBus provides a separate "jobs" API for when you want to wait for an event to be handled.
|
|
182
|
+
|
|
183
|
+
:::caution
|
|
184
|
+
The jobs API assumes there is only one handler for each job event.
|
|
185
|
+
If there are multiple handlers, only the first result is returned.
|
|
186
|
+
:::
|
|
187
|
+
|
|
188
|
+
This example runs a job that randomly succeeds of fails, then displays the result.
|
|
189
|
+
|
|
190
|
+
<Tabs
|
|
191
|
+
defaultValue="sender"
|
|
192
|
+
values={[
|
|
193
|
+
{ label: 'Sender', value: 'sender' },
|
|
194
|
+
{ label: 'Handler', value: 'handler' },
|
|
195
|
+
{ label: 'Types', value: 'types' },
|
|
196
|
+
]}
|
|
197
|
+
>
|
|
198
|
+
<TabItem value="types">
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { symbolToken } from '@servicetitan/react-ioc';
|
|
202
|
+
|
|
203
|
+
export interface JobResult {
|
|
204
|
+
data: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const JOB_EVENT = symbolToken<JobResult>('example:job');
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
</TabItem>
|
|
211
|
+
<TabItem value="sender">
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
import { Button, Text } from '@servicetitan/anvil2';
|
|
215
|
+
import { useDependencies } from '@servicetitan/react-ioc';
|
|
216
|
+
import { EVENT_BUS_TOKEN } from '@servicetitan/web-components';
|
|
217
|
+
import { useCallback, useState, Fragment } from 'react';
|
|
218
|
+
|
|
219
|
+
import { JOB_EVENT } from './types';
|
|
220
|
+
|
|
221
|
+
function JobRunner() {
|
|
222
|
+
const [eventBus] = useDependencies(EVENT_BUS_TOKEN);
|
|
223
|
+
const [result, setResult] = useState<{ data?: number; error?: string }>();
|
|
224
|
+
|
|
225
|
+
const handleRunJob = useCallback(async () => {
|
|
226
|
+
setResult(undefined);
|
|
227
|
+
try {
|
|
228
|
+
// Send job event and wait for result
|
|
229
|
+
const { data } = await eventBus.job.emit(JOB_EVENT);
|
|
230
|
+
setResult({ data });
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (typeof error === 'string') {
|
|
233
|
+
setResult({ error });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}, [eventBus]);
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<Fragment>
|
|
240
|
+
<Button onClick={handleRunJob}>Run Job</Button>
|
|
241
|
+
{result && <Text variant="body">{result.data ?? result.error}</Text>}
|
|
242
|
+
</Fragment>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
</TabItem>
|
|
248
|
+
<TabItem value="handler">
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
import { useDependencies } from '@servicetitan/react-ioc';
|
|
252
|
+
import { EVENT_BUS_TOKEN, JobEventHandler } from '@servicetitan/web-components';
|
|
253
|
+
import { useCallback, useEffect } from 'react';
|
|
254
|
+
|
|
255
|
+
import { JOB_EVENT, JobResult } from './types';
|
|
256
|
+
|
|
257
|
+
function JobHandler() {
|
|
258
|
+
const [eventBus] = useDependencies(EVENT_BUS_TOKEN);
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Job handlers can resolve with a result, or reject with an error.
|
|
262
|
+
* This handler generates a random number from 0 thru 10 and rejects if the number is <5.
|
|
263
|
+
*/
|
|
264
|
+
|
|
265
|
+
const handleJob = useCallback<JobEventHandler<JobResult>>(({ resolve, reject }) => {
|
|
266
|
+
const getValue = () => Math.round(10 * Math.random());
|
|
267
|
+
|
|
268
|
+
setTimeout(() => {
|
|
269
|
+
const value = getValue();
|
|
270
|
+
if (value >= 5) {
|
|
271
|
+
resolve({ data: value });
|
|
272
|
+
} else {
|
|
273
|
+
reject(`Error: value is ${value}`);
|
|
274
|
+
}
|
|
275
|
+
}, 2000);
|
|
276
|
+
}, []);
|
|
277
|
+
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
eventBus.job.on(JOB_EVENT, handleJob); // Register job handler
|
|
280
|
+
return () => {
|
|
281
|
+
eventBus.job.off(JOB_EVENT, handleJob); // Remove job handler
|
|
282
|
+
};
|
|
283
|
+
}, [eventBus, handleJob]);
|
|
284
|
+
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
</TabItem>
|
|
290
|
+
</Tabs>
|
|
291
|
+
|
|
292
|
+
## API
|
|
293
|
+
|
|
294
|
+
### emit
|
|
295
|
+
|
|
296
|
+
Sends an event, with optional data.
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
emit(event: string | symbol | number, data: JSON | JSONPrimitive)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
#### Parameters
|
|
303
|
+
|
|
304
|
+
| Name | Type | Description |
|
|
305
|
+
| :------ | :--------------------------- | :-------------------------------------------- |
|
|
306
|
+
| `event` | `string \| symbol \| number` | The event to send. |
|
|
307
|
+
| `data` | `JSON \| JSONPrimitive` | Optional data to pass to the event's handler. |
|
|
308
|
+
|
|
309
|
+
#### Event data
|
|
310
|
+
|
|
311
|
+
Event data must serializable, meaning it must be a primitive type (i.e., `string | number | boolean | null`) or objects or arrays containing only primitive types.
|
|
312
|
+
Specifically:
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
export type JSONPrimitive = string | number | boolean | null | undefined;
|
|
316
|
+
export type JSON = { [key: string]: JSONPrimitive | JSON[] | JSON };
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### Usage
|
|
320
|
+
|
|
321
|
+
If the EventBus is not typed (see [typedEventBusToken](#typedeventbustoken)), `event` and `data` are not restricted.
|
|
322
|
+
|
|
323
|
+
Otherwise, `event` must be one of the declared events and `data` must match the declared event's type.
|
|
324
|
+
For example,
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { EventBus } from '@servicetitan/web-components';
|
|
328
|
+
|
|
329
|
+
interface ExampleEvents {
|
|
330
|
+
'example:click': undefined;
|
|
331
|
+
'example:input': string;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function test(eventBus: EventBus<ExampleEvents>) {
|
|
335
|
+
// Note: eventBus is typed EventBus<ExampleEvents>
|
|
336
|
+
|
|
337
|
+
eventBus.emit('example:click'); // OK
|
|
338
|
+
eventBus.emit('example:click', 'something'); // ERROR: Argument of type 'string' is not assignable to parameter of type 'undefined'.
|
|
339
|
+
|
|
340
|
+
eventBus.emit('example:input', 'something'); // OK
|
|
341
|
+
eventBus.emit('example:input'); // ERROR: Expected 2 arguments, but got 1
|
|
342
|
+
eventBus.emit('example:input', 42); // ERROR: Argument of type 'number' is not assignable to parameter of type 'string'
|
|
343
|
+
|
|
344
|
+
eventBus.emit('other-event'); // ERROR: Argument of type '"other-event"' is not assignable to parameter of type 'keyof ExampleEvents | SymbolToken<any>'.
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### on
|
|
349
|
+
|
|
350
|
+
Registers an event handler.
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
on(event: string | symbol | number, handler: (data?: JSON | JSONPrimitive) => void)
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
#### Parameters
|
|
357
|
+
|
|
358
|
+
| Name | Type | Description |
|
|
359
|
+
| :-------- | :--------------------------------------- | :----------------------------------------- |
|
|
360
|
+
| `event` | `string \| symbol \| number` | The event to handle. |
|
|
361
|
+
| `handler` | `(data?: JSON \| JSONPrimitive) => void` | The handler to call when the event occurs. |
|
|
362
|
+
|
|
363
|
+
If the EventBus is not typed (see [typedEventBusToken](#typedeventbustoken)), `event` and `data` are not restricted.
|
|
364
|
+
|
|
365
|
+
Otherwise, `event` must be one of the declared events and `data` must match the declared event's type.
|
|
366
|
+
For example,
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import { EventBus } from '@servicetitan/web-components';
|
|
370
|
+
|
|
371
|
+
interface ExampleEvents {
|
|
372
|
+
'example:click': undefined;
|
|
373
|
+
'example:input': string;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function test(eventBus: EventBus<ExampleEvents>) {
|
|
377
|
+
// Note: eventBus is typed EventBus<ExampleEvents>
|
|
378
|
+
|
|
379
|
+
eventBus.on('example:click', () => {}); // OK
|
|
380
|
+
eventBus.on('example:click', (_value: string) => {}); // ERROR: Type 'undefined' is not assignable to type 'string'
|
|
381
|
+
|
|
382
|
+
eventBus.on('example:input', () => {}); // OK
|
|
383
|
+
eventBus.on('example:input', (_value: string) => {}); // OK
|
|
384
|
+
eventBus.on('example:input', (_value: number) => {}); // ERROR: Type 'string' is not assignable to type 'number'
|
|
385
|
+
|
|
386
|
+
eventBus.on('other-event', () => {}); // ERROR: Argument of type '"other-event"' is not assignable to parameter of type 'SymbolToken<any> | keyof ExampleEvents'.
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### off
|
|
391
|
+
|
|
392
|
+
Deregisters an event handler.
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
off(event: string | symbol number, handler: (data?: JSON | JSONPrimitive) => void)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
#### Parameters
|
|
399
|
+
|
|
400
|
+
| Name | Type | Description |
|
|
401
|
+
| :-------- | :--------------------------------------- | :--------------------------------- |
|
|
402
|
+
| `event` | `string \| symbol \| number` | The previously registered event. |
|
|
403
|
+
| `handler` | `(data?: JSON \| JSONPrimitive) => void` | The previously registered handler. |
|
|
404
|
+
|
|
405
|
+
### job.emit
|
|
406
|
+
|
|
407
|
+
Sends a "job" event and returns the result, or an error.
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
job.emit(event: symbolToken<T>): Promise<T>
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
#### Parameters
|
|
414
|
+
|
|
415
|
+
| Name | Type | Description |
|
|
416
|
+
| :------ | :--------------- | :------------------------------------------------------------------------------------------------------------------------ |
|
|
417
|
+
| `event` | `symbolToken<T>` | The event to send. Use a [symbolToken](#symboltoken) to ensure that senders and handlers agree on the job's return value. |
|
|
418
|
+
|
|
419
|
+
#### Returns
|
|
420
|
+
|
|
421
|
+
Returns a Promise that resolves with the handler's result, or rejects with an error.
|
|
422
|
+
|
|
423
|
+
### job.on
|
|
424
|
+
|
|
425
|
+
Registers a "job" event handler.
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
job.on(event: symbolToken<T>, handler: JobEventHandler<T>);
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
#### Parameters
|
|
432
|
+
|
|
433
|
+
| Name | Type | Description |
|
|
434
|
+
| :-------- | :------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
435
|
+
| `event` | `symbolToken<T>` | The event to send. Use a [symbolToken](#symboltoken) to ensure that senders and handlers agree on the job's return value. |
|
|
436
|
+
| `handler` | `JobEventHandler<T>` | The handler to call when the event occurs, where `T` is the jobs's return value.<br />The handler is passed `resolve` and `reject` callbacks. Call `resolve` to return the result. Call `reject` to return an error. |
|
|
437
|
+
|
|
438
|
+
### job.off
|
|
439
|
+
|
|
440
|
+
Deregisters a "job" event handler.
|
|
441
|
+
|
|
442
|
+
#### Parameters
|
|
443
|
+
|
|
444
|
+
| Name | Type | Description |
|
|
445
|
+
| :-------- | :------------------- | :--------------------------------- |
|
|
446
|
+
| `event` | `symbolToken<T>` | The previously registered event. |
|
|
447
|
+
| `handler` | `JobEventHandler<T>` | The previously registered handler. |
|
|
448
|
+
|
|
449
|
+
### typedEventBusToken
|
|
450
|
+
|
|
451
|
+
Use `typedEventBusToken` to specialize the generic `EventBus` type so that [emit](#emit), [on](#on) and [off](#off) only allow specific events.
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
function typedEventBusToken<T>(): SymbolToken<T>;
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
It returns an `EVENT_BUS_TOKEN` that resolves to the specified type of EventBus.
|
|
458
|
+
|
|
459
|
+
#### Examples
|
|
460
|
+
|
|
461
|
+
This example declares a `CustomEventBus` that only
|
|
462
|
+
allows **"example:click"** and **"example:input"**, and that requires **"example:input"** to be sent with a string value.
|
|
463
|
+
|
|
464
|
+
`typedEventBusToken` returns a convenient shorthand for using `CustomEventBus`.
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
import { useDependencies } from '@servicetitan/react-ioc';
|
|
468
|
+
import { EVENT_BUS_TOKEN, typedEventBusToken } from '@servicetitan/web-components';
|
|
469
|
+
|
|
470
|
+
interface CustomEvents {
|
|
471
|
+
'example:click': undefined;
|
|
472
|
+
'example:input': string;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const CUSTOM_EVENT_BUS_TOKEN = typedEventBusToken<CustomEvents>();
|
|
476
|
+
|
|
477
|
+
function Test() {
|
|
478
|
+
// eventBus is not typed: emit() and on() allow any events and data
|
|
479
|
+
const [eventBus] = useDependencies(EVENT_BUS_TOKEN);
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* customEventBus is typed: emit() and on() allow only keys of CustomEvents and matching data
|
|
483
|
+
* Using CUSTOM_EVENT_BUS_TOKEN is equivalent to:
|
|
484
|
+
* useDependencies(EVENT_BUS_TOKEN as SymbolToken<CustomEventBus>)
|
|
485
|
+
*/
|
|
486
|
+
const [customEventBus] = useDependencies(CUSTOM_EVENT_BUS_TOKEN);
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### symbolToken
|
|
491
|
+
|
|
492
|
+
`symbolToken` creates a JavaScript symbol that is tightly associated with a type.
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
function symbolToken<T>(name: string): symbol;
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
It allows the EventBus APIs to infer from events the types of their data and results.
|
|
499
|
+
|
|
500
|
+
#### Job events
|
|
501
|
+
|
|
502
|
+
For example, to define the result of a job event:
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
import { symbolToken } from '@servicetitan/react-ioc';
|
|
506
|
+
import { EventBus } from '@servicetitan/web-components';
|
|
507
|
+
|
|
508
|
+
// Create JOB_EVENT symbol with type { data: string }
|
|
509
|
+
const JOB_EVENT = symbolToken<{ data: string }>('JOB_EVENT');
|
|
510
|
+
|
|
511
|
+
async function test(eventBus: EventBus) {
|
|
512
|
+
// And then job.emit(JOB_EVENT) infers the return type is { data: string }
|
|
513
|
+
const result = await eventBus.job.emit(JOB_EVENT);
|
|
514
|
+
}
|
|
515
|
+
```
|
|
@@ -1,271 +1,230 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Web Components
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
#### [CHANGELOG (@servicetitan/web-components)](https://github.com/servicetitan/uikit/blob/master/packages/web-components/CHANGELOG.md)
|
|
6
|
-
|
|
7
|
-
`@servicetitan/web-components` is used to build, register, and load Microfrontends (MFEs) into host applications.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
The `Loader` component is used to load an MFE into a host application. See [Microfrontends (MFEs)](/docs/frontend/micro-frontends) for more information about how MFEs are injected.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```tsx
|
|
16
|
-
...
|
|
17
|
-
|
|
18
|
-
import { Loader } from '@servicetitan/web-components';
|
|
19
|
-
|
|
20
|
-
...
|
|
21
|
-
|
|
22
|
-
export const Foo: React.FC = () => {
|
|
23
|
-
...
|
|
24
|
-
return (
|
|
25
|
-
...
|
|
26
|
-
<Loader src="https://unpkg.servicetitan.com/{package_name}@{semver_range}" />
|
|
27
|
-
...
|
|
28
|
-
);
|
|
29
|
-
};
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
| Name | Description |
|
|
35
|
-
| :----------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
-
| `src` | path to an MFE root folder |
|
|
37
|
-
| `fallbackSrc` | optional alternative path to an MFE root folder (if request for src returns error) |
|
|
38
|
-
| `data` | additional data passed to an MFE as an object, where values are serializable, and preferably memoized ([see below](#passing-data-from-host-to-mfe)) |
|
|
39
|
-
| `basename` | prefix for all MFE URLs, you should inject it by the `BASENAME_TOKEN` and pass to the appropriate router property |
|
|
40
|
-
| `loadingFallback` | optional ReactNode to render when the MFE is loading |
|
|
41
|
-
| `loadingFallbackDelayed` | controls whether to render the loading fallback immediately, or after a short delay [(see below)](#fallback-delay) |
|
|
42
|
-
| `errorFallback` | optional ReactNode to render when the MFE fails to load |
|
|
43
|
-
| `className` | additional CSS classes for the MFE web component element |
|
|
44
|
-
| `cache` | optional cache strategy for the MFE [(see below)](#cache-strategy) |
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
To prevent the loading fallback from flickering on then off when an MFE loads quickly, `Loader` does not render the fallback immediately. It waits a moment (currently 200ms) then renders the fallback only if the MFE hasn't finished loading. Set `loadingFallbackDelayed` to `false` to always render the fallback, regardless how quickly the MFE loads.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
When an MFE is loaded, `Loader` fetches the version and bundle data from the `src` (or `fallbackSrc`) endpoint and stores the information in a cache. Then, if the MFE is unloaded and subsequently reloaded, it reuses the cached information. Use `cache` to control whether or how long `Loader` caches an MFE's data:
|
|
53
|
-
|
|
54
|
-
- `true`: cache the data for the standard interval (currently 15 minutes). This is the default value.
|
|
55
|
-
- `false`: do not cache the data; (re)fetch it every time the MFE is loaded
|
|
56
|
-
- `-1`: cache the data indefinitely (i.e., for the duration of the user's session)
|
|
57
|
-
- _<number>_: cache the data for the specified _<number>_ of milliseconds
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Use `data` to pass data from your host application to the MFE. The data should be an object where keys are strings and the values are any serializable data.
|
|
62
|
-
|
|
63
|
-
To avoid performance issues with re-rendering and re-serializing the value, we recommend memoizing the `data` object passed to `<Loader>`.
|
|
64
|
-
|
|
65
|
-
**Reminder:** Don't casually share data between the host and MFEs. When possible, MFEs should load the data they need instead of relying on the host to pass it.
|
|
66
|
-
|
|
67
|
-
:::note
|
|
68
|
-
Before version v22.12.0 of `@servicetitan/web-components` package, the `data` prop only supported data of type `Record<string, string>`
|
|
69
|
-
:::
|
|
70
|
-
|
|
71
|
-
```tsx title="Host Application"
|
|
72
|
-
import { useMemo } from 'react';
|
|
73
|
-
import { Loader } from '@servicetitan/web-components';
|
|
74
|
-
...
|
|
75
|
-
export const HostApp = () => {
|
|
76
|
-
...
|
|
77
|
-
const data = useMemo(() => {
|
|
78
|
-
foo: ['one', 'two', 'three'],
|
|
79
|
-
bar: dependency,
|
|
80
|
-
}, [dependency]);
|
|
81
|
-
return (
|
|
82
|
-
...
|
|
83
|
-
<Loader
|
|
84
|
-
src="https://unpkg.servicetitan.com/{package_name}@{semver_range}"
|
|
85
|
-
data={data}
|
|
86
|
-
/>
|
|
87
|
-
...
|
|
88
|
-
);
|
|
89
|
-
};
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Data can be accessed within the MFE using props.
|
|
93
|
-
|
|
94
|
-
```tsx title="MFE Application"
|
|
95
|
-
interface MFEAppData {
|
|
96
|
-
foo: string[];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export const MFEApp = (props: MFEAppData) => {
|
|
100
|
-
const { foo } = props;
|
|
101
|
-
};
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Data must be serializable, due to the nature of how it is passed through a web component to reach your MFE component. This means that you cannot pass functions or other non-serializable data types.
|
|
105
|
-
|
|
106
|
-
```tsx title="Host Application"
|
|
107
|
-
...
|
|
108
|
-
<Loader data={{ date: new Date("2020-05-12T23:50:21.817Z") }} />
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
```tsx title="MFE Application"
|
|
112
|
-
...
|
|
113
|
-
export const MFEApp = (props: MyType) => {
|
|
114
|
-
props.date; // '2020-05-12T23:50:21.817Z'
|
|
115
|
-
};
|
|
116
|
-
// RESULTS IN A STRING, NOT A DATE OBJECT
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
As of version v22.12.0 of `@servicetitan/web-components`, if data passed into the `data` prop of `<Loader>` changes during runtime, the data will re-render within your MFE component where appropriate. Previous versions would re-mounting the entire MFE application when data changed.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Data can be also be accessed within the MFE using the `useMFEDataContext` hook. You must provide the type of the data you expect to
|
|
124
|
-
|
|
125
|
-
```tsx
|
|
126
|
-
import { useMFEDataContext } from '@servicetitan/web-components';
|
|
127
|
-
...
|
|
128
|
-
interface MFEAppData {
|
|
129
|
-
bar: string;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export const MFEApp = () => {
|
|
133
|
-
const { bar } = useMFEDataContext<MFEAppData>();
|
|
134
|
-
};
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
Some metadata is also provided to MFEs, and is accessible through the `useMFEMetadataContext` hook.
|
|
140
|
-
|
|
141
|
-
```tsx
|
|
142
|
-
import { useMFEMetadataContext } from '@servicetitan/web-components';
|
|
143
|
-
...
|
|
144
|
-
export const MFEApp = () => {
|
|
145
|
-
const { shadowRoot, portalShadowRoot } = useMFEMetadataContext();
|
|
146
|
-
};
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
The following metadata about an MFE is available from `useMFEMetadataContext` hook.
|
|
150
|
-
|
|
151
|
-
| Name | Type | Description |
|
|
152
|
-
| :----------------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------ |
|
|
153
|
-
| `shadowRoot` | ShadowRoot | ShadowRoot root node of which the MFE is rendered within |
|
|
154
|
-
| `portalShadowRoot` | ShadowRoot | ShadowRoot root node of the "portal" tied to the MFE, which is used to render elements in a div directly under the body element |
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
`getValueForEnvironment` detects the
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
|
175
|
-
|
|
|
176
|
-
|
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return () => {
|
|
233
|
-
eventBus?.off('my-custom-event', myCustomEventHandler);
|
|
234
|
-
};
|
|
235
|
-
}, []);
|
|
236
|
-
|
|
237
|
-
...
|
|
238
|
-
};
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
#### Emitting
|
|
242
|
-
|
|
243
|
-
```tsx title="host"
|
|
244
|
-
...
|
|
245
|
-
|
|
246
|
-
import { Loader, LoaderRef } from '@servicetitan/web-components';
|
|
247
|
-
|
|
248
|
-
...
|
|
249
|
-
|
|
250
|
-
export const Bar: FC = () => {
|
|
251
|
-
const ref = useRef<LoaderRef>(null);
|
|
252
|
-
|
|
253
|
-
useEffect(() => {
|
|
254
|
-
const id = setInterval(() => {
|
|
255
|
-
ref.current?.emit('my-custom-event');
|
|
256
|
-
}, 1000);
|
|
257
|
-
|
|
258
|
-
return () => {
|
|
259
|
-
clearInterval(id);
|
|
260
|
-
};
|
|
261
|
-
}, []);
|
|
262
|
-
|
|
263
|
-
...
|
|
264
|
-
|
|
265
|
-
return (
|
|
266
|
-
...
|
|
267
|
-
<Loader ref={ref} src="https://unpkg.servicetitan.com/@servicetitan/examples" />
|
|
268
|
-
...
|
|
269
|
-
);
|
|
270
|
-
};
|
|
271
|
-
```
|
|
1
|
+
---
|
|
2
|
+
title: Web Components
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
#### [CHANGELOG (@servicetitan/web-components)](https://github.com/servicetitan/uikit/blob/master/packages/web-components/CHANGELOG.md)
|
|
6
|
+
|
|
7
|
+
`@servicetitan/web-components` is used to build, register, and load Microfrontends (MFEs) into host applications.
|
|
8
|
+
|
|
9
|
+
## Loader
|
|
10
|
+
|
|
11
|
+
The `Loader` component is used to load an MFE into a host application. See [Microfrontends (MFEs)](/docs/frontend/micro-frontends) for more information about how MFEs are injected.
|
|
12
|
+
|
|
13
|
+
### Usage
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
import { Loader } from '@servicetitan/web-components';
|
|
19
|
+
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
export const Foo: React.FC = () => {
|
|
23
|
+
...
|
|
24
|
+
return (
|
|
25
|
+
...
|
|
26
|
+
<Loader src="https://unpkg.servicetitan.com/{package_name}@{semver_range}" />
|
|
27
|
+
...
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Props
|
|
33
|
+
|
|
34
|
+
| Name | Description |
|
|
35
|
+
| :----------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| `src` | path to an MFE root folder |
|
|
37
|
+
| `fallbackSrc` | optional alternative path to an MFE root folder (if request for src returns error) |
|
|
38
|
+
| `data` | additional data passed to an MFE as an object, where values are serializable, and preferably memoized ([see below](#passing-data-from-host-to-mfe)) |
|
|
39
|
+
| `basename` | prefix for all MFE URLs, you should inject it by the `BASENAME_TOKEN` and pass to the appropriate router property |
|
|
40
|
+
| `loadingFallback` | optional ReactNode to render when the MFE is loading |
|
|
41
|
+
| `loadingFallbackDelayed` | controls whether to render the loading fallback immediately, or after a short delay [(see below)](#fallback-delay) |
|
|
42
|
+
| `errorFallback` | optional ReactNode to render when the MFE fails to load |
|
|
43
|
+
| `className` | additional CSS classes for the MFE web component element |
|
|
44
|
+
| `cache` | optional cache strategy for the MFE [(see below)](#cache-strategy) |
|
|
45
|
+
|
|
46
|
+
### Loading fallback delay {#fallback-delay}
|
|
47
|
+
|
|
48
|
+
To prevent the loading fallback from flickering on then off when an MFE loads quickly, `Loader` does not render the fallback immediately. It waits a moment (currently 200ms) then renders the fallback only if the MFE hasn't finished loading. Set `loadingFallbackDelayed` to `false` to always render the fallback, regardless how quickly the MFE loads.
|
|
49
|
+
|
|
50
|
+
### Cache strategy {#cache-strategy}
|
|
51
|
+
|
|
52
|
+
When an MFE is loaded, `Loader` fetches the version and bundle data from the `src` (or `fallbackSrc`) endpoint and stores the information in a cache. Then, if the MFE is unloaded and subsequently reloaded, it reuses the cached information. Use `cache` to control whether or how long `Loader` caches an MFE's data:
|
|
53
|
+
|
|
54
|
+
- `true`: cache the data for the standard interval (currently 15 minutes). This is the default value.
|
|
55
|
+
- `false`: do not cache the data; (re)fetch it every time the MFE is loaded
|
|
56
|
+
- `-1`: cache the data indefinitely (i.e., for the duration of the user's session)
|
|
57
|
+
- _<number>_: cache the data for the specified _<number>_ of milliseconds
|
|
58
|
+
|
|
59
|
+
### Passing data from Host to MFE
|
|
60
|
+
|
|
61
|
+
Use `data` to pass data from your host application to the MFE. The data should be an object where keys are strings and the values are any serializable data.
|
|
62
|
+
|
|
63
|
+
To avoid performance issues with re-rendering and re-serializing the value, we recommend memoizing the `data` object passed to `<Loader>`.
|
|
64
|
+
|
|
65
|
+
**Reminder:** Don't casually share data between the host and MFEs. When possible, MFEs should load the data they need instead of relying on the host to pass it.
|
|
66
|
+
|
|
67
|
+
:::note
|
|
68
|
+
Before version v22.12.0 of `@servicetitan/web-components` package, the `data` prop only supported data of type `Record<string, string>`
|
|
69
|
+
:::
|
|
70
|
+
|
|
71
|
+
```tsx title="Host Application"
|
|
72
|
+
import { useMemo } from 'react';
|
|
73
|
+
import { Loader } from '@servicetitan/web-components';
|
|
74
|
+
...
|
|
75
|
+
export const HostApp = () => {
|
|
76
|
+
...
|
|
77
|
+
const data = useMemo(() => {
|
|
78
|
+
foo: ['one', 'two', 'three'],
|
|
79
|
+
bar: dependency,
|
|
80
|
+
}, [dependency]);
|
|
81
|
+
return (
|
|
82
|
+
...
|
|
83
|
+
<Loader
|
|
84
|
+
src="https://unpkg.servicetitan.com/{package_name}@{semver_range}"
|
|
85
|
+
data={data}
|
|
86
|
+
/>
|
|
87
|
+
...
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Data can be accessed within the MFE using props.
|
|
93
|
+
|
|
94
|
+
```tsx title="MFE Application"
|
|
95
|
+
interface MFEAppData {
|
|
96
|
+
foo: string[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const MFEApp = (props: MFEAppData) => {
|
|
100
|
+
const { foo } = props;
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Data must be serializable, due to the nature of how it is passed through a web component to reach your MFE component. This means that you cannot pass functions or other non-serializable data types.
|
|
105
|
+
|
|
106
|
+
```tsx title="Host Application"
|
|
107
|
+
...
|
|
108
|
+
<Loader data={{ date: new Date("2020-05-12T23:50:21.817Z") }} />
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```tsx title="MFE Application"
|
|
112
|
+
...
|
|
113
|
+
export const MFEApp = (props: MyType) => {
|
|
114
|
+
props.date; // '2020-05-12T23:50:21.817Z'
|
|
115
|
+
};
|
|
116
|
+
// RESULTS IN A STRING, NOT A DATE OBJECT
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
As of version v22.12.0 of `@servicetitan/web-components`, if data passed into the `data` prop of `<Loader>` changes during runtime, the data will re-render within your MFE component where appropriate. Previous versions would re-mounting the entire MFE application when data changed.
|
|
120
|
+
|
|
121
|
+
## useMFEDataContext
|
|
122
|
+
|
|
123
|
+
Data can be also be accessed within the MFE using the `useMFEDataContext` hook. You must provide the type of the data you expect to receive.
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import { useMFEDataContext } from '@servicetitan/web-components';
|
|
127
|
+
...
|
|
128
|
+
interface MFEAppData {
|
|
129
|
+
bar: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const MFEApp = () => {
|
|
133
|
+
const { bar } = useMFEDataContext<MFEAppData>();
|
|
134
|
+
};
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## useMFEMetadataContext
|
|
138
|
+
|
|
139
|
+
Some metadata is also provided to MFEs, and is accessible through the `useMFEMetadataContext` hook.
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
import { useMFEMetadataContext } from '@servicetitan/web-components';
|
|
143
|
+
...
|
|
144
|
+
export const MFEApp = () => {
|
|
145
|
+
const { shadowRoot, portalShadowRoot } = useMFEMetadataContext();
|
|
146
|
+
};
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The following metadata about an MFE is available from `useMFEMetadataContext` hook.
|
|
150
|
+
|
|
151
|
+
| Name | Type | Description |
|
|
152
|
+
| :----------------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------ |
|
|
153
|
+
| `shadowRoot` | ShadowRoot | ShadowRoot root node of which the MFE is rendered within |
|
|
154
|
+
| `portalShadowRoot` | ShadowRoot | ShadowRoot root node of the "portal" tied to the MFE, which is used to render elements in a div directly under the body element |
|
|
155
|
+
|
|
156
|
+
## getValueForEnvironment
|
|
157
|
+
|
|
158
|
+
`getValueForEnvironment` detects the host environment and returns a corresponding value.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
function getValueForEnvironment(
|
|
162
|
+
values: Record<string, string>,
|
|
163
|
+
defaultEnvironment: keyof typeof values = 'qa',
|
|
164
|
+
hostname: string = window.location.hostname
|
|
165
|
+
);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
:::caution
|
|
169
|
+
When no value is provided for the detected environment, `getValueForEnvironment` returns `undefined`.
|
|
170
|
+
:::
|
|
171
|
+
|
|
172
|
+
### Parameters
|
|
173
|
+
|
|
174
|
+
| Name | Type | Description |
|
|
175
|
+
| :------------------- | :----------------------- | :------------------------------------------------------------------------------------------------------------------------ |
|
|
176
|
+
| `values` | `Record<string, string>` | Object that maps each environment to a value (see below). |
|
|
177
|
+
| `defaultEnvironment` | `keyof typeof values` | The environment to use when the current environment is not recognized or is not included in `values`. Defaults to `"qa"`. |
|
|
178
|
+
| `hostname` | `string` | The hostname of the current environment. Defaults to `window.location.hostname` |
|
|
179
|
+
|
|
180
|
+
The recognized environments are:
|
|
181
|
+
|
|
182
|
+
| Environment | Description |
|
|
183
|
+
| :---------------- | :--------------------------------------------------------------------------- |
|
|
184
|
+
| **dev** | Development environment (i.e., `localhost`, `127.0.0.1`) |
|
|
185
|
+
| **go** | Production environment |
|
|
186
|
+
| **qa** | QA environment |
|
|
187
|
+
| **next** | Next environment |
|
|
188
|
+
| **stage** | Staging environment |
|
|
189
|
+
| **test** | Unit test environment (i.e., `process.env.NODE_ENV === 'test'`) |
|
|
190
|
+
| `<string>` | Custom environment that matches against hostname `<string>.servicetitan.com` |
|
|
191
|
+
| `<string>.st.dev` | Custom environment that matches against hostname `<string>.st.dev` |
|
|
192
|
+
|
|
193
|
+
### Custom environments
|
|
194
|
+
|
|
195
|
+
Custom environments are for applications and services that run separately from the Monolith.
|
|
196
|
+
For example, to associate **prod** with `my-service.servicetitan.com` and **stage** with `my-service-stage.st.dev`:
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"my-service": "prod", // Matches my-service.servicetitan.com
|
|
201
|
+
"stage.st.dev": "stage" // Matches *stage.st.dev
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Examples
|
|
206
|
+
|
|
207
|
+
Determine LaunchDarkly client-side ID for TitanAdvisor project.
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
function useTitanAdvisorClientSideID() {
|
|
211
|
+
// Environment won't change and is safe to memoize
|
|
212
|
+
return useMemo(() => {
|
|
213
|
+
return getValueForEnvironment({
|
|
214
|
+
dev: '669fe6e5cc97eb103be7a620',
|
|
215
|
+
go: '669fe5bff00a0c0fa60b25e4',
|
|
216
|
+
next: '669fe5bff00a0c0fa60b25e4', // same a 'go' but could also be 'qa'
|
|
217
|
+
qa: '669fe6d6f00a0c0fa60b2721',
|
|
218
|
+
stage: '669fe65979abf310b860236e',
|
|
219
|
+
});
|
|
220
|
+
}, []);
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
:::info
|
|
225
|
+
Note that because the environment is fixed for the life the application, it is safe to memoize the return value.
|
|
226
|
+
:::
|
|
227
|
+
|
|
228
|
+
:::caution
|
|
229
|
+
Do not call `getValueForEnvironment` globally, outside a React component or hook. That usage assumes that all the information needed to determine the environment is available immediately when the Javascript runtime loads and before the application is initialized. While that might work, it might also change in the future.
|
|
230
|
+
:::
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@servicetitan/docs-uikit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "30.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,5 +16,5 @@
|
|
|
16
16
|
"cli": {
|
|
17
17
|
"webpack": false
|
|
18
18
|
},
|
|
19
|
-
"gitHead": "
|
|
19
|
+
"gitHead": "ebfab3b30337bab2d2c7b1bf6a1caeedc262c884"
|
|
20
20
|
}
|