NGConf 2019 - Not Every App is a SPA

Juri Strumpflohner - Jul 1 '19 - - Dev Community

Disclaimer

This is my personal summary of the sessions from ngconf. While I summarize the things with my own words, the material used such as images, graphs, source code examples are not my own. Most of them are from the Youtube videos or slide of the respective speakers of the various sessions.

Other sessions?

This article is cross-posted from my blog. If you wanna read the original one, also covering other sessions, head over to the original article ยป.

Follow me on twitter.

Not Every App is a SPA

Rob Wormald

Rob targets the graph mentioned by Igor about the current field Angular apps are being adopted.

Going forward the team's goal is to target the two missing edges in this graph.

Small-medium Apps, Demos, Edu Apps

To target this left side of the graph, where small to medium apps reside, the answer is definitely Angular Elements.

If this sounds new to you, check out my related article.

Mixed environments are also a good example where Angular Elements fits in nicely:

  • Lots of different frameworks
  • Not everyone can start from greenfield
  • Google has this problem too (Angular, AngularJS, Dart, GWT, Polymer,...)
  • Mini-apps running on 3rd party sites
  • NgUpgrade

In the context of Angular Elements, the registration process for bundling a single component as an Angular Element is currently (< Angular v7) still quite verbose:

@NgModule({
  imports: [BrowserModule, CommonModule],
  declarations: [HelloWorld],
  entryComponents: [HelloWorld]
})
class HelloWorldModule {}
Enter fullscreen mode Exit fullscreen mode

And then it needs to be registered as an Angular Element:

platformBrowser()
  .bootstrapModule(HelloWorldModule)
  .then(({injector}) => {
    const HelloWorldElement = createCustomElement(HelloWorld, {injector});
    customElements.define('hello-world', HelloWorldElement);
  });
Enter fullscreen mode Exit fullscreen mode

How is that going to change with Ivy?

The simplest way of rendering a component in Ivy is the following:

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

@Component({
  selector: 'hello-world',
  template: `...`
})
class HelloWorld {
  @Input() name: string;
  @Output() nameChange = new EventEmitter();
  changeName = () => this.nameChange.emit(this.name);
}

renderComponent(HelloWorld);
Enter fullscreen mode Exit fullscreen mode

So how can we make this an Angular Element in Ivy? Rob shows on stage how that will look like.

import { renderComponent } from '@angular/core';
import { HelloWorld } from './hello-world.component';

// manually define the host rather than let Angular look for it
// then pass it as a 2nd argument to the renderComponent
const host = document.querySelector('hello-world');

renderComponent(HelloWorld, { host });

// create a custom element using the native browser API
class HelloWorldElement extends HTMLElement {}
Enter fullscreen mode Exit fullscreen mode

This is the first step. Next, we can create a Custom Element using the native browser API and invoke the renderComponent from there.

import { renderComponent } from '@angular/core';
import { HelloWorld } from './hello-world.component';

// create a custom element using the native browser API
class HelloWorldElement extends HTMLElement {
  component: HelloWorld;
  constructor()  {
    super();
    // associate "this" as the host element
    this.component = renderComponent(HelloWorld, { host: this })
  }
}
Enter fullscreen mode Exit fullscreen mode

Note how we pass this (which is the Custom Element instance as the host to the render function). We can also add properties which we simply wrap.

import { renderComponent, detectChanges } from '@angular/core';
import { HelloWorld } from './hello-world.component';

// create a custom element using the native browser API
class HelloWorldElement extends HTMLElement {
  component: HelloWorld;
  constructor()  {
    super();
    // associate "this" as the host element
    this.component = renderComponent(HelloWorld, { host: this })
  }

  set name(value) {
    this.component.name = value;
    detectChangs(this.component);
  }
  get name() {
    return this.component.name;
  }
}
Enter fullscreen mode Exit fullscreen mode

detectChanges can just be imported from Angular. It's just a function ๐Ÿ’ช (no DI necessarily needed to inject the ChangeDetectorRef etc..)!

To have attributes, we just continue using the native browser APIs.

import { renderComponent, detectChanges } from '@angular/core';
import { HelloWorld } from './hello-world.component';

// create a custom element using the native browser API
class HelloWorldElement extends HTMLElement {
  static observedAttributes = ['name'];
  component: HelloWorld;
  constructor()  {
    super();
    // associate "this" as the host element
    this.component = renderComponent(HelloWorld, { host: this })
  }

  attributeChangedCallback(attr, oldValue, newValue) {
    this.name = newValue;
  }

  set name(value) {...}
  get name() {...}
}
Enter fullscreen mode Exit fullscreen mode

Now this just to show how easy it is to build it by yourself with Ivy. You don't have to do this every time. Most likely this will look like this with Ivy:

import { withNgComponent } from '@angular/elements';
import { HelloWorld } from './hello-world.component';

// create a Custom Element that wraps the Angular Component
const HelloWorldElement = withNgComponent(HelloWorld);

// register it
customElements.define('hello-world', HelloWorldElement);
Enter fullscreen mode Exit fullscreen mode

No platforms, no modules ๐ŸŽ‰ ๐ŸŽ‰ You can of course still use the Injector if you want:

...
// create a Custom Element that wraps the Angular Component
const HelloWorldElement = withNgComponent(HelloWorld, {injector});
...
Enter fullscreen mode Exit fullscreen mode

In many cases you already have an Angular Component that you want to turn into an Element. But what if you don't want to have an Angular Component, but just an Angular Element? ๐Ÿค” Basically you just want the benefit the Angular templating system gives you. The "problem" right now is that we have the NgModule which tells the compiler which dependencies are needed and helps it optimize the final outcome. Technically Ivy doesn't need a NgModule, but still, we need to have a way to tell the component what other directives/components live in its template. One proposal (<< this is an EARLY PROPOSAL the team wants feedback on) is to allow to register the dependencies directly in the @Component tag, very much like you already can with providers and what was already there in Angular RC4 (yes I remember ๐Ÿ˜…). Something like this:

@Component({
  selector: 'hello-world',
  template: `...`,
  providers: [SomeService],
  deps: [SomeDirective, SomePipe]
})
class HelloWorld {}
Enter fullscreen mode Exit fullscreen mode

This is definitely more verbose, but also more direct and "simpler" if you want. To achieve the final goal of just having an Ng Element (without an Angular Component) could look something like this (based on what we've discussed before):

import { NgElement, withElement } from '@angular/elements';
...
@NgElement({
  selector: 'hello-world',
  template: `...`,
  providers: [SomeService],
  deps: [SomeDirective, SomePipe]
})
class HelloWorld extends withNgElement {}
Enter fullscreen mode Exit fullscreen mode

This gives you an Angular Element without an Angular Component. Something that might make sense in some scenarios, like when building a design system.

Scaling Up - or What is project "Angular Photon"?

To the other side of the chart: scaling up.

In this context (during the keynote - see further up), the name **Angular Photon" came up. Imporant:

It's a research project for experimenting and "deciding how to build the right tools for the next gen of Angular Developers". It's a project in collaboration with

  • Google Shopping Express (build with Angular)
  • Wiz

Loading components as they are needed is a big part. As a sneak peek this is what it might look like

import { withLazyNgComponent } from '@angular/elements';

// create a Custom Element that wraps the Angular Component
const HelloWorldElement = withLazyNgComponent(() => import('./hellow-world.component'));

// register it
customElements.define('hello-world', HelloWorldElement);
Enter fullscreen mode Exit fullscreen mode

Note the withLazyNgComponent that fetches the necessary scripts only when really needed.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .