@ngneat/overview
Version:
The Template for Success in Template Management
444 lines (431 loc) • 21.7 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, model, inject, TemplateRef, effect, untracked, Directive, input, ViewContainerRef, DestroyRef, createComponent, InjectionToken, Injector, ApplicationRef, EnvironmentInjector, signal, Component } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { startWith, filter, map, distinctUntilChanged } from 'rxjs/operators';
class TeleportService {
constructor() {
this.outlets = new BehaviorSubject('');
this.ports = new Map();
}
outlet$(name) {
return this.outlets.pipe(
// Immediately check current ports on subscription so that a teleportTo
// registered after its outlet doesn't miss the already-emitted newOutlet event.
// Without this, BehaviorSubject only replays one name, so only the last
// registered outlet is visible to new subscribers.
startWith(name), filter((current) => current === name), map((name) => this.ports.get(name)), distinctUntilChanged());
}
newOutlet(name) {
this.outlets.next(name);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: TeleportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: TeleportService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: TeleportService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class TeleportDirective {
constructor() {
this.teleportTo = model(...(ngDevMode ? [undefined, { debugName: "teleportTo" }] : /* istanbul ignore next */ []));
this.viewRef = null;
this.tpl = inject(TemplateRef);
this.service = inject(TeleportService);
effect((onCleanup) => {
const teleportTo = this.teleportTo();
if (!teleportTo)
return;
// outlet$() emits immediately if the outlet is already registered, or
// waits for it to appear — covering both sync and async outlet creation.
const subscription = this.service.outlet$(teleportTo).subscribe((outlet) => {
if (!outlet)
return;
// untracked() prevents signals read during view creation (e.g. in the
// teleported template's directives) from being tracked as dependencies
// of this effect, which would cause spurious re-runs and view recreation.
this.viewRef = untracked(() => {
return outlet.createEmbeddedView(this.tpl);
});
});
// onCleanup runs before the next effect execution and on directive destroy,
// ensuring the subscription and the remote view are always released together.
onCleanup(() => {
subscription.unsubscribe();
this.viewRef?.destroy();
this.viewRef = null;
});
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: TeleportDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.6", type: TeleportDirective, isStandalone: true, selector: "[teleportTo]", inputs: { teleportTo: { classPropertyName: "teleportTo", publicName: "teleportTo", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { teleportTo: "teleportToChange" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: TeleportDirective, decorators: [{
type: Directive,
args: [{
selector: '[teleportTo]',
}]
}], ctorParameters: () => [], propDecorators: { teleportTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "teleportTo", required: false }] }, { type: i0.Output, args: ["teleportToChange"] }] } });
class TeleportOutletDirective {
constructor() {
this.teleportOutlet = input(...(ngDevMode ? [undefined, { debugName: "teleportOutlet" }] : /* istanbul ignore next */ []));
this.vcr = inject(ViewContainerRef);
this.service = inject(TeleportService);
effect(() => {
const teleportOutlet = this.teleportOutlet();
if (teleportOutlet) {
this.service.ports.set(teleportOutlet, this.vcr);
this.service.newOutlet(teleportOutlet);
}
});
inject(DestroyRef).onDestroy(() => {
this.service.ports.delete(this.teleportOutlet());
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: TeleportOutletDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.6", type: TeleportOutletDirective, isStandalone: true, selector: "[teleportOutlet]", inputs: { teleportOutlet: { classPropertyName: "teleportOutlet", publicName: "teleportOutlet", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: TeleportOutletDirective, decorators: [{
type: Directive,
args: [{
selector: '[teleportOutlet]',
}]
}], ctorParameters: () => [], propDecorators: { teleportOutlet: [{ type: i0.Input, args: [{ isSignal: true, alias: "teleportOutlet", required: false }] }] } });
class CompRef {
constructor(options) {
this.options = options;
if (options.vcr) {
// Creating through a ViewContainerRef inserts the component into the existing
// Angular view tree, so it participates in the host's change detection naturally.
this.ref = options.vcr.createComponent(options.component, {
index: options.vcr.length,
injector: options.injector || options.vcr.injector,
bindings: options.bindings,
directives: options.directives,
});
}
else {
// Without a ViewContainerRef the component is created detached from any view tree.
// Attaching to ApplicationRef keeps it inside Angular's change detection cycle so
// it still updates — otherwise the view would be permanently stale.
this.ref = createComponent(options.component, {
elementInjector: options.injector,
environmentInjector: options.environmentInjector,
bindings: options.bindings,
directives: options.directives,
});
options.appRef.attachView(this.ref.hostView);
}
}
setInput(input, value) {
this.ref.setInput(input, value);
return this;
}
setInputs(inputs) {
Object.keys(inputs).forEach((input) => {
this.ref.setInput(input, inputs[input]);
});
return this;
}
detectChanges() {
this.ref.hostView.detectChanges();
return this;
}
updateContext(context) {
// Context is held in a signal so the component can react to updates reactively
// without requiring an explicit change detection call from the outside.
this.options.contextSignal?.set(context);
return this;
}
appendTo(container) {
container.appendChild(this.getElement());
return this;
}
removeFrom(container) {
container.removeChild(this.getElement());
return this;
}
getRawContent() {
return this.getElement().outerHTML;
}
getElement() {
return this.ref.location.nativeElement;
}
destroy() {
this.ref.destroy();
// When there's no ViewContainerRef we manually attached to ApplicationRef,
// so we must also manually detach — otherwise Angular keeps running change
// detection on a destroyed view, causing errors.
!this.options.vcr && this.options.appRef.detachView(this.ref.hostView);
this.ref = null;
}
}
// Runtime type guards are needed because Content is a union that TypeScript can't
// narrow automatically at runtime — TemplateRef, component classes, and strings are
// structurally indistinguishable without these checks.
function isTemplateRef(value) {
return value instanceof TemplateRef;
}
function isComponent(value) {
return typeof value === 'function';
}
function isString(value) {
return typeof value === 'string';
}
// CompRef and TplRef store their inner Angular view refs differently, so this
// normalises access when the raw Angular view (e.g. for change detection) is needed.
function getViewRef(value) {
return value instanceof CompRef ? value.ref.hostView : value.ref;
}
class TplRef {
constructor(args) {
this.args = args;
if (this.args.vcr) {
// Creating through a ViewContainerRef inserts the view into the Angular view tree,
// giving it automatic change detection via its host component's cycle.
this.ref = this.args.vcr.createEmbeddedView(this.args.tpl, this.args.context || {}, { injector: args.injector });
this.ref.detectChanges();
}
else {
// Without a ViewContainerRef the view sits outside the component tree, so we attach
// it to ApplicationRef to keep it inside Angular's change detection — then trigger
// an initial check since no parent cycle will do it for us.
this.ref = this.args.tpl.createEmbeddedView(this.args.context || {}, args.injector);
this.ref.detectChanges();
this.args.appRef.attachView(this.ref);
}
}
detectChanges() {
this.ref.detectChanges();
return this;
}
getElement() {
const rootNodes = this.ref.rootNodes;
if (rootNodes.length === 1 && rootNodes[0] === Node.ELEMENT_NODE) {
this.element = rootNodes[0];
}
else {
// Templates can project multiple root nodes (e.g. siblings or ng-container contents).
// Wrapping them ensures callers always receive a single element they can append,
// remove, or measure without handling multi-root edge cases themselves.
this.element = document.createElement('div');
this.element.append(...rootNodes);
}
return this.element;
}
destroy() {
if (this.ref.rootNodes[0] !== 1) {
// Only remove the wrapper element from the DOM if it was actually inserted there.
// Skipping this for VCR-managed views avoids double-removal, since Angular tears
// those down through the view tree itself.
this.element?.parentNode.removeChild(this.element);
this.element = null;
}
if (!this.args.vcr) {
// Mirror the manual attachView from construction — Angular won't clean this up
// on its own for views that live outside the component tree.
this.args.appRef.detachView(this.ref);
}
this.ref.destroy();
this.ref = null;
}
updateContext(context) {
// Mutating the existing context object (rather than replacing it) preserves
// any template variable bindings that already hold a reference to the context.
Object.assign(this.ref.context, context);
return this;
}
}
// Plain strings have no Angular lifecycle, so this adapter exists purely to satisfy
// the ViewRef contract — letting callers treat all content types uniformly without
// special-casing strings throughout the codebase.
class StringRef {
constructor(value) {
this.value = value;
}
getElement() {
return this.value;
}
detectChanges() {
return this;
}
updateContext() {
return this;
}
destroy() { }
}
class ViewUnsupportedContentTypeError extends Error {
constructor() {
super(typeof ngDevMode !== 'undefined' && ngDevMode ? 'Type of content is not supported' : '');
}
}
// Components rendered dynamically often need external data but can't receive it via
// @Input() because they're instantiated programmatically, not by a template. An
// injection token lets the component pull context from its injector without coupling
// to the caller's API or requiring a shared base class.
const VIEW_CONTEXT = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'Component context' : '');
class ViewService {
constructor() {
this.injector = inject(Injector);
this.appRef = inject(ApplicationRef);
this.environmentInjector = inject(EnvironmentInjector);
}
createComponent(component, options = {}) {
return untracked(() => {
let injector = options.injector ?? this.injector;
let contextSignal;
if (options.context) {
contextSignal = signal(options.context);
// Wrap the context in a child injector so the VIEW_CONTEXT token is only visible
// to this component and its descendants — not leaked into the broader injector tree.
injector = Injector.create({
providers: [
{
provide: VIEW_CONTEXT,
useValue: contextSignal.asReadonly(),
},
],
parent: injector,
});
}
return new CompRef({
component,
vcr: options.vcr,
injector,
appRef: this.appRef,
environmentInjector: options.environmentInjector || this.environmentInjector,
contextSignal,
bindings: options.bindings,
directives: options.directives,
});
});
}
createTemplate(tpl, options = {}) {
return untracked(() => {
return new TplRef({
vcr: options.vcr,
appRef: this.appRef,
tpl,
context: options.context,
injector: options.injector,
});
});
}
createView(content, viewOptions = {}) {
return untracked(() => {
if (isTemplateRef(content)) {
return this.createTemplate(content, viewOptions);
}
else if (isComponent(content)) {
return this.createComponent(content, viewOptions);
}
else if (isString(content)) {
return new StringRef(content);
}
else {
throw new ViewUnsupportedContentTypeError();
}
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: ViewService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: ViewService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: ViewService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
// Convenience wrapper so dynamically-rendered components don't need to know about
// the internal VIEW_CONTEXT token — they just call injectViewContext<MyContext>().
function injectViewContext() {
return inject(VIEW_CONTEXT);
}
class DynamicViewComponent {
constructor() {
this.content = input(...(ngDevMode ? [undefined, { debugName: "content" }] : /* istanbul ignore next */ []));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: DynamicViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.6", type: DynamicViewComponent, isStandalone: true, selector: "dynamic-view", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: ` <div [innerHTML]="content()"></div> `, isInline: true }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: DynamicViewComponent, decorators: [{
type: Component,
args: [{
selector: 'dynamic-view',
template: ` <div [innerHTML]="content()"></div> `,
}]
}], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }] } });
class DynamicViewDirective {
constructor() {
this.view = input(undefined, { ...(ngDevMode ? { debugName: "view" } : /* istanbul ignore next */ {}), alias: 'dynamicView' });
this.injector = input(undefined, { ...(ngDevMode ? { debugName: "injector" } : /* istanbul ignore next */ {}), alias: 'dynamicViewInjector' });
this.context = input(undefined, { ...(ngDevMode ? { debugName: "context" } : /* istanbul ignore next */ {}), alias: 'dynamicViewContext' });
this.inputs = input(undefined, { ...(ngDevMode ? { debugName: "inputs" } : /* istanbul ignore next */ {}), alias: 'dynamicViewInputs' });
this.defaultTpl = inject(TemplateRef);
this.vcr = inject(ViewContainerRef);
this.viewService = inject(ViewService);
inject(DestroyRef).onDestroy(() => {
this.viewRef?.destroy();
});
}
ngOnInit() {
this.resolveContentType();
}
ngOnChanges(changes) {
const viewChanged = changes.view && !changes.view.isFirstChange();
const contextChanged = changes.context && !changes.context.isFirstChange();
const inputsChanged = changes.inputs && !changes.inputs.isFirstChange();
if (viewChanged) {
this.resolveContentType();
}
else if (contextChanged) {
this.viewRef.updateContext(this.context());
}
else if (isComponent(this.view()) && inputsChanged) {
this.viewRef.setInputs(this.inputs() || {});
}
}
resolveContentType() {
this.viewRef?.destroy();
const view = this.view();
const injector = this.injector();
const context = this.context();
if (isString(view)) {
const viewRef = (this.viewRef = this.viewService.createComponent(DynamicViewComponent, {
vcr: this.vcr,
injector,
}));
viewRef.setInput('content', view).detectChanges();
}
else if (isComponent(view)) {
this.viewRef = this.viewService.createComponent(view, {
vcr: this.vcr,
injector: injector ?? this.vcr.injector,
context,
});
const inputs = this.inputs();
if (inputs) {
this.viewRef.setInputs(inputs);
}
}
else {
this.viewRef = this.viewService.createView(view || this.defaultTpl, {
vcr: this.vcr,
injector: injector ?? this.vcr.injector,
context,
});
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: DynamicViewDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.6", type: DynamicViewDirective, isStandalone: true, selector: "[dynamicView]", inputs: { view: { classPropertyName: "view", publicName: "dynamicView", isSignal: true, isRequired: false, transformFunction: null }, injector: { classPropertyName: "injector", publicName: "dynamicViewInjector", isSignal: true, isRequired: false, transformFunction: null }, context: { classPropertyName: "context", publicName: "dynamicViewContext", isSignal: true, isRequired: false, transformFunction: null }, inputs: { classPropertyName: "inputs", publicName: "dynamicViewInputs", isSignal: true, isRequired: false, transformFunction: null } }, usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: DynamicViewDirective, decorators: [{
type: Directive,
args: [{
selector: '[dynamicView]',
}]
}], ctorParameters: () => [], propDecorators: { view: [{ type: i0.Input, args: [{ isSignal: true, alias: "dynamicView", required: false }] }], injector: [{ type: i0.Input, args: [{ isSignal: true, alias: "dynamicViewInjector", required: false }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "dynamicViewContext", required: false }] }], inputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "dynamicViewInputs", required: false }] }] } });
/**
* Generated bundle index. Do not edit.
*/
export { CompRef, DynamicViewDirective, StringRef, TeleportDirective, TeleportOutletDirective, TplRef, ViewService, ViewUnsupportedContentTypeError, getViewRef, injectViewContext, isComponent, isString, isTemplateRef };
//# sourceMappingURL=ngneat-overview.mjs.map