Angular elements with NGXS
A lot has been written about NGXS (State Management) and Angular elements (Custom Web Components), but when you would like to combine them a lot less has been publish about this particular subject. For example, is it even possible to use NGXS together with angular elements?
TL;DR — of course :)
In this post I’ve put all the pieces together to achieve the above goal, because a small mistake, and I can tell from experience, can sent you on a mision impossible bug hunt due to very complex/cryptic error messages. There is no fun in doing that and it might prevent you from actually experiencing the true fun and power of both libraries working together!
Setup
First, create a new project using Angular CLI
$> ng new demo
Change directory into the newly created folder and install angular elements
$> yarn add @angular/elements
Now you’re ready to turn any angular component into a custom element.
For NGXS you’ll need to add the following packages
$> yarn add @ngxs/store
$> yarn add @ngxs-labs/dispatch-decorator @ngxs/devtools-plugin
Sometimes using one or more angular projects on a single page, the bundles collide and nothing will work. This has todo with the fact that Webpack creates the same namespaced object for each bundle on the window object. You can see this if you inspect the `window.webpackJsonp` object. Anyway, to fix this, you can use Ngx Build Plus which is described in much detail in this post. In short, just do
$> yarn add ngx-build-plus -D
$> ng add ngx-build-plus
create a file called `webpack.extra.js` wich defines the namespace to be used
module.exports = {
output: {
jsonpFunction: ‘myUniqueNamspace’
}
}
and you can build a single bundle as follows
ng build my-project -prod --output-hashing=none --extra-webpack-config ./webpack.extra.js --single-bundle
Hello world
T o show how an angular component transforms into a custom web-component we first create an Angular Component
$> ng g c HelloWorld
To transformation it into a custom element a couple of changes in ./srcp/app/app.module.ts are required. So change it from
import { BrowserModule } from ‘@angular/platform-browser’;
import { NgModule } from ‘@angular/core’;import { AppComponent } from ‘./app.component’;
import { HelloWorldComponent } from ‘./hello-world/hello-world.component’;@NgModule({
declarations: [ AppComponent, HelloWorldComponent],
imports: [ BrowserModule ],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
into
import { BrowserModule } from ‘@angular/platform-browser’;
import { NgModule, Injector } from ‘@angular/core’;
import {createCustomElement} from ‘@angular/elements’;import { HelloWorldComponent } from ‘./hello-world/hello-world.component’;@NgModule({
declarations: [ HelloWorldComponent ],
imports: [ BrowserModule ],
providers: []
})
export class AppModule {
constructor(private injector: Injector) {} ngDoBootstrap(): void {
const el = createCustomElement(HelloWorldComponent, { injector: this.injector });
customElements.define(‘my-own-element’, el);
}
}
and you’re all set. Note that AppComponent is completely gone and as of Angular 9 entryComponents
is not needed anymore!
Now, let’s see all this in action. For that, edit ./src/index.html and replace the app-root element with our newly created custom element
<my-own-element></my-own-element>
Build it and run it
$> yarn build
$> npx live-server dist/demo
On http://localhost:8080 you can see it now all in action!
NGXS
With all that in place, it is now time to add state management. With NGXS the state definition and action are combined as follows
import { State, Action, StateContext } from ‘@ngxs/store’;export class Increment {
static readonly type = ‘[Counter] Increment’;
}export class Decrement {
static readonly type = ‘[Counter] Decrement’;
}@State<number>({
name: ‘counter’,
defaults: 0
})
export class CounterState {
@Action(Increment)
increment(ctx: StateContext<number>) {
ctx.setState(ctx.getState() + 1);
} @Action(Decrement)
decrement(ctx: StateContext<number>) {
ctx.setState(ctx.getState() — 1);
}
}
Note that the state is just a number (defaults: 0
) with two actions (Increment and Decrement). I know it is not a very realistic state object, but it serves the purpose of this post very well!
Inside app.module.ts
you need to configure NGXS with your state class
import { CounterState } from "./counter.actions";
import { NgxsDispatchPluginModule } from "@ngxs-labs/dispatch-decorator";
import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin';
import { environment } from "src/environments/environment";…
imports: [
BrowserModule,
NgxsModule.forRoot([CounterState]),
NgxsDispatchPluginModule.forRoot(),
NgxsReduxDevtoolsPluginModule.forRoot({
name: ‘NGXS store’,
disabled: environment.production
})
],
…
Thats all, now the store and actions are accessible in the components
import { Component } from “@angular/core”;
import { Dispatch } from “@ngxs-labs/dispatch-decorator”;import { CounterState, Increment, Decrement } from “../counter.actions”;
import { Observable } from “rxjs”;
import { Select } from “@ngxs/store”;@Component({
template: `<p>value={{counter$ | async}}</p>
<button (click)="increment()">Increase</button>
<button (click)="decrement()">Decrease</button>`,
})
export class HelloWorldComponent {
@Select(CounterState) counter$: Observable<number>;
@Dispatch() increment = () => new Increment();
@Dispatch() decrement = () => new Decrement();
}
If you build and run this it should all work (or checkout this demo). Also if you have installed the Redux DevTools extension, you can inspect the state object at any time, replay state changes and a lot more cool stuff!
Final Verdict
These two libraries play very well together, are very performant and require almost zero boilerplate code.
Cheers