Higher Order Components in Angular

Slides Reference: bit.ly/ng-hoc

What we can do with Angular (v8)?

  • Can write isolated component and use them across
  • Add directive wherever additional behaviour needed 
  • Meta programming upto some extent
    • Loading module lazily
    • Loading component lazily.
  • Content projection
  • Add animations.

@pankajparkar

Angular V9

  • Ivy have arrived and added lot of things
    • Optimisation
    • Small footprint
    • etc.
  • Additionally ivy allowed to do meta programming
    • Dynamically component rendering
    • Higher Order Component

@pankajparkar

Pankaj P. Parkar

Senior Software Engineer, Deskera

  • MS MVP

  • Angular GDE

  • Opensource Maintainer

  • Stackoverflow Topuser

About Me!

@pankajparkar

We're going to use private API's here

In angular private API name generally starts with θ (Theta)

@pankajparkar

What is dynamic Component loading?

Load component on-demand whenever needed and render it onto the DOM

<button (click)="loadComponent()">Load Lazy Component</button> 
loadComponent() {
  import('./lazy-load/lazy-load.component')
  .then(({LazyLoadComponent}) => {
    ɵrenderComponent(LazyLoadComponent, {
      injector,
      host: 'my-lazy-load-component'
    })
  });
}
<div class="cotent">
  Some other content
  <my-lazy-load-component></my-lazy-load-component>
</div>

Higher Order Component

What 🤔?

Wait

@pankajparkar

Higher Order Functions

In javascript we have

  • map
  • filter
  • reduce

A higher-order function is a function that can take another function as an argument, or that returns a function as a result

@pankajparkar

function add (x, y) {
  return x + y
}

function addFive (x, addReference) {
  return addReference(x, 5)
}

addFive(10, add) // 15

Higher Order Function (eg.)

@pankajparkar

function add (x,y) {
  return x + y
}

function higherOrderFunction (x, callback) {
  return callback(x, 5)
}

higherOrderFunction(10, add)

Higher Order Function (eg.)

@pankajparkar

function add (x, y) {
  return x + y
}

function addFive (x, addReference) {
  return addReference(x, 5)
}

function addTen (x, addReference) {
  return addReference(x, 10)
}

function addTwenty (x, addReference) {
  return addReference(x, 20)
}

addFive(10, add) // 15
addTen(10, add) // 20
addTwenty(10, add) // 30

Higher Order Function (eg.)

function add (x, y) {
  return x + y
}

function makeAdder (x, addReference) {
  return function (y) {
    return addReference(x, y)
  }
}

const addFive = makeAdder(5, add)
const addTen = makeAdder(10, add)
const addTwenty = makeAdder(20, add)

addFive(10) // 15
addTen(10) // 20
addTwenty(10) // 30

Higher Order Function (eg.)

@pankajparkar

function higherOrderFunction (callback) {
  return function () {
    return callback()
  }
}

Higher Order Function (eg.)

@pankajparkar

Higher Order Component

const NewComponent = withTooltip(PageComponent);

Function takes an input as a component and generate another component

PageComponent

function withTooltip(componentRef) {
  // Attach events to show
  // Tooltip on hover
  return componentRef;
}

PageComponent

withTooltip

@pankajparkar

Ivy Working

export class PageComponent {
    constructor() {}
    ngOnInit() {}
}
PageComponent.ɵfac = function PageComponent_Factory(t) { 
    return new (t || PageComponent)();
};
PageComponent.ɵcmp = i0.ɵɵdefineComponent({
    type: PageComponent, 
    selectors: [["app-pager"]],
    ngContentSelectors: _c0,
    template: function PageComponent_Template(rf, ctx) { if (rf & 1) {
        i0.ɵɵprojectionDef();
        i0.ɵɵelementStart(0, "header");
        i0.ɵɵtext(1, "Header is here");
        i0.ɵɵelementEnd();
    } },
    directives: [i1.PageComponent], 
    styles: ["[_nghost-%COMP%] {\n    display: flex;\n    justify-content: space-around;\n    flex-flow: column nowrap;\n    header {\n        height: 20px;\n    }\n    .section {\n        height: calc(100vh-40px);\n    }\n    footer {\n        height: 20px;\n    }\n}"] });
    /*@__PURE__*/ 
    (function () { i0.ɵsetClassMetadata(PageComponent, [{
        type: Component,
        args: [{
            selector: 'app-pager',
            templateUrl: './pager.component.html',
            styleUrls: ['./pager.component.scss']
        }]
    }], function () { return []; }, null); 
})();
@Component({
  selector: 'app-page',
  templateUrl: './page.component.html',
  styleUrls: ['./page.component.scss']
})
export class PageComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

How to create HOC in Angular?

Can be achieved using two Types

  • Decorator based
  • Function based

@pankajparkar

Decorator

@withHoc()
@Component({...})
export class PageComponent {
  @Input() id: string;
  ...
}

@pankajparkar

Function

@Component({...})
export class PageComponent {
  @Input() pageName: string;
  ...
}

const PageWithRouter = withRouter(PageComponent)

const routes = [
  {path: 'page', component: PageComponent},
  {path: 'page/:id', component: PageWithRouter}
]

@pankajparkar

export function fadeIn() {
  return (cmpType) => {
    console.dir(cmpType)
    const originalFactory = cmpType.ɵfac;
    cmpType.ɵfac = (...args) => {
      const cmp = originalFactory(...args);
    }
  }
}

Decorator

@pankajparkar

export function fadeIn(cmpType) {
  console.dir(cmpType)
  const originalFactory = cmpType.ɵfac;
  cmpType.ɵfac = (...args) => {
    originalFactory(...args);
  }
}

Functions

@pankajparkar

import { Component, OnInit,
	Injector, ElementRef, Input } from '@angular/core';
import { fadeIn } from '../hoc/decorators/animation/fadeIn';

@fadeIn()
@Component({
  selector: 'app-page',
  templateUrl: './page.component.html',
  styleUrls: ['./page.component.scss'],
})
export class PageComponent implements OnInit {

  @Input() test: string;

  constructor(
    private injector: Injector
  ) {}

  ngAfterViewInit() {
    console.log(this, 'After View INit')
  }

  ngOnInit() {
    console.log('routeParams', this.test);
  }
}
import { ElementRef, ɵComponentType, ɵComponentDef, INJECTOR, ɵɵdirectiveInject } from '@angular/core';
import { AnimationBuilder, query, style, stagger, animate } from '@angular/animations';
import { cloneDeep } from 'lodash';

function _buildAnimation(builder) { ... }

export function fadeIn() {
  return (cmpType) => {
    const originalFactory = cmpType.ɵfac;
    cmpType.ɵfac = (...args) => {
      const cmp = originalFactory(...args);
      const originalAfterViewInit = cmp.afterViewInit;
      cmp.afterViewInit = function afterViewInit(...args) {
        if (originalAfterViewInit) {
          originalAfterViewInit.apply(this, ...args);
        }
        const injector = this.injector;
        const el = injector.get(ElementRef);
        const builder = injector.get(AnimationBuilder);
        const animation = _buildAnimation(builder);
        animation.create(el.nativeElement).play();
      }
      return cmp;
    };
    return cmpType;
  };
}

function _buildAnimation(builder) {
  return builder.build([
    query('*', [
      style({ opacity: 0, transform: 'translateY(-50px)' }),
      stagger(100, [
        animate('500ms', 
          style({ opacity: 1, transform: 'none'
        }))
      ])
    ])
  ]);
}

@pankajparkar

@pankajparkar

More use cases

  • withRoute - Pass parameters directly to component. 
  • withStore - Make available ngRx store inside comp.
  • withTooltip - On hover of component open tooltip
  • withPopover - On hover over on popover
  • withAnimation - different kind of animations 

@pankajparkar

Reuse HOC contained component (future)

@pankajparkar

const NewPageComponent = withSelector(
  withStyle(
   withFadeIn(withRoute(PageComponent))
  ), 
  'new-page-component'
);
@NgModule({
  declarations: [
    PageComponent,
    NewPageComponent,
    ...
  ],
  imports: [...],
  bootstrap: [AppComponent],
})

@pankajparkar

Reference

@pankajparkar

Made with Slides.com