How to Compile Angular Element Into Web Component w/ Webpack or Angular CLI?
Asked Answered
O

4

17

I built a simple Web Component via Angular using Pascal Precht's tutorial, which you can see working HERE. It auto-magically compiles in the on Stackblitz in the link, but not locally.

My end goal is to have the code for the resulting Web Component in a separate file locally. Eventually, I will upload it somewhere and pull it in via a single <script> tag, just like normal raw-html/javascript Web Components. I think the question speaks for itself, but you can read the details below if you would like:


Details:

To summarize my code in the link above, I have a very basic component:

import { Component } from '@angular/core';

@Component({
  selector: 'hello-world',
  template: `<h1>Hello world</h1>`
})

export class HelloComponent  {}

and I have a module:

import { NgModule, Injector } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements'
import { HelloComponent } from './hello.component';

@NgModule({
  imports: [BrowserModule],
  declarations: [HelloComponent],
  entryComponents: [HelloComponent]
})

export class AppModule { 
  constructor(private injector: Injector) {}
  ngDoBootstrap() {
    const HelloElement = createCustomElement(HelloComponent, {
      injector: this.injector 
    });

    customElements.define('hello-world', HelloElement);
  }
}

Here is an explanation of the module above:

  1. Add my component to the entryComponents array so it's not taken out by the angular tree-shaker (since it's not reachable on app boot: entryComponents: [HelloComponent]
  2. Run my component through the createCustomElement function so that I can use it as a regular html Web Component:

    const HelloElement = createCustomElement(HelloComponent, { injector: this.injector });

  3. Finally, I ask Angular to compile this component in main.ts:

    platformBrowserDynamic().bootstrapModule(AppModule);


Here is the stuff I read / watched fully (among dozens of other links - most of which are dated, like the original Angular Elements intro):

Web Components from Scratch by Tomek Sułkowski (He never compiles it separately)
Web Components with CLI (Same problem)
Web Components by Academind (Yet again, this guy also uses them within Angular apps)

Thank you for any help.

Obrian answered 31/5, 2018 at 15:6 Comment(1)
@PrasannaSasne I didn't find it to be the solution to the problem, though it did help me to understand the problem, which is why you got the bounty.Obrian
G
9
import { NgModule} from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HelloComponent } from './hello.component';
import { AppComponent } from './app.component';

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent, HelloComponent],
  entryComponents: [HelloComponent],
  bootstrap: [AppComponent]
})
export class AppModule { }

make sure you use

npm install --save @angular/elements and add "@webcomponents/custom-elements" : "^1.0.8" in package.json. After that run npm install & along with that you need to un-comment below lines from polyfills.ts

This adds a polyfill which is required for custom elements to work.

import '@webcomponents/custom-elements/custom-elements.min'; import '@webcomponents/custom-elements/src/native-shim';

<my-tag message="This is rendered dynamically">stack Overflow</my-tag>

Angular doesn't compile this above code, but angular elements fixes this issue by allowing to take our angular component and put it into totally encapsulated self bootstrapping HTML element which you can dump into your angular app in this following way for e.g and which will still work.

In AppComponent.ts file

 import { Component, Injector } from '@angular/core'; 
 import { createCustomElement } from '@angular/elements'
 import { DomSanitizer } from '@angular/platform-browser';

 import { HelloComponent } from './hello.component';

 @Component({
  selector: 'app-root',
  template: '<div [innerHtml]="title"></div>',
  styleUrls: ['./app.component.css']
 })
 export class AppComponent {
 title = null;

 constructor(injector: Injector, domsanitizer: DomSanitizer){
   const customElement = createCustomElement(HelloComponent, {injector: 
   injector});

   //this feature is not provided by angular it is provided by javascript
   //this allows us to register custom web component
   customElements.define('my-tag', customElement);
   //instead of 'hello-world' i've used 'my-tag'    
   setTimeout(() => {
    //security mechanism is used to avoid cross site attacks
    this.title = domsanitizer.bypassSecurityTrustHtml('<my-tag message="This 
    is rendered dynamically">stack Overflow</my-tag>');     
    }, 1000);
   }
 }

And inside your HelloComponent

 import { Component, OnInit, Input } from '@angular/core';

 @Component({
 selector: 'hello-world',
 template: `<div> hello component -- {{ message }}</div>`,
 styles: [`
  div {
    border: 1px solid black;
    background-color: red;
    padding: 1%;
   }
 `]
})
export class HelloComponent implements OnInit {
@Input() message : string;

constructor() { }

ngOnInit() {
}
}

Now this is loaded as native web component.Still only usable in angular projects, but already usable for dyanamic content like this.

I hope this will help you to run your code locally

Generalship answered 11/6, 2018 at 7:26 Comment(0)
J
9

Current Angular version doesn’t provide an option to export component as single local file which can used in any non angular application. However it can be achieved by making changes in building and deployment steps. In my example I have created two angular elements a button and alert message. Both components are compiled and exported as single local file which I’m loading in a plain html file with javascript.

Here are the steps follows: 1. Add ButtonComponent and AlertComponent in entryComponent list. In ngDoBootstrap and define them as custom elements. 
This is how my app.module looks:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';

import { AppComponent } from './app.component';
import { ButtonComponent } from './button/button.component';
import { AlertComponent } from './alert/alert.component';

@NgModule({
  declarations: [AppComponent, ButtonComponent, AlertComponent],
  imports: [BrowserModule],
  entryComponents: [ButtonComponent, AlertComponent]
})
export class AppModule {
  constructor(private injector: Injector) {
  }

  ngDoBootstrap() {
    const customButton = createCustomElement(ButtonComponent, { injector: this.injector });
    customElements.define('my-button', customButton);

    const alertElement = createCustomElement(AlertComponent, { injector: this.injector});
    customElements.define('my-alert', alertElement);
  }
}

  1. Here is my button component:
import {
  Input,
  Component,
  ViewEncapsulation,
  EventEmitter,
  Output
} from '@angular/core';

@Component({
  selector: 'custom-button',
  template: `<button (click)="handleClick()">{{label}}</button>`,
  styles: [
    `
    button {
      border: solid 3px;
      padding: 8px 10px;
      background: #bada55;
      font-size: 20px;
    }
  `
  ],
  encapsulation: ViewEncapsulation.Native
})
export class ButtonComponent {
  @Input() label = 'default label';
  @Output() action = new EventEmitter<number>();
  private clicksCt = 0;

  handleClick() {
    this.clicksCt++;
    this.action.emit(this.clicksCt);
  }
}
  1. Here is my alert component:
import { Component, Input, OnInit } from '@angular/core';
@Component({
  selector: 'alert-message',
  template: '<div>Alert Message: {{message}}</div>',
  styles: [
    `
    div {
      border: 1px solid #885800;
      background-color: #ffcd3f;
      padding: 10px;
      color: red;
      margin:10px;
      font-family: Arial;

    }
    `]
})
export class AlertComponent {
  @Input () message: string;
}
  1. Build configurations in angular.json:
"build": {
  "builder": "@angular-devkit/build-angular:browser",
  "options": {
    "outputPath": "dist",
    "index": "src/index.html",
    "main": "src/main.ts",
    "polyfills": "src/polyfills.ts",
    "tsConfig": "src/tsconfig.app.json",
    "assets": ["src/favicon.ico", "src/assets"],
    "styles": ["src/styles.css"],
    "scripts": [
      {
        "input":
          "node_modules/document-register-element/build/document-register-element.js"
      }
    ]
  },
  "configurations": {
    "production": {
      "fileReplacements": [
        {
          "replace": "src/environments/environment.ts",
          "with": "src/environments/environment.prod.ts"
        }
      ],
      "optimization": true,
      "outputHashing": "all",
      "sourceMap": false,
      "extractCss": true,
      "namedChunks": false,
      "aot": true,
      "extractLicenses": true,
      "vendorChunk": false,
      "buildOptimizer": true
    }
  }
},
"serve": {
  "builder": "@angular-devkit/build-angular:dev-server",
  "options": {
    "browserTarget": "angular6-elements:build"
  },
  "configurations": {
    "production": {
      "browserTarget": "angular6-elements:build:production"
    }
  }
},
"extract-i18n": {
  "builder": "@angular-devkit/build-angular:extract-i18n",
  "options": {
    "browserTarget": "angular6-elements:build"
  }
}
  1. After build, I concatenate runtime, polyfills, script js files into single script file and export elements.js which contains the custom elements 
(optional: gzip those files)
serve it using http-server deploy --gzip
"scripts": {
  "ng": "ng",
  "start": "ng serve",
  "build": "ng build --prod --output-hashing=none",
  "package": "npm run package-base && npm run package-elements",
  "package-base": "cat dist/{runtime,polyfills,scripts}.js | gzip > deploy/script.js.gz",
  "package-elements": "cat dist/main.js | gzip > deploy/elements.js.gz",
  "serve": "http-server deploy --gzip",
  "test": "ng test",
  "lint": "ng lint",
  "e2e": "ng e2e"
}
  1. Finally I include script.js and elements.js in index.html (in deploy directory) to tell the browser about the custom elements. 
Now my-button and my-alert can be included in index.html
. In this example, the button is shown on-load and Alert message is added dynamically (with counter number) on click of the button.
Here is the code:
    <!DOCTYPE html>
    <html lang="en">

    <head>
      <meta charset="UTF-8">
      <title>Custom Button Test Page</title>
      <script src="script.js"></script>
      <script src="elements.js"></script>
    </head>

    <body>
      <my-button label="Show Alert Message!"></my-button>

      <p></p>

      <div id="message-container"></div>

      <script>
        const button = document.querySelector('my-button');
        const msgContainer = document.querySelector('#message-container');
        button.addEventListener('action', (event) => {
          console.log(`"action" emitted: ${event.detail}`);
          button.setAttribute("label", "Show Next Alert Message!");
          msgContainer.innerHTML += `<my-alert message="Here is a message #${event.detail} created dynamically using ng elements!!!"></my-alert>`;
        });

      </script>
    </body>

    </html>

Here is my link for my git repo

Hope this will help!

Thanks.

Jeremiad answered 12/6, 2018 at 16:22 Comment(0)
D
4

Hello there.

If i am understanding correctly, you want to generate a Web Component (lets say <my-component></my-component) and then with a simple script tag to get the .js file to initialize that component and add it on any html page you want to.

In my GitHub repository i have created a simple Todo List Component. That component follows the Angular Elements Principles and also, i have installed some file managment libraries for webpack to also pack the JS into one JS file.

You can check this repository out and see if that helps you out. Just clone it and then run npm install followed by npm run build:elements Feel free to contact me if anything goes south.

Also check this guide. This guy has helped me a lot.

Best of luck

Deese answered 13/6, 2018 at 14:20 Comment(0)
A
2

From what I read the packaging specific for Angular Elements components for easy use outside Angular will come with Angular 7.

What you can do now is to create and angular application with the cli.

ng new YourAppName

Add the Angular Elements library with:

ng add @angular/elements

This adds also all required polyfills as described in the official Angular Documentation.

Then you change the AppModule to not be a bootstrap module but just register the custom elements. You remove the bootstrap from the NgModule and ad the components as entry components. Then register the components as custom elements in the ngDoBootstrap hook. I made both the default AppComponent and HelloComponent custom elements. This is how my app module looks like:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';

import { AppComponent } from './app.component';
import { HelloComponent } from '../hello/hello.component';

@NgModule({
  declarations: [
    AppComponent,
    HelloComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  entryComponents: [AppComponent, HelloComponent]
})
export class AppModule {
  constructor(private injector: Injector) {
  }

  ngDoBootstrap() {
    customElements.define('app-root', createCustomElement(AppComponent, {injector: this.injector}));
    customElements.define('hello-world', createCustomElement(HelloComponent, {injector: this.injector}));
  }
 }

Then you can use the elements in the index.html like elements for example like this:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>ElementsTest</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <div>
    <app-root></app-root>
  </div>
  <div>
    <hello-world></hello-world>
  </div>
</body>
</html>

If you build this with ng build --prod you get minimized packages that you could use also now in other html pages by including the package scripts as they get included by the compiler in the index.html file.

I have added my sample to GitHub. In the history you can see what I have changed from the initial cli application.

Akvavit answered 11/6, 2018 at 22:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.