Angular elements with NGXS

Lucas Calje
4 min readFeb 10, 2020

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

--

--