Browse Source

Merge pull request #3544 from abpframework/feat/3197

Introduced a Generic Service for Content Projection
pull/3545/head
Mehmet Erim 6 years ago
committed by GitHub
parent
commit
c3cfdf2058
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 101
      docs/en/UI/Angular/Container-Strategy.md
  2. 78
      docs/en/UI/Angular/Content-Projection-Service.md
  3. 117
      docs/en/UI/Angular/Context-Strategy.md
  4. 14
      docs/en/UI/Angular/Dom-Insertion-Service.md
  5. 1
      docs/en/UI/Angular/Dom-Strategy.md
  6. 2
      docs/en/UI/Angular/Loading-Strategy.md
  7. 200
      docs/en/UI/Angular/Projection-Strategy.md
  8. 4
      docs/en/docs-nav.json
  9. 1
      npm/ng-packs/packages/core/src/lib/models/index.ts
  10. 4
      npm/ng-packs/packages/core/src/lib/models/utility.ts
  11. 14
      npm/ng-packs/packages/core/src/lib/services/content-projection.service.ts
  12. 2
      npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts
  13. 1
      npm/ng-packs/packages/core/src/lib/services/index.ts
  14. 44
      npm/ng-packs/packages/core/src/lib/strategies/container.strategy.ts
  15. 47
      npm/ng-packs/packages/core/src/lib/strategies/context.strategy.ts
  16. 3
      npm/ng-packs/packages/core/src/lib/strategies/index.ts
  17. 176
      npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts
  18. 80
      npm/ng-packs/packages/core/src/lib/tests/container.strategy.spec.ts
  19. 38
      npm/ng-packs/packages/core/src/lib/tests/content-projection.service.spec.ts
  20. 79
      npm/ng-packs/packages/core/src/lib/tests/context.strategy.spec.ts
  21. 36
      npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts
  22. 276
      npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts
  23. 26
      npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts

101
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)

78
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 `<body>` 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<T extends Type<any> | TemplateRef<any>>(
projectionStrategy: ProjectionStrategy<T>,
injector = this.injector,
): ComponentRef<C> | EmbeddedViewRef<C>
```
- `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)

117
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<InferredInstanceOf<T>>) {}
```
- `T` refers to component type here, i.e. `Type<C>`.
- `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<InferredInstanceOf<T>>): Partial<InferredInstanceOf<T>>
```
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<InferredContextOf<T>>) {}
```
- `T` refers to template context type here, i.e. `TemplateRef<C>`.
- `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<InferredContextOf<T>>
```
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<InferredContextOf<T>>)
```
This strategy will help you pass the given context to the projected component.
### Template
```js
CONTEXT_STRATEGY.Template(context: Partial<InferredContextOf<T>>)
```
This strategy will help you pass the given context to the projected template.
## See Also
- [ProjectionStrategy](./Projection-Strategy.md)

14
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 `<script>` or `<style>` element with given content in the DOM at the desired position.
You can use the `insertContent` method of `DomInsertionService` to create a `<script>` or `<style>` element with given content in the DOM at the desired position. There is also the `projectContent` method for dynamically rendering components and templates.
### How to Insert Scripts
@ -48,7 +46,6 @@ In the example above, `<script>alert()</script>` 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 `<style>` element with given `content` and place it in the designated DOM position.
@ -74,18 +71,17 @@ In the example above, `<style>body {margin: 0;}</style>` 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)

1
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)

2
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

200
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<T>
```
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 `<body>`.
### 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<T>
```
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<T>
```
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<T>,
)
```
Sets given context to the component and places it at the **end** of `<body>` tag in the document.
### AppendComponentToContainer
```js
PROJECTION_STRATEGY.AppendComponentToContainer(
component: T,
containerRef: ViewContainerRef,
contextStrategy?: ComponentContextStrategy<T>,
)
```
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<T>,
)
```
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<T>,
)
```
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<T>,
)
```
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<T>,
)
```
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<T>,
)
```
Clears the container, sets given context to the template, and places it **in the cleared** the container.
## See Also
- [DomInsertionService](./Dom-Insertion-Service.md)

4
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"

1
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';

4
npm/ng-packs/packages/core/src/lib/models/utility.ts

@ -0,0 +1,4 @@
import { TemplateRef, Type } from '@angular/core';
export type InferredInstanceOf<T> = T extends Type<infer U> ? U : never;
export type InferredContextOf<T> = T extends TemplateRef<infer U> ? U : never;

14
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<T extends Type<any> | TemplateRef<any>>(
projectionStrategy: ProjectionStrategy<T>,
injector = this.injector,
) {
return projectionStrategy.injectContent(injector);
}
}

2
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<number>();
insertContent(contentStrategy: ContentStrategy) {
const hash = generateHash(contentStrategy.content);

1
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';

44
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);
},
};

47
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<T = any> {
constructor(public context: Partial<ContextType<T>>) {}
/* tslint:disable-next-line:no-unused-variable */
setContext(componentRef?: ComponentRef<InferredInstanceOf<T>>): Partial<ContextType<T>> {
return this.context;
}
}
export class NoContextStrategy<
T extends Type<any> | TemplateRef<any> = any
> extends ContextStrategy<T> {
constructor() {
super(undefined);
}
}
export class ComponentContextStrategy<T extends Type<any> = any> extends ContextStrategy<T> {
setContext(componentRef: ComponentRef<InferredInstanceOf<T>>): Partial<InferredInstanceOf<T>> {
Object.keys(this.context).forEach(key => (componentRef.instance[key] = this.context[key]));
componentRef.changeDetectorRef.detectChanges();
return this.context;
}
}
export class TemplateContextStrategy<T extends TemplateRef<any> = any> extends ContextStrategy<T> {
setContext(): Partial<InferredContextOf<T>> {
return this.context;
}
}
export const CONTEXT_STRATEGY = {
None<T extends Type<any> | TemplateRef<any> = any>() {
return new NoContextStrategy<T>();
},
Component<T extends Type<any> = any>(context: Partial<InferredInstanceOf<T>>) {
return new ComponentContextStrategy<T>(context);
},
Template<T extends TemplateRef<any> = any>(context: Partial<InferredContextOf<T>>) {
return new TemplateContextStrategy<T>(context);
},
};
type ContextType<T> = T extends Type<infer U> | TemplateRef<infer U> ? U : never;

3
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';

176
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<T = any> {
constructor(public content: T) {}
abstract injectContent(injector: Injector): ComponentRefOrEmbeddedViewRef<T>;
}
export class ComponentProjectionStrategy<T extends Type<any>> extends ProjectionStrategy<T> {
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<InferredInstanceOf<T>>(this.content);
const componentRef = this.containerStrategy.containerRef.createComponent(
factory,
this.containerStrategy.getIndex(),
injector,
);
this.contextStrategy.setContext(componentRef);
return componentRef as ComponentRefOrEmbeddedViewRef<T>;
}
}
export class RootComponentProjectionStrategy<T extends Type<any>> extends ProjectionStrategy<T> {
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<InferredInstanceOf<T>>(this.content)
.create(injector);
this.contextStrategy.setContext(componentRef);
appRef.attachView(componentRef.hostView);
const element: HTMLElement = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0];
this.domStrategy.insertElement(element);
return componentRef as ComponentRefOrEmbeddedViewRef<T>;
}
}
export class TemplateProjectionStrategy<T extends TemplateRef<any>> extends ProjectionStrategy<T> {
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<T>;
}
}
export const PROJECTION_STRATEGY = {
AppendComponentToBody<T extends Type<unknown>>(component: T, context?: InferredInstanceOf<T>) {
return new RootComponentProjectionStrategy<T>(
component,
context && CONTEXT_STRATEGY.Component(context),
);
},
AppendComponentToContainer<T extends Type<unknown>>(
component: T,
containerRef: ViewContainerRef,
context?: InferredInstanceOf<T>,
) {
return new ComponentProjectionStrategy<T>(
component,
CONTAINER_STRATEGY.Append(containerRef),
context && CONTEXT_STRATEGY.Component(context),
);
},
AppendTemplateToContainer<T extends TemplateRef<unknown>>(
templateRef: T,
containerRef: ViewContainerRef,
context?: InferredContextOf<T>,
) {
return new TemplateProjectionStrategy<T>(
templateRef,
CONTAINER_STRATEGY.Append(containerRef),
context && CONTEXT_STRATEGY.Template(context),
);
},
PrependComponentToContainer<T extends Type<unknown>>(
component: T,
containerRef: ViewContainerRef,
context?: InferredInstanceOf<T>,
) {
return new ComponentProjectionStrategy<T>(
component,
CONTAINER_STRATEGY.Prepend(containerRef),
context && CONTEXT_STRATEGY.Component(context),
);
},
PrependTemplateToContainer<T extends TemplateRef<unknown>>(
templateRef: T,
containerRef: ViewContainerRef,
context?: InferredContextOf<T>,
) {
return new TemplateProjectionStrategy<T>(
templateRef,
CONTAINER_STRATEGY.Prepend(containerRef),
context && CONTEXT_STRATEGY.Template(context),
);
},
ProjectComponentToContainer<T extends Type<unknown>>(
component: T,
containerRef: ViewContainerRef,
context?: InferredInstanceOf<T>,
) {
return new ComponentProjectionStrategy<T>(
component,
CONTAINER_STRATEGY.Clear(containerRef),
context && CONTEXT_STRATEGY.Component(context),
);
},
ProjectTemplateToContainer<T extends TemplateRef<unknown>>(
templateRef: T,
containerRef: ViewContainerRef,
context?: InferredContextOf<T>,
) {
return new TemplateProjectionStrategy<T>(
templateRef,
CONTAINER_STRATEGY.Clear(containerRef),
context && CONTEXT_STRATEGY.Template(context),
);
},
};
type ComponentRefOrEmbeddedViewRef<T> = T extends Type<infer U>
? ComponentRef<U>
: T extends TemplateRef<infer C>
? EmbeddedViewRef<C>
: never;

80
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),
);
});
});

38
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: '<div class="foo">bar</div>' })
class TestComponent {}
// createServiceFactory does not accept entryComponents directly
@NgModule({
declarations: [TestComponent],
entryComponents: [TestComponent],
})
class TestModule {}
let componentRef: ComponentRef<TestComponent>;
let spectator: SpectatorService<ContentProjectionService>;
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');
});
});
});

79
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<any>;
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));
});
});

36
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<HTMLStyleElement>;
let spectator: SpectatorService<DomInsertionService>;
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);
});
});
});

276
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: '<div class="foo">{{ bar || baz }}</div>',
})
class TestComponent {
bar: string;
baz = 'baz';
}
@Component({
template: '<ng-container #container></ng-container>',
})
class HostComponent {
@ViewChild('container', { static: true, read: ViewContainerRef })
containerRef: ViewContainerRef;
}
let containerStrategy: ContainerStrategy;
let spectator: Spectator<HostComponent>;
let componentRef: ComponentRef<TestComponent>;
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: '<div class="foo">{{ bar || baz }}</div>',
})
class TestComponent {
bar: string;
baz = 'baz';
}
@Component({ template: '' })
class HostComponent {}
let spectator: Spectator<HostComponent>;
let componentRef: ComponentRef<TestComponent>;
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: `
<ng-template #template let-bar>
<div class="foo">{{ bar || baz }}</div>
</ng-template>
<ng-container #container></ng-container>
`,
})
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<HostComponent>;
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<typeof templateRef>({ $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()),
);
},
);
});

26
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<void>();
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;
}

Loading…
Cancel
Save