Angular web components workspace part 1

9 easy steps to create Angular web components ‘factory’ from scratch.

Oleksii Com
5 min readAug 16, 2021

It is the first part an article, the second one you can find by following link: Part 2

Some time ago I had a project for creating Angular web components library, which could be used as independent widgets on various resources. Main requirements were formed:

Each web component should:

  1. be built as one .js file/module
  2. include it’s own .css
  3. have their own dependencies, it’s obviously dependency of projects shouldn’t mix up in finial module.

Before we start we need install Node.js and Angular CLI. Information about Node.js installation we can find on official site Node.js

npm install -g @angular/cli

1. Create workspace and child projects

We generate workspace and two child projects. Notice, Angular automatically adds project settings to angular.json file, more information about properties we can find on angular workspace config page

ng new angular-web-components-workspace
ng g application first-web-app
ng g application second-web-app

2. Add Angular elements to project

We are going to create angular-elements also called web components, for this we add current dependency, it’ll add to ‘dependencies’ of workspace(as the other dependencies) so we’ll able to use them across workspace.

ng add @angular/elements

3. Register custom element

Now we should register our root element component as customElement and define name of future web component as first parameter customElements.define method.

      /* projects/first-web-app/src/app/app.module.ts */@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
})
export class AppModule {
constructor(private injector: Injector) {}

ngDoBootstrap(): void {
const el = createCustomElement(AppComponent, {injector: this.injector});
customElements.define('first-web-app', el);
}
}

4. Choose your UI frameworks

Lets imagine we want to use bootstrap for “first-web-app” and bulma for “second-web-app” web components accordingly.

npm i bootstrap
npm i bulma

Similarly we can add UI frameworks to certain project which will be built to “dist” with others bundles.

                        /* angular.json */"first-web-app": {
...
"architect": {
"build": {
"options": {
"styles": [ <-- add UI libraries here
"projects/first-web-app/src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css"
]
}
}
}
},

and second project

                       /* angular.json */"second-web-app": {
...
"architect": {
"build": {
"options": {
"styles": [ <-- add UI libraries here
"projects/second-web-app/src/styles.scss",
"node_modules/bulma/bulma.sass"
]
}
}
}
}

Now we have completed projects with encapsulated dependencies, we are ready for building. Angular gives us appropriate command for that.

5. Build projects

Before building change “outputHashing” in angular.json to “none”, it will allow us build bundles without specific suffixes.

"outputHashing": "none"

and build projects

ng build --project first-web-app
ng build --project second-web-app

Eventually we have child built projects, which can be used as independent micro front-end modules.

But it’s too inconvenient, each web component has at least 3 .js files also we have style.css which includes chosen UI framework. Also we want encapsulate css styles, to prevent conflict with css styles of page when web component uses. So we aim concat js bundles, encapsulate css and deliver web component by one .js module.

6. Build web component ‘factory’.

First of all let’s add new script to package.json.

"build:components": "node builder/builder.js"

Now we’re ready to create our builder, which helps us concatenate js bundles, incapsulate css with specific wrapper and merge prepared js file with encapsulated css. Create “builder” folder on root workspace level and create “builder.js” inside.

                   /* builder/builder.js */const fileSystem = require('fs');
const bundleName = process.argv[2];

const buildWebComponent = () => {
fileSystem.readdir(`dist/${bundleName}`, async (err) => {
if (!err) {

} else {
console.error(err)
}
});
}

buildWebComponent();

7. Concat built bundles

For concatenating bundles we need install extra dependencies

npm i concat
npm i fs-extra

Now we read js bundles, create ‘components’ folder and put concatenating bundle to it.

             /* builder/concat-js-bundles.js */const fsExtra = require('fs-extra');
const concat = require('concat');

const concatJsBundles = async (projectName) => {
const files = [
`./dist/${projectName}/polyfills.js`,
`./dist/${projectName}/main.js`,
`./dist/${projectName}/runtime.js`
];

await fsExtra.ensureDir(`components`);
await concat(files, `components/${projectName}.js`);
}
module.exports = concatJsBundles;

And update builder.js file.

                    /* builder/builder.js */const fileSystem = require('fs');
const concatJsBundles = require('./concat-js-bundles.js');
const bundleName = process.argv[2];

const buildWebComponent = () => {
fileSystem.readdir(`dist/${bundleName}`, async (err) => {
if (!err) {
await concatJsBundles(bundleName);
} else {
console.error(err)
}
});
}

buildWebComponent();

8. Encapsulate css

On next step we’re going to add wrapper as class, tag, or id selector. To do it we need install ‘postcss’ and ‘postcss-prefix-selector’.

npm i postcss
npm i postcss-prefix-selector

Create get-encapsulated-css.js

            /* builder/get-encapsulated-css.js */const fileSystem = require('fs');
const prefixer = require('postcss-prefix-selector')
const postcss = require('postcss');

const getEncapsulatedCss = async (bundleName) => {
const css = fileSystem.readFileSync(`./dist/${bundleName}/styles.css`, "utf8");
return postcss().use(
prefixer(
{
prefix: `${bundleName}`,
exclude: [],
transform: (prefix, selector, prefixedSelector) => prefixedSelector
}
)
).process(css).css;
}
module.exports = getEncapsulatedCss;

And add one more line to builder.

                  /* builder/builder.js */const fileSystem = require('fs');
const concatJsBundles = require('./concat-js-bundles.js');
const getEncapsulatedCss = require('./get-encapsulated-css.js');

const bundleName = process.argv[2];

const buildWebComponent = () => {
fileSystem.readdir(`dist/${bundleName}`, async (err) => {
if (!err) {
await concatJsBundles(bundleName);
const encapsulatedCss = await getEncapsulatedCss(bundleName);
} else {
console.error(err)
}
});
}

buildWebComponent();

9. Combine js bundle with css

Finally we put prepared css to js module and append it to js bundle created on 7th step.

                  /* builder/merge-to-js.js */const fileSystem = require('fs');

const mergeToJs = (bundleName, encapsulatedCss) => {
const cssModule = `(function() {
const css = ${JSON.stringify(encapsulatedCss)};
const tag = document.createElement('style');

tag.type = 'text/css';
tag.appendChild(document.createTextNode(css));

document.getElementsByTagName("head")[0].appendChild(tag);
})()`

fileSystem.appendFile(
`components/${bundleName}.js`,
cssModule,
(err) => {
if (err) {
console.log(err)
}
}
);
}

module.exports = mergeToJs;

Out builder appears this way now.

                   /* builder/builder.js */const fileSystem = require('fs');
const concatJsBundles = require('./concat-js-bundles.js');
const getEncapsulatedCss = require('./get-encapsulated-css.js');
const mergeToJs = require('./merge-to-js');
const bundleName = process.argv[2];

const buildWebComponent = () => {
fileSystem.readdir(`dist/${bundleName}`, async (err) => {
if (!err) {
await concatJsBundles(bundleName);
const encapsulatedCss = await getEncapsulatedCss(bundleName);
mergeToJs(bundleName, encapsulatedCss);
} else {
console.error(err)
}
});
}

buildWebComponent();

Congrats, we crossed finish line and ready to build web components :)

npm run build:components <project-name>

Now we have full set up for creating totally independent, web components based on Angular, and small bonus below…

If you need IE11

Generally speaking customElements already supported most of modern browsers, but if we need support IE11 you may faced with problems. Stackoverflow issue. Let’s install appropriate polyfill.

npm i @webcomponents/webcomponentsjs

…and add polyfills to our project.

Our project doesn’t have own package file, for adding dependency we can use angular.json of workspace and setup polyfills there."first-web-app": {
...
"architect": {
"build": {
"options": {
"styles": [
"projects/first-web-app/src/styles.scss"
],
"scripts": [ <--- add .js libraries here
{
"bundleName": "polyfill-webcomp-es5",
"input": "node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"
},
{
"bundleName": "polyfill-webcomp",
"input": "node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"
}
]
}
}
}
},

You can find code on my GitHub repository

--

--