Legacy project setup
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. 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
- Add
@genesislcap/foundation-store
as a dependency in yourpackage.json
file. Whenever you change the dependencies of your project, ensure you run the$ npm run bootstrap
(ornpm install
for React and Angular) command again.
{
...
"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.
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
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
@customElement({
name,
template,
styles,
})
export class MainApplication extends EventEmitter<StoreEventDetailMap>(GenesisElement) {
@App app: App;
@Connect connect!: Connect;
@Container container!: Container;
@Store store: Store;
}
main.template.ts
export const MainTemplate: ViewTemplate<MainApplication> = html`
<foundation-router
:config=${(x) => x.config}
:store=${(x) => x.store}
></foundation-router>
`;
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 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'
.
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.
// ./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
- Genesis
- React
- Angular
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.
/**
* @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<StoreEventDetailMap>(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.
function bootstrapApp() {
const rootEelement = document.getElementById('root');
if (rootEelement) {
ReactDOM.createRoot(rootEelement!).render(
<React.StrictMode>
<App rootElement={rootEelement} />
</React.StrictMode>,
)
}
}
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.
interface AppProps {
rootElement: HTMLElement;
}
const App: React.FC<AppProps> = ({ 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 (
<AuthProvider>
<RoutesProvider>
<HistoryRouter history={history as any}>
<Routes>
<Route path="*" element={<DynamicLayout />} />
</Routes>
</HistoryRouter>
</RoutesProvider>
</AuthProvider>
);
};
customEventFactory
is a helper function to create events in the expected payload format. You can either import it from the PBC module if you're using it, or copy this function into your codebase.
export function customEventFactory(type: string, detail?: any) {
return new CustomEvent(type, {
bubbles: true,
cancelable: true,
composed: true,
detail,
});
}
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.
@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() {
}
}
customEventFactory
is a helper function to create events in the expected payload format. You can either import it from the PBC module if you're using it, or copy this function into your codebase.
export function customEventFactory(type: string, detail?: any) {
return new CustomEvent(type, {
bubbles: true,
cancelable: true,
composed: true,
detail,
});
}