mirror of https://github.com/abpframework/abp.git
152 changed files with 3239 additions and 1153 deletions
@ -1,3 +0,0 @@ |
|||
## AutoMapper Integration |
|||
|
|||
TODO |
|||
@ -1,3 +1,267 @@ |
|||
# Object Extensions |
|||
|
|||
TODO |
|||
ABP Framework provides an **object extension system** to allow you to **add extra properties** to an existing object **without modifying** the related class. This allows to extend functionalities implemented by a depended [application module](Modules/Index.md), especially when you want to [extend entities](Customizing-Application-Modules-Extending-Entities.md) and [DTOs](Customizing-Application-Modules-Overriding-Services.md) defined by the module. |
|||
|
|||
> Object extension system is not normally not needed for your own objects since you can easily add regular properties to your own classes. |
|||
|
|||
## IHasExtraProperties Interface |
|||
|
|||
This is the interface to make a class extensible. It simply defines a `Dictionary` property: |
|||
|
|||
````csharp |
|||
Dictionary<string, object> ExtraProperties { get; } |
|||
```` |
|||
|
|||
Then you can add or get extra properties using this dictionary. |
|||
|
|||
### Base Classes |
|||
|
|||
`IHasExtraProperties` interface is implemented by several base classes by default: |
|||
|
|||
* Implemented by the `AggregateRoot` class (see [entities](Entities.md)). |
|||
* Implemented by `ExtensibleEntityDto`, `ExtensibleAuditedEntityDto`... base [DTO](Data-Transfer-Objects.md) classes. |
|||
* Implemented by the `ExtensibleObject`, which is a simple base class can be inherited for any type of object. |
|||
|
|||
So, if you inherit from these classes, your class will also be extensible. If not, you can always implement it manually. |
|||
|
|||
### Fundamental Extension Methods |
|||
|
|||
While you can directly use the `ExtraProperties` property of a class, it is suggested to use the following extension methods while working with the extra properties. |
|||
|
|||
#### SetProperty |
|||
|
|||
Used to set the value of an extra property: |
|||
|
|||
````csharp |
|||
user.SetProperty("Title", "My Title"); |
|||
user.SetProperty("IsSuperUser", true); |
|||
```` |
|||
|
|||
`SetProperty` returns the same object, so you can chain it: |
|||
|
|||
````csharp |
|||
user.SetProperty("Title", "My Title") |
|||
.SetProperty("IsSuperUser", true); |
|||
```` |
|||
|
|||
#### GetProperty |
|||
|
|||
Used to read the value of an extra property: |
|||
|
|||
````csharp |
|||
var title = user.GetProperty<string>("Title"); |
|||
|
|||
if (user.GetProperty<bool>("IsSuperUser")) |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
* `GetProperty` is a generic method and takes the object type as the generic parameter. |
|||
* Returns the default value if given property was not set before (default value is `0` for `int`, `false` for `bool`... etc). |
|||
|
|||
##### Non Primitive Property Types |
|||
|
|||
If your property type is not a primitive (int, bool, enum, string... etc) type, then you need to use non-generic version of the `GetProperty` which returns an `object`. |
|||
|
|||
#### HasProperty |
|||
|
|||
Used to check if the object has a property set before. |
|||
|
|||
#### RemoveProperty |
|||
|
|||
Used to remove a property from the object. Use this methods instead of setting a `null` value for the property. |
|||
|
|||
### Some Best Practices |
|||
|
|||
Using magic strings for the property names is dangerous since you can easily type the property name wrong - it is not type safe. Instead; |
|||
|
|||
* Define a constant for your extra property names |
|||
* Create extension methods to easily set your extra properties. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
public static class IdentityUserExtensions |
|||
{ |
|||
private const string TitlePropertyName = "Title"; |
|||
|
|||
public static void SetTitle(this IdentityUser user, string title) |
|||
{ |
|||
user.SetProperty(TitlePropertyName, title); |
|||
} |
|||
|
|||
public static string GetTitle(this IdentityUser user) |
|||
{ |
|||
return user.GetProperty<string>(TitlePropertyName); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Then you can easily set or get the `Title` property: |
|||
|
|||
````csharp |
|||
user.SetTitle("My Title"); |
|||
var title = user.GetTitle(); |
|||
```` |
|||
|
|||
## Object Extension Manager |
|||
|
|||
While you can set arbitrary properties to an extensible object (which implements the `IHasExtraProperties` interface), `ObjectExtensionManager` is used to explicitly define extra properties for extensible classes. |
|||
|
|||
Explicitly defining an extra property has some use cases: |
|||
|
|||
* Allows to control how the extra property is handled on object to object mapping (see the section below). |
|||
* Allows to define metadata for the property. For example, you can map an extra property to a table field in the database while using the [EF Core](Entity-Framework-Core.md). |
|||
|
|||
> `ObjectExtensionManager` implements the singleton pattern (`ObjectExtensionManager.Instance`) and you should define object extensions before your application startup. The [application startup template](Startup-Templates/Application.md) has some pre-defined static classes to safely define object extensions inside. |
|||
|
|||
### AddOrUpdate |
|||
|
|||
`AddOrUpdate` is the main method to define a extra properties or update extra properties for an object. |
|||
|
|||
Example: Define extra properties for the `IdentityUser` entity: |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance |
|||
.AddOrUpdate<IdentityUser>(options => |
|||
{ |
|||
options.AddOrUpdateProperty<string>("SocialSecurityNumber"); |
|||
options.AddOrUpdateProperty<bool>("IsSuperUser"); |
|||
} |
|||
); |
|||
```` |
|||
|
|||
### AddOrUpdateProperty |
|||
|
|||
While `AddOrUpdateProperty` can be used on the `options` as shown before, if you want to define a single extra property, you can use the shortcut extension method too: |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance |
|||
.AddOrUpdateProperty<IdentityUser, string>("SocialSecurityNumber"); |
|||
```` |
|||
|
|||
Sometimes it would be practical to define a single extra property to multiple types. Instead of defining one by one, you can use the following code: |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance |
|||
.AddOrUpdateProperty<string>( |
|||
new[] |
|||
{ |
|||
typeof(IdentityUserDto), |
|||
typeof(IdentityUserCreateDto), |
|||
typeof(IdentityUserUpdateDto) |
|||
}, |
|||
"SocialSecurityNumber" |
|||
); |
|||
```` |
|||
|
|||
#### Property Configuration |
|||
|
|||
`AddOrUpdateProperty` can also get an action that can perform additional configuration on the property definition. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance |
|||
.AddOrUpdateProperty<IdentityUser, string>( |
|||
"SocialSecurityNumber", |
|||
options => |
|||
{ |
|||
options.CheckPairDefinitionOnMapping = false; |
|||
}); |
|||
```` |
|||
|
|||
> See the "Object to Object Mapping" section to understand the `CheckPairDefinitionOnMapping` option. |
|||
|
|||
`options` has a dictionary, named `Configuration` which makes the object extension definitions even extensible. It is used by the EF Core to map extra properties to table fields in the database. See the [extending entities](Customizing-Application-Modules-Extending-Entities.md) document. |
|||
|
|||
## Object to Object Mapping |
|||
|
|||
Assume that you've added an extra property to an extensible entity object and used auto [object to object mapping](Object-To-Object-Mapping.md) to map this entity to an extensible DTO class. You need to be careful in such a case, because the extra property may contain a **sensitive data** that should not be available to clients. |
|||
|
|||
This section offers some **good practices** to control your extra properties on object mapping. |
|||
|
|||
### MapExtraPropertiesTo |
|||
|
|||
`MapExtraPropertiesTo` is an extension method provided by the ABP Framework to copy extra properties from an object to another in a controlled manner. Example usage: |
|||
|
|||
````csharp |
|||
identityUser.MapExtraPropertiesTo(identityUserDto); |
|||
```` |
|||
|
|||
`MapExtraPropertiesTo` **requires to define properties** (as described above) in **both sides** (`IdentityUser` and `IdentityUserDto` in this case) in order to copy the value to the target object. Otherwise, it doesn't copy the value even if it does exists in the source object (`identityUser` in this example). There are some ways to overload this restriction. |
|||
|
|||
#### MappingPropertyDefinitionChecks |
|||
|
|||
`MapExtraPropertiesTo` gets an additional parameter to control the definition check for a single mapping operation: |
|||
|
|||
````csharp |
|||
identityUser.MapExtraPropertiesTo( |
|||
identityUserDto, |
|||
MappingPropertyDefinitionChecks.None |
|||
); |
|||
```` |
|||
|
|||
> Be careful since `MappingPropertyDefinitionChecks.None` copies all extra properties without any check. `MappingPropertyDefinitionChecks` enum has other members too. |
|||
|
|||
If you want to completely disable definition check for a property, you can do it while defining the extra property (or update an existing definition) as shown below: |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance |
|||
.AddOrUpdateProperty<IdentityUser, string>( |
|||
"SocialSecurityNumber", |
|||
options => |
|||
{ |
|||
options.CheckPairDefinitionOnMapping = false; |
|||
}); |
|||
```` |
|||
|
|||
#### Ignored Properties |
|||
|
|||
You may want to ignore some properties on a specific mapping operation: |
|||
|
|||
````csharp |
|||
identityUser.MapExtraPropertiesTo( |
|||
identityUserDto, |
|||
ignoredProperties: new[] {"MySensitiveProp"} |
|||
); |
|||
```` |
|||
|
|||
Ignored properties are not copied to the target object. |
|||
|
|||
#### AutoMapper Integration |
|||
|
|||
If you're using the [AutoMapper](https://automapper.org/) library, the ABP Framework also provides an extension method to utilize the `MapExtraPropertiesTo` method defined above. |
|||
|
|||
You can use the `MapExtraProperties()` method inside your mapping profile. |
|||
|
|||
````csharp |
|||
public class MyProfile : Profile |
|||
{ |
|||
public MyProfile() |
|||
{ |
|||
CreateMap<IdentityUser, IdentityUserDto>() |
|||
.MapExtraProperties(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
It has the same parameters with the `MapExtraPropertiesTo` method. |
|||
|
|||
## Entity Framework Core Database Mapping |
|||
|
|||
If you're using the EF Core, you can map an extra property to a table field in the database. Example: |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance |
|||
.AddOrUpdateProperty<IdentityUser, string>( |
|||
"SocialSecurityNumber", |
|||
options => |
|||
{ |
|||
options.MapEfCore(b => b.HasMaxLength(32)); |
|||
} |
|||
); |
|||
```` |
|||
|
|||
See the [Entity Framework Core Integration document](Entity-Framework-Core.md) for more. |
|||
@ -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) |
|||
@ -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 be placed inside it. In addition, the given context will be applied and `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) |
|||
@ -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) |
|||
@ -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) |
|||
@ -1,3 +1,198 @@ |
|||
# 如何对MVC / Razor页面应用程序使用Azure Active Directory身份验证 |
|||
|
|||
TODO... |
|||
本文介绍了如何将AzureAD集成到ABP应用程序中,用 **Azure Active Directory** 凭据使用 OAuth 2.0 登录. |
|||
|
|||
添加Azure Active Directory到ABP框架非常简单,只需要正确的完成几个配置. |
|||
|
|||
为了覆盖更多范围,我们演示两种不同的集成AzureAD的**方法**. |
|||
|
|||
1. **AddAzureAD**: 该方法使用微软[AzureAD UI nuget 包](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/),在网络上搜索如何将AzureAD集成到应用程序时,这个包是最流行的. |
|||
|
|||
2. **AddOpenIdConnect**: 该方法使用默认的[OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/). 它不仅可用于AzureAD,还可用于所有OpenId连接. |
|||
|
|||
> 这些方法之间的功能**没有区别**,AddAzureAD是具有预定义Cookie设置的OpenIdConnection([源](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADAuthenticationBuilderExtensions.cs#L122))的抽象方法. |
|||
> |
|||
> 但是默认配置的登录方案在与ABP应用程序集成方面存在关键差异,下面将对此进行说明. |
|||
|
|||
## 1. AddAzureAD |
|||
|
|||
这个方法使用 [Microsoft AzureAD UI nuget 包](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/),它是最常用的集成AzureAD方法. |
|||
|
|||
如果选择这种方法,需要将 `Microsoft.AspNetCore.Authentication.AzureAD.UI` 软件包安装到 **.Web** 项目中. 由于AddAzureAD扩展使用[配置绑定](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#default-configuration),你需要更改 **.Web** 项目中的appsettings.json文件. |
|||
|
|||
#### **更改 `appsettings.json`** |
|||
|
|||
你添加向 `appsettings.json` 添加新的配置节,在配置 `OpenIdConnectOptions` 时绑定配置: |
|||
|
|||
````json |
|||
"AzureAd": { |
|||
"Instance": "https://login.microsoftonline.com/", |
|||
"TenantId": "<your-tenant-id>", |
|||
"ClientId": "<your-client-id>", |
|||
"Domain": "domain.onmicrosoft.com", |
|||
"CallbackPath": "/signin-azuread-oidc" |
|||
} |
|||
```` |
|||
|
|||
> 这里重要的配置是CallbackPath. 值必须与你的 Azure AD-> app registrations-> Authentication -> RedirectUri 之一相同. |
|||
|
|||
然后你需要配置 `OpenIdConnectOptions` 完成集成. |
|||
|
|||
#### 配置 OpenIdConnectOptions |
|||
|
|||
在你的 **.Web** 项目找到 **ApplicationWebModule** 使用以下代码修改 `ConfigureAuthentication` 方法: |
|||
|
|||
````csharp |
|||
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
|||
{ |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
|||
context.Services.AddAuthentication() |
|||
.AddIdentityServerAuthentication(options => |
|||
{ |
|||
options.Authority = configuration["AuthServer:Authority"]; |
|||
options.RequireHttpsMetadata = false; |
|||
options.ApiName = "Acme.BookStore"; |
|||
}) |
|||
.AddAzureAD(options => configuration.Bind("AzureAd", options)); |
|||
|
|||
context.Services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => |
|||
{ |
|||
options.Authority = options.Authority + "/v2.0/"; |
|||
options.ClientId = configuration["AzureAd:ClientId"]; |
|||
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
|||
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
|||
options.RequireHttpsMetadata = false; |
|||
|
|||
options.TokenValidationParameters.ValidateIssuer = false; |
|||
options.GetClaimsFromUserInfoEndpoint = true; |
|||
options.SaveTokens = true; |
|||
options.SignInScheme = IdentityConstants.ExternalScheme; |
|||
|
|||
options.Scope.Add("email"); |
|||
}); |
|||
} |
|||
```` |
|||
|
|||
> **不要忘记:** |
|||
> |
|||
> * 在 `AddAuthentication()` 之后添加 `.AddAzureAD(options => configuration.Bind("AzureAd", options))` . 它绑定了你的 AzureAD 配置并且容易忘记. |
|||
> * 添加 `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`. 它会禁用默认的 Microsoft claim type 映射. |
|||
> * 添加 `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier)`. 映射 [ClaimTypes.NameIdentifier](https://github.com/dotnet/runtime/blob/6d395de48ac718a913e567ae80961050f2a9a4fa/src/libraries/System.Security.Claims/src/System/Security/Claims/ClaimTypes.cs#L59) 很重要,因为默认SignIn Manager和行为使用这个claim type用于外部登录信息. |
|||
> * 添加 `options.SignInScheme = IdentityConstants.ExternalScheme` 因为 [默认登录方法为 `AzureADOpenID`](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADOpenIdConnectOptionsConfiguration.cs#L35). |
|||
> * 如果你使用的是 **v2.0** 端点,应添加 `options.Scope.Add("email")` 因为 v2.0 端点不会将 `email` 做为默认值返回. [账户模块](../Modules/Account.md) 使用 `email` claim 来 [注册外部账户](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L215). |
|||
|
|||
你已经完成了集成. |
|||
|
|||
## 2. 替代方法: AddOpenIdConnect |
|||
|
|||
如果你不想在应用程序安装一个额外的NuGet包,你可以使用默认的[OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/),它适用于所有的OpenId连接,包括AzureAD外部认证. |
|||
|
|||
你不必使用 `appsettings.json` 配置, 但将AzureAD信息放在 `appsettings.json` 是一个很好的做法. |
|||
|
|||
为了从 `appsettings.json` 获取AzureAD信息在 `OpenIdConnectOptions` 配置使用,只需要在你的 **.Web** 项目中的 `appsettings.json` 添加一个新的配置节: |
|||
|
|||
````json |
|||
"AzureAd": { |
|||
"Instance": "https://login.microsoftonline.com/", |
|||
"TenantId": "<your-tenant-id>", |
|||
"ClientId": "<your-client-id>", |
|||
"Domain": "domain.onmicrosoft.com", |
|||
"CallbackPath": "/signin-azuread-oidc" |
|||
} |
|||
```` |
|||
|
|||
然后在你的 **.Web** 项目的 **ApplicationWebModule** 用以下代码修改 `ConfigureAuthentication` 方法: |
|||
|
|||
````csharp |
|||
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
|||
{ |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
|||
|
|||
context.Services.AddAuthentication() |
|||
.AddIdentityServerAuthentication(options => |
|||
{ |
|||
options.Authority = configuration["AuthServer:Authority"]; |
|||
options.RequireHttpsMetadata = false; |
|||
options.ApiName = "BookStore"; |
|||
}) |
|||
.AddOpenIdConnect("AzureOpenId", "Azure Active Directory OpenId", options => |
|||
{ |
|||
options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/"; |
|||
options.ClientId = configuration["AzureAd:ClientId"]; |
|||
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
|||
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
|||
options.RequireHttpsMetadata = false; |
|||
options.SaveTokens = true; |
|||
options.GetClaimsFromUserInfoEndpoint = true; |
|||
|
|||
options.Scope.Add("email"); |
|||
}); |
|||
} |
|||
```` |
|||
|
|||
集成结束. 请记住你可以连接任何其他外部认证供应商. |
|||
|
|||
## 本文的源代码 |
|||
|
|||
你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization)找到已完成的示例源码. |
|||
|
|||
# FAQ |
|||
|
|||
* Help! `GetExternalLoginInfoAsync` 返回 `null`! |
|||
|
|||
* 有两方面的原因; |
|||
|
|||
1. 你在尝试验证错误的方案. 检查是否设置 **SignInScheme** 为 `IdentityConstants.ExternalScheme`: |
|||
|
|||
````csharp |
|||
options.SignInScheme = IdentityConstants.ExternalScheme; |
|||
```` |
|||
|
|||
2. 你的 `ClaimTypes.NameIdentifier` 为 `null`. 检查是否添加 claim 映射: |
|||
|
|||
````csharp |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
|||
```` |
|||
|
|||
* Help! 我一直得到 ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** 错误! |
|||
|
|||
* 如果你在appsettings设置 **CallbackPath** 为: |
|||
|
|||
````csharp |
|||
"AzureAd": { |
|||
... |
|||
"CallbackPath": "/signin-azuread-oidc" |
|||
} |
|||
```` |
|||
|
|||
你在azure门户的应用程序**重定向URI**必须具有之类 `https://localhost:44320/signin-azuread-oidc` 的<u>域</u>, 而不仅是 `/signin-azuread-oidc`. |
|||
|
|||
* Help! 我一直得到 ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** 错误! |
|||
|
|||
* 当你使用 Azure Authority **v2.0 端点** 而不请求 `email` 域, 会发生这些情况. [Abp 创建用户检查了唯一的邮箱](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). 只需添加 |
|||
|
|||
````csharp |
|||
options.Scope.Add("email"); |
|||
```` |
|||
|
|||
到你的 openid 配置. |
|||
|
|||
* 如何**调试/监视**在映射之前获得的声明? |
|||
|
|||
* 你可以在 openid 配置下加一个简单的事件在映射之前进行调试,例如: |
|||
|
|||
````csharp |
|||
options.Events.OnTokenValidated = (async context => |
|||
{ |
|||
var claimsFromOidcProvider = context.Principal.Claims.ToList(); |
|||
await Task.CompletedTask; |
|||
}); |
|||
```` |
|||
|
|||
## 另请参阅 |
|||
|
|||
* [如何为MVC / Razor页面应用程序自定义登录页面](Customize-Login-Page-MVC.md). |
|||
* [如何为ABP应用程序定制SignIn Manager](Customize-SignIn-Manager.md). |
|||
@ -1,3 +1,101 @@ |
|||
# 如何为ABP应用程序定制SignIn Manager |
|||
|
|||
TODO... |
|||
在使用[应用程序启动模板](../Startup-Templates/Application.md)创建新项目后,你可能想要扩展或更改SignIn Manager的默认行为,以满足你需要的身份验证和注册流程. ABP[账户模块](../Modules/Account.md)使用[身份管理模块](../Modules/Identity.md)做为SignIn Manager,而[身份管理模块](../Modules/Identity.md)使用默认的[Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs)([参阅此处]((https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17))). |
|||
|
|||
编写自定义SignIn Manager,你需要扩展[Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs)类并注入到DI容器. |
|||
|
|||
本文介绍了如何为你自己的应用程序自定义SignIn Manager. |
|||
|
|||
## 创建 CustomSignInManager |
|||
|
|||
创建一个类并继承自Microsoft Identity 包的 [SignInMager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs). |
|||
|
|||
````csharp |
|||
public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser> |
|||
{ |
|||
public CustomSignInManager( |
|||
Microsoft.AspNetCore.Identity.UserManager<Volo.Abp.Identity.IdentityUser> userManager, |
|||
Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, |
|||
Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory, |
|||
Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor, |
|||
Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>> logger, |
|||
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes, |
|||
Microsoft.AspNetCore.Identity.IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation) |
|||
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) |
|||
{ |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> 重点是使用**Volo.Abp.Identity.IdentityUser**做为泛型参数,而不是应用程序的AppUser. |
|||
|
|||
然后你可以覆盖SignIn Manager的任何方法并且为你的身份验证和注册流程添加需要的方法和属性. |
|||
|
|||
## 重写 GetExternalLoginInfoAsync 方法 |
|||
|
|||
在这个用例中我们重写第三方身份验证时使用的 `GetExternalLoginInfoAsync` 方法实现. |
|||
|
|||
一个好的开始是从复制[源码](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Identity/Core/src/SignInManager.cs#L638-L674)而不是从零开始. 在这个用例中我们对源码进行较少的修改,为了帮助理解概念它显式显示了方法和属性的命名空间. |
|||
|
|||
````csharp |
|||
public override async Task<Microsoft.AspNetCore.Identity.ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null) |
|||
{ |
|||
var auth = await Context.AuthenticateAsync(Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme); |
|||
var items = auth?.Properties?.Items; |
|||
if (auth?.Principal == null || items == null || !items.ContainsKey("LoginProviderKey")) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
if (expectedXsrf != null) |
|||
{ |
|||
if (!items.ContainsKey("XsrfKey")) |
|||
{ |
|||
return null; |
|||
} |
|||
var userId = items[XsrfKey] as string; |
|||
if (userId != expectedXsrf) |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); |
|||
var provider = items[LoginProviderKey] as string; |
|||
if (providerKey == null || provider == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName |
|||
?? provider; |
|||
return new Microsoft.AspNetCore.Identity.ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) |
|||
{ |
|||
AuthenticationTokens = auth.Properties.GetTokens() |
|||
}; |
|||
} |
|||
```` |
|||
|
|||
要使你自定义的SignIn Manager类生效,你需要将其注册[依赖注入系统](../Dependency-Injection.md)中. |
|||
|
|||
## 注册到依赖注入 |
|||
|
|||
应该使用 [IdentityBuilder](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityBuilder.cs) 的 [IdentityBuilderExtensions](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/IdentityBuilderExtensions.cs) 类的 **AddSignInManager** 扩展方法注册 `CustomSignInManager`. |
|||
|
|||
在你的 `.Web` 项目找到 `YourProjectNameWebModule` 的 `PreConfigureServices` 方法添加以下代码替换老的 `SignInManager`: |
|||
|
|||
````csharp |
|||
PreConfigure<IdentityBuilder>(identityBuilder => |
|||
{ |
|||
identityBuilder.AddSignInManager<CustomSignInManager>(); |
|||
}); |
|||
```` |
|||
|
|||
## 本文的源代码 |
|||
|
|||
你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization)找到已完成的示例源码. |
|||
|
|||
## 另请参阅 |
|||
|
|||
* [如何为MVC / Razor页面应用程序自定义登录页面](Customize-Login-Page-MVC.md). |
|||
* [身份管理模块](../Modules/Identity.md). |
|||
|
|||
@ -0,0 +1,9 @@ |
|||
export const enum eAccountComponents { |
|||
Login = 'Account.LoginComponent', |
|||
Register = 'Account.RegisterComponent', |
|||
ManageProfile = 'Account.ManageProfileComponent', |
|||
TenantBox = 'Account.TenantBoxComponent', |
|||
AuthWrapper = 'Account.AuthWrapperComponent', |
|||
ChangePassword = 'Account.ChangePasswordComponent', |
|||
PersonalSettings = 'Account.PersonalSettingsComponent', |
|||
} |
|||
@ -1,5 +1,6 @@ |
|||
export * from './lib/account.module'; |
|||
export * from './lib/components'; |
|||
export * from './lib/enums/components'; |
|||
export * from './lib/tokens'; |
|||
export * from './lib/models'; |
|||
export * from './lib/services'; |
|||
|
|||
@ -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; |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
}, |
|||
}; |
|||
@ -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; |
|||
@ -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'; |
|||
|
|||
@ -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; |
|||
@ -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), |
|||
); |
|||
}); |
|||
}); |
|||
@ -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'); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -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)); |
|||
}); |
|||
}); |
|||
@ -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()), |
|||
); |
|||
}, |
|||
); |
|||
}); |
|||
@ -0,0 +1,3 @@ |
|||
export const enum eFeatureManagementComponents { |
|||
FeatureManagement = 'FeatureManagement.FeatureManagementComponent', |
|||
} |
|||
@ -1,2 +1,3 @@ |
|||
export * from './lib/feature-management.module'; |
|||
export * from './lib/components'; |
|||
export * from './lib/enums/components'; |
|||
|
|||
@ -0,0 +1,4 @@ |
|||
export const enum eIdentityComponents { |
|||
Roles = 'Identity.RolesComponent', |
|||
Users = 'Identity.UsersComponent', |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export const enum ePermissionManagementComponents { |
|||
PermissionManagement = 'PermissionManagement.PermissionManagementComponent', |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export const enum eSettingManagementComponents { |
|||
SettingManagement = 'SettingManagement.SettingManagementComponent', |
|||
} |
|||
@ -1,2 +1,3 @@ |
|||
export * from './lib/setting-management.module'; |
|||
export * from './lib/components/setting-management.component'; |
|||
export * from './lib/enums/components'; |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
export const enum eTenantManagementComponents { |
|||
Tenants = 'TenantManagement.TenantsComponent', |
|||
} |
|||
@ -1,6 +1,7 @@ |
|||
export * from './lib/tenant-management.module'; |
|||
export * from './lib/actions'; |
|||
export * from './lib/components'; |
|||
export * from './lib/enums/components'; |
|||
export * from './lib/models'; |
|||
export * from './lib/services'; |
|||
export * from './lib/states'; |
|||
|
|||
@ -0,0 +1,5 @@ |
|||
export const enum eThemeBasicComponents { |
|||
ApplicationLayout = 'Theme.ApplicationLayoutComponent', |
|||
AccountLayout = 'Theme.AccountLayoutComponent', |
|||
EmptyLayout = 'Theme.EmptyLayoutComponent', |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue