mirror of https://github.com/abpframework/abp.git
committed by
GitHub
59 changed files with 1696 additions and 440 deletions
@ -1,13 +1,16 @@ |
|||||
import { Component, Input, TemplateRef } from '@angular/core'; |
import { Component, Input, TemplateRef } from '@angular/core'; |
||||
|
import { Account } from '../../models/account'; |
||||
|
|
||||
@Component({ |
@Component({ |
||||
selector: 'abp-auth-wrapper', |
selector: 'abp-auth-wrapper', |
||||
templateUrl: './auth-wrapper.component.html', |
templateUrl: './auth-wrapper.component.html', |
||||
|
exportAs: 'abpAuthWrapper', |
||||
}) |
}) |
||||
export class AuthWrapperComponent { |
export class AuthWrapperComponent |
||||
|
implements Account.AuthWrapperComponentInputs, Account.AuthWrapperComponentOutputs { |
||||
@Input() |
@Input() |
||||
mainContentRef: TemplateRef<any>; |
readonly mainContentRef: TemplateRef<any>; |
||||
|
|
||||
@Input() |
@Input() |
||||
cancelContentRef: TemplateRef<any>; |
readonly cancelContentRef: TemplateRef<any>; |
||||
} |
} |
||||
|
|||||
@ -1,50 +1,74 @@ |
|||||
<abp-auth-wrapper [mainContentRef]="mainContentRef" [cancelContentRef]="cancelContentRef"> |
<abp-auth-wrapper |
||||
<ng-template #mainContentRef> |
*abpReplaceableTemplate="{ |
||||
<h4>{{ 'AbpAccount::Login' | abpLocalization }}</h4> |
componentKey: 'Account.AuthWrapperComponent', |
||||
<strong> |
inputs: { |
||||
{{ 'AbpAccount::AreYouANewUser' | abpLocalization }} |
mainContentRef: { value: mainContentRef }, |
||||
<a class="text-decoration-none" routerLink="/account/register">{{ 'AbpAccount::Register' | abpLocalization }}</a> |
cancelContentRef: { value: cancelContentRef } |
||||
</strong> |
} |
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()" validateOnSubmit class="mt-4"> |
}" |
||||
<div class="form-group"> |
[mainContentRef]="mainContentRef" |
||||
<label for="login-input-user-name-or-email-address">{{ |
[cancelContentRef]="cancelContentRef" |
||||
'AbpAccount::UserNameOrEmailAddress' | abpLocalization |
> |
||||
}}</label> |
</abp-auth-wrapper> |
||||
|
<ng-template #mainContentRef> |
||||
|
<h4>{{ 'AbpAccount::Login' | abpLocalization }}</h4> |
||||
|
<strong> |
||||
|
{{ 'AbpAccount::AreYouANewUser' | abpLocalization }} |
||||
|
<a class="text-decoration-none" routerLink="/account/register">{{ |
||||
|
'AbpAccount::Register' | abpLocalization |
||||
|
}}</a> |
||||
|
</strong> |
||||
|
<form [formGroup]="form" (ngSubmit)="onSubmit()" validateOnSubmit class="mt-4"> |
||||
|
<div class="form-group"> |
||||
|
<label for="login-input-user-name-or-email-address">{{ |
||||
|
'AbpAccount::UserNameOrEmailAddress' | abpLocalization |
||||
|
}}</label> |
||||
|
<input |
||||
|
class="form-control" |
||||
|
type="text" |
||||
|
id="login-input-user-name-or-email-address" |
||||
|
formControlName="username" |
||||
|
autocomplete="username" |
||||
|
autofocus |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label for="login-input-password">{{ 'AbpAccount::Password' | abpLocalization }}</label> |
||||
|
<input |
||||
|
class="form-control" |
||||
|
type="password" |
||||
|
id="login-input-password" |
||||
|
formControlName="password" |
||||
|
autocomplete="current-password" |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="form-check" validationTarget validationStyle> |
||||
|
<label class="form-check-label" for="login-input-remember-me"> |
||||
<input |
<input |
||||
class="form-control" |
class="form-check-input" |
||||
type="text" |
type="checkbox" |
||||
id="login-input-user-name-or-email-address" |
id="login-input-remember-me" |
||||
formControlName="username" |
formControlName="remember" |
||||
autofocus |
|
||||
/> |
/> |
||||
</div> |
{{ 'AbpAccount::RememberMe' | abpLocalization }} |
||||
<div class="form-group"> |
</label> |
||||
<label for="login-input-password">{{ 'AbpAccount::Password' | abpLocalization }}</label> |
|
||||
<input class="form-control" type="password" id="login-input-password" formControlName="password" /> |
|
||||
</div> |
|
||||
<div class="form-check" validationTarget validationStyle> |
|
||||
<label class="form-check-label" for="login-input-remember-me"> |
|
||||
<input class="form-check-input" type="checkbox" id="login-input-remember-me" formControlName="remember" /> |
|
||||
{{ 'AbpAccount::RememberMe' | abpLocalization }} |
|
||||
</label> |
|
||||
</div> |
|
||||
<abp-button |
|
||||
[loading]="inProgress" |
|
||||
buttonType="submit" |
|
||||
name="Action" |
|
||||
buttonClass="btn-block btn-lg mt-3 btn btn-primary" |
|
||||
> |
|
||||
{{ 'AbpAccount::Login' | abpLocalization }} |
|
||||
</abp-button> |
|
||||
</form> |
|
||||
</ng-template> |
|
||||
<ng-template #cancelContentRef> |
|
||||
<div class="card-footer text-center border-0"> |
|
||||
<a routerLink="/"> |
|
||||
<button type="button" name="Action" value="Cancel" class="px-2 py-0 btn btn-link"> |
|
||||
{{ 'AbpAccount::Cancel' | abpLocalization }} |
|
||||
</button> |
|
||||
</a> |
|
||||
</div> |
</div> |
||||
</ng-template> |
<abp-button |
||||
</abp-auth-wrapper> |
[loading]="inProgress" |
||||
|
buttonType="submit" |
||||
|
name="Action" |
||||
|
buttonClass="btn-block btn-lg mt-3 btn btn-primary" |
||||
|
> |
||||
|
{{ 'AbpAccount::Login' | abpLocalization }} |
||||
|
</abp-button> |
||||
|
</form> |
||||
|
</ng-template> |
||||
|
<ng-template #cancelContentRef> |
||||
|
<div class="card-footer text-center border-0"> |
||||
|
<a routerLink="/"> |
||||
|
<button type="button" name="Action" value="Cancel" class="px-2 py-0 btn btn-link"> |
||||
|
{{ 'AbpAccount::Cancel' | abpLocalization }} |
||||
|
</button> |
||||
|
</a> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
|||||
@ -1,32 +1,57 @@ |
|||||
<abp-auth-wrapper [mainContentRef]="mainContentRef"> |
<abp-auth-wrapper |
||||
<ng-template #mainContentRef> |
*abpReplaceableTemplate="{ |
||||
<h4>{{ 'AbpAccount::Register' | abpLocalization }}</h4> |
componentKey: 'Account.AuthWrapperComponent', |
||||
<strong> |
inputs: { |
||||
{{ 'AbpAccount::AlreadyRegistered' | abpLocalization }} |
mainContentRef: { value: mainContentRef } |
||||
<a class="text-decoration-none" routerLink="/account/login">{{ 'AbpAccount::Login' | abpLocalization }}</a> |
} |
||||
</strong> |
}" |
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()" validateOnSubmit class="mt-4"> |
[mainContentRef]="mainContentRef" |
||||
<div class="form-group"> |
> |
||||
<label for="input-user-name">{{ 'AbpAccount::UserName' | abpLocalization }}</label |
|
||||
><span> * </span |
|
||||
><input autofocus type="text" id="input-user-name" class="form-control" formControlName="username" /> |
|
||||
</div> |
|
||||
<div class="form-group"> |
|
||||
<label for="input-email-address">{{ 'AbpAccount::EmailAddress' | abpLocalization }}</label |
|
||||
><span> * </span><input type="email" id="input-email-address" class="form-control" formControlName="email" /> |
|
||||
</div> |
|
||||
<div class="form-group"> |
|
||||
<label for="input-password">{{ 'AbpAccount::Password' | abpLocalization }}</label |
|
||||
><span> * </span><input type="password" id="input-password" class="form-control" formControlName="password" /> |
|
||||
</div> |
|
||||
<abp-button |
|
||||
[loading]="inProgress" |
|
||||
buttonType="submit" |
|
||||
name="Action" |
|
||||
buttonClass="btn-block btn-lg mt-3 btn btn-primary" |
|
||||
> |
|
||||
{{ 'AbpAccount::Register' | abpLocalization }} |
|
||||
</abp-button> |
|
||||
</form> |
|
||||
</ng-template> |
|
||||
</abp-auth-wrapper> |
</abp-auth-wrapper> |
||||
|
<ng-template #mainContentRef> |
||||
|
<h4>{{ 'AbpAccount::Register' | abpLocalization }}</h4> |
||||
|
<strong> |
||||
|
{{ 'AbpAccount::AlreadyRegistered' | abpLocalization }} |
||||
|
<a class="text-decoration-none" routerLink="/account/login">{{ |
||||
|
'AbpAccount::Login' | abpLocalization |
||||
|
}}</a> |
||||
|
</strong> |
||||
|
<form [formGroup]="form" (ngSubmit)="onSubmit()" validateOnSubmit class="mt-4"> |
||||
|
<div class="form-group"> |
||||
|
<label for="input-user-name">{{ 'AbpAccount::UserName' | abpLocalization }}</label |
||||
|
><span> * </span |
||||
|
><input |
||||
|
autofocus |
||||
|
type="text" |
||||
|
id="input-user-name" |
||||
|
class="form-control" |
||||
|
formControlName="username" |
||||
|
autocomplete="username" |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label for="input-email-address">{{ 'AbpAccount::EmailAddress' | abpLocalization }}</label |
||||
|
><span> * </span |
||||
|
><input type="email" id="input-email-address" class="form-control" formControlName="email" /> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label for="input-password">{{ 'AbpAccount::Password' | abpLocalization }}</label |
||||
|
><span> * </span |
||||
|
><input |
||||
|
type="password" |
||||
|
id="input-password" |
||||
|
class="form-control" |
||||
|
formControlName="password" |
||||
|
autocomplete="current-password" |
||||
|
/> |
||||
|
</div> |
||||
|
<abp-button |
||||
|
[loading]="inProgress" |
||||
|
buttonType="submit" |
||||
|
name="Action" |
||||
|
buttonClass="btn-block btn-lg mt-3 btn btn-primary" |
||||
|
> |
||||
|
{{ 'AbpAccount::Register' | abpLocalization }} |
||||
|
</abp-button> |
||||
|
</form> |
||||
|
</ng-template> |
||||
|
|||||
@ -0,0 +1,29 @@ |
|||||
|
import { TemplateRef } from '@angular/core'; |
||||
|
|
||||
|
export namespace Account { |
||||
|
export interface AuthWrapperComponentInputs { |
||||
|
readonly mainContentRef: TemplateRef<any>; |
||||
|
readonly cancelContentRef?: TemplateRef<any>; |
||||
|
} |
||||
|
|
||||
|
// tslint:disable-next-line: no-empty-interface
|
||||
|
export interface AuthWrapperComponentOutputs {} |
||||
|
|
||||
|
// tslint:disable-next-line: no-empty-interface
|
||||
|
export interface TenantBoxComponentInputs {} |
||||
|
|
||||
|
// tslint:disable-next-line: no-empty-interface
|
||||
|
export interface TenantBoxComponentOutputs {} |
||||
|
|
||||
|
// tslint:disable-next-line: no-empty-interface
|
||||
|
export interface PersonalSettingsComponentInputs {} |
||||
|
|
||||
|
// tslint:disable-next-line: no-empty-interface
|
||||
|
export interface PersonalSettingsComponentOutputs {} |
||||
|
|
||||
|
// tslint:disable-next-line: no-empty-interface
|
||||
|
export interface ChangePasswordComponentInputs {} |
||||
|
|
||||
|
// tslint:disable-next-line: no-empty-interface
|
||||
|
export interface ChangePasswordComponentOutputs {} |
||||
|
} |
||||
@ -1,3 +1,4 @@ |
|||||
|
export * from './account'; |
||||
export * from './options'; |
export * from './options'; |
||||
export * from './user'; |
export * from './user'; |
||||
export * from './tenant'; |
export * from './tenant'; |
||||
|
|||||
@ -1,5 +1,6 @@ |
|||||
export * from './config.actions'; |
export * from './config.actions'; |
||||
export * from './loader.actions'; |
export * from './loader.actions'; |
||||
export * from './profile.actions'; |
export * from './profile.actions'; |
||||
|
export * from './replaceable-components.actions'; |
||||
export * from './rest.actions'; |
export * from './rest.actions'; |
||||
export * from './session.actions'; |
export * from './session.actions'; |
||||
|
|||||
@ -0,0 +1,6 @@ |
|||||
|
import { ReplaceableComponents } from '../models/replaceable-components'; |
||||
|
|
||||
|
export class AddReplaceableComponent { |
||||
|
static readonly type = '[ReplaceableComponents] Add'; |
||||
|
constructor(public payload: ReplaceableComponents.ReplaceableComponent) {} |
||||
|
} |
||||
@ -1,2 +1,3 @@ |
|||||
export * from './dynamic-layout.component'; |
export * from './dynamic-layout.component'; |
||||
|
export * from './replaceable-route-container.component'; |
||||
export * from './router-outlet.component'; |
export * from './router-outlet.component'; |
||||
|
|||||
@ -0,0 +1,39 @@ |
|||||
|
import { Component, OnDestroy, OnInit, Type } from '@angular/core'; |
||||
|
import { ActivatedRoute } from '@angular/router'; |
||||
|
import { Store } from '@ngxs/store'; |
||||
|
import { distinctUntilChanged } from 'rxjs/operators'; |
||||
|
import { ABP } from '../models/common'; |
||||
|
import { ReplaceableComponents } from '../models/replaceable-components'; |
||||
|
import { ReplaceableComponentsState } from '../states/replaceable-components.state'; |
||||
|
import { takeUntilDestroy } from '../utils/rxjs-utils'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'abp-replaceable-route-container', |
||||
|
template: ` |
||||
|
<ng-container *ngComponentOutlet="externalComponent || defaultComponent"></ng-container> |
||||
|
`,
|
||||
|
}) |
||||
|
export class ReplaceableRouteContainerComponent implements OnInit, OnDestroy { |
||||
|
defaultComponent: Type<any>; |
||||
|
|
||||
|
componentKey: string; |
||||
|
|
||||
|
externalComponent: Type<any>; |
||||
|
|
||||
|
constructor(private route: ActivatedRoute, private store: Store) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.defaultComponent = this.route.snapshot.data.replaceableComponent.defaultComponent; |
||||
|
this.componentKey = (this.route.snapshot.data |
||||
|
.replaceableComponent as ReplaceableComponents.RouteData).key; |
||||
|
|
||||
|
this.store |
||||
|
.select(ReplaceableComponentsState.getComponent(this.componentKey)) |
||||
|
.pipe(takeUntilDestroy(this), distinctUntilChanged()) |
||||
|
.subscribe((res = {} as ReplaceableComponents.ReplaceableComponent) => { |
||||
|
this.externalComponent = res.component; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() {} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
import { Directive, Output, EventEmitter, ElementRef, AfterViewInit } from '@angular/core'; |
||||
|
|
||||
|
@Directive({ selector: '[abpInit]' }) |
||||
|
export class InitDirective implements AfterViewInit { |
||||
|
@Output('abpInit') readonly init = new EventEmitter<ElementRef<any>>(); |
||||
|
|
||||
|
constructor(private elRef: ElementRef) {} |
||||
|
|
||||
|
ngAfterViewInit() { |
||||
|
this.init.emit(this.elRef); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,164 @@ |
|||||
|
import { |
||||
|
ComponentFactoryResolver, |
||||
|
Directive, |
||||
|
Injector, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
SimpleChanges, |
||||
|
TemplateRef, |
||||
|
Type, |
||||
|
ViewContainerRef, |
||||
|
} from '@angular/core'; |
||||
|
import { Store } from '@ngxs/store'; |
||||
|
import { Subscription } from 'rxjs'; |
||||
|
import { filter } from 'rxjs/operators'; |
||||
|
import { ABP } from '../models/common'; |
||||
|
import { ReplaceableComponents } from '../models/replaceable-components'; |
||||
|
import { ReplaceableComponentsState } from '../states/replaceable-components.state'; |
||||
|
import { takeUntilDestroy } from '../utils/rxjs-utils'; |
||||
|
import compare from 'just-compare'; |
||||
|
import snq from 'snq'; |
||||
|
|
||||
|
@Directive({ selector: '[abpReplaceableTemplate]' }) |
||||
|
export class ReplaceableTemplateDirective implements OnInit, OnDestroy, OnChanges { |
||||
|
@Input('abpReplaceableTemplate') |
||||
|
data: ReplaceableComponents.ReplaceableTemplateDirectiveInput<any, any>; |
||||
|
|
||||
|
providedData = { inputs: {}, outputs: {} } as ReplaceableComponents.ReplaceableTemplateData< |
||||
|
any, |
||||
|
any |
||||
|
>; |
||||
|
|
||||
|
context = {} as any; |
||||
|
|
||||
|
externalComponent: Type<any>; |
||||
|
|
||||
|
defaultComponentRef: any; |
||||
|
|
||||
|
defaultComponentSubscriptions = {} as ABP.Dictionary<Subscription>; |
||||
|
|
||||
|
initialized = false; |
||||
|
|
||||
|
constructor( |
||||
|
private injector: Injector, |
||||
|
private templateRef: TemplateRef<any>, |
||||
|
private cfRes: ComponentFactoryResolver, |
||||
|
private vcRef: ViewContainerRef, |
||||
|
private store: Store, |
||||
|
) { |
||||
|
this.context = { |
||||
|
initTemplate: ref => { |
||||
|
this.resetDefaultComponent(); |
||||
|
this.defaultComponentRef = ref; |
||||
|
this.setDefaultComponentInputs(); |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.store |
||||
|
.select(ReplaceableComponentsState.getComponent(this.data.componentKey)) |
||||
|
.pipe( |
||||
|
filter( |
||||
|
(res = {} as ReplaceableComponents.ReplaceableComponent) => |
||||
|
!this.initialized || !compare(res.component, this.externalComponent), |
||||
|
), |
||||
|
takeUntilDestroy(this), |
||||
|
) |
||||
|
.subscribe((res = {} as ReplaceableComponents.ReplaceableComponent) => { |
||||
|
this.vcRef.clear(); |
||||
|
this.externalComponent = res.component; |
||||
|
if (this.defaultComponentRef) { |
||||
|
this.resetDefaultComponent(); |
||||
|
} |
||||
|
|
||||
|
if (res.component) { |
||||
|
this.setProvidedData(); |
||||
|
const customInjector = Injector.create({ |
||||
|
providers: [{ provide: 'REPLACEABLE_DATA', useValue: this.providedData }], |
||||
|
parent: this.injector, |
||||
|
}); |
||||
|
this.vcRef.createComponent( |
||||
|
this.cfRes.resolveComponentFactory(res.component), |
||||
|
0, |
||||
|
customInjector, |
||||
|
); |
||||
|
} else { |
||||
|
this.vcRef.createEmbeddedView(this.templateRef, this.context); |
||||
|
} |
||||
|
|
||||
|
this.initialized = true; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges) { |
||||
|
if (snq(() => changes.data.currentValue.inputs) && this.defaultComponentRef) { |
||||
|
this.setDefaultComponentInputs(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() {} |
||||
|
|
||||
|
setDefaultComponentInputs() { |
||||
|
if (!this.defaultComponentRef || (!this.data.inputs && !this.data.outputs)) return; |
||||
|
|
||||
|
if (this.data.inputs) { |
||||
|
for (const key in this.data.inputs) { |
||||
|
if (this.data.inputs.hasOwnProperty(key)) { |
||||
|
if (!compare(this.defaultComponentRef[key], this.data.inputs[key].value)) { |
||||
|
this.defaultComponentRef[key] = this.data.inputs[key].value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (this.data.outputs) { |
||||
|
for (const key in this.data.outputs) { |
||||
|
if (this.data.outputs.hasOwnProperty(key)) { |
||||
|
if (!this.defaultComponentSubscriptions[key]) { |
||||
|
this.defaultComponentSubscriptions[key] = this.defaultComponentRef[key].subscribe( |
||||
|
value => { |
||||
|
this.data.outputs[key](value); |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setProvidedData() { |
||||
|
this.providedData = { ...this.data, inputs: {} }; |
||||
|
|
||||
|
if (!this.data.inputs) return; |
||||
|
Object.defineProperties(this.providedData.inputs, { |
||||
|
...Object.keys(this.data.inputs).reduce( |
||||
|
(acc, key) => ({ |
||||
|
...acc, |
||||
|
[key]: { |
||||
|
enumerable: true, |
||||
|
configurable: true, |
||||
|
get: () => this.data.inputs[key].value, |
||||
|
...(this.data.inputs[key].twoWay && { |
||||
|
set: newValue => { |
||||
|
this.data.inputs[key].value = newValue; |
||||
|
this.data.outputs[`${key}Change`](newValue); |
||||
|
}, |
||||
|
}), |
||||
|
}, |
||||
|
}), |
||||
|
{}, |
||||
|
), |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
resetDefaultComponent() { |
||||
|
Object.keys(this.defaultComponentSubscriptions).forEach(key => { |
||||
|
this.defaultComponentSubscriptions[key].unsubscribe(); |
||||
|
}); |
||||
|
this.defaultComponentSubscriptions = {} as ABP.Dictionary<Subscription>; |
||||
|
this.defaultComponentRef = null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
import { Type, EventEmitter } from '@angular/core'; |
||||
|
import { ABP } from './common'; |
||||
|
import { Subject, BehaviorSubject } from 'rxjs'; |
||||
|
|
||||
|
export namespace ReplaceableComponents { |
||||
|
export interface State { |
||||
|
replaceableComponents: ReplaceableComponent[]; |
||||
|
} |
||||
|
|
||||
|
export interface ReplaceableComponent { |
||||
|
component: Type<any>; |
||||
|
key: string; |
||||
|
} |
||||
|
|
||||
|
export interface ReplaceableTemplateDirectiveInput< |
||||
|
I, |
||||
|
O extends { [K in keyof O]: EventEmitter<any> | Subject<any> } |
||||
|
> { |
||||
|
inputs: { -readonly [K in keyof I]: { value: I[K]; twoWay?: boolean } }; |
||||
|
outputs: { -readonly [K in keyof O]: (value: ABP.ExtractFromOutput<O[K]>) => void }; |
||||
|
componentKey: string; |
||||
|
} |
||||
|
|
||||
|
export interface ReplaceableTemplateData< |
||||
|
I, |
||||
|
O extends { [K in keyof O]: EventEmitter<any> | Subject<any> } |
||||
|
> { |
||||
|
inputs: ReplaceableTemplateInputs<I>; |
||||
|
outputs: ReplaceableTemplateOutputs<O>; |
||||
|
componentKey: string; |
||||
|
} |
||||
|
|
||||
|
export type ReplaceableTemplateInputs<T> = { |
||||
|
[K in keyof T]: T[K]; |
||||
|
}; |
||||
|
|
||||
|
export type ReplaceableTemplateOutputs< |
||||
|
T extends { [K in keyof T]: EventEmitter<any> | Subject<any> } |
||||
|
> = { |
||||
|
[K in keyof T]: (value: ABP.ExtractFromOutput<T[K]>) => void; |
||||
|
}; |
||||
|
|
||||
|
export interface RouteData<T = any> { |
||||
|
key: string; |
||||
|
defaultComponent: Type<T>; |
||||
|
} |
||||
|
} |
||||
@ -1,3 +1,4 @@ |
|||||
export * from './profile.state'; |
export * from './replaceable-components.state'; |
||||
export * from './config.state'; |
export * from './config.state'; |
||||
|
export * from './profile.state'; |
||||
export * from './session.state'; |
export * from './session.state'; |
||||
|
|||||
@ -0,0 +1,50 @@ |
|||||
|
import { State, Action, StateContext, Selector, createSelector } from '@ngxs/store'; |
||||
|
import { AddReplaceableComponent } from '../actions/replaceable-components.actions'; |
||||
|
import { ReplaceableComponents } from '../models/replaceable-components'; |
||||
|
import snq from 'snq'; |
||||
|
|
||||
|
@State<ReplaceableComponents.State>({ |
||||
|
name: 'ReplaceableComponentsState', |
||||
|
defaults: { replaceableComponents: [] } as ReplaceableComponents.State, |
||||
|
}) |
||||
|
export class ReplaceableComponentsState { |
||||
|
@Selector() |
||||
|
static getAll({ |
||||
|
replaceableComponents, |
||||
|
}: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent[] { |
||||
|
return replaceableComponents || []; |
||||
|
} |
||||
|
|
||||
|
static getComponent(key: string) { |
||||
|
const selector = createSelector( |
||||
|
[ReplaceableComponentsState], |
||||
|
(state: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent => { |
||||
|
return snq(() => state.replaceableComponents.find(component => component.key === key)); |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
return selector; |
||||
|
} |
||||
|
|
||||
|
@Action(AddReplaceableComponent) |
||||
|
replaceableComponentsAction( |
||||
|
{ getState, patchState }: StateContext<ReplaceableComponents.State>, |
||||
|
{ payload }: AddReplaceableComponent, |
||||
|
) { |
||||
|
let { replaceableComponents } = getState(); |
||||
|
|
||||
|
const index = snq( |
||||
|
() => replaceableComponents.findIndex(component => component.key === payload.key), |
||||
|
-1, |
||||
|
); |
||||
|
if (index > -1) { |
||||
|
replaceableComponents[index] = payload; |
||||
|
} else { |
||||
|
replaceableComponents = [...replaceableComponents, payload]; |
||||
|
} |
||||
|
|
||||
|
patchState({ |
||||
|
replaceableComponents, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; |
||||
|
import { NgxsModule, Store } from '@ngxs/store'; |
||||
|
import { ReplaceableComponentsState } from '../states/replaceable-components.state'; |
||||
|
import { Component } from '@angular/core'; |
||||
|
import { AddReplaceableComponent } from '../actions'; |
||||
|
|
||||
|
@Component({ selector: 'abp-dummy', template: 'dummy works' }) |
||||
|
class DummyComponent {} |
||||
|
|
||||
|
describe('ReplaceableComponentsState', () => { |
||||
|
let spectator: SpectatorHost<DummyComponent>; |
||||
|
const createHost = createHostFactory({ |
||||
|
component: DummyComponent, |
||||
|
imports: [NgxsModule.forRoot([ReplaceableComponentsState])], |
||||
|
}); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
spectator = createHost('<abp-dummy></abp-dummy>'); |
||||
|
}); |
||||
|
|
||||
|
it('should add a component to the state', () => { |
||||
|
const store = spectator.get(Store); |
||||
|
expect(store.selectSnapshot(ReplaceableComponentsState.getAll)).toEqual([]); |
||||
|
store.dispatch(new AddReplaceableComponent({ component: DummyComponent, key: 'Dummy' })); |
||||
|
expect(store.selectSnapshot(ReplaceableComponentsState.getComponent('Dummy'))).toEqual({ |
||||
|
component: DummyComponent, |
||||
|
key: 'Dummy', |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it('should replace a exist component', () => { |
||||
|
const store = spectator.get(Store); |
||||
|
store.dispatch(new AddReplaceableComponent({ component: DummyComponent, key: 'Dummy' })); |
||||
|
store.dispatch(new AddReplaceableComponent({ component: null, key: 'Dummy' })); |
||||
|
expect(store.selectSnapshot(ReplaceableComponentsState.getComponent('Dummy'))).toEqual({ |
||||
|
component: null, |
||||
|
key: 'Dummy', |
||||
|
}); |
||||
|
expect(store.selectSnapshot(ReplaceableComponentsState.getAll)).toHaveLength(1); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,66 @@ |
|||||
|
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; |
||||
|
import { Component } from '@angular/core'; |
||||
|
import { ActivatedRoute } from '@angular/router'; |
||||
|
import { Store } from '@ngxs/store'; |
||||
|
import { of, Subject, BehaviorSubject } from 'rxjs'; |
||||
|
import { ReplaceableRouteContainerComponent } from '../components/replaceable-route-container.component'; |
||||
|
import { ReplaceableComponentsState } from '../states'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'abp-external-component', |
||||
|
template: '<p>external</p>', |
||||
|
}) |
||||
|
export class ExternalComponent {} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'abp-default-component', |
||||
|
template: '<p>default</p>', |
||||
|
}) |
||||
|
export class DefaultComponent {} |
||||
|
|
||||
|
const activatedRouteMock = { |
||||
|
snapshot: { |
||||
|
data: { |
||||
|
replaceableComponent: { |
||||
|
defaultComponent: DefaultComponent, |
||||
|
key: 'TestModule.TestComponent', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
describe('ReplaceableRouteContainerComponent', () => { |
||||
|
const selectResponse = new BehaviorSubject(undefined); |
||||
|
const mockSelect = jest.fn(() => selectResponse); |
||||
|
|
||||
|
let spectator: SpectatorHost<ReplaceableRouteContainerComponent>; |
||||
|
const createHost = createHostFactory({ |
||||
|
component: ReplaceableRouteContainerComponent, |
||||
|
providers: [ |
||||
|
{ provide: ActivatedRoute, useValue: activatedRouteMock }, |
||||
|
{ provide: Store, useValue: { select: mockSelect } }, |
||||
|
], |
||||
|
declarations: [ExternalComponent, DefaultComponent], |
||||
|
entryComponents: [DefaultComponent, ExternalComponent], |
||||
|
}); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
spectator = createHost('<abp-replaceable-route-container></abp-replaceable-route-container>', { |
||||
|
detectChanges: true, |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it('should display the default component', () => { |
||||
|
expect(spectator.query('p')).toHaveText('default'); |
||||
|
}); |
||||
|
|
||||
|
it("should display the external component if it's available in store.", () => { |
||||
|
selectResponse.next({ component: ExternalComponent }); |
||||
|
spectator.detectChanges(); |
||||
|
expect(spectator.query('p')).toHaveText('external'); |
||||
|
|
||||
|
selectResponse.next({ component: null }); |
||||
|
spectator.detectChanges(); |
||||
|
expect(spectator.query('p')).toHaveText('default'); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,175 @@ |
|||||
|
import { Component, EventEmitter, Inject, Input, OnInit, Optional, Output } from '@angular/core'; |
||||
|
import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; |
||||
|
import { Store } from '@ngxs/store'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { ReplaceableTemplateDirective } from '../directives'; |
||||
|
import { ReplaceableComponents } from '../models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'abp-default-component', |
||||
|
template: ` |
||||
|
<p>default</p> |
||||
|
`,
|
||||
|
exportAs: 'abpDefaultComponent', |
||||
|
}) |
||||
|
class DefaultComponent implements OnInit { |
||||
|
@Input() |
||||
|
oneWay; |
||||
|
|
||||
|
@Input() |
||||
|
twoWay: boolean; |
||||
|
|
||||
|
@Output() |
||||
|
readonly twoWayChange = new EventEmitter<boolean>(); |
||||
|
|
||||
|
@Output() |
||||
|
readonly someOutput = new EventEmitter<string>(); |
||||
|
|
||||
|
ngOnInit() {} |
||||
|
|
||||
|
setTwoWay(value) { |
||||
|
this.twoWay = value; |
||||
|
this.twoWayChange.emit(value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'abp-external-component', |
||||
|
template: ` |
||||
|
<p>external</p> |
||||
|
`,
|
||||
|
}) |
||||
|
class ExternalComponent { |
||||
|
constructor( |
||||
|
@Optional() |
||||
|
@Inject('REPLACEABLE_DATA') |
||||
|
public data: ReplaceableComponents.ReplaceableTemplateData<any, any>, |
||||
|
) {} |
||||
|
} |
||||
|
|
||||
|
describe('ReplaceableTemplateDirective', () => { |
||||
|
const selectResponse = new Subject(); |
||||
|
const mockSelect = jest.fn(() => selectResponse); |
||||
|
|
||||
|
let spectator: SpectatorDirective<ReplaceableTemplateDirective>; |
||||
|
const createDirective = createDirectiveFactory({ |
||||
|
directive: ReplaceableTemplateDirective, |
||||
|
providers: [{ provide: Store, useValue: { select: mockSelect } }], |
||||
|
declarations: [DefaultComponent, ExternalComponent], |
||||
|
entryComponents: [ExternalComponent], |
||||
|
}); |
||||
|
|
||||
|
describe('without external component', () => { |
||||
|
const twoWayChange = jest.fn(a => a); |
||||
|
const someOutput = jest.fn(a => a); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
spectator = createDirective( |
||||
|
` |
||||
|
<div *abpReplaceableTemplate="{inputs: {oneWay: {value: oneWay}, twoWay: {value: twoWay, twoWay: true}}, outputs: {twoWayChange: twoWayChange, someOutput: someOutput}, componentKey: 'TestModule.TestComponent'}; let initTemplate = initTemplate"> |
||||
|
<abp-default-component #defaultComponent="abpDefaultComponent"></abp-default-component> |
||||
|
</div> |
||||
|
`,
|
||||
|
{ hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } }, |
||||
|
); |
||||
|
selectResponse.next(undefined); |
||||
|
const component = spectator.query(DefaultComponent); |
||||
|
spectator.directive.context.initTemplate(component); |
||||
|
spectator.detectChanges(); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => twoWayChange.mockClear()); |
||||
|
|
||||
|
it('should display the default template when store response is undefined', () => { |
||||
|
expect(spectator.query('abp-default-component')).toBeTruthy(); |
||||
|
}); |
||||
|
|
||||
|
it('should be setted inputs and outputs', () => { |
||||
|
const component = spectator.query(DefaultComponent); |
||||
|
expect(component.oneWay).toEqual({ label: 'Test' }); |
||||
|
expect(component.twoWay).toEqual(false); |
||||
|
}); |
||||
|
|
||||
|
it('should change the component inputs', () => { |
||||
|
const component = spectator.query(DefaultComponent); |
||||
|
spectator.setHostInput({ oneWay: 'test' }); |
||||
|
component.setTwoWay(true); |
||||
|
component.someOutput.emit('someOutput emitted'); |
||||
|
expect(component.oneWay).toBe('test'); |
||||
|
expect(twoWayChange).toHaveBeenCalledWith(true); |
||||
|
expect(someOutput).toHaveBeenCalledWith('someOutput emitted'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('with external component', () => { |
||||
|
const twoWayChange = jest.fn(a => a); |
||||
|
const someOutput = jest.fn(a => a); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
spectator = createDirective( |
||||
|
` |
||||
|
<div *abpReplaceableTemplate="{inputs: {oneWay: {value: oneWay}, twoWay: {value: twoWay, twoWay: true}}, outputs: {twoWayChange: twoWayChange, someOutput: someOutput}, componentKey: 'TestModule.TestComponent'}; let initTemplate = initTemplate"> |
||||
|
<abp-default-component #defaultComponent="abpDefaultComponent"></abp-default-component> |
||||
|
</div> |
||||
|
`,
|
||||
|
{ hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } }, |
||||
|
); |
||||
|
selectResponse.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => twoWayChange.mockClear()); |
||||
|
|
||||
|
it('should display the external component', () => { |
||||
|
expect(spectator.query('p')).toHaveText('external'); |
||||
|
}); |
||||
|
|
||||
|
it('should be injected the data object', () => { |
||||
|
const externalComponent = spectator.query(ExternalComponent); |
||||
|
expect(externalComponent.data).toEqual({ |
||||
|
componentKey: 'TestModule.TestComponent', |
||||
|
inputs: { oneWay: { label: 'Test' }, twoWay: false }, |
||||
|
outputs: { someOutput, twoWayChange }, |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it('should be worked all data properties', () => { |
||||
|
const externalComponent = spectator.query(ExternalComponent); |
||||
|
spectator.setHostInput({ oneWay: 'test' }); |
||||
|
externalComponent.data.inputs.twoWay = true; |
||||
|
externalComponent.data.outputs.someOutput('someOutput emitted'); |
||||
|
expect(externalComponent.data.inputs.oneWay).toBe('test'); |
||||
|
expect(twoWayChange).toHaveBeenCalledWith(true); |
||||
|
expect(someOutput).toHaveBeenCalledWith('someOutput emitted'); |
||||
|
|
||||
|
spectator.setHostInput({ twoWay: 'twoWay test' }); |
||||
|
expect(externalComponent.data.inputs.twoWay).toBe('twoWay test'); |
||||
|
}); |
||||
|
|
||||
|
it('should be worked correctly the default component when the external component has been removed from store', () => { |
||||
|
expect(spectator.query('p')).toHaveText('external'); |
||||
|
const externalComponent = spectator.query(ExternalComponent); |
||||
|
spectator.setHostInput({ oneWay: 'test' }); |
||||
|
externalComponent.data.inputs.twoWay = true; |
||||
|
selectResponse.next({ component: null, key: 'TestModule.TestComponent' }); |
||||
|
spectator.detectChanges(); |
||||
|
const component = spectator.query(DefaultComponent); |
||||
|
spectator.directive.context.initTemplate(component); |
||||
|
expect(spectator.query('abp-default-component')).toBeTruthy(); |
||||
|
|
||||
|
expect(component.oneWay).toEqual('test'); |
||||
|
expect(component.twoWay).toEqual(true); |
||||
|
}); |
||||
|
|
||||
|
it('should reset default component subscriptions', () => { |
||||
|
selectResponse.next({ component: null, key: 'TestModule.TestComponent' }); |
||||
|
const component = spectator.query(DefaultComponent); |
||||
|
spectator.directive.context.initTemplate(component); |
||||
|
spectator.detectChanges(); |
||||
|
const unsubscribe = jest.fn(() => {}); |
||||
|
spectator.directive.defaultComponentSubscriptions.twoWayChange.unsubscribe = unsubscribe; |
||||
|
|
||||
|
selectResponse.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); |
||||
|
expect(unsubscribe).toHaveBeenCalled(); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
File diff suppressed because it is too large
Loading…
Reference in new issue