Skip to main content

Examples and tutorial

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.

In your project you should have a blank store setup that looks like the following:

Base store file

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<StoreEventDetailMap> {}
}

class DefaultStore extends AbstractStoreRoot<Store, StoreEventDetailMap> 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.

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.

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.

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.

import { storeService } from './path/to/store';

storeService.getStore(); // access the store

Test components

We're going to be linking two components together using a variable in the store. The first component is an example <grid-pro>. Open the collapsed section to see the mock data.

Sample data used to configure the example grid

@customElement({
name: 'example-grid',
template: html`<rapid-grid-pro ${ref('grid')}></rapid-grid-pro>`,
})
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
})
}
}

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.

@customElement({
name: 'set-filter',
template: html`
<rapid-radio-group
value="${sync((x) => x.radioGroupValue)}"
>
<rapid-radio value="both">Both</rapid-radio>
<rapid-radio value="income">Income</rapid-radio>
<rapid-radio value="expense">Expense</rapid-radio>
</rapid-radio-group>
`,
})
export class SetFilter extends GenesisElement {
@observable radioGroupValue: string;
radioGroupValueChanged(_, newValue: string) {
console.log(newValue);
}
}

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.
export enum StoreEvents {
SetFilter = 'set-filter',
}
  1. 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.
export type StoreEventDetailMap = StoreRootEventDetailMap & {
[StoreEvents.SetFilter]: 'income' | 'expense' | 'both';
};
  1. 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.
export interface Store extends StoreRoot {
readonly transactionTypeFilter: StoreEventDetailMap[StoreEvents.SetFilter]
onFilterEvent(event: CustomEvent<StoreEventDetailMap[StoreEvents.SetFilter]>): void;
}
  1. 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.
class DefaultStore extends AbstractStoreRoot<Store, StoreEventDetailMap> implements Store {

@observable transactionTypeFilter: StoreEventDetailMap[StoreEvents.SetFilter] = 'both';

onFilterEvent = this.createListener<StoreEventDetailMap[StoreEvents.SetFilter]>(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.

@customElement({
name: 'set-filter',
template: html`
<rapid-radio-group
value="${sync((x) => x.radioGroupValue)}"
>
<rapid-radio value="both">Both</rapid-radio>
<rapid-radio value="income">Income</rapid-radio>
<rapid-radio value="expense">Expense</rapid-radio>
</rapid-radio-group>
`,
})
export class SetFilter extends EventEmitter<StoreEventDetailMap>(GenesisElement) {
@observable radioGroupValue: StoreEventDetailMap[StoreEvents.SetFilter];
radioGroupValueChanged(_, newValue: StoreEventDetailMap[StoreEvents.SetFilter]) {
this.$emit(StoreEvents.SetFilter, newValue);
}
}

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 <grid-pro-client-side-datasource> which means the filtering cannot be done via a template binding directly. Instead the binding property on a reference to the store is used.

@customElement({
name: 'example-grid',
template: html`<rapid-grid-pro ${ref('grid')}></rapid-grid-pro>`,
})
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))
)
}
}

The 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.

Complete example

Full example code for the store

Full example code for the two components