diff --git a/docs/en/UI/Angular/Container-Strategy.md b/docs/en/UI/Angular/Container-Strategy.md new file mode 100644 index 0000000000..3610c5ddd8 --- /dev/null +++ b/docs/en/UI/Angular/Container-Strategy.md @@ -0,0 +1,101 @@ +# ContainerStrategy + +`ContainerStrategy` is an abstract class exposed by @abp/ng.core package. There are two container strategies extending it: `ClearContainerStrategy` and `InsertIntoContainerStrategy`. Implementing the same methods and properties, both of these strategies help you define how your containers will be prepared and where your content will be projected. + + + +## API + +`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**. + + +### constructor + +```js +constructor( + public containerRef: ViewContainerRef, + private index?: number, // works only in InsertIntoContainerStrategy +) +``` + +- `containerRef` is the `ViewContainerRef` that will be used when projecting the content. + + +### getIndex + +```js +getIndex(): number +``` + +This method return the given index clamped by `0` and `length` of the `containerRef`. For strategies without an index, it returns `0`. + + +### prepare + +```js +prepare(): void +``` + +This method is called before content projection. Based on used container strategy, it either clears the container or does nothing (noop). + + + +## ClearContainerStrategy + +`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**. + + + +## InsertIntoContainerStrategy + +`InsertIntoContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **project your content at a specific node index in the container**. + + + +## Predefined Container Strategies + +Predefined container strategies are accessible via `CONTAINER_STRATEGY` constant. + + +### Clear + +```js +CONTAINER_STRATEGY.Clear(containerRef: ViewContainerRef) +``` + +Clears given container before content projection. + + +### Append + +```js +CONTAINER_STRATEGY.Append(containerRef: ViewContainerRef) +``` + +Projected content will be appended to the container. + + +### Prepend + +```js +CONTAINER_STRATEGY.Prepend(containerRef: ViewContainerRef) +``` + +Projected content will be prepended to the container. + + +### Insert + +```js +CONTAINER_STRATEGY.Insert( + containerRef: ViewContainerRef, + index: number, +) +``` + +Projected content will be inserted into to the container at given index (clamped by `0` and `length` of the `containerRef`). + + +## See Also + +- [ProjectionStrategy](./Projection-Strategy.md) diff --git a/docs/en/UI/Angular/Content-Projection-Service.md b/docs/en/UI/Angular/Content-Projection-Service.md new file mode 100644 index 0000000000..6bd30e44c6 --- /dev/null +++ b/docs/en/UI/Angular/Content-Projection-Service.md @@ -0,0 +1,78 @@ +# Content Projection + +You can use the `ContentProjectionService` in @abp/ng.core package in order to project content in an easy and explicit way. + +## Getting Started + +You do not have to provide the `ContentProjectionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. + +```js +import { ContentProjectionService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private contentProjectionService: ContentProjectionService) {} +} +``` + +## Usage + +You can use the `projectContent` method of `ContentProjectionService` to render components and templates dynamically in your project. + +### How to Project Components to Root Level + +If you pass a `RootComponentProjectionStrategy` as the first parameter of `projectContent` method, the `ContentProjectionService` will resolve the projected component and place it at the root level. If provided, it will also pass the component a context. + +```js +const strategy = PROJECTION_STRATEGY.AppendComponentToBody( + SomeOverlayComponent, + { someOverlayProp: "SOME_VALUE" } +); + +const componentRef = this.ContentProjectionService.projectContent(strategy); +``` + +In the example above, `SomeOverlayComponent` component will placed at the **end** of `` and a `ComponentRef` will be returned. Additionally, the given context will be applied, so `someOverlayProp` of the component will be set to `SOME_VALUE`. + +> You should keep the returned `ComponentRef` instance, as it is a reference to the projected component and you will need that reference to destroy the projected view and the component instance. + +### How to Project Components and Templates into a Container + +If you pass a `ComponentProjectionStrategy` or `TemplateProjectionStrategy` as the first parameter of `projectContent` method, and a `ViewContainerRef` as the second parameter of that strategy, the `ContentProjectionService` will project the component or template to the given container. If provided, it will also pass the component or the template a context. + +```js +const strategy = PROJECTION_STRATEGY.ProjectComponentToContainer( + SomeComponent, + viewContainerRefOfTarget, + { someProp: "SOME_VALUE" } +); + +const componentRef = this.ContentProjectionService.projectContent(strategy); +``` + +In this example, the `viewContainerRefOfTarget`, which is a `ViewContainerRef` instance, will be cleared and `SomeComponent` component will placed inside it. Moreover, the given context will be applied, so `someProp` of the component will be set to `SOME_VALUE`. + +> You should keep the returned `ComponentRef` or `EmbeddedViewRef`, as they are a reference to the projected content and you will need them to destroy it when necessary. + +Please refer to [ProjectionStrategy](./Projection-Strategy.md) to see all available projection strategies and how you can build your own projection strategy. + +## API + +### projectContent + +```js +projectContent | TemplateRef>( + projectionStrategy: ProjectionStrategy, + injector = this.injector, +): ComponentRef | EmbeddedViewRef +``` + +- `projectionStrategy` parameter is the primary focus here and is explained above. +- `injector` parameter is the `Injector` instance you can pass to the projected content. It is not used in `TemplateProjectionStrategy`. + + +## What's Next? + +- [TrackByService](./Track-By-Service.md) diff --git a/docs/en/UI/Angular/Context-Strategy.md b/docs/en/UI/Angular/Context-Strategy.md new file mode 100644 index 0000000000..a474c50ad6 --- /dev/null +++ b/docs/en/UI/Angular/Context-Strategy.md @@ -0,0 +1,117 @@ +# ContextStrategy + +`ContextStrategy` is an abstract class exposed by @abp/ng.core package. There are three context strategies extending it: `ComponentContextStrategy`, `TemplateContextStrategy`, and `NoContextStrategy`. Implementing the same methods and properties, all of these strategies help you define how projected content will get their context. + + + +## ComponentContextStrategy + +`ComponentContextStrategy` is a class that extends `ContextStrategy`. It lets you **pass context to a projected component**. + + +### constructor + +```js +constructor(public context: Partial>) {} +``` + +- `T` refers to component type here, i.e. `Type`. +- `InferredInstanceOf` is a utility type exposed by @abp/ng.core package. It infers component shape. +- `context` will be mapped to properties of the projected component. + + +### setContext + +```js +setContext(componentRef: ComponentRef>): Partial> +``` + +This method maps each prop of the context to the component property with the same name and calls change detection. It returns the context after mapping. + + + +## TemplateContextStrategy + +`TemplateContextStrategy` is a class that extends `ContextStrategy`. It lets you **pass context to a projected template**. + + +### constructor + +```js +constructor(public context: Partial>) {} +``` + +- `T` refers to template context type here, i.e. `TemplateRef`. +- `InferredContextOf` is a utility type exposed by @abp/ng.core package. It infers context shape. +- `context` will be mapped to properties of the projected template. + + +### setContext + +```js +setContext(): Partial> +``` + +This method does nothing and only returns the context, because template context is not mapped but passed in as parameter to `createEmbeddedView` method. + + + +## NoContextStrategy + +`NoContextStrategy` is a class that extends `ContextStrategy`. It lets you **skip passing any context to projected content**. + + +### constructor + +```js +constructor() +``` + +Unlike other context strategies, `NoContextStrategy` contructor takes no parameters. + + +### setContext + +```js +setContext(): undefined +``` + +Since there is no context, this method gets no parameters and will return `undefined`. + + + +## Predefined Context Strategies + +Predefined context strategies are accessible via `CONTEXT_STRATEGY` constant. + + +### None + +```js +CONTEXT_STRATEGY.None() +``` + +This strategy will not pass any context to the projected content. + + +### Component + +```js +CONTEXT_STRATEGY.Component(context: Partial>) +``` + +This strategy will help you pass the given context to the projected component. + + +### Template + +```js +CONTEXT_STRATEGY.Template(context: Partial>) +``` + +This strategy will help you pass the given context to the projected template. + + +## See Also + +- [ProjectionStrategy](./Projection-Strategy.md) diff --git a/docs/en/UI/Angular/Dom-Insertion-Service.md b/docs/en/UI/Angular/Dom-Insertion-Service.md index 5d7714d948..d5ea9fe3a2 100644 --- a/docs/en/UI/Angular/Dom-Insertion-Service.md +++ b/docs/en/UI/Angular/Dom-Insertion-Service.md @@ -1,8 +1,7 @@ -# How to Insert Scripts and Styles +# Dom Insertion (of Scripts and Styles) You can use the `DomInsertionService` in @abp/ng.core package in order to insert scripts and styles in an easy and explicit way. - ## Getting Started You do not have to provide the `DomInsertionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. @@ -20,8 +19,7 @@ class DemoComponent { ## Usage -You can use the `insertContent` method of `DomInsertionService` to create a `` element will place at the **end Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. - ### How to Insert Styles If you pass a `StyleContentStrategy` instance as the first parameter of `insertContent` method, the `DomInsertionService` will create a `` element will place at t Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. - ## API ### insertContent ```js -insertContent(strategy: ContentStrategy): void +insertContent(contentStrategy: ContentStrategy): void ``` -`strategy` parameter is the primary focus here and is explained above. +- `contentStrategy` parameter is the primary focus here and is explained above. ## What's Next? -- [TrackByService](./Track-By-Service.md) +- [ContentProjectionService](./Content-Projection-Service.md) diff --git a/docs/en/UI/Angular/Dom-Strategy.md b/docs/en/UI/Angular/Dom-Strategy.md index 2318e13205..e7b6c68b0f 100644 --- a/docs/en/UI/Angular/Dom-Strategy.md +++ b/docs/en/UI/Angular/Dom-Strategy.md @@ -87,3 +87,4 @@ DOM_STRATEGY.BeforeElement(target: HTMLElement) - [LazyLoadService](./Lazy-Load-Service.md) - [LoadingStrategy](./Loading-Strategy.md) - [ContentStrategy](./Content-Strategy.md) +- [ProjectionStrategy](./Projection-Strategy.md) diff --git a/docs/en/UI/Angular/Loading-Strategy.md b/docs/en/UI/Angular/Loading-Strategy.md index 1a7e7b362f..5322d13eda 100644 --- a/docs/en/UI/Angular/Loading-Strategy.md +++ b/docs/en/UI/Angular/Loading-Strategy.md @@ -57,7 +57,7 @@ This method creates and returns an observable stream that emits on success and t ## Predefined Loading Strategies -Predefined content security strategies are accessible via `LOADING_STRATEGY` constant. +Predefined loading strategies are accessible via `LOADING_STRATEGY` constant. ### AppendAnonymousScriptToHead diff --git a/docs/en/UI/Angular/Projection-Strategy.md b/docs/en/UI/Angular/Projection-Strategy.md new file mode 100644 index 0000000000..4d546566b3 --- /dev/null +++ b/docs/en/UI/Angular/Projection-Strategy.md @@ -0,0 +1,200 @@ +# ProjectionStrategy + +`ProjectionStrategy` is an abstract class exposed by @abp/ng.core package. There are three projection strategies extending it: `ComponentProjectionStrategy`, `RootComponentProjectionStrategy`, and `TemplateProjectionStrategy`. Implementing the same methods and properties, all of these strategies help you define how your content projection will work. + + + +## ComponentProjectionStrategy + +`ComponentProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a component into a container**. + + +### constructor + +```js +constructor( + component: T, + private containerStrategy: ContainerStrategy, + private contextStrategy?: ContextStrategy, +) +``` + +- `component` is class of the component you would like to project. +- `containerStrategy` is the `ContainerStrategy` that will be used when projecting the component. +- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) + +Please refer to [ContainerStrategy](./Container-Strategy.md) and [ContextStrategy](./Context-Strategy.md) documentation for their usage. + + +### injectContent + +```js +injectContent(injector: Injector): ComponentRef +``` + +This method prepares the container, resolves the component, sets its context, and projects it to the container. It returns a `ComponentRef` instance, which you should keep in order to clear projected components later on. + + + +## RootComponentProjectionStrategy + +`RootComponentProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a component into the document**, such as appending it to ``. + + +### constructor + +```js +constructor( + component: T, + private contextStrategy?: ContextStrategy, + private domStrategy?: DomStrategy, +) +``` + +- `component` is class of the component you would like to project. +- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) +- `domStrategy` is the `DomStrategy` that will be used when inserting component. (_default: AppendToBody_) + +Please refer to [ContextStrategy](./Context-Strategy.md) and [DomStrategy](./Dom-Strategy.md) documentation for their usage. + + +### injectContent + +```js +injectContent(injector: Injector): ComponentRef +``` + +This method resolves the component, sets its context, and projects it to the document. It returns a `ComponentRef` instance, which you should keep in order to clear projected components later on. + + + +## TemplateProjectionStrategy + +`TemplateProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a template into a container**. + + +### constructor + +```js +constructor( + template: T, + private containerStrategy: ContainerStrategy, + private contextStrategy?: ContextStrategy, +) +``` + +- `template` is `TemplateRef` you would like to project. +- `containerStrategy` is the `ContainerStrategy` that will be used when projecting the component. +- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) + +Please refer to [ContainerStrategy](./Container-Strategy.md) and [ContextStrategy](./Context-Strategy.md) documentation for their usage. + + +### injectContent + +```js +injectContent(): EmbeddedViewRef +``` + +This method prepares the container, and projects the template together with the defined context to it. It returns an `EmbeddedViewRef`, which you should keep in order to clear projected templates later on. + + + +## Predefined Projection Strategies + +Predefined projection strategies are accessible via `PROJECTION_STRATEGY` constant. + + +### AppendComponentToBody + +```js +PROJECTION_STRATEGY.AppendComponentToBody( + component: T, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the component and places it at the **end** of `` tag in the document. + + +### AppendComponentToContainer + +```js +PROJECTION_STRATEGY.AppendComponentToContainer( + component: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the component and places it at the **end** of the container. + + +### AppendTemplateToContainer + +```js +PROJECTION_STRATEGY.AppendTemplateToContainer( + templateRef: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the template and places it at the **end** of the container. + + +### PrependComponentToContainer + +```js +PROJECTION_STRATEGY.PrependComponentToContainer( + component: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the component and places it at the **beginning** of the container. + + +### PrependTemplateToContainer + +```js +PROJECTION_STRATEGY.PrependTemplateToContainer( + templateRef: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the template and places it at the **beginning** of the container. + + +### ProjectComponentToContainer + +```js +PROJECTION_STRATEGY.ProjectComponentToContainer( + component: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Clears the container, sets given context to the component, and places it **in the cleared** the container. + + +### ProjectTemplateToContainer + +```js +PROJECTION_STRATEGY.ProjectTemplateToContainer( + templateRef: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Clears the container, sets given context to the template, and places it **in the cleared** the container. + + +## See Also + +- [DomInsertionService](./Dom-Insertion-Service.md) diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 471e215e3a..20214b8447 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -353,6 +353,10 @@ "text": "DomInsertionService", "path": "UI/Angular/Dom-Insertion-Service.md" }, + { + "text": "ContentProjectionService", + "path": "UI/Angular/Content-Projection-Service.md" + }, { "text": "TrackByService", "path": "UI/Angular/Track-By-Service.md" diff --git a/npm/ng-packs/packages/core/src/lib/models/index.ts b/npm/ng-packs/packages/core/src/lib/models/index.ts index c38a38bfaf..c0950c3dee 100644 --- a/npm/ng-packs/packages/core/src/lib/models/index.ts +++ b/npm/ng-packs/packages/core/src/lib/models/index.ts @@ -6,3 +6,4 @@ export * from './profile'; export * from './replaceable-components'; export * from './rest'; export * from './session'; +export * from './utility'; diff --git a/npm/ng-packs/packages/core/src/lib/models/utility.ts b/npm/ng-packs/packages/core/src/lib/models/utility.ts new file mode 100644 index 0000000000..e84ad15340 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/models/utility.ts @@ -0,0 +1,4 @@ +import { TemplateRef, Type } from '@angular/core'; + +export type InferredInstanceOf = T extends Type ? U : never; +export type InferredContextOf = T extends TemplateRef ? U : never; diff --git a/npm/ng-packs/packages/core/src/lib/services/content-projection.service.ts b/npm/ng-packs/packages/core/src/lib/services/content-projection.service.ts new file mode 100644 index 0000000000..dfcdea330a --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/services/content-projection.service.ts @@ -0,0 +1,14 @@ +import { Injectable, Injector, TemplateRef, Type } from '@angular/core'; +import { ProjectionStrategy } from '../strategies/projection.strategy'; + +@Injectable({ providedIn: 'root' }) +export class ContentProjectionService { + constructor(private injector: Injector) {} + + projectContent | TemplateRef>( + projectionStrategy: ProjectionStrategy, + injector = this.injector, + ) { + return projectionStrategy.injectContent(injector); + } +} diff --git a/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts b/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts index f1a59e4e43..d4b30b731d 100644 --- a/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts @@ -4,7 +4,7 @@ import { generateHash } from '../utils'; @Injectable({ providedIn: 'root' }) export class DomInsertionService { - readonly inserted = new Set(); + readonly inserted = new Set(); insertContent(contentStrategy: ContentStrategy) { const hash = generateHash(contentStrategy.content); diff --git a/npm/ng-packs/packages/core/src/lib/services/index.ts b/npm/ng-packs/packages/core/src/lib/services/index.ts index a64e721c67..f8b016bbd1 100644 --- a/npm/ng-packs/packages/core/src/lib/services/index.ts +++ b/npm/ng-packs/packages/core/src/lib/services/index.ts @@ -1,6 +1,7 @@ export * from './application-configuration.service'; export * from './auth.service'; export * from './config-state.service'; +export * from './content-projection.service'; export * from './dom-insertion.service'; export * from './lazy-load.service'; export * from './localization.service'; diff --git a/npm/ng-packs/packages/core/src/lib/strategies/container.strategy.ts b/npm/ng-packs/packages/core/src/lib/strategies/container.strategy.ts new file mode 100644 index 0000000000..dfe169e402 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/strategies/container.strategy.ts @@ -0,0 +1,44 @@ +import { ViewContainerRef } from '@angular/core'; + +export abstract class ContainerStrategy { + constructor(public containerRef: ViewContainerRef) {} + + abstract getIndex(): number; + + prepare(): void {} +} + +export class ClearContainerStrategy extends ContainerStrategy { + getIndex(): number { + return 0; + } + + prepare() { + this.containerRef.clear(); + } +} + +export class InsertIntoContainerStrategy extends ContainerStrategy { + constructor(containerRef: ViewContainerRef, private index: number) { + super(containerRef); + } + + getIndex() { + return Math.min(Math.max(0, this.index), this.containerRef.length); + } +} + +export const CONTAINER_STRATEGY = { + Clear(containerRef: ViewContainerRef) { + return new ClearContainerStrategy(containerRef); + }, + Append(containerRef: ViewContainerRef) { + return new InsertIntoContainerStrategy(containerRef, containerRef.length); + }, + Prepend(containerRef: ViewContainerRef) { + return new InsertIntoContainerStrategy(containerRef, 0); + }, + Insert(containerRef: ViewContainerRef, index: number) { + return new InsertIntoContainerStrategy(containerRef, index); + }, +}; diff --git a/npm/ng-packs/packages/core/src/lib/strategies/context.strategy.ts b/npm/ng-packs/packages/core/src/lib/strategies/context.strategy.ts new file mode 100644 index 0000000000..21007eae1c --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/strategies/context.strategy.ts @@ -0,0 +1,47 @@ +import { ComponentRef, TemplateRef, Type } from '@angular/core'; +import { InferredContextOf, InferredInstanceOf } from '../models'; + +export abstract class ContextStrategy { + constructor(public context: Partial>) {} + + /* tslint:disable-next-line:no-unused-variable */ + setContext(componentRef?: ComponentRef>): Partial> { + return this.context; + } +} + +export class NoContextStrategy< + T extends Type | TemplateRef = any +> extends ContextStrategy { + constructor() { + super(undefined); + } +} + +export class ComponentContextStrategy = any> extends ContextStrategy { + setContext(componentRef: ComponentRef>): Partial> { + Object.keys(this.context).forEach(key => (componentRef.instance[key] = this.context[key])); + componentRef.changeDetectorRef.detectChanges(); + return this.context; + } +} + +export class TemplateContextStrategy = any> extends ContextStrategy { + setContext(): Partial> { + return this.context; + } +} + +export const CONTEXT_STRATEGY = { + None | TemplateRef = any>() { + return new NoContextStrategy(); + }, + Component = any>(context: Partial>) { + return new ComponentContextStrategy(context); + }, + Template = any>(context: Partial>) { + return new TemplateContextStrategy(context); + }, +}; + +type ContextType = T extends Type | TemplateRef ? U : never; diff --git a/npm/ng-packs/packages/core/src/lib/strategies/index.ts b/npm/ng-packs/packages/core/src/lib/strategies/index.ts index 2d6be484dd..2e621e7907 100644 --- a/npm/ng-packs/packages/core/src/lib/strategies/index.ts +++ b/npm/ng-packs/packages/core/src/lib/strategies/index.ts @@ -1,5 +1,8 @@ +export * from './container.strategy'; export * from './content-security.strategy'; export * from './content.strategy'; +export * from './context.strategy'; export * from './cross-origin.strategy'; export * from './dom.strategy'; export * from './loading.strategy'; +export * from './projection.strategy'; diff --git a/npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts b/npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts new file mode 100644 index 0000000000..e7a62383a2 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts @@ -0,0 +1,176 @@ +import { + ApplicationRef, + ComponentFactoryResolver, + ComponentRef, + EmbeddedViewRef, + Injector, + TemplateRef, + Type, + ViewContainerRef, +} from '@angular/core'; +import { InferredContextOf, InferredInstanceOf } from '../models/utility'; +import { ContainerStrategy, CONTAINER_STRATEGY } from './container.strategy'; +import { ContextStrategy, CONTEXT_STRATEGY } from './context.strategy'; +import { DomStrategy, DOM_STRATEGY } from './dom.strategy'; + +export abstract class ProjectionStrategy { + constructor(public content: T) {} + + abstract injectContent(injector: Injector): ComponentRefOrEmbeddedViewRef; +} + +export class ComponentProjectionStrategy> extends ProjectionStrategy { + constructor( + component: T, + private containerStrategy: ContainerStrategy, + private contextStrategy: ContextStrategy = CONTEXT_STRATEGY.None(), + ) { + super(component); + } + + injectContent(injector: Injector) { + this.containerStrategy.prepare(); + + const resolver = injector.get(ComponentFactoryResolver) as ComponentFactoryResolver; + const factory = resolver.resolveComponentFactory>(this.content); + + const componentRef = this.containerStrategy.containerRef.createComponent( + factory, + this.containerStrategy.getIndex(), + injector, + ); + this.contextStrategy.setContext(componentRef); + + return componentRef as ComponentRefOrEmbeddedViewRef; + } +} + +export class RootComponentProjectionStrategy> extends ProjectionStrategy { + constructor( + component: T, + private contextStrategy: ContextStrategy = CONTEXT_STRATEGY.None(), + private domStrategy: DomStrategy = DOM_STRATEGY.AppendToBody(), + ) { + super(component); + } + + injectContent(injector: Injector) { + const appRef = injector.get(ApplicationRef); + const resolver = injector.get(ComponentFactoryResolver) as ComponentFactoryResolver; + const componentRef = resolver + .resolveComponentFactory>(this.content) + .create(injector); + + this.contextStrategy.setContext(componentRef); + + appRef.attachView(componentRef.hostView); + const element: HTMLElement = (componentRef.hostView as EmbeddedViewRef).rootNodes[0]; + this.domStrategy.insertElement(element); + + return componentRef as ComponentRefOrEmbeddedViewRef; + } +} + +export class TemplateProjectionStrategy> extends ProjectionStrategy { + constructor( + templateRef: T, + private containerStrategy: ContainerStrategy, + private contextStrategy = CONTEXT_STRATEGY.None(), + ) { + super(templateRef); + } + + injectContent() { + this.containerStrategy.prepare(); + + const embeddedViewRef = this.containerStrategy.containerRef.createEmbeddedView( + this.content, + this.contextStrategy.context, + this.containerStrategy.getIndex(), + ); + embeddedViewRef.detectChanges(); + + return embeddedViewRef as ComponentRefOrEmbeddedViewRef; + } +} + +export const PROJECTION_STRATEGY = { + AppendComponentToBody>(component: T, context?: InferredInstanceOf) { + return new RootComponentProjectionStrategy( + component, + context && CONTEXT_STRATEGY.Component(context), + ); + }, + AppendComponentToContainer>( + component: T, + containerRef: ViewContainerRef, + context?: InferredInstanceOf, + ) { + return new ComponentProjectionStrategy( + component, + CONTAINER_STRATEGY.Append(containerRef), + context && CONTEXT_STRATEGY.Component(context), + ); + }, + AppendTemplateToContainer>( + templateRef: T, + containerRef: ViewContainerRef, + context?: InferredContextOf, + ) { + return new TemplateProjectionStrategy( + templateRef, + CONTAINER_STRATEGY.Append(containerRef), + context && CONTEXT_STRATEGY.Template(context), + ); + }, + PrependComponentToContainer>( + component: T, + containerRef: ViewContainerRef, + context?: InferredInstanceOf, + ) { + return new ComponentProjectionStrategy( + component, + CONTAINER_STRATEGY.Prepend(containerRef), + context && CONTEXT_STRATEGY.Component(context), + ); + }, + PrependTemplateToContainer>( + templateRef: T, + containerRef: ViewContainerRef, + context?: InferredContextOf, + ) { + return new TemplateProjectionStrategy( + templateRef, + CONTAINER_STRATEGY.Prepend(containerRef), + context && CONTEXT_STRATEGY.Template(context), + ); + }, + ProjectComponentToContainer>( + component: T, + containerRef: ViewContainerRef, + context?: InferredInstanceOf, + ) { + return new ComponentProjectionStrategy( + component, + CONTAINER_STRATEGY.Clear(containerRef), + context && CONTEXT_STRATEGY.Component(context), + ); + }, + ProjectTemplateToContainer>( + templateRef: T, + containerRef: ViewContainerRef, + context?: InferredContextOf, + ) { + return new TemplateProjectionStrategy( + templateRef, + CONTAINER_STRATEGY.Clear(containerRef), + context && CONTEXT_STRATEGY.Template(context), + ); + }, +}; + +type ComponentRefOrEmbeddedViewRef = T extends Type + ? ComponentRef + : T extends TemplateRef + ? EmbeddedViewRef + : never; diff --git a/npm/ng-packs/packages/core/src/lib/tests/container.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/container.strategy.spec.ts new file mode 100644 index 0000000000..e85e7b5a50 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/container.strategy.spec.ts @@ -0,0 +1,80 @@ +import { ViewContainerRef } from '@angular/core'; +import { + ClearContainerStrategy, + CONTAINER_STRATEGY, + InsertIntoContainerStrategy, +} from '../strategies'; + +describe('ClearContainerStrategy', () => { + const containerRef = ({ + clear: jest.fn(), + length: 7, + } as any) as ViewContainerRef; + + describe('#getIndex', () => { + it('should return 0', () => { + const strategy = new ClearContainerStrategy(containerRef); + expect(strategy.getIndex()).toBe(0); + }); + }); + + describe('#prepare', () => { + it('should call clear method of containerRef once', () => { + const strategy = new ClearContainerStrategy(containerRef); + strategy.prepare(); + expect(strategy.getIndex()).toBe(0); + expect(containerRef.clear).toHaveBeenCalledTimes(1); + }); + }); +}); + +describe('InsertIntoContainerStrategy', () => { + const containerRef = ({ + clear: jest.fn(), + length: 7, + } as any) as ViewContainerRef; + + describe('#getIndex', () => { + test.each` + index | expected + ${0} | ${0} + ${4} | ${4} + ${9} | ${7} + ${-1} | ${0} + ${Infinity} | ${7} + `( + 'should return $expected when index is given $index', + ({ index, expected }: { index: number; expected: number }) => { + const strategy = new InsertIntoContainerStrategy(containerRef, index); + expect(strategy.getIndex()).toBe(expected); + }, + ); + }); + + describe('#prepare', () => { + it('should not call clear method of containerRef', () => { + const strategy = new InsertIntoContainerStrategy(containerRef, 0); + strategy.prepare(); + expect(containerRef.clear).not.toHaveBeenCalled(); + }); + }); +}); + +describe('CONTAINER_STRATEGY', () => { + const containerRef = ({ + clear: jest.fn(), + length: 7, + } as any) as ViewContainerRef; + + test.each` + name | Strategy | index + ${'Clear'} | ${ClearContainerStrategy} | ${undefined} + ${'Append'} | ${InsertIntoContainerStrategy} | ${containerRef.length} + ${'Prepend'} | ${InsertIntoContainerStrategy} | ${0} + ${'Insert'} | ${InsertIntoContainerStrategy} | ${4} + `('should successfully map $name to $Strategy.name', ({ name, Strategy, index }) => { + expect(CONTAINER_STRATEGY[name](containerRef, index)).toEqual( + new Strategy(containerRef, index), + ); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/tests/content-projection.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/content-projection.service.spec.ts new file mode 100644 index 0000000000..30f9f92e73 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/content-projection.service.spec.ts @@ -0,0 +1,38 @@ +import { Component, ComponentRef, NgModule } from '@angular/core'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator'; +import { ContentProjectionService } from '../services'; +import { PROJECTION_STRATEGY } from '../strategies'; + +describe('ContentProjectionService', () => { + @Component({ template: '
bar
' }) + class TestComponent {} + + // createServiceFactory does not accept entryComponents directly + @NgModule({ + declarations: [TestComponent], + entryComponents: [TestComponent], + }) + class TestModule {} + + let componentRef: ComponentRef; + let spectator: SpectatorService; + const createService = createServiceFactory({ + service: ContentProjectionService, + imports: [TestModule], + }); + + beforeEach(() => (spectator = createService())); + + afterEach(() => componentRef.destroy()); + + describe('#projectContent', () => { + it('should call injectContent of given projectionStrategy and return what it returns', () => { + const strategy = PROJECTION_STRATEGY.AppendComponentToBody(TestComponent); + componentRef = spectator.service.projectContent(strategy); + const foo = document.querySelector('body > ng-component > div.foo'); + + expect(componentRef).toBeInstanceOf(ComponentRef); + expect(foo.textContent).toBe('bar'); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/tests/context.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/context.strategy.spec.ts new file mode 100644 index 0000000000..461c397457 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/context.strategy.spec.ts @@ -0,0 +1,79 @@ +import { ComponentRef } from '@angular/core'; +import { + ComponentContextStrategy, + CONTEXT_STRATEGY, + NoContextStrategy, + TemplateContextStrategy, +} from '../strategies'; +import { uuid } from '../utils'; + +describe('ComponentContextStrategy', () => { + describe('#setContext', () => { + let componentRef: ComponentRef; + + beforeEach( + () => + (componentRef = { + instance: { + x: '', + y: '', + z: '', + }, + changeDetectorRef: { + detectChanges: jest.fn(), + }, + } as any), + ); + + test.each` + props | values + ${['x']} | ${[uuid()]} + ${['x', 'y']} | ${[uuid(), uuid()]} + ${['x', 'y', 'z']} | ${[uuid(), uuid(), uuid()]} + `( + 'should set $props as $values and call detectChanges once', + ({ props, values }: { props: string[]; values: string[] }) => { + const context = {}; + props.forEach((prop, i) => { + context[prop] = values[i]; + }); + + const strategy = new ComponentContextStrategy(context); + strategy.setContext(componentRef); + + expect(props.every(prop => componentRef.instance[prop] === context[prop])).toBe(true); + expect(componentRef.changeDetectorRef.detectChanges).toHaveBeenCalledTimes(1); + }, + ); + }); +}); + +describe('NoContextStrategy', () => { + describe('#setContext', () => { + it('should return undefined', () => { + const strategy = new NoContextStrategy(); + expect(strategy.setContext(null)).toBeUndefined(); + }); + }); +}); + +describe('TemplateContextStrategy', () => { + describe('#setContext', () => { + it('should return context', () => { + const context = { x: uuid() }; + const strategy = new TemplateContextStrategy(context); + expect(strategy.setContext()).toEqual(context); + }); + }); +}); + +describe('CONTEXT_STRATEGY', () => { + test.each` + name | Strategy + ${'Component'} | ${ComponentContextStrategy} + ${'None'} | ${NoContextStrategy} + ${'Template'} | ${TemplateContextStrategy} + `('should successfully map $name to $Strategy.name', ({ name, Strategy }) => { + expect(CONTEXT_STRATEGY[name](undefined)).toEqual(new Strategy(undefined)); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts index 2570636197..f8e8565496 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts @@ -3,13 +3,43 @@ import { DomInsertionService } from '../services'; import { CONTENT_STRATEGY } from '../strategies'; describe('DomInsertionService', () => { + let styleElements: NodeListOf; let spectator: SpectatorService; const createService = createServiceFactory(DomInsertionService); beforeEach(() => (spectator = createService())); - it('should be insert an element', () => { - spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}')); - expect(spectator.service.inserted.has(1437348290)).toBe(true); + afterEach(() => styleElements.forEach(element => element.remove())); + + describe('#insertContent', () => { + it('should be able to insert given content', () => { + spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}')); + styleElements = document.head.querySelectorAll('style'); + expect(styleElements.length).toBe(1); + expect(styleElements[0].textContent).toBe('.test {}'); + }); + + it('should insert only once', () => { + expect(spectator.service.inserted.has(1437348290)).toBe(false); + + spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}')); + styleElements = document.head.querySelectorAll('style'); + + expect(styleElements.length).toBe(1); + expect(styleElements[0].textContent).toBe('.test {}'); + expect(spectator.service.inserted.has(1437348290)).toBe(true); + + spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}')); + styleElements = document.head.querySelectorAll('style'); + + expect(styleElements.length).toBe(1); + expect(styleElements[0].textContent).toBe('.test {}'); + expect(spectator.service.inserted.has(1437348290)).toBe(true); + }); + + it('should be able to insert given content', () => { + spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}')); + expect(spectator.service.inserted.has(1437348290)).toBe(true); + }); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts new file mode 100644 index 0000000000..a0b3bb61bb --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts @@ -0,0 +1,276 @@ +import { + Component, + ComponentRef, + EmbeddedViewRef, + TemplateRef, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { + ComponentProjectionStrategy, + ContainerStrategy, + CONTAINER_STRATEGY, + CONTEXT_STRATEGY, + DOM_STRATEGY, + PROJECTION_STRATEGY, + RootComponentProjectionStrategy, + TemplateProjectionStrategy, +} from '../strategies'; + +describe('ComponentProjectionStrategy', () => { + @Component({ + template: '
{{ bar || baz }}
', + }) + class TestComponent { + bar: string; + baz = 'baz'; + } + + @Component({ + template: '', + }) + class HostComponent { + @ViewChild('container', { static: true, read: ViewContainerRef }) + containerRef: ViewContainerRef; + } + + let containerStrategy: ContainerStrategy; + let spectator: Spectator; + let componentRef: ComponentRef; + + const createComponent = createComponentFactory({ + component: HostComponent, + entryComponents: [TestComponent], + }); + + beforeEach(() => { + spectator = createComponent({}); + containerStrategy = CONTAINER_STRATEGY.Clear(spectator.component.containerRef); + }); + + afterEach(() => { + componentRef.destroy(); + spectator.detectChanges(); + }); + + describe('#injectContent', () => { + it('should should insert content into container and return a ComponentRef', () => { + const strategy = new ComponentProjectionStrategy(TestComponent, containerStrategy); + componentRef = strategy.injectContent(spectator); + spectator.detectChanges(); + + const div = spectator.query('div.foo'); + expect(div.textContent).toBe('baz'); + expect(componentRef).toBeInstanceOf(ComponentRef); + }); + + it('should be able to map context to projected component', () => { + const contextStrategy = CONTEXT_STRATEGY.Component({ bar: 'bar' }); + const strategy = new ComponentProjectionStrategy( + TestComponent, + containerStrategy, + contextStrategy, + ); + componentRef = strategy.injectContent(spectator); + spectator.detectChanges(); + + const div = spectator.query('div.foo'); + expect(div.textContent).toBe('bar'); + expect(componentRef.instance.bar).toBe('bar'); + }); + }); +}); + +describe('RootComponentProjectionStrategy', () => { + @Component({ + template: '
{{ bar || baz }}
', + }) + class TestComponent { + bar: string; + baz = 'baz'; + } + + @Component({ template: '' }) + class HostComponent {} + + let spectator: Spectator; + let componentRef: ComponentRef; + + const createComponent = createComponentFactory({ + component: HostComponent, + entryComponents: [TestComponent], + }); + + beforeEach(() => { + spectator = createComponent({}); + }); + + afterEach(() => { + componentRef.destroy(); + spectator.detectChanges(); + }); + + describe('#injectContent', () => { + it('should should insert content into body and return a ComponentRef', () => { + const strategy = new RootComponentProjectionStrategy(TestComponent); + componentRef = strategy.injectContent(spectator); + spectator.detectChanges(); + + const div = document.querySelector('body > ng-component > div.foo'); + expect(div.textContent).toBe('baz'); + expect(componentRef).toBeInstanceOf(ComponentRef); + componentRef.destroy(); + spectator.detectChanges(); + }); + + it('should be able to map context to projected component', () => { + const contextStrategy = CONTEXT_STRATEGY.Component({ bar: 'bar' }); + const strategy = new RootComponentProjectionStrategy(TestComponent, contextStrategy); + componentRef = strategy.injectContent(spectator); + spectator.detectChanges(); + + const div = document.querySelector('body > ng-component > div.foo'); + expect(div.textContent).toBe('bar'); + expect(componentRef.instance.bar).toBe('bar'); + }); + }); +}); + +describe('TemplateProjectionStrategy', () => { + @Component({ + template: ` + +
{{ bar || baz }}
+
+ + `, + }) + class HostComponent { + @ViewChild('container', { static: true, read: ViewContainerRef }) + containerRef: ViewContainerRef; + + @ViewChild('template', { static: true }) + templateRef: TemplateRef<{ $implicit?: string }>; + + baz = 'baz'; + } + + let containerStrategy: ContainerStrategy; + let spectator: Spectator; + let embeddedViewRef: EmbeddedViewRef<{ $implicit?: string }>; + + const createComponent = createComponentFactory({ + component: HostComponent, + }); + + beforeEach(() => { + spectator = createComponent({}); + containerStrategy = CONTAINER_STRATEGY.Clear(spectator.component.containerRef); + }); + + afterEach(() => { + embeddedViewRef.destroy(); + spectator.detectChanges(); + }); + + describe('#injectContent', () => { + it('should should insert content into container and return an EmbeddedViewRef', () => { + const templateRef = spectator.component.templateRef; + const strategy = new TemplateProjectionStrategy(templateRef, containerStrategy); + embeddedViewRef = strategy.injectContent(); + spectator.detectChanges(); + + const div = spectator.query('div.foo'); + expect(div.textContent).toBe('baz'); + expect(embeddedViewRef).toHaveProperty('detectChanges'); + expect(embeddedViewRef).toHaveProperty('markForCheck'); + expect(embeddedViewRef).toHaveProperty('detach'); + expect(embeddedViewRef).toHaveProperty('reattach'); + expect(embeddedViewRef).toHaveProperty('destroy'); + expect(embeddedViewRef).toHaveProperty('rootNodes'); + expect(embeddedViewRef).toHaveProperty('context'); + }); + + it('should be able to map context to projected template', () => { + const templateRef = spectator.component.templateRef; + const contextStrategy = CONTEXT_STRATEGY.Template({ $implicit: 'bar' }); + const strategy = new TemplateProjectionStrategy( + templateRef, + containerStrategy, + contextStrategy, + ); + embeddedViewRef = strategy.injectContent(); + spectator.detectChanges(); + + const div = spectator.query('div.foo'); + expect(div.textContent).toBe('bar'); + expect(embeddedViewRef.context).toEqual(contextStrategy.context); + }); + }); +}); + +describe('PROJECTION_STRATEGY', () => { + const content = undefined; + const containerRef = ({ length: 0 } as any) as ViewContainerRef; + let context: any; + + test.each` + name | Strategy | containerStrategy + ${'AppendComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Append} + ${'AppendTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Append} + ${'PrependComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend} + ${'PrependTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend} + ${'ProjectComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} + ${'ProjectTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} + `( + 'should successfully map $name to $Strategy.name with $containerStrategy.name container strategy and $contextStrategy.name context strategy', + ({ name, Strategy, containerStrategy }) => { + expect(PROJECTION_STRATEGY[name](content, containerRef, context)).toEqual( + new Strategy(content, containerStrategy(containerRef), CONTEXT_STRATEGY.None()), + ); + }, + ); + test.each` + name | Strategy | domStrategy + ${'AppendComponentToBody'} | ${RootComponentProjectionStrategy} | ${DOM_STRATEGY.AppendToBody} + `( + 'should successfully map $name to $Strategy.name with $domStrategy.name dom strategy', + ({ name, Strategy, domStrategy }) => { + expect(PROJECTION_STRATEGY[name](content, context)).toEqual( + new Strategy(content, CONTEXT_STRATEGY.None(), domStrategy()), + ); + }, + ); + + test.each` + name | Strategy | containerStrategy | contextStrategy + ${'AppendComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Append} | ${CONTEXT_STRATEGY.Component} + ${'AppendTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Append} | ${CONTEXT_STRATEGY.Template} + ${'PrependComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend} | ${CONTEXT_STRATEGY.Component} + ${'PrependTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend} | ${CONTEXT_STRATEGY.Template} + ${'ProjectComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} | ${CONTEXT_STRATEGY.Component} + ${'ProjectTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} | ${CONTEXT_STRATEGY.Template} + `( + 'should successfully map $name to $Strategy.name with $containerStrategy.name container strategy and $contextStrategy.name context strategy', + ({ name, Strategy, containerStrategy, contextStrategy }) => { + context = { x: true }; + expect(PROJECTION_STRATEGY[name](content, containerRef, context)).toEqual( + new Strategy(content, containerStrategy(containerRef), contextStrategy(context)), + ); + }, + ); + + test.each` + name | Strategy | contextStrategy | domStrategy + ${'AppendComponentToBody'} | ${RootComponentProjectionStrategy} | ${CONTEXT_STRATEGY.Component} | ${DOM_STRATEGY.AppendToBody} + `( + 'should successfully map $name to $Strategy.name with $contextStrategy.name context strategy and $domStrategy.name dom strategy', + ({ name, Strategy, domStrategy, contextStrategy }) => { + context = { x: true }; + expect(PROJECTION_STRATEGY[name](content, context)).toEqual( + new Strategy(content, contextStrategy(context), domStrategy()), + ); + }, + ); +}); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts index e00cda5b55..2527ab033d 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts @@ -102,17 +102,7 @@ export class ModalComponent implements OnDestroy { destroy$ = new Subject(); get isFormDirty(): boolean { - let node: HTMLDivElement; - if (!this.modalContent) { - node = document.getElementById('modal-container') as HTMLDivElement; - } - - const nodes = getFlatNodes( - ((node || this.modalContent.nativeElement).querySelector('#abp-modal-body') as HTMLElement) - .childNodes, - ); - - return hasNgDirty(nodes); + return Boolean(document.querySelector('.modal-dialog .ng-dirty')); } constructor(private renderer: Renderer2, private confirmationService: ConfirmationService) {} @@ -178,17 +168,3 @@ export class ModalComponent implements OnDestroy { this.init.emit(); } } - -function getFlatNodes(nodes: NodeList): HTMLElement[] { - return Array.from(nodes).reduce( - (acc, val) => [ - ...acc, - ...(val.childNodes && val.childNodes.length ? getFlatNodes(val.childNodes) : [val]), - ], - [], - ); -} - -function hasNgDirty(nodes: HTMLElement[]) { - return nodes.findIndex(node => (node.className || '').indexOf('ng-dirty') > -1) > -1; -}