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;
-}