Using Custom Elements in an Angular TypeScript Project

in angular •  7 years ago 

Introduction

Custom web elements are the future of web development. Their ease of use and modularization of HTML, CSS and JS make them perfect for sharing in multiple applications with as little work as possible. Importing a custom element into a project is as easy adding a CSS file. Then use them like any other HTML element, passing configuration and properties into them without the need to write any JS.

<link rel="import" href="../../../../assets/oc-mt/src/app/polymerComponents/mt-img-suite/mt-img-suite.html"/>
   
    
<mt-img-suite editor-id="editorSuite"
              image-editor-config-url="../../../../assets/config/hiddenButtons-image-editor-config.json"
              stylesheet="../../../../assets/css/img-editor-MVI.css"
              mobile-controls="false"
              edit-eyes="true"
></mt-img-suite>

Custom elements are not only a great way to encapsulate and share modularized functions and features, they’re also part of the HTML5 specification. That means they can be used in any web application that adheres to web standards.

Unfortunately, some frameworks don’t fully play by the rules and need some tweaking to perform as one would expect. Among the worst offenders of all time is AngularJS. However, fortunately, custom elements are so powerful that even the Angular developers yielded to the custom element revolution and decided to embrace their usage. This was one of the main reasons Angular 2 was created. Basically, it was a near complete rewrite of the project to utilize more of the browser’s built in ability to manage custom elements, which is nearly 10 times more efficient than AngularJS's parsing approach.

Today, Angular 2 plays along very nicely with custom elements. While Angular still comes with its own quirks, terminology and approaches, custom elements can be used in any Angular project without any limitations.

The below topics discuss the main hurdles I’ve come across while using custom elements in an Angular 5 project developed in TypeScript. While it may seem like a lot, I’m convinced that once these quirks are known and addressed, TypeScript, Angular and the custom elements can all play nicely without any limitations.

Static Assets

TypeScript uses dynamic dependency injection during build/run time and, using Webpack, concatenates the dependencies into a series of bundles that are downloaded and executed in a certain order. This is a great approach for JS files and for HTML files, which get transferred as string data and parsed as needed. However, the parsing performed on the client does not appear to support HTML files that have JS <script> elements in them and vice-versa. The <script> content will be interpreted as a string when the HTML is injected in the DOM.

The workaround to this is to place custom HTML5 elements inside the static asset directory, “src/assets/”, with all of their dependencies. In summary, within the “assets” directory, the entire set of custom elements needs to exist in a flat structure, including dependencies and stylesheets. Then, in the Angular HTML templates, you can import the custom elements by referencing the static asset location as follows:

<link rel="import" href="../../../../assets/oc-mt/src/app/polymerComponents/mt-img-suite/mt-img-suite.html"/>

Note the "/assets/” string in the href path above. TypeScript has a rule to identify this as a static dependency, which won’t be bundled by Webpack. Instead, the file will be served independently with the browser’s native approach to fetching dependencies. This means that all the assets will be available at runtime, but only the assets referenced in the code will actually be transferred to the client.

Internal Dependencies Within the Static Assets Directory Resolve Without Issue

Just something to note...
Within the custom elements, dependencies will be resolved using their relative path as long as the dependencies live within the flat static asset directory. Therefore, in this approach, there’s no need to modify any of the dependency import paths within the elements.

CUSTOM_ELEMENTS_SCHEMA in Module Definition

By default, Angular will throw an error when using custom HTML elements. In order to use the browser’s built in custom web element mechanism and make Angular compliant to HTML5 recommendations, you need to tell Angular to expect custom elements. This can be accomplished in each module definition with the CUSTOM_ELEMENTS_SCHEMA. An example is as follows:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@NgModule({
  schemas: [ CUSTOM_ELEMENTS_SCHEMA ],  // Needed to use custom web elements
  imports: [],
  declarations: [ImgEditorComponent]
})
export class ImgEditorModule { }

In the above example, the CUSTOM_ELEMENTS_SCHEMA definition is pulled out of the Angular package. It’s then added to the schema property in the module decoration object. In short, this simply tells Angular to ignore not having a factory definition for elements during template parsing and let the browser do its thing once the DOM is generated.

Multiple Dependency Instances

Custom elements use the browser’s built in ability to determine when shared dependencies already exist and make previously fetched/installed dependencies available without reloading them. This is done by using each dependency’s absolute path. Since Angular-TypeScript projects use Webpack for dependency resolution, their location is lost during the transpile/build step and the browser’s built in method doesn’t work.

Unfortunately, there’s no way for custom elements to know what global dependencies are unpacked and available without making the custom elements TypeScript components or building custom logic to check each dependency. The result is that custom elements using HTML5 specifications may define one instance of a dependency while the TypeScript components use a different instance. For example, the custom elements will all share the same version of jQuery if they import it from the same path in the flat, static asset directory structure. Meanwhile, Angular will have its own version defined in the global scope or a version scoped within individual views/modules/components.

There isn’t really anything that needs to be done about this in most cases. It’s just something to be aware of since it could lead to issues that may be hard to identify. For example, if certain libraries within the custom elements attach themselves to jQuery ($), they probably won’t be available within Angular if you try to use them. They’ll need to be attached separately in Angular also and vice-versa.

Style Encapsulation

When the Shadow DOM is used, it’s possible for custom elements to fully encapsulate their style class definitions so that only the child elements within them are affected. This allows global style definitions (themes) to be applied to custom elements without the risk of the custom elements’ internal style definitions affecting elements outside their scope.

Unfortunately, Angular’s synthesized Shadow DOM approach of style encapsulation either fully encapsulates style classes or does not encapsulate them at all. By default, global style classes do not make it into custom elements and custom element style definitions do not affect elements outside their scope (module, view, etc.). I have not found a way to allow global style definitions (themes) into custom elements and, at the same time, encapsulate custom element style definitions from creeping out. Therefore, the workaround is to disable style encapsulation completely and refactor each element’s style classes and definitions to only impact child elements within the custom elements. This can be a tedious task, but it does enforce good practices.

To disable style encapsulation completely in Angular, the encapsulation: ViewEncapsulation.None property can be added to the component decorator as follows:

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

@Component({
  selector: 'mvi-img-editor',
  templateUrl: './img-editor.component.html',
  styleUrls: ['./img-editor.component.css'],
  encapsulation: ViewEncapsulation.None // Required to have styles applied to custom elements
})

The above property turns off the styling encapsulation completely making it possible (and likely) that styles will leak out of the custom elements and into other areas of the DOM. To fix this, each custom element’s style classes and definitions may need to be refactored to target more specific scopes. For example, if a custom element has a style definition for div, it may need to be refactored to only affect divs within the component as follows:

mt-img-suite div {
...
}

Angular Navigation Destroys and Re-Creates Views

Typically, in a single page application, views are simply hidden and shown when navigating between them. Meanwhile, Angular’s built in routing completely destroys the views and recreates them when navigating. There are several issues with this approach that need to be accounted for. For example, any references in memory need to be recreated with each navigation.

Additionally, event handlers attached to elements outside the scope of the custom element need to be meticulously managed. If the custom element attaches an event handler to the document or body of the DOM during initialization, the handlers will be attached over and over each time the view loads, causing them to be executed multiple times when the event they're attached to triggers.

A simple solution to preventing multiple event handler executions is to simply remove an event listener before it's attached. For example, the following code removes a listener before attaching it:

document.removeEventListener('editorSuiteExportClicked', this.processExportedImage);
document.addEventListener('editorSuiteExportClicked', this.processExportedImage);

In the above example, the first time this module loads, the removeEventListener() method will do nothing since the listener hasn’t been attached yet. The second time it loads, it will remove the previously attached listener so that the handler is only fired once for that event, regardless of how many times Angular initializes the same module when routing.

Alternatively, a common approach in dealing with Angular’s built in, destructive routing is to use the AngularUI UI-Router library. Not only does the UI-Router have a non-destructive approach, it has several options that facilitate additional use cases, such as nested views and dynamic states.

Summary

I’d like to say that this guide simply proves why Angular is a bad choice for any project. However, I’m sure you’ve been dictated to use Angular from a manager or boss who’s never created a web application and only wants you to use it because “everyone else is using it”. Regardless, after going through the trials and making a few tweaks, I believe custom elements fit extremely well in Angular projects without any functional limitations.

Considering that several libraries need to be entirely re-written or re-factored to even come close to working in Angular, the above steps really aren’t that bad. Actually, I consider most of the above work to simply be project configuration and enforcement of best practices. Once configured, I’m sure you’ll find that there’s not much refactoring at all.

Hopefully, this guide is enough to get you started with custom elements in an Angular 2, 3, 4, 5 (, 6?) project. If not, or if you find any additional hurdles, hoops or shortcuts, please let me know and I’ll add the info. Until then… may your product’s only limitation be imagination.

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Congratulations @a-diddy! You have completed some achievement on Steemit and have been rewarded with new badge(s) :

You published your First Post

Click on any badge to view your own Board of Honor on SteemitBoard.
For more information about SteemitBoard, click here

If you no longer want to receive notifications, reply to this comment with the word STOP

By upvoting this notification, you can help all Steemit users. Learn how here!

Congratulations @a-diddy! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 2 years!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Do not miss the last post from @steemitboard:

The new SteemFest⁴ badge is ready
Vote for @Steemitboard as a witness to get one more award and increased upvotes!