diff --git a/docs/001_develop/03_client-capabilities/015_layout-management/10_foundation-layout.mdx b/docs/001_develop/03_client-capabilities/015_layout-management/10_foundation-layout.mdx new file mode 100644 index 0000000000..ec307f4551 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/015_layout-management/10_foundation-layout.mdx @@ -0,0 +1,1294 @@ +--- +title: 'Foundation Layout' +sidebar_label: 'Foundation Layout' +id: foundation-layout +keywords: [web, layout, foundation layout, frontend, ui, golden layout] +tags: + - web + - layout + - foundation layout + - frontend + - ui + - golden layout +--- + +# Genesis Foundation UI App Layout + +[![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/) +[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](https://www.typescriptlang.org/) + +## Declarative HTML API + +The following example shows the usage of the declarative API with `zero-charts` and the output that it produces. +```html + + + + x.barConfig} + :data=${(x) => x.barData} + > + + + + x.stockConfiguration} + :data=${(x) => x.stockData} + > + + + x.roseConfig} + :data=${(x) => x.roseData} + :legendParser=${(x) => x.roseLegendParser()} + > + + + + +``` + +![Example output of the declarative API with the zero charts](./docs/img/foundation-layout-example.png) + +1. Register the layout with the design system. This is probably in a file called **components.ts**, located where you call `.register()` on the design system. + +```javascript +FoundationDesignSystem: registerFoundationDesignSystem().register( + // ...Other registrations + // add foundationLayoutComponents here + foundationLayoutComponents, +) +``` + +- This registers the three custom elements for use in your application. The components will use the prefix of the design system as their prefix. For example, the root component will be `` in the `Foundation` design system, and `` in the `Zero (alpha)` design system. + +:::info +For the rest of this document, the components will be referred to with the `foundation-` prefix. +::: + +### Custom styling + +The required class, template, and base styles are exported as part of the package, allowing a client to customise the styling of the layout system via design system extensions. + +:::tip +All the customisable styles of the layout system are contained within the styles for [``](./docs/api/foundation-layout.foundationlayout.md). So if you want to customise the styles in addition to setting the css variables, you only need to set the styles here. +::: + +### [Top Level Component ``](./docs/api/foundation-layout.foundationlayout.md) + +Top level web component, which is used to initialise a custom layout + +- **reload-buffer** : numerical attribute that controls the buffer between how long the layout is reloaded. The default +is 500ms. In this case, the layout is only reloaded if the child elements of the layout region are manipulated +once every 500ms. This is to stop the layout being reloaded over and over for every single item during initialisation. +The higher the value is, the more performant the component is - but the first load will appear to take longer. +- **auto-save-key** : optional string which if set, will enable autosaving the layout under this key in local +storage. See [here](#autosaving-layout) for more. + +:::tip +This only applies for usage with the declarative HTML API. When the layout first loads after this amount of time, +it emits an [event](#events). +::: + +### [Layout Regions](./docs/api/foundation-layout.foundationlayoutregion.md) + +If you don't specify the `type` of the layout region, it defaults to `type="horizontal"`; + +- **type**: `vertical`, `horizontal`, `tabs` (default `horizontal`). +- **size**: optional string parameter defining size, [see here](#sizing). + +#### `` + +Indicates to the layout system that all immediate children are (by default) to be split equally within the available space of this +component using n-1 column split(s). Can be nested within other horizontal and vertical regions. + +#### `` + +Indicates to the layout system that all immediate children are (by default) to be split equally among the available space of this +component using n-1 row split(s). Can be nested within other horizontal and vertical regions. + +#### `` + +Indicates to the layout system that all immediate children are to be added as tabs in the available space of this component, +with a tab for each child. The tabs are ordered according to which child the layout item is (e.g. the second `` + of the tab split is the second tab). The first child will be the one that is open by default. Can be nested within horizontal + and vertical regions, but cannot have more layout sections inside it. + +### [Layout Item ``](./docs/api/foundation-layout.foundationlayoutitem.md) + +Wrapper component that lives inside a layout section and wraps the client content. All content must be inside a layout item; +otherwise, a runtime error will be thrown when the layout attempts to render itself on screen. + +- **title**: string defining the title of the pane that contains the content. Defaults to `Item x`, where `x` is the pane number. +- **closable**: boolean defining whether this element is closable - Default false. +- **size**: optional string parameter defining size, [see here](#sizing). +- **registration**: optional string, which manually sets the registered name for the pane - [see here](#dynamic-registration-and-adding-items). By default, each item that doesn't have the `registration` attribute set will be a string registered sequentially starting at `"1"`. + +### Sizing + +The layout sections and layout item all have an _optional_ attribute: + +- **size**: string defining the size. For rows, it specifies height. For columns, it specifies width. Has format ``. + Currently only supports units `fr` and `%`. Space is first proportionally allocated to items with sizeUnit `%`. If there is any space + left over (less than 100% allocated), then the remainder is allocated to the items with unit `fr` according to the fractional size. + If more than 100% is allocated, then an extra 50% is allocated to items with unit `fr` and is allocated to each item according to its + fractional size. All item sizes are then adjusted to bring the total back to 100%. + +:::info +The size defines the size of the component _compared_ to the siblings _within_ the context of the component's parent. +::: + +## JavaScript API + +The JavaScript API is [accessed through the methods on the root layout object](./docs/api/foundation-layout.foundationlayout.md) and allows for saving/loading the layout state, and dynamically adding items to the layout at runtime. + +### Dynamic registration and adding items + +To have a pane displayed on the layout system, it must be *registered* with the layout system. When using the [declarative API](#declarative-html-api), the layout system takes care of this for you, but as you start to add items dynamically and then serialise the layout, you need to consider which panes are registered. See [this contained example](#contained-example), which allows the user to add pre-determined items to the layout dynamically. + +:::tip +If you are only using the declarative API, and not using any dynamic integrations with JavaScript, then you shouldn't need to set the registration names of any items, as all the same items will be registered when you load a previously saved layout. If you are dynamically adding items as well, it is highly recommended to set the registration names of items manually. This makes it easier to figure out what is and is not registered. +* When using the declarative API, use the `registration` attribute on the `` component. +* When using the JavaScript API, set the `registration` optional parameter on the [registered element config](./docs/api/foundation-layout.registeredelementconfig.md). +::: + +#### [Register Item](./docs/api/foundation-layout.foundationlayout.registeritem.md) + +This API enables you to register an item at runtime, but it will not be displayed in the layout. This could be used to register components in anticipation of displaying them when loading a serialised layout - [see this example](#loading-serialised-layouts). + +#### [Add Item](./docs/api/foundation-layout.foundationlayout.additem.md) + +Add an item or items that have previously been registered with the layout. + +#### [Remove Items](./docs/api/foundation-layout.foundationlayout.removeitems.md) + +Dynamically remove items from the layout. See linked API for side effects and options. + +#### [Layout Required Registrations](./docs/api/foundation-layout.foundationlayout.layoutrequiredregistrations.md) + +Static function to read a layout config. It returns a list of all the required registrations required to load it in the layout system. [See this example](#loading-serialised-layouts). + +#### [Get Current Registrations](./docs/api/foundation-layout.foundationlayout.registereditems.md) + +Returns a list of all the items currently registered with the layout system. + +:::tip +Use this function over `.layoutRequiredRegistrations(layout: SerialisedLayout)` to get the *current* registrations, because that will miss any items that are currently registered with the layout system, but which are not shown on the layout. +::: + +### Serialising layout + +The JavaScript API can be used to save and load layout states manually. This only describes the state of the dynamic layout itself. It is the responsibility of each component within the layout to serialise its own state, if required. +To enable autosaving the layout, see [here](#autosaving-layout). + +#### [Get Layout](./docs/api/foundation-layout.foundationlayout.getlayout.md) + +Get an object describing the current layout so that it can be restored at a later date. This does not save any data internally to the layout. It is up to the client to store this state where appropriate for later recall (browser local storage, persistence layer, etc.). Use the [autosaving layout](#autosaving-layout) feature to get the layout to do this for you with local storage. + +You can store state for an instance of an item, and that will be saved inline. See [managing state](#managing-the-state). + +#### [Load Layout](./docs/api/foundation-layout.foundationlayout.loadlayout.md) + +Loads a serialised layout. All items that are described in the config to load must already be registered with the layout system - using either the declarative or JavaScript API. If there are items missing (could be due either to missing items or to a mismatch of registered names) then a `LayoutUsageError` will be thrown containing the names of the missing items. Alternatively, you can request placeholder items to be added. + +## Events + +### [Emitted Events](./docs/api/foundation-layout.layoutemitevents.md) + +Certain actions that are performed by the user interacting with the layout emit events. See the API document (in the link above) for the events and when they're emitted. Interacting with these events allows your client code to interact dynamically with the layout, such as enabling/disabling buttons to add items to the layout when they're removed/added. + +### [Received Events](./docs/api/foundation-layout.layoutreceiveevents.md) + +Certain events are listened to by the container for each component, enabling the component to interact with the layout. For example, a component could emit an event to change the title of the containing window: + +```typescript +this.$emit(eventType, eventDetail) +``` +Each event requires a certain detail to process the event - see [the map of events to their required details](./docs/api/foundation-layout.layoutreceiveeventsdetail.md). + +## Customising header buttons + +You can add custom buttons on layout items, and then control their behaviour. See [the custom button API](./docs/api/foundation-layout.custombutton.md) for the full definition. Setting this is optional. If you do define it, you must define it as an array, which enables you to add multiple custom buttons. + +* The `svg` parameter controls the icon that is displayed for your button. The format must be a base64 image definition. See the format (as explained in the linked api document above), and then replace the text around the `<< >>` part with a base64 encoded definition of the svg you wish to use. +* The `onClick` parameter will register a callback with the button. When the user clicks the button, your callback will be called. The callback receives a reference to the clicked button element, and to the element that is contained in the layout item associated with the clicked button. + +Different layout instances can have their own custom buttons, or they can share definitions. You are not able to have fine-grained control over each layout item, though; so if a layout has a custom button, then every item that it contains will have the button. + +### Applying the custom button + +To ensure that every item gets the button as expected, you need to ensure that you apply the custom button definitions as early as possible. If you are using the html API then you'll probably want to apply the definitions in the template. + +```html + buttonDefinition}> + ... + +``` + +If you are only using the javascript API then you should just apply the property as soon as you can. +```typescript +layout.customButtons = buttonDefinition; +``` + +### Renaming example + +See this example of [creating a custom button](#custom-item-renaming-header-button), which enables the user to rename an item. + +## Autosaving layout + +You can set the layout to autosave in local storage as the user interacts with it. To do this, set the `auto-save-key` attribute to a unique string on the root element; the layout will be saved in this key. The layout will be saved for later recall in local storage whenever the user performs the following actions: + +- adding an item +- removing an item +- resizing items using the divider +- dragging items around the layout + +When you have enabled autosave, you are still able to use the manual [serialising commands](#serialising-layout). + +### Reloading the layout + +The function [tryLoadLayoutFromLocalStorage()](./docs/api/foundation-layout.foundationlayout.tryloadlayoutfromlocalstorage.md) is used to rehydrate the layout from local storage, when `auto-save-key` is enabled. +If you are using the declarative API, then this function is called for you automatically. + +If you are manually registering items (too) using the JavaScript API, you must [call this function manually](#contained-example) immediately after you have finished registering all the items. + +### Layout placeholder + +If the layout is auto-loaded with items that are missing from the registration, then a placeholder item is displayed instead. Additionally, the close option is added to the pane. This accounts for you removing an item from a layout that a user has autosaved in their config. + +You can change the text of the placeholder using the observable binding `:missingItemPlaceholder`. This is a function that takes a string (the missing registration name) and returns the string to use as the placeholder. A default is set, but you can override it. See the override implementation [in this example](#contained-example). + +### Invalidating the cache + +As explained in the previous section, a placeholder item is added if an item is no longer registered for the auto-loaded layout. This accounts for removing an item. However, there is the reverse issue if you are only using the declarative API; if you add a new item and the user already has an autosaved layout, then that will be loaded - which effectively hides the new item you've added. + +In this case, you must invalidate the autosaved layout cache. The cleanest and easiest implementation is to add a hash onto the end of your `auto-save-key`, which will start a new autosave for this table (and reload the default, containing your new layout item). + +## Contained elements + +This section concerns the behaviour of elements inside the layout. If you are using simple elements or Genesis-supplied elements, this is less of a concern; but if you are building complex custom components yourself, you need this information. + +### Element lifecycle (gating) + +Some actions that the user can perform with items in the layout will run the component lifecycle functions (`connectedCallback` and `disconnectedCallback`) when you don't want them to run: +- when an item is dragged around the layout +- potentially, when another item is removed from the layout +- potentially, when new items are added to the layout +- when an item is maximised or minimised + +For example, if you have a component with a loaded resource on the layout (such as a grid with a `grid-pro-genesis-datasource`) and you add a new item to the layout with the JavaScript API, then the component with the loaded resource will have to reload too. It is important that any such element accounts for this, including such requirements as caching data, or resizing correctly. + +In the `@genesislcap/foundation-utils` package, there is a mix-in class `LifecycleMixin` which exposes two protected members: + +- `shouldRunConnect` +- `shouldRunDisconnect` + +These can be used to gate specific functionality. + +For example, if there are parts of `disconnectedCallback` that you don't want to run when the item is being dragged around the layout, you can gate it behind a `(!this.shouldRunDisconnect) return;` early return. See [this example](#resource-intensive-component-resetting-in-layout) and [this example](#consuming-lifecycle-value-multiple-times). + +:::warning +At the very least, you must run `super` calls to the lifecycle methods, or else your custom element will not work correctly. +::: + +### Resource-intensive components +You do not need to de-register a component that is registered in the layout while it is not in use. However, if you have a component that is extremely resource-intensive, then you can use this lifecycle control method to ensure that it only consumes resources when it is in use. + +- When the element is part of the layout registry, then `shouldRunConnect` will be false and you can use this to ensure that your component isn't doing unnecessary work while part of the cache. + +- Once the component is actually initialised in the layout on the DOM, then `shouldRunConnect` will be true, and you can then perform all the required initialisation. + +### Managing the state + +Items inside the layout can save and restore the state using various methods, but it can become difficult to manage the state if you're adding the same item to the layout multiple times (multiple instances of the same web component). + +You can implement the [LayoutComponentWithState](./docs/api/foundation-layout.layoutcomponentwithstate.md) interface, which enables you to save and load the state *per instance* of your components. See the linked interface and the associated functions API documentation for examples and explanations of usage. + +Usage of this interface is optional; if you do not need to manage the state for your components in this way, then simply do not implement the interface. + +:::warning +The layout system only interacts with the immediately contained items - so if you have components that contain other components, each top-level component must interact with the contained components to manage their states. +::: + +:::danger +Each layout item can contain multiple components, and most of the time there are no extra considerations when doing this. However, the state of each component in an instance is saved in order of the components on the DOM, so if the serialised state is manually changed to have the items out of order with their state, then the incorrect states will be passed into each item. This should not occur during defined behaviour, but is possible if the end-user is able to change the state passed into `loadLayout()` manually. +::: + +### Element cloning + +To enable you to add multiple items from the same `registration`, the layout system clones elements to add to the layout. +This is the case both when items are added with `.addItem()`, and when they are added using the declarative API. Under the hood, this uses the Node [cloneNode](https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode) api. +There are certain limitations to this function, especially when using custom elements with the shadow DOM. [See troubleshooting example](#binding-events-inline-in-the-declarative-api). + +:::tip +As a general rule, if you need to have elements with FAST bindings inside the layout, wrap them in custom elements. +::: + +If you are writing your own custom element that needs to work inside the layout, follow these steps. + +1. In the `@genesislcap/foundation-utils` package, there is a mix-in class `LifecycleMixin`, which overrides the `cloneNode` API. + +```typescript +// Make a call to `deepClone` and manually clone children +override cloneNode(deep?: boolean): Node { + const thisClone = this.deepClone(); + if (deep) { + Array.from(this.childNodes).forEach((child) => { + thisClone.appendChild(child.cloneNode(true)); + }); + } + return thisClone; +} + +// Create a new element of the same name and copy over attributes +deepClone(): Node { + const copy = document.createElement(this.tagName.toLowerCase()); + this.getAttributeNames().forEach((at) => copy.setAttribute(at, this.getAttribute(at))); + return copy; +} +``` + +2. You can then extend the cloning functionality for your specific requirements. For example, our charts component needs to copy over `config` and `data` parameters. + +```typescript +export class G2PlotChart extends LifecycleMixin(FoundationElement) { + ... + override deepClone(): Node { + const copy = super.deepClone() as G2PlotChart; + copy.config = structuredClone(this.config); + copy.data = structuredClone(this.data); + return copy; + } + ... +} +``` +Some items you'll probably want to copy over are `eventListeners` and other non-attribute configuration elements on your custom element. + +## Examples + +### Simple example + +Simple example with a vertical split and two items that will take up equal space. + +```html + + + + + + + + + + +``` + +Will be rendered as: + +``` ++-----------------------------------------------------+ +| | +| Component 1 Contents | +| | ++-----------------------------------------------------+ +| | +| Component 2 Contents | +| | ++-----------------------------------------------------+ +``` + +### Nested example + +A slightly more complicated example: + +```html + + + + + + + + + + + + + + + + + +``` + +Would render the following: + +``` ++-------------+---------------------------------------+ +| | | +| | Component 2 Contents | +| Component | | +| 1 +---------------------------------------+ +| Contents | | +| | Component 3 Contents | +| | | ++-------------+---------------------------------------+ +``` + +Component 1 has a Close button. By default, Component 1 would be 50% width and 2 and 3 would take up the other 50% width, but here we set `25%` +as the width of Component 1 layout item (width because it is the size in the context of a vertical split). + +### Multi-nested example + +If instead we had: + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +This would render the following: + +``` ++-------------+------------+-------------+------------+ +| | | | | +| | Comp 2 | Comp 3 | Comp 4 | +| Component | | | | +| 1 +------------+-------------+------------+ +| Contents |_5_|_6_| | +| | Component 5 Contents | +| | | ++-------------+---------------------------------------+ +``` +Component 1 has a **Close** button. Component 1 takes up 25% of the initial width. Components 2,3,4 take up a third of the _remaining_ width between them +(default behaviour) and 5 and 6 are tabbed. + +### `repeat` directive + +You can use [FAST template directives](https://www.fast.design/docs/fast-element/using-directives) such as `repeat` + +```javascript +interface Position { + symbol: string; +} + +class Commodities extends FASTElement { + positions: Position[] // Not @observable - see following section + + ... +} + +const template = html` + + + ${when(x => x.positions, html` + + + `)} + +`; +``` + +For an example where the `Commodities` object has three positions, you will see the following output: +``` ++-----------------------------------------------------+ +| Component 1 Contents | ++-----------------------------------------------------+ +| Component 2 Contents | ++-----------------------------------------------------+ +| Component 3 Contents | ++-----------------------------------------------------+ +``` + + +:::note +`` is just an example component; it doesn't exist within `foundation-ui`. +::: + +### `when` directive + +Using the `when` directive: + +```javascript +@customElement({ + name: 'my-element', + template, +}) +class Analytics extends FASTElement { + showIndexFunds = true; // not @observable +} + +var template = html` + + + + + + + + ${when(x => x.showIndexFunds, html` + + + + `)} + + + +`; +``` + +You would see both items rendered like this: +``` ++---------------------------------------------+ +| Stocks Chart | ++---------------------------------------------+ +| Index Chart | ++---------------------------------------------+ +``` + +If you had `showIndexFunds = false;` then only the `Stocks Chart` would be rendered. + +:::danger +Directives are for initialising the layout only and should *not* be used with changing `@observable` attributes, which would cause the +layout to reinitialise incorrectly - this will duplicate the panels. For example, you can use the `when` directive to conditionally render a pane during initialisation, but not to toggle whether to show/hide the pane afterwards. +See [this example](#observables-with-directives). +::: + +### Multiple instances + +Consider the following example: +```html +
+
+ + +

Item 1

+

Item 2

+
+
+
+
+ + +

Item 3

+

Item 4

+
+
+
+
+``` + +This describes the following layout: +``` ++---------------------------------------------+ +| Item 1 | ++---------------------------------------------+ +| Item 2 | ++---------------------------------------------+ + ++----------------------+----------------------+ +| | | +| Item 3 | Item 4 | +| | | ++----------------------+----------------------+ +``` +Here a grid region has been used to style two completely separate instances of the dynamic layout. Even though we have named each +`

` sequentially, the two layouts are completely separate and the default titles of each tab (Item 1 and Item 2 for both layouts), +will reflect this. You can configure each layout separately, and you cannot drag layout items from one layout into the other one. + +:::info +This is just an example; you could have more than two layouts on a page or style them with a different method to the grid. +::: + +### Adding items dynamically + +This is an example of using the JavaScript API to add items to the layout at runtime. Before reading this example, you should familiarise yourself with the [API Section](#javascript-api). + +Say you want the user to be able to choose between three different types of item that can be put onto the layout - a profile-management table, a pie chart, and a column chart. + +```typescript +// Can either create an element and initialise it completely using JavaScript +const profileManagement = document.createElement('profile-management'); +// Or could grab a reference to one you create via FAST markup +const pieChart = document.getElementById('pie-chart'); +// In idiomatic FAST we can have a reference using `ref` directive +// const colChart = this.columnChart; +``` + +We can then register these elements with the layout system. Registering it with the layout system removes it from its original location. +```typescript +// Using a duplicate registration name is a runtime error +this.layout.registerItem('profile', [profileManagement]); +this.layout.registerItem('pie', [pieChart]); +this.layout.registerItem('colChart', [this.columnChart]); +``` + +Finally, use the `addItem` API to add a pane onto the layout using a previously registered item. +```typescript +this.layout.addItem({ + registration: 'profile', + name: 'Profile Management', + closable: true, +}) +``` +Using `addItem` with a `registration` that has not been set is a runtime error. Remember `addItem` has an optional second parameter for setting the placement of the new pane. + +:::tip +Items registered using the declarative API use the same pool of registration names, so you can also use `addItem` to add them to the layout too. +::: + +#### Contained example + +This is a complete example of the above, omitting imports. + +```typescript +// template +export const template = html` +
+
+ x.addItem('1')}>Test 1 + x.addItem('2')}>Test 2 + x.addItem('3')}>Test 3 +
+
+ x.missingItemOverride()} + ${ref('containedExampleLayout')} + > +
+
+`; + +// class +@customElement({ + name: 'contained-example', + template, +}) +export class ContainedExample extends FASTElement { + containedExampleLayout: FoundationLayout; + private _addedPaneCount = 0; + + connectedCallback(): void { + super.connectedCallback(); + + const h1 = document.createElement('h1'); + h1.innerHTML = 'Example 1'; + const p1 = document.createElement('p'); + p1.innerHTML = 'Ex 1'; + + const h2 = document.createElement('h2'); + h2.innerHTML = 'Example 2'; + const p2 = document.createElement('p'); + p2.innerHTML = 'Ex 2'; + + const h3 = document.createElement('h3'); + h3.innerHTML = 'Example 3'; + const p3 = document.createElement('p'); + p3.innerHTML = 'Ex 3'; + + this.containedExampleLayout.registerItem('1', [h1, p1]); + this.containedExampleLayout.registerItem('2', [h2, p2]); + this.containedExampleLayout.registerItem('3', [h3, p3]); + this.containedExampleLayout.tryLoadLayoutFromLocalStorage(); + } + + addItem(registration: string) { + this.containedExampleLayout.addItem({ + registration, + title: `${registration} (${(this._addedPaneCount += 1)})`, + closable: true, + }); + } + + missingItemOverride = () => (missingItem: string) => `Missing Item: ${missingItem}`; +} +``` + +### Loading serialised layouts + +This is an elaborate example of using the JavaScript API with consideration of the registered names. Before reading this example, you should familiarise yourself with the [API Section](#javascript-api): + +```html + + + + + + + + + + +``` + +We can use `layoutRequiredRegistrations()` on the config returned from `getLayout()` to see the registered names that are required to load the layout. + +```javascript +const layout = document.querySelector('foundation-layout'); // as FoundatonLayout in TypeScript; +const layoutConfig = layout.getLayout(); +console.log(FoundatonLayout.layoutRequiredRegistrations(layoutConfig)) +``` +This will log `['trades','users']` because these are the two registered panes. You can then load any layout that only contains either/both of these items. + +Consider the situation where we dynamically add an item to the right-hand side of the layout. +```javascript +const newItem = document.createElement('p'); //simple example +newItem.innerText = 'Test'; + +layout.registerItem(test, [newItem]); +const layoutConfigTwo = layout.getLayout() +console.log(FoundationLayout.layoutRequiredRegistrations(layoutConfigTwo)); +``` +Now we get `[ "test", "trades", "users"]` as the output, because to load `layoutConfigTwo` we now need all three of those registered panes. + +Consider now where the user refreshes the page to go back to the original state of the layout with just the two elements added, but then tries to load: + +`layoutConfigTwo`: +```javascript +// User has refreshed page + +console.log(layout.registeredItems()); +// Ouputs ['trades','users'] + +layout.loadLayout(layoutConfigTwo); +// Uncaught Error: Trying to load layout with extra components. The component(s) not currently loaded are "test" +``` +Notice the error message says that the `test` component is missing. This is because it was required as part of the layout when we used `getLayout()`, but it hasn't been added as part of the layout now. If we added the item using `registerItem()` we could subsequently run `layout.loadLayout(layoutConfigTwo);` to load the layout successfully. + +:::warning +Just because an item is not displayed on the layout does not mean it is not registered. `.getLayout()` gets only the current layout config, so you cannot use it to see every single item that is currently registered (unless every item is added). This is why you should use `.registeredItems()` to get the currently registered items. +::: + +#### Proactively registering items + +Here is a simple approach to ensure that all items are registered when you load a layout; loop through all the items that you could possibly load and register them. + +```javascript +const allItems = [ + {registration: 'trades', elements: [...], }, + {registration: 'users', elements: [...], }, + {registration: 'profiles', elements: [...], }, + {registration: 'notifications', elements: [...], }, +]; + +allItems.forEach(({registration, elements}) => { + layout.registerItem(registration, elements); +}) +``` +Now all those items will be registered with the layout for potential use when calling `loadLayout()`, or added using `addItem()`. + +#### Reactively registering items + +Alternatively, you could query the current layout and the layout you want to load to see if there are any missing registered items; you can then register the missing ones. Using our previous examples: + +```javascript +const currentRegistrations = FoundatonLayout.registeredItems(); +// ['trades','users'] +const requiredRegistrations = FoundatonLayout.layoutRequiredRegistrations(layoutConfigTwo); +// ['test','trades','users'] + +// We can see 'test' is missing and therefore we should register it +layout.registerItem(test, [element]); +``` + +:::info +Only items _missing_ from the `requiredRegistrations` are an issue. If there are items in the `currentRegistrations` that are not in `requiredRegistrations`, this is *not* an issue - because these will simply be unused registrations. +::: + +:::warning +If you are calling `registerItem` manually and are using the autosave feature, [see here](#reloading-the-layout). +::: + +### Custom item renaming header button + +Here is an example of creating a custom button for the layout. When the button is clicked, it prompts the user for a name, and will rename the item in the layout. + +```typescript +export const layoutCustomButtons: CustomButton[] = [ + { + svg: LAYOUT_ICONS.renameSVG, + onClick: (button: HTMLElement, elem: HTMLElement) => { + const title = prompt('New name?'); + const event: LayoutReceiveEventsDetail['changeTitle'] = { + title, + mode: 'replace', + }; + elem.dispatchEvent( + new CustomEvent(LayoutReceiveEvents.changeTitle, { detail: event, bubbles: true }), + ); + }, + }, +]; +``` + +You can import `LAYOUT_ICONS`, `CustomButton`, `LayoutReceiveEvents`, and `LayoutReceiveEventsDetail` from the foundation-layout package, to get strong typing. + +:::warning +You'll probably want to improve this callback function to handle cases where the user doesn't enter a prompt value. +::: + +## Incorrect examples + +The following section contains examples of incorrect usage, which are useful for troubleshooting. + +### Non-layout child + +The following example is invalid: + +```html + + +

My splits

+ + + + + + +
+
+``` +This is because there is a child of one of the layout regions which isn't another layout region or layout item (the `

`). This will throw a runtime error. + +### Layout region in tabs + +The following example is invalid: + +```html + + + + + + + + + + + + + + + + + + +``` +This is because you cannot have more layout regions nested inside a tab region. You will get undefined behaviour. + + +### Multiple items in root + +The following example is invalid: + +```html + + + + + + + + + + + +``` +This is because you cannot have multiple layout elements as the immediate child of the layout root. You will get a runtime error. + +### Multiple nested layouts + +The following example is invalid: + +```html + + + + + + + + +``` +Where the markup of `another-component` is something like: +```html + + + + + + + + + + + + + +``` + +This is because you cannot have an instance of the layout nested inside of another layout instance. You could try adding multiple items at once using `.addItem([elem1,..,elemN])` instead. + +### Nested item + +The following example is invalid: + +```html + + + + + + + + + + +``` +This is because you cannot have `` inside other ``. You will get a runtime error. + +### Observables with directives + +The following is invalid: + +```javascript +@customElement({ + name: 'my-element', + template, +}) +class Analytics extends FASTElement { + @observable showIndexFunds = true; + + toggleShowIndexFunds() { + this.showIndexFunds = !this.showIndexFunds; + } +} + +var template = html` + + + + + + + + ${when(x => x.showIndexFunds, html` + + + + `)} + + + +`; +``` + +Initially, you will see both items correctly rendered like this: +``` ++---------------------------------------------+ +| Stocks Chart | ++---------------------------------------------+ +| Index Chart | ++---------------------------------------------+ +``` +But as the user clicks the toggle button, the `Index Chart` will not be taken away and added back in. +Instead, it will be added as a duplicate every time the observable is set true. Additionally, the contents +of the panel will be wiped as duplicates are added. + +To work around this, you would use FAST directives inside custom web components inside the layout. + +### Binding events inline in the declarative API +The following example is invalid: + +```html + + + x.doSomething()} /> + + +``` + +Because of a limitation in the [cloneNode() API](https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode), event listeners are *not* copied. +This process is part of the process of adding an item to the layout, using both the declarative HTML and JavaScript APIs. So while you will see +a checkbox on the screen as part of the layout, the event listener will *not* fire when you `change` the checkbox. This applies to all items and events. + +The idiomatic FAST way of implementing this event binding is to create a custom element and attach the event internally. + +```typescript +// template +export const exampleComponentTemplate = html` + +`; + +// model +@customElement({ + name: 'example-component', + template: exampleComponentTemplate, +}) +export class ExampleComponent extends FASTElement { + checkbox: Checkbox; + doSomething() { } // do something important +} +``` + +You can then use the custom component in the layout: + +```html + + + + + +``` + +See [here](#custom-components-to-handle-bindings-and-event-listeners) for a thorough technical explanation. + +### New layout item not displaying +Say you have the following layout, [the simple example](#simple-example), with autosave enabled. +```html + + + + + + + + + + +``` + +The user of your layout will move things around and this will cache the layout. Say you then update the layout to add an item. +```html + + + + + + + + + + + + + +``` + +You and the user will still only see the first two items. This is because the cached layout is being loaded, which does not contain the +new item. To fix this, you must [invalidate the cache](#invalidating-the-cache). + +### Resource-intensive component resetting in layout + +Say you have a component which has to initialise a resource-heavy or long-awaited asynchronous task, such as the following: +```typescript +@customElement({ + name: 'mock-connected', +}) +export class MockConnected extends FASTElement { + @observable resource = ''; + + async connectedCallback(): Promise { + super.connectedCallback(); + // Simulate doing some work with an external service + } + + async disconnectedCallback(): Promise { + super.disconnectedCallback(); + // Simulate cleaning an external service + } +} +``` + +As explained in the [lifecycle info section](#element-lifecycle), this component may have its `disconnectedCallback` and `connectedCallback` lifecycle at unnecessary times, effectively wasting time re-initialising a potentially heavy resource. + +Use `LifecycleMixin` to access properties on the class, which can be used to run lifecycle functionality more thoughtfully. In the following example, the resource-intensive tasks are called conditionally - only when needed. + +```typescript +@customElement({ + name: 'mock-connected', +}) +export class MockConnected extends LifecycleMixin(FASTElement) { + @observable resource = ''; + + async connectedCallback(): Promise { + super.connectedCallback(); + const shouldRunConnect = this.shouldRunConnect; + DOM.queueUpdate(async () => { + if (!shouldRunConnect) return; + await this.init(); + }); + } + + async disconnectedCallback(): Promise { + super.disconnectedCallback(); + const shouldRunDisconnect = this.shouldRunDisconnect; + DOM.queueUpdate(async () => { + if (!shouldRunDisconnect) return; + await this.deInit(); + }); + } + + // Simulate doing work with an external service + async init(): Promise { } + + // Simulate cleaning an external service + async deInit(): Promise { } +} +``` + +The above is quite a comprehensive example, but it doesn't necessarily have to be so complicated. You might just want to exit early from the connected callback without using the `DOM.queueUpdate` functionality. However, it is useful for handling the `async` setup process properly. + +:::warning +It is important to capture the parameter in the example above (e.g. `const shouldRunDisconnect = shouldRunDisconnect`) so that the information is cached at the time of the lifecycle change, for use when the `DOM.queueUpdate` work is performed. This is not required if you run your lifecycle methods synchronously; however, if you follow the pattern above, you need to schedule the `async` functionality to run after the layout considers the relevant lifecycle-gating functionality (such as dragging) to be complete. +::: + +### Consuming lifecycle value multiple times + +Consider the following example, where multiple bits of functionality are being gated with `shouldRunConnect`: +```typescript +@customElement({ + name: 'mock-connected', +}) +export class MockConnected extends LifecycleMixin(FASTElement) { + @observable resource = ''; + + async connectedCallback(): Promise { + super.connectedCallback(); + console.log("shouldRunConnect: " + this.shouldRunConnect) + if (this.shouldRunConnect) { + await this.init(); + } + await otherSetup(this.shouldRunConnect); + } + + // Simulate doing work with an external service + async init(): Promise { } + async otherSetup(connectToResource: boolean): Promise {} + // Similar setup in disconnectedCallback... +} +``` + +In this example, when you have this item inside the layout, the functionality will not correctly be gated when you add or remove other items as intended. + +This is because `shouldRunConnect` (and `shouldRunDisconnect`) perform a check to see whether the layout has performed an event that should gate functionality; reading the value multiple times will incorrectly signal that there hasn't been another lifecycle event upon subsequent reads during the same cycle. The mental model you can use here is thinking of consuming the check when you read the variable. + +Therefore, if you want to use the value multiple times in the `connectedCallback` and `disconnectedCallback` functions, you should cache the variable. + +** You should only read the variables `this.shouldRunConnect` and `this.shouldRunDisconnect` once per `shouldRunConnect` and `shouldRunDisconnect` cycle respectively. ** + +```typescript + async connectedCallback(): Promise { + super.connectedCallback(); + if (this.shouldRunConnect) { + console.log("shouldRunConnect: " + this.shouldRunConnect) + await this.init(); + await otherSetup(true); + } else { + await otherSetup(false); + } + } + // or.... + async connectedCallback(): Promise { + super.connectedCallback(); + const runFullConnect = this.shouldRunConnect; + console.log("shouldRunConnect: " + runFullConnect) + if (runFullConnect) { + await this.init(); + } + await otherSetup(runFullConnect); + } +``` + +:::danger +The same limitation applies if you're checking the variable multiple times because you have a hierarchy of extending classes. Again, you should cache the variable for checking in this case. +::: + +## Supplementary information + +### Custom components to handle bindings and event listeners +As shown in [this example](#binding-events-inline-in-the-declarative-api), you need to wrap html that uses fast bindings and event listeners into their own custom +components. This section is a technical explanation for why this is necessary. It is required that we make use of `cloneNode` to allow the layout to add multiple instances +of a registered component. + +Consider the following, which is the order of events for loading the layout when using html that includes bindings. + +1. As the DOM is parsed, the elements inside the layout are created. At this point, the bindings are attached and the event listeners are created, and the `connectedCallback` lifecycle method executes. +2. Once all the elements contained in the layout have been created, the layout itself initialises\*. +3. As part of the initialisation process, it moves the element from the DOM and puts it internally into a document fragment as part of the layout registration cache. +4. We then load golden layout with the layout config and the registered items, where the registered items create a clone of the items in the document fragment. + +The issue occurs during step four - the clone from `cloneNode` doesn't have the event listeners, so the new copy (which is the one you see on the layout) has no event listeners. Compare this with the similar but different process if you've wrapped up the html into its own custom component. + +1. As the DOM is parsed, the elements inside the layout are created. At this point, the bindings are attached and the event listeners are created, and the `connectedCallback` lifecycle method executes. +2. Once all the elements contained in the layout have been created, the layout itself initialises\*. +3. As part of the initialisation process, it moves the element from the DOM and puts it internally into a document fragment as part of the layout registration cache. This is just a tag, such as ``, not a definition that includes bindings or event listeners. +4. We then load golden layout with the layout config and the registered items, where the registered items create a clone of the items in the document fragment. +5. When that clone is put on the DOM, it is a custom element. And so it calls the lifecycle method again `connectedCallback` as well as other initialisation methods that include attaching the event listener to the component as required. + +>>\* It initialises after the timeout specified by the `reload-buffer` attribute if using the declarative HTML API, or if steps `3` and `4` occur during calls to `registerItem` and `addItem` respectively. + +## Installation + +To enable this module in your application, follow the steps below. + +1. Add `@genesislcap/foundation-layout` as a dependency in your `package.json` file. Whenever you change the dependencies of your project, ensure you run the `$ npm run bootstrap` command again. You can find more information in the [package.json basics](../../../../../web/basics/package-json-basics/) page. + +```json +{ + ... + "dependencies": { + ... + "@genesislcap/foundation-layout": "latest" + ... + }, + ... +} +``` + +## [API Docs](./docs/api/index.md) + +## License + +Note: this project provides front-end dependencies and uses licensed components listed in the next section; thus, licenses for those components are required during development. Contact [Genesis Global](https://genesis.global/contact-us/) for more details. + +### Licensed components +Genesis low-code platform diff --git a/docs/001_develop/03_client-capabilities/015_layout-management/layout_05_binding_events_store.mdx b/docs/001_develop/03_client-capabilities/015_layout-management/layout_05_binding_events_store.mdx index 29df482b44..92465f08cc 100644 --- a/docs/001_develop/03_client-capabilities/015_layout-management/layout_05_binding_events_store.mdx +++ b/docs/001_develop/03_client-capabilities/015_layout-management/layout_05_binding_events_store.mdx @@ -64,7 +64,7 @@ This works because the bindings and events run _after_ the cloneNode() call, so The question you may ask with the above answer is how you access the properties which are represented in `${ ... }`. If they were statically defined functions or `const` values on the class, then you can move them onto the `ContainerElement` class. More likely though they are dynamically generated. -This is where we'll want to use some formal state management. This section will rely on your knowledge of FoundationStore . The store is already set up out of the box with applications created from Genesis Create or using the Genesis CLI `genx`. +This is where we'll want to use some formal state management. This section will rely on your knowledge of [`foundation-store`](../../state-management/). The store is already set up out of the box with applications created from Genesis Create or using the Genesis CLI `genx`. ### Pre-layout example @@ -231,4 +231,4 @@ WrapperTwo; export class MyElement extends GenesisElement { } ``` -We've covered the pitfalls that occur when trying to add the layout to a group of elements that share state and bindings, and a way to solve that via the foundation store. +We've covered the pitfalls that occur when trying to add the layout to a group of elements that share state and bindings, and a way to solve that via the [`foundation-store`](../../state-management/). diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/_category_.yml b/docs/001_develop/03_client-capabilities/017_state-management/docs/_category_.yml new file mode 100644 index 0000000000..4b135cde05 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/_category_.yml @@ -0,0 +1 @@ +className: 'hidden' diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore._constructor_.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore._constructor_.md new file mode 100644 index 0000000000..e20626adec --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore._constructor_.md @@ -0,0 +1,23 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [(constructor)](./foundation-store.abstractstore._constructor_.md) + +## AbstractStore.(constructor) + +Constructs a new instance of the `AbstractStore` class + +**Signature:** + +```typescript +constructor(...storeFragments: Store[]); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| storeFragments | [Store](./foundation-store.store.md)\[\] | The child store fragments this store fragment will manage. | + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.addstorefragments.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.addstorefragments.md new file mode 100644 index 0000000000..006ecffaa7 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.addstorefragments.md @@ -0,0 +1,27 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [addStoreFragments](./foundation-store.abstractstore.addstorefragments.md) + +## AbstractStore.addStoreFragments() method + +Lazily add store fragments. + +**Signature:** + +```typescript +addStoreFragments(...storeFragments: Store[]): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| storeFragments | [Store](./foundation-store.store.md)\[\] | Store fragments to add. | + +**Returns:** + +void + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.binding.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.binding.md new file mode 100644 index 0000000000..c87dc0ba48 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.binding.md @@ -0,0 +1,32 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [binding](./foundation-store.abstractstore.binding.md) + +## AbstractStore.binding() method + +An api to allow the observation of values and arbitrary bindings outside the template engine. + +**Signature:** + +```typescript +binding(token: ((store: this) => TReturn) | keyof this, subscriberChangeCallback?: SubscriberChangeCallback | undefined, isVolatileBinding?: boolean, context?: ExecutionContext): BindingObserver; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| token | ((store: this) => TReturn) \| keyof this | A store lookup token which can take various forms. | +| subscriberChangeCallback | [SubscriberChangeCallback](./foundation-store.subscriberchangecallback.md)<TReturn> \| undefined | _(Optional)_ [SubscriberChangeCallback](./foundation-store.subscriberchangecallback.md) | +| isVolatileBinding | boolean | _(Optional)_ Indicates the binding is volatile. | +| context | ExecutionContext | _(Optional)_ | + +**Returns:** + +BindingObserver<this, TReturn, TParent> + +An rxjs Observable + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.bindingasrx.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.bindingasrx.md new file mode 100644 index 0000000000..cf674d3aef --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.bindingasrx.md @@ -0,0 +1,19 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [bindingAsRx](./foundation-store.abstractstore.bindingasrx.md) + +## AbstractStore.bindingAsRx() method + + +**Signature:** + +```typescript +bindingAsRx(): RXObservable; +``` +**Returns:** + +RXObservable<this> + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.bindingasrx_1.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.bindingasrx_1.md new file mode 100644 index 0000000000..fdb7382877 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.bindingasrx_1.md @@ -0,0 +1,26 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [bindingAsRx](./foundation-store.abstractstore.bindingasrx_1.md) + +## AbstractStore.bindingAsRx() method + + +**Signature:** + +```typescript +bindingAsRx(key: TKey): RXObservable; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| key | TKey | | + +**Returns:** + +RXObservable<this\[TKey\]> + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.bindingasrx_2.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.bindingasrx_2.md new file mode 100644 index 0000000000..871bd91a59 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.bindingasrx_2.md @@ -0,0 +1,26 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [bindingAsRx](./foundation-store.abstractstore.bindingasrx_2.md) + +## AbstractStore.bindingAsRx() method + + +**Signature:** + +```typescript +bindingAsRx(getter: (store: this) => TReturn): RXObservable; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| getter | (store: this) => TReturn | | + +**Returns:** + +RXObservable<TReturn> + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.commit.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.commit.md new file mode 100644 index 0000000000..a3faadcad9 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.commit.md @@ -0,0 +1,31 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [commit](./foundation-store.abstractstore.commit.md) + +## AbstractStore.commit property + +> This API is provided as a beta preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The value commit proxy. + +**Signature:** + +```typescript +protected readonly commit: this; +``` + +## Remarks + +this.commit has the same interface as the store itself, so props are strongly typed. + +## Example + + +```ts +this.commit.propX = value; +``` + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.commitvalue.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.commitvalue.md new file mode 100644 index 0000000000..51efaaebc3 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.commitvalue.md @@ -0,0 +1,31 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [commitValue](./foundation-store.abstractstore.commitvalue.md) + +## AbstractStore.commitValue() method + +> This API is provided as a beta preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Alternative value commit api. + +**Signature:** + +```typescript +protected commitValue(key: K, value: TStore[K]): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| key | K | The property key from the store's interface. | +| value | TStore\[K\] | The value to set. | + +**Returns:** + +void + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.connect.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.connect.md new file mode 100644 index 0000000000..731eb2a7af --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.connect.md @@ -0,0 +1,27 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [connect](./foundation-store.abstractstore.connect.md) + +## AbstractStore.connect() method + +Connects this store fragment. + +**Signature:** + +```typescript +connect(root: TStoreRoot): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| root | TStoreRoot | The store root fragment. | + +**Returns:** + +void + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.createasynclistener.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.createasynclistener.md new file mode 100644 index 0000000000..da9d60a7ac --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.createasynclistener.md @@ -0,0 +1,55 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [createAsyncListener](./foundation-store.abstractstore.createasynclistener.md) + +## AbstractStore.createAsyncListener property + +Creates an async event listener. + +**Signature:** + +```typescript +protected createAsyncListener: (keys: KeyOrKeys, token: (detail: TDetail, event?: CustomEvent) => Promise) => EventListener; +``` + +## Remarks + +You can think of this like an `effect` in the redux sense. You should not commit values to the store in these, instead raise subsequent events to be handled synchronously, where commits are allowed. + +## Example 1 + +Creating an interface defined handler for a single event key. + +```ts +onDomainAction = this.createAsyncListener( + 'domain-action', + async ({ id, message }) => + this.invokeAsyncAPI( + async () => this.domainService.action(id, message), + 'domain-action-error', + 'domain-action-success' + ) +); +``` + +## Example 2 + +Creating an anonymous handler in the constructor for multiple event keys. + +```ts +this.createAsyncListener( + [ + 'columns-changed', + 'types-changed', + 'max-rows-changed', + 'max-view-changed', + 'order-by-changed', + 'reverse-changed', + ], + async (_, { type }) => this.emit('domain-load') +); +``` + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.createerrorlistener.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.createerrorlistener.md new file mode 100644 index 0000000000..b1c60119bf --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.createerrorlistener.md @@ -0,0 +1,21 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [createErrorListener](./foundation-store.abstractstore.createerrorlistener.md) + +## AbstractStore.createErrorListener property + +Creates an error event listener. + +**Signature:** + +```typescript +protected createErrorListener: (keys: KeyOrKeys, token?: (detail: TDetail, event?: CustomEvent) => void) => EventListener; +``` + +## Remarks + +This logs and stores errors by event key in the store fragment's [ErrorMap](./foundation-store.errormap.md), allowing multiple errors to co-exist, and be presentable to the user via the UI for further action or dismissal. + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.createlistener.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.createlistener.md new file mode 100644 index 0000000000..e5fa0a0726 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.createlistener.md @@ -0,0 +1,21 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [createListener](./foundation-store.abstractstore.createlistener.md) + +## AbstractStore.createListener property + +Creates an event listener. + +**Signature:** + +```typescript +protected createListener: (keys: KeyOrKeys, token: (detail: TDetail, event?: CustomEvent) => void) => EventListener; +``` + +## Remarks + +You can think of this like a `reducer` in the redux sense. You are allowed to commit values to the store in these synchronous handlers. + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.disconnect.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.disconnect.md new file mode 100644 index 0000000000..063e4dd9c8 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.disconnect.md @@ -0,0 +1,27 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [disconnect](./foundation-store.abstractstore.disconnect.md) + +## AbstractStore.disconnect() method + +Disconnects this store fragment. + +**Signature:** + +```typescript +disconnect(root: TStoreRoot): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| root | TStoreRoot | The store root fragment. | + +**Returns:** + +void + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.emit.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.emit.md new file mode 100644 index 0000000000..fe3fd50d23 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.emit.md @@ -0,0 +1,31 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [emit](./foundation-store.abstractstore.emit.md) + +## AbstractStore.emit() method + +Emit events to the stores directly via the standard event flow. + +**Signature:** + +```typescript +protected emit(...args: (TEventDetailMap & TInternalEventDetailMap)[K] extends void ? [key: K] : [key: K, detail: (TEventDetailMap & TInternalEventDetailMap)[K]]): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| args | (TEventDetailMap & TInternalEventDetailMap)\[K\] extends void ? \[key: K\] : \[key: K, detail: (TEventDetailMap & TInternalEventDetailMap)\[K\]\] | | + +**Returns:** + +void + +## Remarks + +By default we allow stores to emit everything in their TEventDetailMap, however TInternalEventDetailMap can be provided to define additional internal events like x-success, x-error etc, or public events from other store fragments. + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.errors.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.errors.md new file mode 100644 index 0000000000..4619506d5e --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.errors.md @@ -0,0 +1,16 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [errors](./foundation-store.abstractstore.errors.md) + +## AbstractStore.errors property + +Contains any errors the store may have, see [ErrorMap](./foundation-store.errormap.md). + +**Signature:** + +```typescript +errors: ErrorMap; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.invokeasyncapi.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.invokeasyncapi.md new file mode 100644 index 0000000000..c1e36ee25d --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.invokeasyncapi.md @@ -0,0 +1,49 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [invokeAsyncAPI](./foundation-store.abstractstore.invokeasyncapi.md) + +## AbstractStore.invokeAsyncAPI() method + +A convenience method to invoke an async api and emit success and error events. + +**Signature:** + +```typescript +protected invokeAsyncAPI(api: () => Promise, error: keyof (TEventDetailMap & TInternalEventDetailMap), success?: keyof (TEventDetailMap & TInternalEventDetailMap)): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| api | () => Promise<TResult> | The async service api function. | +| error | keyof (TEventDetailMap & TInternalEventDetailMap) | The event key from the store fragment's event detail map. | +| success | keyof (TEventDetailMap & TInternalEventDetailMap) | _(Optional)_ The event key from the store fragment's event detail map. | + +**Returns:** + +Promise<void> + +## Remarks + +The async api function should throw when it encounters an error. + +## Example + + +```ts +onLoad = this.createAsyncListener('domain-load', async () => + this.invokeAsyncAPI( + async () => { + !this.domainService.initialized && (await this.domainService.initialize(this.asDomainServiceInit())); + return this.domainService.list(); + }, + 'domain-load-error', + 'domain-load-success' + ) +); +``` + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.md new file mode 100644 index 0000000000..8342fea583 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.md @@ -0,0 +1,52 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) + +## AbstractStore class + +The abstract store that concrete store fragments must extend, which differs from the [AbstractStoreRoot](./foundation-store.abstractstoreroot.md). + +**Signature:** + +```typescript +export declare abstract class AbstractStore implements Store +``` +**Implements:** [Store](./foundation-store.store.md) + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(storeFragments)](./foundation-store.abstractstore._constructor_.md) | | Constructs a new instance of the AbstractStore class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [commit](./foundation-store.abstractstore.commit.md) |

protected

readonly

| this | **_(BETA)_** The value commit proxy. | +| [createAsyncListener](./foundation-store.abstractstore.createasynclistener.md) | protected | <TDetail = void, TReturn = void>(keys: KeyOrKeys<TEventDetailMap & TInternalEventDetailMap>, token: (detail: TDetail, event?: CustomEvent<TDetail>) => Promise<TReturn>) => EventListener | Creates an async event listener. | +| [createErrorListener](./foundation-store.abstractstore.createerrorlistener.md) | protected | <TDetail extends Error = Error>(keys: KeyOrKeys<TEventDetailMap & TInternalEventDetailMap>, token?: (detail: TDetail, event?: CustomEvent<TDetail>) => void) => EventListener | Creates an error event listener. | +| [createListener](./foundation-store.abstractstore.createlistener.md) | protected | <TDetail = void>(keys: KeyOrKeys<TEventDetailMap & TInternalEventDetailMap>, token: (detail: TDetail, event?: CustomEvent<TDetail>) => void) => EventListener | Creates an event listener. | +| [errors](./foundation-store.abstractstore.errors.md) | | ErrorMap<TEventDetailMap & TInternalEventDetailMap> | Contains any errors the store may have, see [ErrorMap](./foundation-store.errormap.md). | +| [name](./foundation-store.abstractstore.name.md) | readonly | string | The name of the store fragment. | +| [root](./foundation-store.abstractstore.root.md) | protected | TStoreRoot | The store root fragment. | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [addStoreFragments(storeFragments)](./foundation-store.abstractstore.addstorefragments.md) | | Lazily add store fragments. | +| [binding(token, subscriberChangeCallback, isVolatileBinding, context)](./foundation-store.abstractstore.binding.md) | | An api to allow the observation of values and arbitrary bindings outside the template engine. | +| [bindingAsRx()](./foundation-store.abstractstore.bindingasrx.md) | | | +| [bindingAsRx(key)](./foundation-store.abstractstore.bindingasrx_1.md) | | | +| [bindingAsRx(getter)](./foundation-store.abstractstore.bindingasrx_2.md) | | | +| [commitValue(key, value)](./foundation-store.abstractstore.commitvalue.md) | protected | **_(BETA)_** Alternative value commit api. | +| [connect(root)](./foundation-store.abstractstore.connect.md) | | Connects this store fragment. | +| [disconnect(root)](./foundation-store.abstractstore.disconnect.md) | | Disconnects this store fragment. | +| [emit(args)](./foundation-store.abstractstore.emit.md) | protected | Emit events to the stores directly via the standard event flow. | +| [invokeAsyncAPI(api, error, success)](./foundation-store.abstractstore.invokeasyncapi.md) | protected | A convenience method to invoke an async api and emit success and error events. | +| [removeStoreFragments(storeFragments)](./foundation-store.abstractstore.removestorefragments.md) | | Lazily remove store fragments. | + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.name.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.name.md new file mode 100644 index 0000000000..3073392f2a --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.name.md @@ -0,0 +1,16 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [name](./foundation-store.abstractstore.name.md) + +## AbstractStore.name property + +The name of the store fragment. + +**Signature:** + +```typescript +get name(): string; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.removestorefragments.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.removestorefragments.md new file mode 100644 index 0000000000..f93b8cb03b --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.removestorefragments.md @@ -0,0 +1,27 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [removeStoreFragments](./foundation-store.abstractstore.removestorefragments.md) + +## AbstractStore.removeStoreFragments() method + +Lazily remove store fragments. + +**Signature:** + +```typescript +removeStoreFragments(...storeFragments: Store[]): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| storeFragments | [Store](./foundation-store.store.md)\[\] | Store fragments to remove. | + +**Returns:** + +void + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.root.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.root.md new file mode 100644 index 0000000000..cd98b071a7 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstore.root.md @@ -0,0 +1,16 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStore](./foundation-store.abstractstore.md) > [root](./foundation-store.abstractstore.root.md) + +## AbstractStore.root property + +The store root fragment. + +**Signature:** + +```typescript +protected root: TStoreRoot; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.element.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.element.md new file mode 100644 index 0000000000..901fb6028b --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.element.md @@ -0,0 +1,16 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStoreRoot](./foundation-store.abstractstoreroot.md) > [element](./foundation-store.abstractstoreroot.element.md) + +## AbstractStoreRoot.element property + +The store root element. + +**Signature:** + +```typescript +element: HTMLElement; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.md new file mode 100644 index 0000000000..effd722c8e --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.md @@ -0,0 +1,31 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStoreRoot](./foundation-store.abstractstoreroot.md) + +## AbstractStoreRoot class + +The abstract store root that concrete store roots must extend. + +**Signature:** + +```typescript +export declare abstract class AbstractStoreRoot extends AbstractStore implements StoreRoot +``` +**Extends:** [AbstractStore](./foundation-store.abstractstore.md)<TStore, TEventDetailMap, TInternalEventDetailMap> + +**Implements:** [StoreRoot](./foundation-store.storeroot.md) + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [element](./foundation-store.abstractstoreroot.element.md) | | HTMLElement | The store root element. | +| [onConnected](./foundation-store.abstractstoreroot.onconnected.md) | | EventListener | | +| [onDisconnected](./foundation-store.abstractstoreroot.ondisconnected.md) | | EventListener | | +| [onReady](./foundation-store.abstractstoreroot.onready.md) | | EventListener | | +| [ready](./foundation-store.abstractstoreroot.ready.md) | | boolean | The ready status of the store root. | +| [root](./foundation-store.abstractstoreroot.root.md) | protected | this | | + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.onconnected.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.onconnected.md new file mode 100644 index 0000000000..79fd21a574 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.onconnected.md @@ -0,0 +1,14 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStoreRoot](./foundation-store.abstractstoreroot.md) > [onConnected](./foundation-store.abstractstoreroot.onconnected.md) + +## AbstractStoreRoot.onConnected property + +**Signature:** + +```typescript +onConnected: EventListener; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.ondisconnected.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.ondisconnected.md new file mode 100644 index 0000000000..926c7f7460 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.ondisconnected.md @@ -0,0 +1,14 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStoreRoot](./foundation-store.abstractstoreroot.md) > [onDisconnected](./foundation-store.abstractstoreroot.ondisconnected.md) + +## AbstractStoreRoot.onDisconnected property + +**Signature:** + +```typescript +onDisconnected: EventListener; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.onready.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.onready.md new file mode 100644 index 0000000000..902b6fd451 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.onready.md @@ -0,0 +1,14 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStoreRoot](./foundation-store.abstractstoreroot.md) > [onReady](./foundation-store.abstractstoreroot.onready.md) + +## AbstractStoreRoot.onReady property + +**Signature:** + +```typescript +onReady: EventListener; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.ready.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.ready.md new file mode 100644 index 0000000000..25d2d42fb2 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.ready.md @@ -0,0 +1,16 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStoreRoot](./foundation-store.abstractstoreroot.md) > [ready](./foundation-store.abstractstoreroot.ready.md) + +## AbstractStoreRoot.ready property + +The ready status of the store root. + +**Signature:** + +```typescript +ready: boolean; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.root.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.root.md new file mode 100644 index 0000000000..8ac12b2e5d --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.abstractstoreroot.root.md @@ -0,0 +1,15 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [AbstractStoreRoot](./foundation-store.abstractstoreroot.md) > [root](./foundation-store.abstractstoreroot.root.md) + +## AbstractStoreRoot.root property + + +**Signature:** + +```typescript +protected root: this; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.createerrormap.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.createerrormap.md new file mode 100644 index 0000000000..321196ee0b --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.createerrormap.md @@ -0,0 +1,21 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [createErrorMap](./foundation-store.createerrormap.md) + +## createErrorMap variable + +> Warning: This API is now obsolete. +> +> - Use `createErrorMap` from `@genesislcap/foundation-utils` instead. +> + +A factory to create the error map. + +**Signature:** + +```typescript +createErrorMap: (logger: ErrorMapLogger) => ErrorMap +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errordetailmap.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errordetailmap.md new file mode 100644 index 0000000000..30786f3a82 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errordetailmap.md @@ -0,0 +1,19 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [ErrorDetailMap](./foundation-store.errordetailmap.md) + +## ErrorDetailMap type + +> Warning: This API is now obsolete. +> +> - Use `ErrorDetailMap` from `@genesislcap/foundation-utils` instead. +> + +**Signature:** + +```typescript +export type ErrorDetailMap = Record; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errormap.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errormap.md new file mode 100644 index 0000000000..445798aea6 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errormap.md @@ -0,0 +1,33 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [ErrorMap](./foundation-store.errormap.md) + +## ErrorMap interface + +> Warning: This API is now obsolete. +> +> - Use `ErrorMap` from `@genesislcap/foundation-utils` instead. +> + +**Signature:** + +```typescript +export interface ErrorMap extends Pick, 'get' | 'has' | 'delete' | 'clear' | 'size'> +``` +**Extends:** Pick<Map<keyof TErrorDetailMap, Error>, 'get' \| 'has' \| 'delete' \| 'clear' \| 'size'> + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [messages](./foundation-store.errormap.messages.md) | readonly | string | Error map as messages. | + +## Methods + +| Method | Description | +| --- | --- | +| [set(key, error)](./foundation-store.errormap.set.md) | Set an error by key. | + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errormap.messages.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errormap.messages.md new file mode 100644 index 0000000000..78509c445f --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errormap.messages.md @@ -0,0 +1,16 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [ErrorMap](./foundation-store.errormap.md) > [messages](./foundation-store.errormap.messages.md) + +## ErrorMap.messages property + +Error map as messages. + +**Signature:** + +```typescript +readonly messages: string; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errormap.set.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errormap.set.md new file mode 100644 index 0000000000..aee32cc728 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errormap.set.md @@ -0,0 +1,28 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [ErrorMap](./foundation-store.errormap.md) > [set](./foundation-store.errormap.set.md) + +## ErrorMap.set() method + +Set an error by key. + +**Signature:** + +```typescript +set(key: keyof TErrorDetailMap, error: Error): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| key | keyof TErrorDetailMap | The key. | +| error | Error | The error. | + +**Returns:** + +void + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errormaplogger.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errormaplogger.md new file mode 100644 index 0000000000..2d0dbaa67f --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.errormaplogger.md @@ -0,0 +1,19 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [ErrorMapLogger](./foundation-store.errormaplogger.md) + +## ErrorMapLogger type + +> Warning: This API is now obsolete. +> +> - Use `ErrorMapLogger` from `@genesislcap/foundation-utils` instead. +> + +**Signature:** + +```typescript +export type ErrorMapLogger = (...args: any[]) => void; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.logger.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.logger.md new file mode 100644 index 0000000000..028c453ed5 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.logger.md @@ -0,0 +1,16 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [logger](./foundation-store.logger.md) + +## logger variable + +Logger for the foundation-store package + +**Signature:** + +```typescript +logger: import("@genesislcap/foundation-logger").Logger +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.md new file mode 100644 index 0000000000..c15aae2f20 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.md @@ -0,0 +1,45 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) + +## foundation-store package + +## Abstract Classes + +| Abstract Class | Description | +| --- | --- | +| [AbstractStore](./foundation-store.abstractstore.md) | The abstract store that concrete store fragments must extend, which differs from the [AbstractStoreRoot](./foundation-store.abstractstoreroot.md). | +| [AbstractStoreRoot](./foundation-store.abstractstoreroot.md) | The abstract store root that concrete store roots must extend. | + +## Interfaces + +| Interface | Description | +| --- | --- | +| [ErrorMap](./foundation-store.errormap.md) | | +| [Store](./foundation-store.store.md) | Store interface. | +| [StoreConnectable](./foundation-store.storeconnectable.md) | Store connectable interface. | +| [StoreRoot](./foundation-store.storeroot.md) | Root store interface. | +| [StoreSubscriber](./foundation-store.storesubscriber.md) | | + +## Variables + +| Variable | Description | +| --- | --- | +| [createErrorMap](./foundation-store.createerrormap.md) | A factory to create the error map. | +| [logger](./foundation-store.logger.md) | Logger for the foundation-store package | +| [registerStore](./foundation-store.registerstore.md) | Creates a dependency injection key for the store being registered. | + +## Type Aliases + +| Type Alias | Description | +| --- | --- | +| [ErrorDetailMap](./foundation-store.errordetailmap.md) | | +| [ErrorMapLogger](./foundation-store.errormaplogger.md) | | +| [StoreBinding](./foundation-store.storebinding.md) | | +| [StoreRootEventDetailMap](./foundation-store.storerooteventdetailmap.md) | Store root event key to event detail map. | +| [SubscriberChangeCallback](./foundation-store.subscriberchangecallback.md) | | +| [SubscriberChangeHandler](./foundation-store.subscriberchangehandler.md) | | + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.registerstore.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.registerstore.md new file mode 100644 index 0000000000..f0ee083379 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.registerstore.md @@ -0,0 +1,24 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [registerStore](./foundation-store.registerstore.md) + +## registerStore variable + +Creates a dependency injection key for the store being registered. + +**Signature:** + +```typescript +registerStore: (Base: Constructable, name?: string) => import("@microsoft/fast-foundation").InterfaceSymbol +``` + +## Example + + +```ts +export const TradeEntry = registerStore(DefaultTradeEntry, 'TradeEntry'); +``` + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.addstorefragments.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.addstorefragments.md new file mode 100644 index 0000000000..432c52c4be --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.addstorefragments.md @@ -0,0 +1,27 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [Store](./foundation-store.store.md) > [addStoreFragments](./foundation-store.store.addstorefragments.md) + +## Store.addStoreFragments() method + +Lazily add store fragments. + +**Signature:** + +```typescript +addStoreFragments(...storeFragments: Store[]): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| storeFragments | [Store](./foundation-store.store.md)\[\] | Store fragments to add. | + +**Returns:** + +void + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.binding.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.binding.md new file mode 100644 index 0000000000..7eeaeb2867 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.binding.md @@ -0,0 +1,78 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [Store](./foundation-store.store.md) > [binding](./foundation-store.store.binding.md) + +## Store.binding() method + +An api to allow the observation of values and arbitrary bindings outside the template engine. + +**Signature:** + +```typescript +binding(token: ((store: this) => TReturn) | keyof this, subscriberChangeCallback?: SubscriberChangeCallback | undefined, isVolatileBinding?: boolean, context?: ExecutionContext): BindingObserver; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| token | ((store: this) => TReturn) \| keyof this | A store lookup token which can take various forms. | +| subscriberChangeCallback | [SubscriberChangeCallback](./foundation-store.subscriberchangecallback.md)<TReturn> \| undefined | _(Optional)_ [SubscriberChangeCallback](./foundation-store.subscriberchangecallback.md) | +| isVolatileBinding | boolean | _(Optional)_ Indicates the binding is volatile. | +| context | ExecutionContext | _(Optional)_ | + +**Returns:** + +BindingObserver<this, TReturn, TParent> + +An rxjs Observable + +## Example 1 + +If you bind with a lookup token, TS attains the value type information from the store's interface. + +```ts +this.store.binding(x => x.ready, value => {...}); // value is a boolean +this.store.binding(x => x.someNumericProp, value => {...}); // value is a number +``` + +## Example 2 + +If you use a string token, then you need to provide the value type information. + +```ts +this.store.binding('ready', value => {...}); // value is a boolean +this.store.binding('someNumericProp', value => {...}); // value is a number +``` + +## Example 3 + +You can create your own derived bindings that are not already pre-defined as getters on the store's interface. + +```ts +this.store.binding(x => x.prop1 * x.prop2, value => {...}); +``` + +## Example 4 + +Such bindings can even be volatile providing you pass true for isVolatileBinding. + +```ts +this.store.binding(x => x.someToggle ? x.prop1 : x.prop2, value => {...}, true); +``` + +## Example 5 + +You can also use the underlying BindingObserver api should you prefer. + +```ts +this.store.binding(x => x.someData).subscribe({ + handleChange(binding, args) { + ... + } +}) +``` + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.bindingasrx.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.bindingasrx.md new file mode 100644 index 0000000000..ca95ea31d0 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.bindingasrx.md @@ -0,0 +1,29 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [Store](./foundation-store.store.md) > [bindingAsRx](./foundation-store.store.bindingasrx.md) + +## Store.bindingAsRx() method + +An api to allow the observation of values and arbitrary bindings as Rx observables. + +**Signature:** + +```typescript +bindingAsRx(): RXObservable; +``` +**Returns:** + +RXObservable<this> + +An rxjs Observable of the entire store. + +## Example + + +```ts +const entireStore$ = this.store.bindingAsRx().subscribe(value => {...}); +``` + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.bindingasrx_1.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.bindingasrx_1.md new file mode 100644 index 0000000000..576184b105 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.bindingasrx_1.md @@ -0,0 +1,34 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [Store](./foundation-store.store.md) > [bindingAsRx](./foundation-store.store.bindingasrx_1.md) + +## Store.bindingAsRx() method + +**Signature:** + +```typescript +bindingAsRx(key: TKey): RXObservable; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| key | TKey | Store property key. | + +**Returns:** + +RXObservable<this\[TKey\]> + +An rxjs Observable of that key's value. + +## Example + + +```ts +const ready$ = this.store.bindingAsRx('ready').subscribe(value => {...}); +``` + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.bindingasrx_2.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.bindingasrx_2.md new file mode 100644 index 0000000000..e8264da5d3 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.bindingasrx_2.md @@ -0,0 +1,34 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [Store](./foundation-store.store.md) > [bindingAsRx](./foundation-store.store.bindingasrx_2.md) + +## Store.bindingAsRx() method + +**Signature:** + +```typescript +bindingAsRx(getter: (store: this) => TReturn): RXObservable; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| getter | (store: this) => TReturn | A function that returns a value from the store. | + +**Returns:** + +RXObservable<TReturn> + +An rxjs Observable of the value. + +## Example + + +```ts +const prop1$ = this.store.bindingAsRx(x => x.prop1).subscribe(value => {...}); +``` + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.bindingasrx_3.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.bindingasrx_3.md new file mode 100644 index 0000000000..0ba1e71502 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.bindingasrx_3.md @@ -0,0 +1,27 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [Store](./foundation-store.store.md) > [bindingAsRx](./foundation-store.store.bindingasrx_3.md) + +## Store.bindingAsRx() method + +**Signature:** + +```typescript +bindingAsRx(token?: ((store: this) => TReturn) | keyof this): RXObservable; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| token | ((store: this) => TReturn) \| keyof this | _(Optional)_ Store property key or a function that returns a value from the store. | + +**Returns:** + +RXObservable<TReturn \| this> + +An rxjs Observable of the value. + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.errors.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.errors.md new file mode 100644 index 0000000000..3c6eea7b3c --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.errors.md @@ -0,0 +1,33 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [Store](./foundation-store.store.md) > [errors](./foundation-store.store.errors.md) + +## Store.errors property + +Contains any errors the store may have, see [ErrorMap](./foundation-store.errormap.md). + +**Signature:** + +```typescript +readonly errors: ErrorMap; +``` + +## Example 1 + +Errors can be looked up by event key: + +```ts +${when(x => x.trades.errors.has('trade-insert-error'), html`...`)} +``` + +## Example 2 + +Errors can be bound to collectively: + +```ts +
${x => x.trades.errors.messages}
+``` + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.md new file mode 100644 index 0000000000..21631a8ff9 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.md @@ -0,0 +1,40 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [Store](./foundation-store.store.md) + +## Store interface + +Store interface. + +**Signature:** + +```typescript +export interface Store +``` + +## Remarks + +The public interface available on the injected store fragment. + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [errors](./foundation-store.store.errors.md) | readonly | ErrorMap<EventDetailMap> | Contains any errors the store may have, see [ErrorMap](./foundation-store.errormap.md). | +| [name?](./foundation-store.store.name.md) | readonly | string | _(Optional)_ The name of the store fragment. | + +## Methods + +| Method | Description | +| --- | --- | +| [addStoreFragments(storeFragments)](./foundation-store.store.addstorefragments.md) | Lazily add store fragments. | +| [binding(token, subscriberChangeCallback, isVolatileBinding, context)](./foundation-store.store.binding.md) | An api to allow the observation of values and arbitrary bindings outside the template engine. | +| [bindingAsRx()](./foundation-store.store.bindingasrx.md) | An api to allow the observation of values and arbitrary bindings as Rx observables. | +| [bindingAsRx(key)](./foundation-store.store.bindingasrx_1.md) | | +| [bindingAsRx(getter)](./foundation-store.store.bindingasrx_2.md) | | +| [bindingAsRx(token)](./foundation-store.store.bindingasrx_3.md) | | +| [removeStoreFragments(storeFragments)](./foundation-store.store.removestorefragments.md) | Lazily remove store fragments. | + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.name.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.name.md new file mode 100644 index 0000000000..3216b009c3 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.name.md @@ -0,0 +1,17 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [Store](./foundation-store.store.md) > [name](./foundation-store.store.name.md) + +## Store.name property + +The name of the store fragment. + +**Signature:** + +```typescript +/** @virtual */ +readonly name?: string; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.removestorefragments.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.removestorefragments.md new file mode 100644 index 0000000000..905dc12f3f --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.store.removestorefragments.md @@ -0,0 +1,27 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [Store](./foundation-store.store.md) > [removeStoreFragments](./foundation-store.store.removestorefragments.md) + +## Store.removeStoreFragments() method + +Lazily remove store fragments. + +**Signature:** + +```typescript +removeStoreFragments(...storeFragments: Store[]): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| storeFragments | [Store](./foundation-store.store.md)\[\] | Store fragments to remove. | + +**Returns:** + +void + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storebinding.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storebinding.md new file mode 100644 index 0000000000..67335bf917 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storebinding.md @@ -0,0 +1,16 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [StoreBinding](./foundation-store.storebinding.md) + +## StoreBinding type + +**Signature:** + +```typescript +export type StoreBinding = (store: TStore) => any; +``` +**References:** [Store](./foundation-store.store.md) + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeconnectable.connect.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeconnectable.connect.md new file mode 100644 index 0000000000..dc970ddd20 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeconnectable.connect.md @@ -0,0 +1,27 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [StoreConnectable](./foundation-store.storeconnectable.md) > [connect](./foundation-store.storeconnectable.connect.md) + +## StoreConnectable.connect() method + +Connects this store fragment. + +**Signature:** + +```typescript +connect(root: TStoreRoot): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| root | TStoreRoot | The store root fragment. | + +**Returns:** + +void + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeconnectable.disconnect.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeconnectable.disconnect.md new file mode 100644 index 0000000000..77e4ac6c26 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeconnectable.disconnect.md @@ -0,0 +1,27 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [StoreConnectable](./foundation-store.storeconnectable.md) > [disconnect](./foundation-store.storeconnectable.disconnect.md) + +## StoreConnectable.disconnect() method + +Disconnects this store fragment. + +**Signature:** + +```typescript +disconnect(root: TStoreRoot): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| root | TStoreRoot | The store root fragment. | + +**Returns:** + +void + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeconnectable.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeconnectable.md new file mode 100644 index 0000000000..073b83c977 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeconnectable.md @@ -0,0 +1,24 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [StoreConnectable](./foundation-store.storeconnectable.md) + +## StoreConnectable interface + +Store connectable interface. + +**Signature:** + +```typescript +export interface StoreConnectable +``` + +## Methods + +| Method | Description | +| --- | --- | +| [connect(root)](./foundation-store.storeconnectable.connect.md) | Connects this store fragment. | +| [disconnect(root)](./foundation-store.storeconnectable.disconnect.md) | Disconnects this store fragment. | + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.element.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.element.md new file mode 100644 index 0000000000..734d66226b --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.element.md @@ -0,0 +1,16 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [StoreRoot](./foundation-store.storeroot.md) > [element](./foundation-store.storeroot.element.md) + +## StoreRoot.element property + +The store root element. + +**Signature:** + +```typescript +readonly element: HTMLElement; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.md new file mode 100644 index 0000000000..f4877e451c --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.md @@ -0,0 +1,37 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [StoreRoot](./foundation-store.storeroot.md) + +## StoreRoot interface + +Root store interface. + +**Signature:** + +```typescript +export interface StoreRoot extends Store +``` +**Extends:** [Store](./foundation-store.store.md) + +## Remarks + +The public interface available on the injected store fragment. + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [element](./foundation-store.storeroot.element.md) | readonly | HTMLElement | The store root element. | +| [ready](./foundation-store.storeroot.ready.md) | readonly | boolean | The ready status of the store root. | + +## Methods + +| Method | Description | +| --- | --- | +| [onConnected(event)](./foundation-store.storeroot.onconnected.md) | | +| [onDisconnected(event)](./foundation-store.storeroot.ondisconnected.md) | | +| [onReady(event)](./foundation-store.storeroot.onready.md) | | + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.onconnected.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.onconnected.md new file mode 100644 index 0000000000..21fdb64866 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.onconnected.md @@ -0,0 +1,25 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [StoreRoot](./foundation-store.storeroot.md) > [onConnected](./foundation-store.storeroot.onconnected.md) + +## StoreRoot.onConnected() method + +**Signature:** + +```typescript +onConnected(event: CustomEvent): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | CustomEvent<HTMLElement> | | + +**Returns:** + +void + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.ondisconnected.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.ondisconnected.md new file mode 100644 index 0000000000..60b7bfb70f --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.ondisconnected.md @@ -0,0 +1,25 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [StoreRoot](./foundation-store.storeroot.md) > [onDisconnected](./foundation-store.storeroot.ondisconnected.md) + +## StoreRoot.onDisconnected() method + +**Signature:** + +```typescript +onDisconnected(event: CustomEvent): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | CustomEvent<void> | | + +**Returns:** + +void + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.onready.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.onready.md new file mode 100644 index 0000000000..767e52fcfc --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.onready.md @@ -0,0 +1,25 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [StoreRoot](./foundation-store.storeroot.md) > [onReady](./foundation-store.storeroot.onready.md) + +## StoreRoot.onReady() method + +**Signature:** + +```typescript +onReady(event: CustomEvent): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | CustomEvent<boolean> | | + +**Returns:** + +void + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.ready.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.ready.md new file mode 100644 index 0000000000..c38e177a2c --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storeroot.ready.md @@ -0,0 +1,16 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [StoreRoot](./foundation-store.storeroot.md) > [ready](./foundation-store.storeroot.ready.md) + +## StoreRoot.ready property + +The ready status of the store root. + +**Signature:** + +```typescript +readonly ready: boolean; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storerooteventdetailmap.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storerooteventdetailmap.md new file mode 100644 index 0000000000..ba87409633 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storerooteventdetailmap.md @@ -0,0 +1,20 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [StoreRootEventDetailMap](./foundation-store.storerooteventdetailmap.md) + +## StoreRootEventDetailMap type + +Store root event key to event detail map. + +**Signature:** + +```typescript +export type StoreRootEventDetailMap = { + 'store-connected': HTMLElement; + 'store-disconnected': void; + 'store-ready': boolean; +}; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storesubscriber.handlechange.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storesubscriber.handlechange.md new file mode 100644 index 0000000000..36d5014e99 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storesubscriber.handlechange.md @@ -0,0 +1,14 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [StoreSubscriber](./foundation-store.storesubscriber.md) > [handleChange](./foundation-store.storesubscriber.handlechange.md) + +## StoreSubscriber.handleChange property + +**Signature:** + +```typescript +handleChange: SubscriberChangeHandler; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storesubscriber.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storesubscriber.md new file mode 100644 index 0000000000..4897849dc1 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.storesubscriber.md @@ -0,0 +1,21 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [StoreSubscriber](./foundation-store.storesubscriber.md) + +## StoreSubscriber interface + +**Signature:** + +```typescript +export interface StoreSubscriber +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [handleChange](./foundation-store.storesubscriber.handlechange.md) | | [SubscriberChangeHandler](./foundation-store.subscriberchangehandler.md)<TStore, TReturn> | | + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.subscriberchangecallback.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.subscriberchangecallback.md new file mode 100644 index 0000000000..1295799612 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.subscriberchangecallback.md @@ -0,0 +1,14 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [SubscriberChangeCallback](./foundation-store.subscriberchangecallback.md) + +## SubscriberChangeCallback type + +**Signature:** + +```typescript +export type SubscriberChangeCallback = (value: TReturn, args: BindingObserver) => void; +``` diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.subscriberchangehandler.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.subscriberchangehandler.md new file mode 100644 index 0000000000..c8d980d348 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/foundation-store.subscriberchangehandler.md @@ -0,0 +1,16 @@ +--- +format: md +--- + + +[Home](./index.md) > [@genesislcap/foundation-store](./foundation-store.md) > [SubscriberChangeHandler](./foundation-store.subscriberchangehandler.md) + +## SubscriberChangeHandler type + +**Signature:** + +```typescript +export type SubscriberChangeHandler = (binding: (x?: TStore, context?: ExecutionContext) => TReturn, args: BindingObserver) => void; +``` +**References:** [Store](./foundation-store.store.md) + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/docs/api/index.md b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/index.md new file mode 100644 index 0000000000..0976dfc1e5 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/docs/api/index.md @@ -0,0 +1,21 @@ + +# API documentation + +Welcome to the API documentation for this package. +- To view the API documentation, click on the package name link at the bottom of this page. +- The files are linked to each other in a tree structure. You can drill down to specific implementation details by following the links. A breadcrumb navigation area is displayed at the top of the file to help you navigate and see where you are in the structure. +- To return to the start page for this documentation, click on the **Home** link. +- To leave the API documentation and return to the main documentation, click on one of the links in the main documentation menu, displayed at the top of the page on a desktop or in the hamburger menu on mobile. + + + +[Home](./index.md) + +## API Reference + +## Packages + +| Package | Description | +| --- | --- | +| [@genesislcap/foundation-store](./foundation-store.md) | | + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/examples.mdx b/docs/001_develop/03_client-capabilities/017_state-management/examples.mdx new file mode 100644 index 0000000000..1336abbfe7 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/examples.mdx @@ -0,0 +1,904 @@ +--- +title: 'Examples and tutorial' +sidebar_label: 'Examples and tutorial' +id: client-state-management-examples +keywords: [web, store, events, observable, binding, communication, redux, injection, state, reducer] +tags: + - web + - store + - events + - observable + - binding + - communication + - redux + - injection + - state + - reducer +sidebar_position: 17 +--- + +import { CollapsibleSlot } from '../../../../examples/ui/documentationBase' +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import BaseStore from '../../../_includes/_base_store_setup.mdx' +import EventWrapper from '../../../_includes/_store_event_wrapper.mdx' + +This page will guide you through setting up a simple example of two components communicating via a store shared value. + +## Requirements + +Please note that this document assumes that the store is already set up for you, as it is in all new projects. If you don't have the store setup already see [this page](../client-state-management-legacy-setup). + +In your project you should have a blank store setup that looks like the following: + + + +### Test components + +We're going to be linking two components together using a variable in the store. The first component is an example ``. Open the collapsed section to see the mock data. + + + ```typescript + const columnDefs: ColDef[] = [ + { headerName: 'ID', field: 'id' }, + { headerName: 'Transaction Date', field: 'transactionDate' }, + { headerName: 'Description', field: 'description' }, + { + headerName: 'Amount', + field: 'amount', + valueFormatter: params => { + return params.value.toFixed(2); + } + }, + { headerName: 'Type', field: 'type' }, + { headerName: 'Currency', field: 'currency' }, + { + headerName: 'Balance After', + field: 'balanceAfter', + valueFormatter: params => { + return params.value.toFixed(2); + } + } + ]; + const rowData = [ + { + id: 1, + transactionDate: "2023-05-15", + description: "Quarterly Revenue Payment", + amount: 157892.45, + type: "income", + currency: "USD", + balanceAfter: 203567.89, + }, + { + id: 2, + transactionDate: "2023-05-14", + description: "Equipment Purchase", + amount: -12567.00, + type: "expense", + currency: "USD", + balanceAfter: 191000.89, + }, + { + id: 3, + transactionDate: "2023-05-13", + description: "Consulting Services Fee", + amount: 45000.00, + type: "income", + currency: "USD", + balanceAfter: 236000.89, + } + ]; + ``` + + + + + + + ```typescript + @customElement({ + name: 'example-grid', + template: html``, + }) + export class ExampleGrid extends GenesisElement { + grid: GridPro; + connectedCallback() { + super.connectedCallback(); + DOM.queueUpdate(() => this.grid.gridOptions = { + defaultColDef: { + resizable: true, + filter: true, + }, + columnDefs: columnDefs, // defined in collapsible section above + rowData: rowData, // defined in collapsible section above + }) + } + } + ``` + + + + + + ```typescript + const ExampleGrid = () => { + const baseGridOptions = { + defaultColDef: { + resizable: true, + filter: true, + }, + columnDefs: columnDefs, // defined in collapsible section above + rowData: rowData, // defined in collapsible section above + }; + + return ( + + ); + }; + ``` + + + + ```typescript + @Component({ + selector: 'example-grid', + standalone: true, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + template: ` + + ` + }) + export class ExampleGridComponent implements OnInit { + @ViewChild('gridRef') gridRef: any; + + baseGridOptions = { + defaultColDef: { + resizable: true, + filter: true, + }, + columnDefs: columnDefs, // defined in collapsible section above + rowData: rowData, // defined in collapsible section above + }; + + ngOnInit() { } + } + ``` + + + + +Pasting the above example into your codebase and adding it to the HTML should allow you to see the data on the grid pro. Next, we are going to use a simple component to allow you to filter out certain rows of data. + + + + + + ```typescript + @customElement({ + name: 'set-filter', + template: html` + + Both + Income + Expense + + `, + }) + export class SetFilter extends GenesisElement { + @observable radioGroupValue: string; + radioGroupValueChanged(_, newValue: string) { + console.log(newValue); + } + } + ``` + + + + + ```typescript + const SetFilter = () => { + const [radioValue, setRadioValue] = useState('both'); + + const handleRadioChange = (e: Event) => { + setRadioValue(e.target.value); + } + + return ( + + Both + Income + Expense + + ) + } + ``` + + + + ```typescript + @Component({ + selector: 'set-filter', + standalone: true, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + template: ` + + Both + Income + Expense + + ` + }) + export class SetFilterComponent { + radioValue: string = 'both'; + + handleRadioChange(event: Event) { + const target = event.target as HTMLInputElement; + this.radioValue = target.value; + } + } + ``` + + + + +You can add this second component to your HTML and see the radio input component. However, clicking any of the radio buttons doesn't affect the grid. Next, we are going to use the store to link these components together. + +:::tip +In this simple example you could avoid using the store by just allowing the user to create filters on the grid pro themselves, or both components could be siblings, contained in a parent component which manages the shared state. However, the following example will teach you the basics of how to link components so you can use it as an option during implementation. + +For example, you may want components which cannot be siblings in a different component to link to each other. +::: + +## Configuring the store + +1. *Setup the event* - each action in the store requires a triggering event. We can setup an event name which is simply a string, but it is good practise to type it with an `enum` to aid future refactoring. We can create a simple TypeScript `enum` at the top of the page. In this example we'll call it `set-filter` as we want it to configure the filtering on the grid. +```typescript +export enum StoreEvents { + SetFilter = 'set-filter', +} +``` + +2. *Set the event payload type* - each event can have data associated with it. The data type could be a simple primitive such as `boolean`. Some events you'll create, like `store-init`, are simple and the event triggering in itself is enough information, so they can be typed as `void`. In this example we want to type the different filtering types we're going to allow, so we'll set a union of strings of the allowed values. The pattern we're using is the key is the event name, and the value is the associated type. +```typescript +export type StoreEventDetailMap = StoreRootEventDetailMap & { + [StoreEvents.SetFilter]: 'income' | 'expense' | 'both'; +}; +``` + +3. *Setup variables and handlers on interface* - next we want to fill out the interface with this associated data and handlers. In most cases you'll have one property, and an associated handler. It's good practise to type the property as `readonly` to ensure that you remember to update it via the `.commit` method. The type of the variable and the parameter to the handler functions are both the type described as the union of strings from the previous step - they can be referenced by using the event name to index on the detail map type. +```typescript +export interface Store extends StoreRoot { + readonly transactionTypeFilter: StoreEventDetailMap[StoreEvents.SetFilter] + onFilterEvent(event: CustomEvent): void; +} +``` + +4. *Configure value and handler on implementation* - finally the variable needs to be defined on the store, and the handler implemented(typescript should currently be giving an error because the items added to the interface in step 3 are not currently defined on the implementing class). The `@observable` property is defined and a default value set, in this case the initial option is to show both transaction types (no filtering). Finally, this simple handler only needs to commit the new value to the store. +```typescript +class DefaultStore extends AbstractStoreRoot implements Store { + + @observable transactionTypeFilter: StoreEventDetailMap[StoreEvents.SetFilter] = 'both'; + + onFilterEvent = this.createListener(StoreEvents.SetFilter, (detail) => { + console.log({detail}) + this.commit.transactionTypeFilter = detail; + }) + + constructor() { + super(); + getApp().registerStoreRoot(this); + } +} +``` + +The implementations again use the type `StoreEventDetailMap[StoreEvents.SetFilter]` to set the type of the variable and the parameter to the handler functions. The above example also adds in a `console.log` statement for debugging the next steps, this isn't necessary functionality and should be removed once everything is working. + +## Updating the components + +The final steps are to interact with the store from the two components. Configure the filter component first as that is the component initialising the action (emitting the event). + +In Genesis syntax you can dispatch an event from a standard component, but it's good practise to use the `EventEmitter` mixin to strongly type the component. + + + + + + ```typescript {13-16} + @customElement({ + name: 'set-filter', + template: html` + + Both + Income + Expense + + `, + }) + export class SetFilter extends EventEmitter(GenesisElement) { + @observable radioGroupValue: StoreEventDetailMap[StoreEvents.SetFilter]; + radioGroupValueChanged(_, newValue: StoreEventDetailMap[StoreEvents.SetFilter]) { + this.$emit(StoreEvents.SetFilter, newValue); + } + } + ``` + + + + + ```typescript {2,6,9} + const SetFilter = () => { + const ref = useRef(); + const [radioValue, setRadioValue] = useState('both'); + const handleRadioChange = (e: Event) => { + setRadioValue(e.target.value); + ref.current.dispatchEvent(customEventFactory(StoreEvents.SetFilter, e.target.value)); + } + return ( + + Both + Income + Expense + + ) + } + ``` + + + + + + + ```typescript {14,15,19} + @Component({ + selector: 'set-filter', + standalone: true, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + template: ` + + Both + Income + Expense + + ` + }) + export class SetFilterComponent { + radioValue: StoreEventDetailMap[StoreEvents.SetFilter] = 'both'; + constructor(private el: ElementRef) {} + handleRadioChange(event: Event) { + const target = event.target as HTMLInputElement; + this.radioValue = target.value as StoreEventDetailMap[StoreEvents.SetFilter]; + this.el.nativeElement.dispatchEvent(customEventFactory(StoreEvents.SetFilter, this.radioValue)); + } + } + ``` + + + + + + +At this stage you should be able to activate the `console.log` statement in the store handler added in step 4 by running the app and interacting with the radio buttons. As the selected radio is changed an event is dispatched and should be picked up and set in the store handler. + +The very final step is to use the configured value to filter the grid rows. There are many ways that this can be accomplished. As the property on the store is `@observable`, it could be used directly in a template binding (in a Genesis syntax component), as the store updates the data in the binding would update too. However, in this contrived case where we're filtering grid data manually without using the `` which means the filtering cannot be done via a template binding directly. Instead the `binding` property on a reference to the store is used. + + + + + + ```typescript {6,18-22} + @customElement({ + name: 'example-grid', + template: html``, + }) + export class ExampleGrid extends GenesisElement { + @Store store: Store; + grid: GridPro; + connectedCallback() { + super.connectedCallback(); + DOM.queueUpdate(() => this.grid.gridOptions = { + defaultColDef: { + resizable: true, + filter: true, + }, + columnDefs: columnDefs, + rowData: rowData, + }) + this.store.binding( + (s) => s.transactionTypeFilter, + (detail) => (this.grid.rowData = rowData.filter((row) => detail === 'both' || row.type === detail)) + ) + } + } + ``` + + + + + ```typescript {2,13-22,26} + const ExampleGrid = () => { + const gridRef = useRef(); + + const baseGridOptions = { + defaultColDef: { + resizable: true, + filter: true, + }, + columnDefs: columnDefs, + rowData: rowData + }; + + const [gridOptions, setGridOptions] = useState(baseGridOptions); + useEffect(() => { + storeService.getStore().binding( + (s) => s.transactionTypeFilter, + (detail) => setGridOptions({ + ...baseGridOptions, + rowData: rowData.filter((row) => detail === 'both' || row.type === detail) + }) + ); + }, []); + + return ( + + ); + }; + ``` + + + + + ```typescript {8,24,27-35} + @Component({ + selector: 'example-grid', + standalone: true, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + template: ` + + ` + }) + export class ExampleGridComponent implements OnInit { + @ViewChild('gridRef') gridRef: any; + + baseGridOptions = { + defaultColDef: { + resizable: true, + filter: true, + }, + columnDefs: columnDefs, + rowData: rowData + }; + + gridOptions = this.baseGridOptions; + + ngOnInit() { + getStore().binding( + (s: any) => s.transactionTypeFilter, + (detail: string) => { + this.gridOptions = { + ...this.baseGridOptions, + rowData: this.rowData.filter((row) => detail === 'both' || row.type === detail) + }; + } + ); + } + } + + ``` + + + + + +The [binding](../docs/api/foundation-store.store.binding/) function binds the required `@observable` property in the first argument, and takes a callback function with the updated data as the second. In the above case when `transactionTypeFilter` is updated the `rowData` is filtered in the callback. + +As well as reacting to the `@observable` directly on the html, there are addition binding methods which are documented on the API [here](../docs/api/foundation-store.store/#methods). + +## Complete example + + + ```typescript + import { CustomEventMap } from '@genesislcap/foundation-events'; + import { getApp } from '@genesislcap/foundation-shell/app'; + import { + AbstractStoreRoot, + registerStore, + StoreRoot, + StoreRootEventDetailMap, + } from '@genesislcap/foundation-store'; + import { observable } from '@genesislcap/web-core'; + + export enum StoreEvents { + SetFilter = 'set-filter', + } + + export type StoreEventDetailMap = StoreRootEventDetailMap & { + [StoreEvents.SetFilter]: 'income' | 'expense' | 'both'; + }; + + declare global { + interface HTMLElementEventMap extends CustomEventMap { } + } + + export interface Store extends StoreRoot { + readonly transactionTypeFilter: StoreEventDetailMap[StoreEvents.SetFilter] + onFilterEvent(event: CustomEvent): void; + } + + class DefaultStore extends AbstractStoreRoot implements Store { + + @observable transactionTypeFilter: StoreEventDetailMap[StoreEvents.SetFilter] = 'both' + + onFilterEvent = this.createListener(StoreEvents.SetFilter, (detail) => { + console.log({detail}) + this.commit.transactionTypeFilter = detail + }) + + constructor() { + super(); + + /** + * Register the store root + */ + getApp().registerStoreRoot(this); + } + } + + export const Store = registerStore(DefaultStore, 'Store'); + + // If you are using the react or angular store you will have extra code at the end of the file + // to handle the store's dependency injection. You won't need to alter any of that section. + ``` + + + + + + + + + ```typescript + import { GridPro } from '@genesislcap/rapid-grid-pro'; + import { customElement, DOM, GenesisElement, html, observable, ref, sync } from '@genesislcap/web-core'; + import { ColDef } from '@ag-grid-community/core'; + import { EventEmitter } from '@genesislcap/foundation-events'; + import { Store, StoreEventDetailMap, StoreEvents } from '../../store'; + + @customElement({ + name: 'example-grid', + template: html``, + }) + export class ExampleGrid extends GenesisElement { + @Store store: Store; + grid: GridPro; + private columnDefs: ColDef[] = [ + { headerName: 'ID', field: 'id' }, + { headerName: 'Transaction Date', field: 'transactionDate' }, + { headerName: 'Description', field: 'description' }, + { + headerName: 'Amount', + field: 'amount', + valueFormatter: params => { + return params.value.toFixed(2); + } + }, + { headerName: 'Type', field: 'type' }, + { headerName: 'Currency', field: 'currency' }, + { + headerName: 'Balance After', + field: 'balanceAfter', + valueFormatter: params => { + return params.value.toFixed(2); + } + } + ]; + private rowData = [ + { + id: 1, + transactionDate: "2023-05-15", + description: "Quarterly Revenue Payment", + amount: 157892.45, + type: "income", + currency: "USD", + balanceAfter: 203567.89, + }, + { + id: 2, + transactionDate: "2023-05-14", + description: "Equipment Purchase", + amount: -12567.00, + type: "expense", + currency: "USD", + balanceAfter: 191000.89, + }, + { + id: 3, + transactionDate: "2023-05-13", + description: "Consulting Services Fee", + amount: 45000.00, + type: "income", + currency: "USD", + balanceAfter: 236000.89, + } + ]; + + connectedCallback() { + super.connectedCallback(); + DOM.queueUpdate(() => this.grid.gridOptions = { + defaultColDef: { + resizable: true, + filter: true, + }, + columnDefs: this.columnDefs, + rowData: this.rowData, + }) + this.store.binding( + (s) => s.transactionTypeFilter, + (detail) => (this.grid.rowData = this.rowData.filter((row) => detail === 'both' || row.type === detail)) + ) + } + } + + @customElement({ + name: 'set-filter', + template: html` + + Both + Income + Expense + + `, + }) + export class SetFilter extends EventEmitter(GenesisElement) { + @observable radioGroupValue: StoreEventDetailMap[StoreEvents.SetFilter]; + radioGroupValueChanged(_, newValue: StoreEventDetailMap[StoreEvents.SetFilter]) { + console.log(newValue); + this.$emit(StoreEvents.SetFilter, newValue); + } + } + ``` + + + + + ```typescript + import { ColDef } from '@ag-grid-community/core'; + import { useRef, useState, useEffect } from 'react'; + import { customEventFactory } from '@/pbc/utils'; + import { StoreEvents, storeService } from '../../services/store.service.ts' + + const columnDefs: ColDef[] = [ + { headerName: 'ID', field: 'id' }, + { headerName: 'Transaction Date', field: 'transactionDate' }, + { headerName: 'Description', field: 'description' }, + { + headerName: 'Amount', + field: 'amount', + valueFormatter: params => params.value.toFixed(2) + }, + { headerName: 'Type', field: 'type' }, + { headerName: 'Currency', field: 'currency' }, + { + headerName: 'Balance After', + field: 'balanceAfter', + valueFormatter: params => params.value.toFixed(2) + } + ]; + + const rowData = [ + { + id: 1, + transactionDate: "2023-05-15", + description: "Quarterly Revenue Payment", + amount: 157892.45, + type: "income", + currency: "USD", + balanceAfter: 203567.89, + }, + { + id: 2, + transactionDate: "2023-05-14", + description: "Equipment Purchase", + amount: -12567.00, + type: "expense", + currency: "USD", + balanceAfter: 191000.89, + }, + { + id: 3, + transactionDate: "2023-05-13", + description: "Consulting Services Fee", + amount: 45000.00, + type: "income", + currency: "USD", + balanceAfter: 236000.89, + } + ]; + + const ExampleGrid = () => { + const gridRef = useRef(); + + const baseGridOptions = { + defaultColDef: { + resizable: true, + filter: true, + }, + columnDefs: columnDefs, + rowData: rowData + }; + + const [gridOptions, setGridOptions] = useState(baseGridOptions); + useEffect(() => { + storeService.getStore().binding( + (s) => s.transactionTypeFilter, + (detail) => setGridOptions({...baseGridOptions, rowData: rowData.filter((row) => detail === 'both' || row.type === detail)}) + ); + }, []); + + return ( + + ); + }; + + const SetFilter = () => { + const ref = useRef(); + const [radioValue, setRadioValue] = useState('both'); + const handleRadioChange = (e: Event) => { + setRadioValue(e.target.value); + // customEventFactory implementation defined in earlier example + ref.current.dispatchEvent(customEventFactory(StoreEvents.SetFilter, e.target.value)); + } + return ( + + Both + Income + Expense + + ) + } + ``` + + + + + ```typescript + import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, OnInit, ViewChild } from '@angular/core'; + import { ColDef } from '@ag-grid-community/core'; + import { CommonModule } from '@angular/common'; + import { environment } from '../../../environments/environment'; + import { getStore, StoreEventDetailMap, StoreEvents } from '../../store'; + import { customEventFactory } from '../../../pbc/utils'; + + @Component({ + selector: 'example-grid', + standalone: true, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + template: ` + + ` + }) + export class ExampleGridComponent implements OnInit { + @ViewChild('gridRef') gridRef: any; + + columnDefs: ColDef[] = [ + { headerName: 'ID', field: 'id' }, + { headerName: 'Transaction Date', field: 'transactionDate' }, + { headerName: 'Description', field: 'description' }, + { + headerName: 'Amount', + field: 'amount', + valueFormatter: params => params.value.toFixed(2) + }, + { headerName: 'Type', field: 'type' }, + { headerName: 'Currency', field: 'currency' }, + { + headerName: 'Balance After', + field: 'balanceAfter', + valueFormatter: params => params.value.toFixed(2) + } + ]; + + rowData = [ + { + id: 1, + transactionDate: "2023-05-15", + description: "Quarterly Revenue Payment", + amount: 157892.45, + type: "income", + currency: "USD", + balanceAfter: 203567.89, + }, + { + id: 2, + transactionDate: "2023-05-14", + description: "Equipment Purchase", + amount: -12567.00, + type: "expense", + currency: "USD", + balanceAfter: 191000.89, + }, + { + id: 3, + transactionDate: "2023-05-13", + description: "Consulting Services Fee", + amount: 45000.00, + type: "income", + currency: "USD", + balanceAfter: 236000.89, + } + ]; + + baseGridOptions = { + defaultColDef: { + resizable: true, + filter: true, + }, + columnDefs: this.columnDefs, + rowData: this.rowData + }; + + gridOptions = this.baseGridOptions; + + ngOnInit() { + getStore().binding( + (s: any) => s.transactionTypeFilter, + (detail: string) => { + this.gridOptions = { + ...this.baseGridOptions, + rowData: this.rowData.filter((row) => detail === 'both' || row.type === detail) + }; + } + ); + } + } + + @Component({ + selector: 'set-filter', + standalone: true, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + template: ` + + Both + Income + Expense + + ` + }) + export class SetFilterComponent { + radioValue: StoreEventDetailMap[StoreEvents.SetFilter] = 'both'; + constructor(private el: ElementRef) {} + handleRadioChange(event: Event) { + const target = event.target as HTMLInputElement; + this.radioValue = target.value as StoreEventDetailMap[StoreEvents.SetFilter]; + // customEventFactory implementation defined in earlier example + this.el.nativeElement.dispatchEvent(customEventFactory(StoreEvents.SetFilter, this.radioValue)); + } + } + ``` + + + + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/index.mdx b/docs/001_develop/03_client-capabilities/017_state-management/index.mdx index 62bb2a7090..5ad42ccdd6 100644 --- a/docs/001_develop/03_client-capabilities/017_state-management/index.mdx +++ b/docs/001_develop/03_client-capabilities/017_state-management/index.mdx @@ -2,13 +2,225 @@ title: 'State management' sidebar_label: 'State management' id: client-state-management -keywords: [state, management] +keywords: [web, store, events, observable, binding, communication, redux, injection, state, reducer] tags: -- state -- management + - web + - store + - events + - observable + - binding + - communication + - redux + - injection + - state + - reducer sidebar_position: 17 --- -Coming soon... +import BaseStore from '../../../_includes/_base_store_setup.mdx' + +# Genesis Foundation Store + +The `foundation-store` provides a decoupled and testable way to manage application +state that adheres to our best practices. Using `foundation-store` is completely optional, as you may decide that your +application doesn't warrant a store, or that you would prefer to use a store you are more familiar with. The system is +flexible, so you can use whatever you like to handle application level state management, but you should be mindful of +degrading performance. + +## Background + +Client apps today manage application state in different ways. These apps might leverage common third party stores likes +Redux, or use none at all, peppering `@attr` and `@observable` properties (or equivalent state management in different frameworks) +in different components, and at various levels of DOM hierarchy. With the latter business logic might start creeping into components, +every component becomes smart instead of being dumb, providing shared access to data becomes difficult, tests require lots of mocks, +and things get hard to refactor etc. Large applications should aim to lift state where possible to bring all the benefits mentioned above. + +## Quick start + +If you're familiar with state management libraries such as React's Redux you may find reading through the quick start guide to be all you require. +It introduces a glossary of terms used in `foundation-store` as compared to Redux, and provides a quick look at a complete store setup. + +:::tip +If you are new to the store concept, or are unsure of the integration points from your components to the store, see the more thorough example which guides you +step-by-step [here](./client-state-management-examples). +::: + +### Equivalent Redux terms + +- **Action**. Translates to a standard [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent). +These event types are defined in a store's EventDetailMap. Here's the StoreRootEventDetailMap for example. +- **Dispatch**. Use the component's $emit method in conjunction with the [EventEmitter](../foundation-events/src/eventEmitter/README.md) mixin to strongly type it with store event maps. +- **Action Creator**. Create the CustomEvent.[detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail) +however and whenever you like. When you're ready, emit the event and detail pairing as per an EventDetailMap via the component's +$emit api, which in turn creates and sends the CustomEvent. +- **Reducer**. Use the store's [createListener](./docs/api/foundation-store.abstractstore.createlistener.md) method to +create a synchronous event listener which you can [commit values to the store](./docs/api/foundation-store.abstractstore.commit.md) from. +These listeners only receive events, so new values may come from `CustomEvent.detail` payloads, and / or reading from the store itself which these handlers are members of. +- **Effect**. Use the store's [createAsyncListener](./docs/api/foundation-store.abstractstore.createasynclistener.md) method to create an async event listener which can run Side Effects. +Similar to the Reducer context above, however you should _NOT_ commit values to the store in these, but instead +[emit](./docs/api/foundation-store.abstractstore.emit.md) outcome events, ie. success / failure, which can be handled by synchronous listeners. +- **Slice**. A store fragment. A part of the store with a specific purpose, domain model. +- **Selector**. A simple getter on a store fragment. + +### Setup + +Create a root store.ts file somewhere, for example `./store/store.ts`. This will be the root store for the application, +which may consist for other [store fragments](./client-state-management-usage#using-store-fragments). Each fragment could be considered as a domain, with a single purpose. This +setup allows us to isolate data and provide the component trees access to only the data they really need to function. + +```typescript store.ts +import {CustomEventMap, EventListenerMap, registerEmitter} from '@genesislcap/foundation-events'; +import { + AbstractStoreRoot, + StoreRoot, + StoreRootEventDetailMap, + registerStore, +} from '@genesislcap/foundation-store'; +import {observable, volatile} from '@genesislcap/web-core'; +import {DesignSystem} from './designSystem'; +import {Position} from './position'; +import {Trades} from './trades'; + +/** + * 1: Define any store custom event details for more complex payloads. + * See https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail + */ +export interface StoreZooEventDetail { + zoo: Animal[]; + location: string; +} + +/** + * 2: Define store event to event detail map. + * For the root store this should be a union of StoreRootEventDetailMap. + */ +export type StoreEventDetailMap = StoreRootEventDetailMap & { + 'store-foo': void; + 'store-bar': boolean; + 'store-zoo': StoreZooEventDetail; // < details with more than one property + 'store-zoo-animals': Animal[]; +} + +/** + * 3: Extend built in event maps so that addEventListener/removeEventListener are aware of our events for code completion + */ +declare global { + interface HTMLElementEventMap extends CustomEventMap {} +} + +/** + * 4: Define internal event to event detail map. + */ +type InternalEventDetailMap = { + 'store-zoo-success': SomeResponse; + 'store-zoo-error': Error; +} + +/** + * 5: Define entire readonly store made up of store fragments and or additional properties. + */ +export interface Store extends StoreRoot { + /** + * Store properties + */ + readonly prop1: number; + readonly prop2: number; + readonly someToggle: boolean; + readonly derivedData: number; + readonly volatileDerivedData: number; + /** + * Store fragments + */ + readonly designSystem: DesignSystem; + readonly notifications: Notifications; + readonly positions: Positions; + readonly trades: Trades; + /** + * Store event handlers + */ + onFooEvent(event: CustomEvent): void; + onBarEvent(event: CustomEvent): void; + onZooEvent(event: CustomEvent): void; +} + +/** + * 6: Define the default implementation + */ +class DefaultStore extends AbstractStoreRoot implements Store { + /** + * Store properties + */ + @observable prop1: number = 10; + @observable prop2: number = 20; + @observable someToggle: boolean = true; + + constructor( + /** + * 7: Inject any store fragments + */ + @DesignSystem readonly designSystem: DesignSystem, + @Notifications readonly notifications: Notifications, + @Positions readonly positions: Positions, + @Trades readonly trades: Trades, + ) { + super(...arguments); + + /** + * 8: Listeners not on the public interface can be created anonymously if you prefer + */ + this.createListener('store-zoo-succes', (detail) => { + const {prop1, prop2, ...rest} = detail; + this.commit.prop1 = prop1; + this.commit.prop2 = prop2; + }); + this.createErrorListener('store-zoo'); + } + + /** + * 8: Define your event listeners as per the interface. Please ensure you do so using arrow functions to aid binding. + * These handlers can be async if you would like to do some async work in them. We suggest you don't commit store + * mutations in async functions, instead raise an event which you can handle synchronously and commit from there so + * things are tracked correctly. + */ + onFooEvent = this.createListener('store-foo', detail => {...}); + onBarEvent = this.createListener('store-bar', detail => this.commit.someToggle = detail); // < commit values to the store synchronously + onZooEvent = this.createAsyncListener('store-zoo', async (detail) => + this.invokeAsyncAPI( + async () => this.someAsyncTask(detail), // < likely an injected service, + 'store-zoo-error', + 'store-zoo-success' + ) + ); + + /** + * 9: Create getters for common derived data needs, similar to selectors in the Redux sense. These however do not + * need any special code as they are computing based on properties that are already observable. Derivied data with + * branching code paths needs to be marked as volatile. + */ + + get derivedData(): number { + return this.prop1 * this.prop2; + } + + @volatile + get volatileDerivedData() { + return this.someToggle ? this.prop1 * this.prop2 : this.prop1 + this.prop2; + } +} + +/** + * 10: Register the store which defines the DI key using the interface + */ +export const Store = registerStore(DefaultStore, 'RootStore'); + +// React and angular stores require a layer to work with the dependency injection. See later code snippet in this file +``` + +Your root store is now ready to be injected into your application. Hopefully the above gives you a good idea of general +store setup. The example might look a bit verbose, but in reality you can write a small store fragment in 20+ lines of +code. For example: + + + +## [API Docs](./docs/api/index.md) -Make clear for web client state, separate from Server state machine component \ No newline at end of file diff --git a/docs/001_develop/03_client-capabilities/017_state-management/legacy-setup.mdx b/docs/001_develop/03_client-capabilities/017_state-management/legacy-setup.mdx new file mode 100644 index 0000000000..039d57bd1d --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/legacy-setup.mdx @@ -0,0 +1,344 @@ +--- +title: 'Legacy project setup' +sidebar_label: 'Legacy setup' +id: client-state-management-legacy-setup +keywords: [web, store, events, observable, binding, communication, redux, injection, state, reducer] +tags: + - web + - store + - events + - observable + - binding + - communication + - redux + - injection + - state + - reducer +sidebar_position: 18 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import BaseStore from '../../../_includes/_base_store_setup.mdx' +import EventWrapper from '../../../_includes/_store_event_wrapper.mdx' + +This page guides you through installing the `foundation-store` in your legacy project. This isn't recommended - all new applications +created via Genesis Create or `genx` will install the store for you, and then you can just follow the [example tutorial](../client-state-management-examples). However, if you're adding Genesis into an existing project (or you're using an old Genesis project from before the store was a default component) you can follow this guide. + +## Installation + +1. Add `@genesislcap/foundation-store` as a dependency in your `package.json` file. Whenever you change the dependencies of your project, ensure you run the `$ npm run bootstrap` (or `npm install` for React and Angular) command again. + +```json +{ + ... + "dependencies": { + ... + "@genesislcap/foundation-store": "latest" + ... + }, + ... +} +``` + +Next you need to add in the base store configuration. Once you've followed this page and the store is setup, you can then configure the store for your use. + + + +## Genesis router wiring + +If you are using a Genesis syntax project then you need to wire in the store into your application router. Components that interact with the store will individually require access, but there are steps required at the top level to create the store service. You need to inject the store into the class where you use the router in the template. This is very likely to be your `MainApplication` class. + +`main.ts` +```typescript {10} +@customElement({ + name, + template, + styles, +}) +export class MainApplication extends EventEmitter(GenesisElement) { + @App app: App; + @Connect connect!: Connect; + @Container container!: Container; + @Store store: Store; +} +``` + +`main.template.ts` +```typescript {4} +export const MainTemplate: ViewTemplate = html` + x.config} + :store=${(x) => x.store} + > +`; +``` + +## Store initialisation events + +In addition to the events that you create to handle your business logic, you'll also need to emit events to setup and control +the state of the store itself. + +### `'store-connected'` + +In your main application class you need to fire a `'store-connected'` event in-order to +fully initialise the store. + +The `'store-connected'` event handler needs to be explicitly bound. When the root store handles `'store-connected'`, +it auto binds all the store event listeners to the rootElement. + +At this point you can start emitting strongly typed store events, and they will be handled by their corresponding store. +See [EventEmitter](src/.foundation/events/eventEmitter/README.md) for more information. + +### `'store-ready'` + +As you may be required to do some additional work between the initialisation of the store and the application use, there is +an additional `store-ready` event to dispatch. It's not a hard requirement to emit this, but is considered best practice. If you've no work to +do, you can just emit this right after `'store-connected'`. + +```typescript +this.$emit('store-connected', this); +/** + * Do some other work if needed. + */ +this.$emit('store-ready', true); +``` + +### `'store-disconnected'` + +Emitting `'store-disconnected'` will remove all the previously bound event listeners. + +```typescript +// ./main/main.ts + +disconnectedCallback() { + super.disconnectedCallback(); + this.$emit('store-disconnected'); +} +``` +### Configuration example + +The following snippets are examples of your main application class dispatching the required events + + + + + +Example Main class in Genesis. Highlighted lines are directly related to initialising the store - other configuration may be different in +your application. Other functionality that the main class may be required to perform is omitted from this example. + +```typescript {2-3,14,25-26,34-35,42-58} +/** + * @fires store-connected - Fired when the store is connected. + * @fires store-ready - Fired when the store is ready. + */ +@customElement({ + name, + template, + styles, +}) +export class MainApplication extends EventEmitter(GenesisElement) { + @App app: App; + @Connect connect!: Connect; + @Container container!: Container; + @Store store: Store; + + @inject(MainRouterConfig) config!: MainRouterConfig; + + @observable provider!: any; + @observable ready: boolean = false; + @observable data: any = null; + + async connectedCallback() { + this.registerDIDependencies(); + super.connectedCallback(); + this.addEventListeners(); + this.readyStore(); + DOM.queueUpdate(() => { + configureDesignSystem(this.provider, designTokens); + }); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.removeEventListeners(); + this.disconnectStore(); + } + + selectTemplate() { + return this.ready ? MainTemplate : LoadingTemplate; + } + + protected addEventListeners() { + this.addEventListener('store-connected', this.store.onConnected); + } + + protected removeEventListeners() { + this.removeEventListener('store-connected', this.store.onConnected); + } + + protected readyStore() { + // @ts-ignore + this.$emit('store-connected', this); + this.$emit('store-ready', true); + } + + protected disconnectStore() { + this.$emit('store-disconnected'); + } +} +``` + + + + +In our App class in React we'll need the DOM reference to the root DOM element. This is likely an +element with the id `'root'`. Here is a code snippet you can use to get the reference and pass it +as a prop. + +```typescript +function bootstrapApp() { + const rootEelement = document.getElementById('root'); + if (rootEelement) { + ReactDOM.createRoot(rootEelement!).render( + + + , + ) + } +} +``` + +Example Main class in React. Highlighted lines are directly related to initialising the store - other configuration may be different in +your application. Other functionality that the main class may be required to perform is omitted from this example. + +```typescript tsx {1-3,5-12,20-25,28-31,33} +interface AppProps { + rootElement: HTMLElement; +} + +const App: React.FC = ({ rootElement }) => { + const [isStoreConnected, setIsStoreConnected] = useState(false); + const dispatchCustomEvent = (type: string, detail?: any) => { + rootElement.dispatchEvent(customEventFactory(type, detail)); + }; + const handleStoreConnected = (event: CustomEvent) => { + storeService.onConnected(event); + }; + + setApiHost(); + genesisRegisterComponents(); + configureFoundationLogin({ router: history }); + + useEffect(() => { + registerStylesTarget(document.body, 'main'); + if (!isStoreConnected) { + rootElement.addEventListener('store-connected', handleStoreConnected); + dispatchCustomEvent('store-connected', rootElement); + dispatchCustomEvent('store-ready', true); + setIsStoreConnected(true); + } + + return () => { + if (isStoreConnected) { + rootElement.removeEventListener('store-connected', handleStoreConnected); + dispatchCustomEvent('store-disconnected'); + } + }; + }, [isStoreConnected]); + + return ( + + + + + } /> + + + + + ); +}; +``` + + + + + + +Example Main class in Angular. Highlighted lines are directly related to initialising the store - other configuration may be different in +your application. Other functionality that the main class may be required to perform is omitted from this example. + +```typescript {9,26-27,33-34,41-60} +@Component({ + selector: 'fixedincome-root', + templateUrl: './app.component.html', + styleUrl: './app.component.css', +}) +export class AppComponent implements OnInit, OnDestroy, AfterViewInit { + layoutName?: LayoutComponentName; + title = 'Fixed Income'; + store = getStore(); + + constructor( + private el: ElementRef, + router: Router, + ) { + configureFoundationLogin({ router }); + + // Set layout componet based on route + router.events.subscribe((event: any) => { + if (event instanceof NavigationEnd) { + this.layoutName = getLayoutNameByRoute(event.urlAfterRedirects); + } + }); + } + + ngOnInit() { + this.addEventListeners(); + this.readyStore(); + registerStylesTarget(this.el.nativeElement, 'main'); + this.loadRemotes(); + } + + ngOnDestroy() { + this.removeEventListeners(); + this.disconnectStore(); + } + + async loadRemotes() { + await registerComponents(); + } + + addEventListeners() { + this.el.nativeElement.addEventListener('store-connected', this.store.onConnected); + } + + removeEventListeners() { + this.el.nativeElement.removeEventListener('store-connected', this.store.onConnected); + } + + readyStore() { + this.dispatchCustomEvent('store-connected', this.el.nativeElement); + this.dispatchCustomEvent('store-ready', true); + } + + disconnectStore() { + this.dispatchCustomEvent('store-disconnected'); + } + + dispatchCustomEvent(type: string, detail?: any) { + this.el.nativeElement.dispatchEvent(customEventFactory(type, detail)); + } + + ngAfterViewInit() { + } + +} +``` + + + + + + diff --git a/docs/001_develop/03_client-capabilities/017_state-management/usage-best-practise.mdx b/docs/001_develop/03_client-capabilities/017_state-management/usage-best-practise.mdx new file mode 100644 index 0000000000..a708959fa8 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/017_state-management/usage-best-practise.mdx @@ -0,0 +1,408 @@ +--- +title: 'Usage and best practise' +sidebar_label: 'Usage and best practise' +id: client-state-management-usage +keywords: [web, store, events, observable, binding, communication, redux, injection, state, reducer] +tags: + - web + - store + - events + - observable + - binding + - communication + - redux + - injection + - state + - reducer +sidebar_position: 17 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Store bindings + +To use the values in the store you'll need to bind the properties of the getter inside of your components. There are various ways which are covered in this section. + +You will need to inject a reference to the store in your component whenever you want to use it. + + + + + + ```typescript + @customElement({ + name: 'using-store', + }) + export class UsingStore extends GenesisElement { + @Store store: Store; + connectedCallback() { + super.connectedCallback(); + // access the store + this.store.myStoreVariable; + this.store.binding(); + } + } + ``` + + + + + ```typescript + const UsingStore = () => { + + useEffect(() => { + // access the store + storeService.getStore().myStoreVariable; + storeService.getStore().binding(); + }, []); + + return (

Example

); + }; + ``` + + +
+ + + ```typescript + @Component({ + selector: 'using-store', + standalone: true, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + export class WithStore implements OnInit { + + ngOnInit() { + // access the store + getStore().myStoreVariable; + getStore().binding(); + } + } + + ``` + + +
+ +### Template store bindings + +:::tip +You can only bind directly in the template when using Genesis syntax components. However, you can still apply these concepts +such as setting properties or choosing different HTML fragments in any framework you're using. +::: + +You may wish to monitor the value of `this.store.ready` via template bindings to conditional render a +loading phase in your template. + +```typescript +// ./main/main.template.ts + +${when(x => !x.store.ready, html`Loading...`)} +``` + +...or monitor the value indirectly, to swap out the entire template. + +```typescript +// ./main/main.ts + +selectTemplate() { + return this.store.ready ? MainTemplate : LoadingTemplate; +} + +// ./main/main.template.ts + + +``` + +You may also set properties and attributes of components in the template directly from values in the store. +```typescript + + x.store.tradesFilterCriteria} + > + +``` + +### Direct store bindings + +:::tip +Remember in the following examples you might access the store in a subtly different way depending on which framework you're using. [See here](#store-bindings). +::: + +To bind to the store outside the template engine, you use the store's `binding()` api. The store's `binding()` api is +strongly typed, so you will be unable to bind to a non-existent property or getter. You also don't need to think about +if the data point is a property or a getter, perhaps returning derived data, as the api works the same for both cases. + +```typescript +// TS knows the returned type of value +this.store.binding(x => x.ready, value => {...}); +this.store.binding(x => x.someNumericProp, value => {...}); + +// TS needs some help to type the returned value, so you need to provide that (we hope to fix this). +this.store.binding('ready', value => {...}); +this.store.binding('someNumericProp', value => {...}); + +// You can even create your own derived binding that are not already defined as getters in the store +this.store.binding(x => x.prop1 * x.prop2, value => {...}); + +// These can even be volatile providing you pass true for isVolatileBinding +this.store.binding(x => x.someToggle ? x.prop1 : x.prop2, value => {...}, true); +``` + +Here is an example of using the underlying +[bindingObserver](https://www.fast.design/docs/fast-element/observables-and-state#bindings) as per docs. + +```typescript +this.store.binding(x => x.prop1).subscribe({ + handleChange(source) { // < note that the source is the bindingObserver itself, x => x.prop1, and not the value of the change + ... + } +}) +``` + +Updates are batch processed, so if a bunch of properties are updated in the store that any of the stores getters or +derived bindings you've passed in via the `binding()` api use, they will by design tick only once to avoid +any unnecessary updates and potential re-renders. + +### Direct store bindings as RxJS + +Stores also offer a `bindingAsRx()` api which returns a Rxjs Observable to allow you to observe a value using Rxjs which +may be useful depending on the needs of your application. + +```typescript +const entireStore$ = this.store.bindingAsRx().subscribe(value => {...}); + +const ready$ = this.store.bindingAsRx('ready').subscribe(value => {...}); + +const prop1$ = this.store.bindingAsRx(x => x.prop1).subscribe(value => {...}); +``` + +### Binding API + +See the API docs for the different binding methods [here](../docs/api/foundation-store.store/#methods). + + + +## Best practises + +### Committing value mutations + +Stores are read only, so if you try to set a property directly TS will flag this. To commit a value to the store you +must emit a known store event. In these event handlers, you call `this.commit` prepended with the value you want to +mutate. For example: + +```typescript +// some trades store fragment + +onTradeSelected = this.createListener('trade-selected', trade => this.commit.selectedTrade = trade); +``` + +The `this.commit` interface is typed the same as the store fragment itself for full code completion, and simply acts as +a proxy to the underlying store. Currently, an alternative api also exists called commitValue if you prefer. + +```typescript +onTradeSelected = this.createListener('trade-selected', trade => this.commitValue('selectedTrade', trade)); +``` + +The store won't complain if you forget or just don't want to use the commit api, it just means value changes won't be +tracked overtime. + +```typescript +onTradeSelected = this.createListener('trade-selected', trade => this.selectedTrade = trade); +``` + +### Side effects + +When you need to reach out to another part of the system or generally do some async work, you should ensure the initial +event handler is async. We recommend that you don't commit values in this handler, as it may become difficult to track +mutations overtime if other events are occurring, but it's really up to you if you want to just await and commit. +Ideally we want store interactions to be standardised, predictable and traceable. + +```typescript +constructor( + @TradeEntry readonly tradeEntry: TradeEntry, + @TradesService readonly service: TradesService, +) { + super(tradeEntry); // < only pass the super child sub store fragments + + /** + * Listeners not on the public interface can be created anonymously if you prefer + */ + this.createListener('trades-load-success', trades => this.commit.trades = trades); + this.createErrorListener('trades-load-error', () => this.commit.trades = undefined); +} + +onTradesLoad = this.createAsyncListener('trades-load', async (positionId) => + this.invokeAsyncAPI( + async () => this.service.getTrades(positionId), + 'trades-load-error', + 'trades-load-success' + ) +); +``` + +### Errors + +When you use `this.createErrorListener`, stores will automatically keep track of any errors that may occur in a +`store.errors` map that is keyed by the event type. This means your store can hold multiple errors at a time. To output +all the messages at once you can bind to `store.errors.messages`, for example: + +```typescript jsx +
${x => x.trades.errors.messages}
+``` + +You can check for specific errors too: + +```typescript jsx +${when(x => x.trades.errors.has('trade-insert-error'), html`...`)} +``` + +This granularity can be useful if say you wanted to raise error notifications and allow users to clear errors one-by-one. + +```typescript +this.trades.errors.delete('trade-insert-error'); + +// or clear them all +this.trades.errors.clear(); +``` + +Please note that `store.errors` is not a true `Map` so doesn't have all the native `Map` apis, however it should have +most of what you need. You also operate on the errors map directly without raising events. This is like this for +simplicity, and because errors are transient in nature. + +### UI state in stores + +You may want to keep your UI state in your stores alongside the application data. If so consider splitting these up into +separate [fragments](#using-store-fragments), for example: + +- TradeEntry: Trade entry field values (Data). +- TradeEntryUI: Trade entry presentation status, isOpen, isLoading etc. (UI State). + +UI state can of course be kept in the component itself, however there may be reasons to keep it in the store. Consider +if you needed to know if the trade entry was being presented on-screen in a different part of the application. You could +try to listen for the `'trade-entry-ui-open'` event, but until the event hits the target store and is committed, the +true state isn't guaranteed. The store should be the single source of truth. + +With that in mind, we could inject that UI state store fragment and bind to the value of interest, for example: + +```typescript +// some other part of the UI + +this.tradeEntryUI.binding(x => x.isOpen, value => value ? this.stop() : this.start()); +``` + +Keeping UI state in a store also might make it easier for you to save a snapshot of the UI state for rehydration, ie. +what windows did the user have open etc. You may wish to add some middleware to the commit proxy (base store concept) +for converting state transitions into browser history entries, which might allow you to deep link and press browser back +button to say close the modal, with each back button moving backwards though the UI state. Be careful not to blur the +lines between the data and the UI state in our store fragments. + +For example a UI conditional could map to a UI sub fragment, whereas a datapoint could map to the parent fragment. The +component would only need tradeEntry injected to have access to both, depending on how we structure fragments. + +```typescript jsx +${when(x => x.tradeEntry.ui.isOpen, html` + + x.tradeEntry.quantity}> + +`} +``` + +## Using store fragments + +As your application grows, it's possible that the `store.ts` definition becomes extremely large - especially as your handlers (reducers) grow in complexity, mutating nested objects. Instead of having all properties and handlers defined on the root store you can split up different domains into different sub-stores (referred to here as store fragments). To use a store fragment in your custom element simply inject it using its `interface` as the DI key. + +```typescript +export type EventMap = TradeEntryEventDetailMap; + +export class TradeEntryForm extends EventEmitter(GenesisElement) { + @TradeEntry tradeEntry: TradeEntry; +} +``` + +Now in your template you can use the values from the store fragment and raise typed events. + +````typescript jsx +import type {TradeEntryForm, EventMap} from './home'; + +const inputEmit = createInputEmitter(); + + x.tradeEntry.price} + @change="${inputEmit('trade-entry-price-changed')}" +> +```` + +`createEmitChangeAs` is a utility from `@genesislcap/foundation-events` to allow components with string change inputs +to emit their string values as typed event detail payloads. It will warn you if you try to use it with a component that +doesn't have a string target.value type. It's a convenience method only and is the same as writing: + +```typescript jsx + x.tradeEntry.price} + @change="${(x, c) => x.$emit('trade-entry-price-changed', targetValue(c))}" +> +```` + +We will be adding a number of these for Selects and other primitives. You can of course call to a class method and have +that emit the typed event to the store, but the idea with the helpers are to remove some boilerplate and misdirection. + +```typescript jsx + x.instrumentIdChange(c.event.target as Select))}> +``` + +```typescript +instrumentIdChange(target: Select) { + this.$emit('trade-entry-instrumentID-changed', target.selectedOptions[0]?.value); +} +``` + +If the component in question is perhaps a library asset or simply needs to remain unaware of the DI, you can map store +properties to the custom element via its attributes. + +```typescript jsx +// ./some-parent/some-parent.template.ts + + +``` + +If the components are specific to your application and won't be shared, and you've split up your store into appropriate +domain specific fragments, injecting the store fragment directly into where it's needed will greatly reduce mapping +boilerplate. Remember components should only have access to the data they need. If the injected store fragment provides +them more than that, consider splitting that store fragment up further, or reworking your store structure. + +### Accessing values from one store fragment to another + +If you need to read values from another part of the store in your store you may be able to simple inject it, but you +won't need to pass it to the super class for processing if that's done elsewhere. If you would rather not inject it, you +can use `this.root.fragmentX.fragmentY.value`. You should provide root store type information when creating your store +fragment that will be reading root, for example: +```typescript +import type {Store as StoreRoot} from '../store'; +... +class DefaultPositions extends AbstractStore implements Positions { + onPositionSelected = this.createListener('position-selected', (position) => { + const lastTradeId = this.root.trades.selectedTrade.id; + ... + }); +} +``` + +### Testing with the injected dependency in place + +Using a DI based store is very powerful as it allows us to swap out our store fragments when needed, ie. a unit test: + +```typescript jsx +const mock = new TradeEntryMock(); +const Suite = createComponentSuite('TradeEntryForm Test', () => tradeEntryForm(), null, [ + Registration.instance(TradeEntry, mock), +]); +``` + +The code in your component remains as is. diff --git a/docs/001_develop/03_client-capabilities/021_desktop-interoperability/01_foundation-fdc3.mdx b/docs/001_develop/03_client-capabilities/021_desktop-interoperability/01_foundation-fdc3.mdx index 74ecd9f56a..1b1ea7714a 100644 --- a/docs/001_develop/03_client-capabilities/021_desktop-interoperability/01_foundation-fdc3.mdx +++ b/docs/001_develop/03_client-capabilities/021_desktop-interoperability/01_foundation-fdc3.mdx @@ -99,4 +99,9 @@ export class MyComponent extends FASTElement { The FDC3 service is designed to be easily integrated and configured within your application using dependency injection. This approach allows for flexible instantiation and usage of the FDC3 API, ensuring that financial desktop applications can leverage standard communication protocols for enhanced interoperability. +## License +Note: this project provides front-end dependencies and uses licensed components listed in the next section; thus, licenses for those components are required during development. Contact [Genesis Global](https://genesis.global/contact-us/) for more details. + +### Licensed components +Genesis low-code platform diff --git a/docs/001_develop/03_client-capabilities/021_desktop-interoperability/01_foundation-openfin.mdx b/docs/001_develop/03_client-capabilities/021_desktop-interoperability/01_foundation-openfin.mdx index 9c96374df1..3c81285848 100644 --- a/docs/001_develop/03_client-capabilities/021_desktop-interoperability/01_foundation-openfin.mdx +++ b/docs/001_develop/03_client-capabilities/021_desktop-interoperability/01_foundation-openfin.mdx @@ -83,4 +83,9 @@ This package depends on a newer version of typescript which you will also need t > ``` +## License +Note: this project provides front-end dependencies and uses licensed components listed in the next section; thus, licenses for those components are required during development. Contact [Genesis Global](https://genesis.global/contact-us/) for more details. + +### Licensed components +Genesis low-code platform diff --git a/docs/_includes/_base_store_setup.mdx b/docs/_includes/_base_store_setup.mdx new file mode 100644 index 0000000000..9a3718b2ae --- /dev/null +++ b/docs/_includes/_base_store_setup.mdx @@ -0,0 +1,87 @@ + +### Base store file + +```typescript store.ts +import {CustomEventMap} from '@genesislcap/foundation-events'; +import {AbstractStore, Store, registerStore} from '@genesislcap/foundation-store'; +import {observable} from '@genesislcap/web-core'; + +export interface Store extends StoreRoot {} + +export type StoreEventDetailMap = StoreRootEventDetailMap & {}; + +declare global { + interface HTMLElementEventMap extends CustomEventMap {} +} + +class DefaultStore extends AbstractStoreRoot implements Store { + constructor() { + super(); + + /** + * Register the store root + */ + getApp().registerStoreRoot(this); + } +} + +export const Store = registerStore(DefaultStore, 'Store'); + +// React and angular stores require a layer to work with the dependency injection. See following code sections. +``` + +### Angular injection layer + +To be able to access the store from your Angular components you need a class to wrap up the store dependency. +You can add this to the bottom of the `store.ts` file. + +```typescript +import { DI } from '@genesislcap/web-core'; + +export function getStore(): Store { + return DI.getOrCreateDOMContainer().get(Store) as Store; +} +``` + +You can then access the store via the `getStore` function. + +```typescript +import { getStore } from './path/to/store'; + +getStore(); // access the store +``` + +### React injection layer + +To be able to access the store from your React components you need a class to wrap up the store dependency. +You can add this to the bottom of the `store.ts` file. + +```typescript +import { DI } from '@genesislcap/web-core'; + +class StoreService { + private store: any; + + constructor() { + this.store = DI.getOrCreateDOMContainer().get(Store) as Store; + } + + getStore() { + return this.store; + } + + onConnected(event?: CustomEvent) { + this.store.onConnected(event); + } +} + +export const storeService = new StoreService(); +``` + +You can then access the store via the `storeService` import and using the getter function. + +```typescript +import { storeService } from './path/to/store'; + +storeService.getStore(); // access the store +``` diff --git a/docs/_includes/_store_event_wrapper.mdx b/docs/_includes/_store_event_wrapper.mdx new file mode 100644 index 0000000000..80453924d4 --- /dev/null +++ b/docs/_includes/_store_event_wrapper.mdx @@ -0,0 +1,13 @@ +`customEventFactory` is a helper function to create events in the format the store expects. You can either import it from the PBC module if you're using it, or copy this function into your codebase. + + ```typescript + export function customEventFactory(type: string, detail?: any) { + return new CustomEvent(type, { + bubbles: true, + cancelable: true, + composed: true, + detail, + }); + } + ``` + diff --git a/package-lock.json b/package-lock.json index 5e31dd63fd..336d832592 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "@genesislcap/foundation-notifications": "14.217.6", "@genesislcap/foundation-openfin": "14.218.0", "@genesislcap/foundation-reporting": "14.217.6", + "@genesislcap/foundation-store": "14.217.6", "@genesislcap/foundation-testing": "14.217.6", "@genesislcap/foundation-zero": "14.217.6", "@genesislcap/g2plot-chart": "14.217.6", diff --git a/package.json b/package.json index 5b082707f7..c091db7523 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@genesislcap/foundation-comms": "14.217.6", "@genesislcap/foundation-criteria": "14.217.6", "@genesislcap/foundation-entity-management": "14.217.6", + "@genesislcap/foundation-fdc3": "14.217.6", "@genesislcap/foundation-filters": "14.217.6", "@genesislcap/foundation-forms": "14.217.6", "@genesislcap/foundation-header": "14.217.6", @@ -51,9 +52,9 @@ "@genesislcap/foundation-notifications": "14.217.6", "@genesislcap/foundation-openfin": "14.218.0", "@genesislcap/foundation-reporting": "14.217.6", + "@genesislcap/foundation-store": "14.217.6", "@genesislcap/foundation-testing": "14.217.6", "@genesislcap/foundation-zero": "14.217.6", - "@genesislcap/foundation-fdc3": "14.217.6", "@genesislcap/g2plot-chart": "14.217.6", "@genesislcap/grid-pro": "14.217.6", "@genesislcap/grid-tabulator": "14.217.6", diff --git a/plugins/api-docs/dist/manifest.js b/plugins/api-docs/dist/manifest.js index 62edccf181..45ca9ab91b 100644 --- a/plugins/api-docs/dist/manifest.js +++ b/plugins/api-docs/dist/manifest.js @@ -332,5 +332,49 @@ exports.default = { ], }, }, + { + name: "@genesislcap/foundation-store", + enabled: true, + src: { + api_docs: "./docs/api", + readme: "./README.md", + }, + output: { + directory: "./docs/001_develop/03_client-capabilities/017_state-management/", + api_docs: "docs/api", + readme: "17_foundation-store.mdx", + keywords: [ + "web", + "store", + "events", + "observable", + "binding", + "communication", + "redux", + "injection", + "state", + "reducer", + ], + tags: [ + "web", + "store", + "events", + "observable", + "binding", + "communication", + "redux", + "injection", + "state", + "reducer", + ], + pages: [ + { + title: "State management", + sidebar_label: "State management", + id: "client-state-management", + }, + ], + }, + }, ], }; diff --git a/plugins/api-docs/processedMap.js b/plugins/api-docs/processedMap.js index 2d59265889..9056148965 100644 --- a/plugins/api-docs/processedMap.js +++ b/plugins/api-docs/processedMap.js @@ -20,4 +20,5 @@ module.exports = { "@genesislcap/grid-pro": "14.217.6", "@genesislcap/grid-tabulator": "14.217.6", "@genesislcap/foundation-notifications": "14.217.6", + "@genesislcap/foundation-store": "14.217.6", }; diff --git a/plugins/api-docs/src/manifest.ts b/plugins/api-docs/src/manifest.ts index 5bda2845e5..ea55779076 100644 --- a/plugins/api-docs/src/manifest.ts +++ b/plugins/api-docs/src/manifest.ts @@ -539,5 +539,49 @@ export default { ], }, }, + { + name: "@genesislcap/foundation-store", + enabled: true, + src: { + api_docs: "./docs/api", + readme: "./README.md", + }, + output: { + directory: "./docs/001_develop/03_client-capabilities/017_state-management/", + api_docs: "docs/api", + readme: "17_foundation-store.mdx", + keywords: [ + "web", + "store", + "events", + "observable", + "binding", + "communication", + "redux", + "injection", + "state", + "reducer", + ], + tags: [ + "web", + "store", + "events", + "observable", + "binding", + "communication", + "redux", + "injection", + "state", + "reducer", + ], + pages: [ + { + title: "State management", + sidebar_label: "State management", + id: "client-state-management", + }, + ], + }, + }, ], };