Angular 2/4 Dynamic Content Woes

I have been working for sometime on an angular version of my fpnotebook.com website. I migrated from an Angular 1 version  to an Angular 2 (late beta) version.  I have been setting up a backend using asp.net core, and thought I would bring the Angular 2 beta version up to the Angular 4 release.  Overall, the upgrade process was starting off fairly smooth with instructions like these. That’s when I noticed the absence of dynamic content tools in Angular 4.

Side note: AngularJs (version 1) is now just called Angular, not Angular 2 or Angular 4 – just Angular.  But try web searching for Angular – finding relevant version specific content has become difficult.  So I am referring to it as Angular 2/4.

My fpnotebook.com content is medical information organized into 31 books (e.g. cardiology), 722 chapters (e.g. blood pressure) and 6407 pages (e.g. hypertension).  Each page is stored in folders using a book/chapter/page hierarchy,  for example: http://www.fpnotebook.com/CV/Htn/Hyprtnsn.htm.

For the angular app, I keep each page in a json file.  Pages are composed of outlines, and each block of the outline (think roman numerals I, II, III) are stored in an array of page blocks, like this:

{
"Heading": "Risk Factors",
"Content": "<ol><li>See <a href='#/library/CV/Prevent/CrdcRsk' class='LinkPage' data-cui='C0580320'>Cardiac Risk Factor</a>s</li></ol>"
},

Each of these blocks is the template for a component, and up until full release version of angular 2, I could dynamically insert this outline html content and have it be JIT (just-in-time) compiled and function as Angular code, including the links as RouterLinks.  Worked great.

And then came the push for smaller Angular distributions and ahead-of-time compilation (AOT), and away went dynamic content and JIT compilation.  The html can still be inserted into a component, but the html is no longer compiled.  The outline still looks like an outline, but it is not integrated with angular.  This means that RouterLinks, custom components, component events and data, do not work.

I thought there must still be a way, surely with all the old directive functionality that I could not previously understand;  surely there must be a way to do this.  And it would seem there are ways to dynamically insert components with the ComponentFactoryResolver, but I could not find a way to insert a template and have it compiled dynamically by Angular.

I then found this blog entry: Forget $compile in Angular 2, in which the component template html is modified as DOM elements. In effect you use query selectors (similar to jquery css selectors) to find html elements and  attach javascript event listeners directly.  This actually works,  but I initially resisted it.  Going outside of Angular to manipulate the DOM directly is not ideal.

So, I looked for various ways to use Angular facilitated approaches to dynamically create the equivalent to RouterLinks.   I tried various alternatives to using ElementRef, such as @ContentChildren(), but these would not work, since I still had uncompiled html.   I was however able to use the Angular Renderer2 service.

So, given the json format above, and a page component with markup like this:

 <app-page-block-html [innerHTML]="block.Content"></app-page-block-html>

Side Issue: Curiously, Angular removes data elements (e.g. data-cui=”C123456″) as part of its sanitize process of [innerHtml], and this breaks part of my functionality.  I’m not sure why the Angular team chose to black-list data elements.  How can you XSS attack with a data attribute?  However, there is a work around using a pipe here.   Hopefully the Angular team will reconsider – by disallowing innocuous attributes like the data attribute, they force work-arounds (e.g. bypass security methods) that open up larger security holes.

The following component code would load this dynamically with working links:

import { Component, OnInit, Input, Renderer2, ViewChild, ElementRef, ContentChildren, QueryList, Directive } from '@angular/core';
import { Router } from "@angular/router";

@Component({
 selector: 'app-page-block-html',
 templateUrl: './page-block-html.component.html',
 styleUrls: ['./page-block-html.component.less']
})

export class PageBlockHtmlComponent implements OnInit {
 
 private clickListeners: Function[] = [];

 constructor(private router: Router,private el:ElementRef, private renderer: Renderer2) { }

 ngAfterViewInit() { // for searching a components template
 const anchorNodes:NodeList = this.el.nativeElement.querySelectorAll('a[href]:not(.LinkRef)'); //or a.LinkPage

 const anchors:Node[] = Array.from(anchorNodes); //or Array.prototype.slice.call(anchorNodes);

 anchors.forEach(anchor => {
      let listener = this.renderer.listen(anchor,'click',e=>{
           e.preventDefault();
           let href = e.srcElement.getAttribute('href');
           this.router.navigateByUrl(href);
      });
 this.clickListeners.push(listener);

 }

 

So another hurdle circumvented with a process that works well despite my wishing there was a more best-practice way.  All due to the sacrifice of JIT and dynamic content for the speed/footprint of Angular with AOT.

 

Advertisements
This entry was posted in Uncategorized. Bookmark the permalink.