Skip to content

Instantly share code, notes, and snippets.

@zackschuster
Last active December 4, 2019 05:27
Show Gist options
  • Save zackschuster/48d77efafd77ae4cfb705bda3e44f756 to your computer and use it in GitHub Desktop.
Save zackschuster/48d77efafd77ae4cfb705bda3e44f756 to your computer and use it in GitHub Desktop.
angular-8-thin-shim-layer
<html>
<head>
<title>Angular App</title>
</head>
<body>
<main>
<ng-content ng-app>loading</ng-content>
</main>
</body>
</html>
import 'zone.js';
import { ApplicationRef, Compiler, Component, ComponentFactory, Injector, NgModule, NgZone, ViewContainerRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
declare module '@angular/core' {
interface ViewContainerRef {
/** @private */
_embeddedViews: { nodes: { renderElement: Element }[] }[]
}
}
// for those who must afflict themselves with the burden of the `@` sigil, a less wearisome path prevails; a warning, though, and beware, for the fruits will yet taste bitter all the same
@Function() // silence me if you wish to know my power
class NgAppDirective {
constructor(vcr: ViewContainerRef) {}
}
export class NgAppComponents {
protected readonly $componentFactories: ComponentFactory<any>[] = [];
/**
* Returns a shallow copy of the current `ViewContainerRef` embedded views array.
*/
protected get $viewStack() {
return this.$vcr._embeddedViews.slice();
}
constructor(protected $compiler: Compiler, protected $vcr: ViewContainerRef) { }
public add(...declarations: any[]) {
this.$componentFactories.push(
...this.$compiler.compileModuleAndAllComponentsSync(
NgModule({ declarations, imports: [CommonModule, FormsModule] })(class ThrowawayModule {})
).componentFactories
);
return this;
}
public view(selector: string) {
const component = this.$componentFactories.find(x => x.selector === selector);
return component == null ? this : this.attach(component);
}
/**
* Remove the first instance of a component on the view stack.
*
* Does nothing if no component is found.
*/
public removeFirst(selector: string) {
const index = this.$viewStack.findIndex(x => {
return x.nodes
.filter(y => y.renderElement != null)
.some(y => y.renderElement.localName === selector);
});
return index === -1 ? this : this.remove(index);
}
/**
* Remove the last instance of a component on the view stack.
*
* Does nothing if no component is found.
*/
public removeLast(selector: string) {
const component = this.$viewStack.reverse().find(x => {
return x.nodes
.filter(y => y.renderElement != null)
.some(y => y.renderElement.localName === selector);
});
if (component == null) {
return this;
}
const index = this.$viewStack.findIndex(x => x === component);
return index === -1 ? this : this.remove(index);
}
public attach<T>(component: ComponentFactory<T>) {
return NgApp.Zone.run(() => {
this.$vcr.createComponent(component);
return this;
});
}
/**
* Remove the component at the given zero-based index on the view stack.
*
* An out-of-bounds index will remove the component at the nearest view stack boundary.
*
* @example `-1` or `0` will remove the first component
* @example `2` will remove the third component
* @example `Infinity` or `$viewStack.length` or `$viewStack.length - 1` will remove the last component
*/
public remove(index: number) {
return NgApp.Zone.run(() => {
this.$vcr.remove(index);
return this;
});
}
public removeAll() {
this.$vcr.clear();
return this;
}
}
export class NgApp {
public static readonly Zone = new NgZone({ enableLongStackTrace: false });
public static bootstrap() {
const declarations = [NgAppDirective];
const entryComponents = [NgAppDirective];
NgModule({ imports: [BrowserModule], declarations, entryComponents })(NgApp);
Component({ selector: '[ng-app]', template: '' })(NgAppDirective);
return platformBrowserDynamic().bootstrapModule(NgApp, { ngZone: NgApp.Zone });
}
public components: NgAppComponents;
public get injector() {
return this.$injector;
}
protected $injector: Injector;
public async ngDoBootstrap(app: ApplicationRef) {
try {
this.$injector = app.bootstrap(NgAppDirective).injector;
this.components = new NgAppComponents(this.$injector.get(Compiler), this.$injector.get(ViewContainerRef));
} catch (err) {
if (err.message.includes('The selector "[ng-app]" did not match any elements') ) {
throw new Error('Must mark the root element with the attribute [ng-app]');
}
throw err;
}
}
}
NgApp.bootstrap()
.then(ref => {
if (window['ngRef']) {
console.info('hot reload detected');
window['ngRef'].destroy();
}
window['ngRef'] = ref;
function $compile(controller: { new(...args: any[]): any }, template: string) {
Reflect.defineProperty(controller.prototype, '$selector', { get() { return selector; }});
Reflect.defineProperty(controller.prototype, '$template', { get() { return template; }});
const selector = controller.name.split(/(?=[A-Z])/g).join('-').replace(/_/g, '-').toLowerCase();
template = template
.replace(/\$ctrl\./g, '')
.replace(/ng-if/g, '*ngIf')
.replace(/ng-model/g, '[(ngModel)]');
Component({ selector, template })(controller);
return controller;
}
// behold the ashen rot ascend! let it glisten in thine eye as snow in a desert, yet curdle in thy belly as milk among maggots!
@Function()
class DynamicCmp {
constructor(vcr: ViewContainerRef) {
console.log({
LookDynamicCmpHasVcr: vcr != null,
ButAtWhatCost: 'https://www.youtube.com/watch?v=KFZ3Qhn5dW8'
});
}
}
class DynamicCmp2 {
private thing: string;
ngOnInit() {
this.thing = 'initial-thing';
setTimeout(() => {
this.thing = 'second-thing';
});
}
}
ref
.instance
.components
.add(
$compile(DynamicCmp, '<div ng-if="true">I am {{$ctrl.$selector}}. I include dynamic-cmp2. <dynamic-cmp2></dynamic-cmp2></div>'),
$compile(DynamicCmp2, '<div ng-if="true">I am {{$ctrl.$selector}}. <input ng-model="$ctrl.thing" /> <span ng-if="$ctrl.thing">{{$ctrl.thing}}</span></div>'),
)
.view('dynamic-cmp')
.view('class-1') // does nothing
.removeFirst('dynamic-cmp2') // does nothing
.add(
$compile(class {}, '<div ng-if="true">I am the first anonymous component, {{$ctrl.$selector}}.</div>'),
$compile(class {}, '<div ng-if="true">I am the second anonymous component, {{$ctrl.$selector}}.</div>'),
$compile(class {}, '<div ng-if="false">I am {{$ctrl.$selector}}. I should instantiate, but not be visible.</div>'),
)
.view('class-1')
.view('class-2')
.view('class-3')
.view('class-1')
.view('class-2')
.removeLast('class-2')
.removeFirst('class-1')
.view('dynamic-cmp2')
// .removeFirst('dynamic-cmp2')
// .removeAll()
.view('class-3');
});
{
"name": "angular",
"version": "0.0.0",
"private": true,
"dependencies": {
"@angular/common": "8.2.14",
"@angular/compiler": "8.2.14",
"@angular/core": "8.2.14",
"@angular/forms": "8.2.14",
"@angular/platform-browser": "8.2.14",
"@angular/platform-browser-dynamic": "8.2.14",
"rxjs": "6.4.0",
"zone.js": "0.10.2"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment