Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 265
VSO Avatar asked May 31 '18 15:05

VSO


People also ask

Are angular components Web Components?

Angular elements are Angular components packaged as custom elements (also called Web Components), a web standard for defining new HTML elements in a framework-agnostic way.

What are custom elements in angular?

Angular Custom Elements are a recent addition to the Angular framework that allows you to create exportable components. In other words, whenever we create an Angular Element, we create a new custom HTML element that can be used on any webpage, even if that webpage does not use Angular at all.


2 Answers

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

like image 190
Prasanna Avatar answered Sep 25 '22 06:09

Prasanna


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.

like image 41
boopathy sambasivam Avatar answered Sep 22 '22 06:09

boopathy sambasivam