DEV Community

Cover image for Framework agnostic component libraries with StencilJs and Nx
Dominik Pieper
Dominik Pieper

Posted on • Updated on • Originally published at pieper.io

Framework agnostic component libraries with StencilJs and Nx

Why Stencil and WebComponents?

Web components are a collection of web standards that allow to create modular and reusable HTML elements. Common features of large frameworks like component lifecycles and dynamic are natively supported. Since they are standards-based only, you don't need a framework to run them; a modern browser is enough.

Web components primarily consist of the following 4 APIs from the web standards:

Custom Elements

The Custom Elemets standard gives us the possibility to define our own HTML elements.

Shadow DOM

The Shadow DOM allows us to encapsulate stylesheets and markup from the rest of the markup so that they no longer affect each other.

HTML Templates

With HTML Templates you can define reusable snippets. Within an HTML template, no scripts are executed, stylesheets are applied, or images are loaded. As long as it is not applied, it is not a regular part of the current document.

ES Modules

The ES Modules specification defines a standard way to import and reuse Javascript files in the most performant way.

Stencil building blocks

Stencil is a web component compiler developed by the team around the Ionicframework. Stencil is not the next framework but a compiler whose output format is standard-compliant web components. There are a few other compilers and frameworks that do this. There is the possibility to work without such a compiler, but this is quite bulky in handling and makes little sense, especially for a larger codebase.

import { Component, Prop, h } from '@stencil/core';

@Component({
    tag: 'my-component',
    styleUrl: 'my-component.css',
    shadow: true,
})
export class MyComponent {

    @Prop() name: string;

    render() {
        return <div>Hello, World! I'm ${this.name}</div>;
    }
}
Enter fullscreen mode Exit fullscreen mode

Depending on personal experience with different frontend frameworks, at first glance, a Stencil component looks something like a mix of Angular and React.
Stencil uses TSX (JSX in Typescript) for templating and Typescript as the leading language for development. Common stylesheet preprocessors like Sass are supported as well as Less and PostCSS.

Hands-on with Nx

For a small example, we now create an empty Nx monorepo:

npx create-nx-workspace --preset=empty
Enter fullscreen mode Exit fullscreen mode

And install the plugins that are important for us:

npm install --save-dev @nrwl/angular @nxext/stencil
Enter fullscreen mode Exit fullscreen mode

With the Stencil plugin we now create a new library.

npx nx g @nxext/stencil:lib core --style='css' --buildable
Enter fullscreen mode Exit fullscreen mode

Framework Integration

Stencil has support to generate extra wrappers for frameworks like Angular, React, Vue and Svelte. Although most of these frameworks can handle web components, they still feel like foreign bodies in the application. I'm using Angular as an example in the examples here.

Using the Stencil Nx plugin, you can generate the outputconfig with the corresponding Angular library.

npx nx g @nxext/stencil:add-outputtarget core --outputType='angular'
Enter fullscreen mode Exit fullscreen mode

This adds the following to stencil.config.ts:

...
angularOutputTarget({
    componentCorePackage: '@agnostic-library/core',
    directivesProxyFile: '../../../libs/core-angular/src/generated/directives/proxies.ts',
    valueAccessorConfigs: angularValueAccessorBindings,
}),
...
Enter fullscreen mode Exit fullscreen mode

After this, we created the following folder structure within the "libs" directory:

|
|-libs|-core
|       |-core-angular
|-...
Enter fullscreen mode Exit fullscreen mode

With each build of the "core" library, wrapper components are now generated with Angular for each stencil component in the "core-angular" library folder. This is an Angular library without its own components, which only contains an Angular module for the export and the generated wrappers.

So that the new Angular library now also makes the components usable, these must be exported. For this, the new Angular module is extended and the new component is made known and usable in applications:

...
import { MyComponent } from '../generated/directives/proxies';

const components = [
   MyComponent
]

@NgModule({
    imports: [CommonModule],
    declarations: components,
    exports: components
})
export class CoreAngularModule {}
Enter fullscreen mode Exit fullscreen mode

An Angular application can now import and use the library, but the Web Components must be made known to the browser for everything to work. Before that, we face the problem that the components are not rendered in the browser. For this, we enter the following into the main.ts of the Angular application:

import { defineCustomElements } from '@agnostic-library/core/loader';

defineCustomElements();
Enter fullscreen mode Exit fullscreen mode

The generated Angular components use the original Web Components and make them feel like native Angular components.

This in itself does not sound very spectacular at first. I have observed over the years, especially in large companies, that the frameworks used for the frontend are changed from time to time, or several are used in parallel by different teams. If you now create component libraries via standards such as Web Components, which correspond to the corporate design, for example, these can be adapted quite quickly for all frameworks. This allows efforts to be bundled because a "build once, use everywhere" approach can be used. Not every component has to be rewritten in every framework, but they can be shared. As a small bonus and since Web Components only need a browser as an environment, they can also be used without a framework, e.g., on the company website or landing pages.

On Github is a small example and in the Nxext Demo a somewhat larger repository with more extensive components, an Angular application with backend.

Top comments (0)