piral-ng
Version:
Plugin for integrating Angular components in Piral.
189 lines (160 loc) • 5.62 kB
text/typescript
import type { PiletApi } from 'piral-core';
import type { BehaviorSubject } from 'rxjs';
import type { NgOptions, ModuleInstanceResult, NgModuleFlags } from './types';
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import {
ApplicationRef,
ComponentFactoryResolver,
ComponentRef,
CUSTOM_ELEMENTS_SCHEMA,
Inject,
NgModule,
NgZone,
} from '@angular/core';
import { PIRAL, PROPS } from './injection';
import { teardown } from './startup';
import { RoutingService } from './RoutingService';
import { findComponents, getAnnotations } from './utils';
import { piralName, propsName } from './constants';
import { SharedModule } from '../common';
interface ModuleDefinition {
active: any;
module: any;
components: Array<any>;
opts: NgOptions;
flags: NgModuleFlags;
}
const availableModules: Array<ModuleDefinition> = [];
function instantiateModule(moduleDef: ModuleDefinition, piral: PiletApi) {
const { module, components } = moduleDef;
const imports = [BrowserModule, SharedModule, module];
const props = { current: undefined };
const createProxy = () =>
new Proxy(props.current, {
get(_, name) {
return props.current[name];
},
});
const providers = [
RoutingService,
{ provide: propsName, useFactory: createProxy, deps: [] },
{ provide: PROPS, useFactory: createProxy, deps: [] },
{ provide: piralName, useFactory: () => piral, deps: [] },
{ provide: PIRAL, useFactory: () => piral, deps: [] },
];
({
imports,
// @ts-ignore
entryComponents: components,
providers,
})
class BootstrapModule {
private appRef: ApplicationRef;
private refs: Array<[any, HTMLElement, ComponentRef<any>]> = [];
constructor(
private resolver: ComponentFactoryResolver,
private zone: NgZone,
public routing: RoutingService,
('NgFlags') private flags: NgModuleFlags,
) {}
ngDoBootstrap(appRef: ApplicationRef) {
this.appRef = appRef;
}
attach(component: any, node: HTMLElement, $props: BehaviorSubject<any>) {
const factory = this.resolver.resolveComponentFactory(component);
props.current = $props.value;
if (factory) {
const ref = this.zone.run(() => this.appRef.bootstrap<any>(factory, node));
const name = (ref.componentType as any)?.ɵcmp?.inputs?.Props;
if (typeof name === 'string' || Array.isArray(name)) {
const sub = $props.subscribe((props) => {
if (typeof ref.setInput === 'function') {
// Here we don't care about the aliased name etc.
ref.setInput('Props', props);
} else if (typeof name === 'string') {
// Legacy mode - just set it directly and trigger CD
ref.instance[name] = props;
ref.changeDetectorRef?.detectChanges();
}
});
ref.onDestroy(() => sub.unsubscribe());
}
this.refs.push([component, node, ref]);
}
}
detach(component: any, node: HTMLElement) {
for (let i = this.refs.length; i--; ) {
const [sourceComponent, sourceNode, ref] = this.refs[i];
if (sourceComponent === component && sourceNode === node) {
ref.destroy();
this.refs.splice(i, 1);
}
}
if (!this.flags?.keepAlive && this.refs.length === 0) {
teardown(BootstrapModule);
}
}
}
return BootstrapModule;
}
export function activateModuleInstance(moduleDef: ModuleDefinition, piral: PiletApi): ModuleInstanceResult {
if (!moduleDef.active) {
moduleDef.active = instantiateModule(moduleDef, piral);
}
return [moduleDef.active, moduleDef.opts, moduleDef.flags];
}
export function getModuleInstance(component: any, standalone: boolean, piral: PiletApi) {
const [moduleDef] = availableModules.filter((m) => m.components.includes(component));
if (moduleDef) {
return activateModuleInstance(moduleDef, piral);
}
if (process.env.NODE_ENV === 'development') {
if (!standalone) {
console.warn(
'Component not found in all defined Angular modules. Make sure to define (using `defineNgModule`) a module with your component(s) referenced in the exports section of the `@NgModule` decorator.',
component,
piral.meta,
);
}
}
return undefined;
}
export function createModuleInstance(component: any, standalone: boolean, piral: PiletApi): ModuleInstanceResult {
const declarations = standalone ? [] : [component];
const importsDef = standalone ? [CommonModule, component] : [CommonModule];
const exportsDef = [component];
const schemasDef = [CUSTOM_ELEMENTS_SCHEMA];
({
declarations,
imports: importsDef,
exports: exportsDef,
schemas: schemasDef,
})
class Module {}
defineModule(Module);
return getModuleInstance(component, standalone, piral);
}
export function findModule(module: any) {
return availableModules.find((m) => m.module === module);
}
export function defineModule(module: any, opts: NgOptions = undefined, flags: NgModuleFlags = undefined) {
const [annotation] = getAnnotations(module);
if (annotation) {
availableModules.push({
active: undefined,
components: findComponents(annotation.exports),
module,
flags,
opts,
});
} else if (typeof module === 'function') {
const state = {
current: undefined,
};
return (selector: string) => ({
component: { selector, module, opts, flags, state },
type: 'ng' as const,
});
}
}