Add Custom Component Feature to ExtensibleForm.

pull/13700/head
Mahmut Gundogdu 3 years ago
parent 517ad15b7a
commit 7f93fd261e

@ -13,6 +13,8 @@ import { RegisterComponent } from './components/register/register.component';
import { ResetPasswordComponent } from './components/reset-password/reset-password.component'; import { ResetPasswordComponent } from './components/reset-password/reset-password.component';
import { eAccountComponents } from './enums/components'; import { eAccountComponents } from './enums/components';
import { AuthenticationFlowGuard } from './guards/authentication-flow.guard'; import { AuthenticationFlowGuard } from './guards/authentication-flow.guard';
import { AccountExtensionsGuard } from './guards';
const routes: Routes = [ const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'login' }, { path: '', pathMatch: 'full', redirectTo: 'login' },
@ -68,7 +70,7 @@ const routes: Routes = [
{ {
path: 'manage', path: 'manage',
component: ReplaceableRouteContainerComponent, component: ReplaceableRouteContainerComponent,
canActivate: [AuthGuard], canActivate: [AuthGuard, AccountExtensionsGuard],
data: { data: {
replaceableComponent: { replaceableComponent: {
key: eAccountComponents.ManageProfile, key: eAccountComponents.ManageProfile,

@ -15,6 +15,11 @@ import { accountConfigOptionsFactory } from './utils/factory-utils';
import { AuthenticationFlowGuard } from './guards/authentication-flow.guard'; import { AuthenticationFlowGuard } from './guards/authentication-flow.guard';
import { ForgotPasswordComponent } from './components/forgot-password/forgot-password.component'; import { ForgotPasswordComponent } from './components/forgot-password/forgot-password.component';
import { ResetPasswordComponent } from './components/reset-password/reset-password.component'; import { ResetPasswordComponent } from './components/reset-password/reset-password.component';
import { ExtensiblePersonalSettingsComponent } from './components/extensible-personal-settings/extensible-personal-settings.component';
import { UiExtensionsModule } from '@abp/ng.theme.shared/extensions';
import { ACCOUNT_EDIT_FORM_PROP_CONTRIBUTORS } from './tokens/extensions.token';
import { AccountExtensionsGuard } from './guards/extensions.guard';
import { HelloComponent } from './components/hello/hello.component';
const declarations = [ const declarations = [
LoginComponent, LoginComponent,
@ -27,13 +32,14 @@ const declarations = [
]; ];
@NgModule({ @NgModule({
declarations: [...declarations], declarations: [...declarations, ExtensiblePersonalSettingsComponent, HelloComponent],
imports: [ imports: [
CoreModule, CoreModule,
AccountRoutingModule, AccountRoutingModule,
ThemeSharedModule, ThemeSharedModule,
NgbDropdownModule, NgbDropdownModule,
NgxValidateCoreModule, NgxValidateCoreModule,
UiExtensionsModule,
], ],
exports: [...declarations], exports: [...declarations],
}) })
@ -49,6 +55,11 @@ export class AccountModule {
useFactory: accountConfigOptionsFactory, useFactory: accountConfigOptionsFactory,
deps: [ACCOUNT_CONFIG_OPTIONS], deps: [ACCOUNT_CONFIG_OPTIONS],
}, },
{
provide: ACCOUNT_EDIT_FORM_PROP_CONTRIBUTORS,
useValue: options.editFormPropContributors,
},
AccountExtensionsGuard,
], ],
}; };
} }

@ -0,0 +1,5 @@
<p>extensible-personal-settings works!</p>
<form [formGroup]="form" (ngSubmit)="save()" validateOnSubmit>
<abp-extensible-form [selectedRecord]="selected"></abp-extensible-form>
</form>

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ExtensiblePersonalSettingsComponent } from './extensible-personal-settings.component';
describe('ExtensiblePersonalSettingsComponent', () => {
let component: ExtensiblePersonalSettingsComponent;
let fixture: ComponentFixture<ExtensiblePersonalSettingsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ExtensiblePersonalSettingsComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ExtensiblePersonalSettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,42 @@
import {Component, Injector, OnInit} from '@angular/core';
import {EXTENSIONS_IDENTIFIER, FormPropData, generateFormFromProps} from "@abp/ng.theme.shared/extensions";
import { eAccountComponents } from '../../enums/components';
import {FormBuilder, FormGroup} from "@angular/forms";
import {ProfileService} from "@abp/ng.account.core/proxy";
@Component({
selector: 'abp-extensible-personal-settings',
templateUrl: './extensible-personal-settings.component.html',
styleUrls: ['./extensible-personal-settings.component.scss'],
providers:[
{
provide: EXTENSIONS_IDENTIFIER,
useValue: eAccountComponents.PersonalSettings,
},
]
})
export class ExtensiblePersonalSettingsComponent implements OnInit {
selected = {a: 1};// hacky for triggering 'edit' mode of extensible-form
form: FormGroup;
constructor(
private profileService: ProfileService,
protected fb: FormBuilder,
protected injector: Injector,
) {}
buildForm() {
const data = new FormPropData(this.injector, this.selected);
this.form = generateFormFromProps(data);
}
ngOnInit(): void {
this.buildForm()
}
save() {
}
}

@ -0,0 +1,15 @@
import {Component, Inject } from '@angular/core';
import {FORM_PROP_DATA_STREAM, FormProp} from "@abp/ng.theme.shared/extensions";
@Component({
selector: 'abp-hello',
template: `<p>hello works! {{name | abpLocalization}}</p>`,
styles: [],
})
export class HelloComponent {
name:string;
constructor(@Inject(FORM_PROP_DATA_STREAM) private propData:FormProp) {
this.name = propData.displayName
}
}

@ -0,0 +1,15 @@
import {ePropType, FormProp} from "@abp/ng.theme.shared/extensions";
import {UpdateProfileDto} from "@abp/ng.account.core/proxy";
import {Validators} from "@angular/forms";
import {HelloComponent} from "../components/hello/hello.component";
export const DEFAULT_PERSONAL_SETTINGS_UPDATE_FORM_PROPS = FormProp.createMany<UpdateProfileDto>([
{
type: ePropType.String,
name: 'userName',
displayName: 'Account::UserName',
id: 'user-name',
validators: () => [Validators.required, Validators.maxLength(256)],
template: HelloComponent
},
])

@ -0,0 +1,42 @@
import {Injectable, Injector} from "@angular/core";
import {CanActivate} from "@angular/router";
import {Observable} from "rxjs";
import {
ExtensionsService,
getObjectExtensionEntitiesFromStore,
mapEntitiesToContributors, mergeWithDefaultProps
} from "@abp/ng.theme.shared/extensions";
import {ConfigStateService} from "@abp/ng.core";
import {tap, map, mapTo} from "rxjs/operators";
import {ACCOUNT_EDIT_FORM_PROP_CONTRIBUTORS, DEFAULT_ACCOUNT_FORM_PROPS} from '../tokens/extensions.token';
import { AccountEditFormPropContributors } from '../models/config-options';
import { eAccountComponents } from '../enums/components';
@Injectable()
export class AccountExtensionsGuard implements CanActivate {
constructor(private injector: Injector) {}
canActivate(): Observable<boolean> {
const extensions: ExtensionsService = this.injector.get(ExtensionsService);
const editFormContributors: AccountEditFormPropContributors =
this.injector.get(ACCOUNT_EDIT_FORM_PROP_CONTRIBUTORS, null) || {};
const configState = this.injector.get(ConfigStateService);
return getObjectExtensionEntitiesFromStore(configState, 'Account').pipe(
map(entities => ({
[eAccountComponents.PersonalSettings]: entities.PersonalSettings,
})),
mapEntitiesToContributors(configState, 'AbpAccount'),
tap(objectExtensionContributors => {
mergeWithDefaultProps(
extensions.editFormProps,
DEFAULT_ACCOUNT_FORM_PROPS,
objectExtensionContributors.editForm,
editFormContributors,
);
}),
mapTo(true),
);
}
}

@ -1 +1,2 @@
export * from './authentication-flow.guard'; export * from './authentication-flow.guard';
export * from './extensions.guard';

@ -1,3 +1,14 @@
import {eAccountComponents} from "../enums";
import { EditFormPropContributorCallback } from '@abp/ng.theme.shared/extensions';
import {UpdateProfileDto} from "@abp/ng.account.core/proxy";
export interface AccountConfigOptions { export interface AccountConfigOptions {
redirectUrl?: string; redirectUrl?: string;
} }
export type AccountEditFormPropContributors = Partial<{
[eAccountComponents.PersonalSettings]: EditFormPropContributorCallback<UpdateProfileDto>[];
}>;
export interface AccountConfigOptions {
editFormPropContributors?: AccountEditFormPropContributors;
}

@ -0,0 +1,17 @@
import {eAccountComponents} from "../enums";
import {DEFAULT_PERSONAL_SETTINGS_UPDATE_FORM_PROPS} from "../defaults/default-personal-settings-form-props";
import {InjectionToken} from "@angular/core";
import {EditFormPropContributorCallback} from "@abp/ng.theme.shared/extensions";
import {UpdateProfileDto} from "@abp/ng.account.core/proxy";
export const DEFAULT_ACCOUNT_FORM_PROPS = {
[eAccountComponents.PersonalSettings]: DEFAULT_PERSONAL_SETTINGS_UPDATE_FORM_PROPS,
};
export const ACCOUNT_EDIT_FORM_PROP_CONTRIBUTORS = new InjectionToken<EditFormPropContributors>(
'ACCOUNT_EDIT_FORM_PROP_CONTRIBUTORS',
);
type EditFormPropContributors = Partial<{
[eAccountComponents.PersonalSettings]: EditFormPropContributorCallback<UpdateProfileDto>[];
}>;

@ -1 +1,2 @@
export * from './config-options.token'; export * from './config-options.token';
export * from './extensions.token';

@ -3,6 +3,12 @@
*abpPermission="prop.permission; runChangeDetection: false" *abpPermission="prop.permission; runChangeDetection: false"
[ngSwitch]="getComponent(prop)" [ngSwitch]="getComponent(prop)"
> >
<ng-template ngSwitchCase="template">
<ng-container *ngComponentOutlet="prop.template;injector:injectorForCustomComponent" >
</ng-container>
</ng-template>
<ng-template ngSwitchCase="input"> <ng-template ngSwitchCase="input">
<ng-template [ngTemplateOutlet]="label"></ng-template> <ng-template [ngTemplateOutlet]="label"></ng-template>
<input <input

@ -1,10 +1,10 @@
import { ABP, AbpValidators, ConfigStateService, TrackByService } from '@abp/ng.core'; import {ABP, AbpValidators, ConfigStateService, TrackByService} from '@abp/ng.core';
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef, Injector,
Input, Input,
OnChanges, OnChanges,
Optional, Optional,
@ -19,17 +19,18 @@ import {
ValidatorFn, ValidatorFn,
Validators, Validators,
} from '@angular/forms'; } from '@angular/forms';
import { NgbDateAdapter, NgbTimeAdapter } from '@ng-bootstrap/ng-bootstrap'; import {NgbDateAdapter, NgbTimeAdapter} from '@ng-bootstrap/ng-bootstrap';
import { Observable, of } from 'rxjs'; import {Observable, of} from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; import {debounceTime, distinctUntilChanged, switchMap} from 'rxjs/operators';
import { DateAdapter } from '../../adapters/date.adapter'; import {DateAdapter} from '../../adapters/date.adapter';
import { TimeAdapter } from '../../adapters/time.adapter'; import {TimeAdapter} from '../../adapters/time.adapter';
import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties'; import {EXTRA_PROPERTIES_KEY} from '../../constants/extra-properties';
import { ePropType } from '../../enums/props.enum'; import {ePropType} from '../../enums/props.enum';
import { FormProp } from '../../models/form-props'; import {FormProp} from '../../models/form-props';
import { PropData } from '../../models/props'; import {PropData} from '../../models/props';
import { selfFactory } from '../../utils/factory.util'; import {selfFactory} from '../../utils/factory.util';
import { addTypeaheadTextSuffix } from '../../utils/typeahead.util'; import {addTypeaheadTextSuffix} from '../../utils/typeahead.util';
import {FORM_PROP_DATA_STREAM} from "../../tokens/extensions.token";
@Component({ @Component({
selector: 'abp-extensible-form-prop', selector: 'abp-extensible-form-prop',
@ -41,8 +42,8 @@ import { addTypeaheadTextSuffix } from '../../utils/typeahead.util';
useFactory: selfFactory, useFactory: selfFactory,
deps: [[new Optional(), new SkipSelf(), ControlContainer]], deps: [[new Optional(), new SkipSelf(), ControlContainer]],
}, },
{ provide: NgbDateAdapter, useClass: DateAdapter }, {provide: NgbDateAdapter, useClass: DateAdapter},
{ provide: NgbTimeAdapter, useClass: TimeAdapter }, {provide: NgbTimeAdapter, useClass: TimeAdapter},
], ],
}) })
export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit { export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
@ -54,6 +55,8 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
@ViewChild('field') private fieldRef!: ElementRef<HTMLElement>; @ViewChild('field') private fieldRef!: ElementRef<HTMLElement>;
public injectorForCustomComponent: Injector
asterisk = ''; asterisk = '';
options$: Observable<ABP.Option<any>[]> = of([]); options$: Observable<ABP.Option<any>[]> = of([]);
@ -62,15 +65,16 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
readonly!: boolean; readonly!: boolean;
disabledFn = (data:PropData) => false; typeaheadModel: any;
get disabled() {
return this.disabledFn(this.data)
}
private readonly form: FormGroup; private readonly form: FormGroup;
typeaheadModel: any; disabledFn = (data: PropData) => false;
get disabled() {
return this.disabledFn(this.data)
}
setTypeaheadValue(selectedOption: ABP.Option<string>) { setTypeaheadValue(selectedOption: ABP.Option<string>) {
this.typeaheadModel = selectedOption || { key: null, value: null }; this.typeaheadModel = selectedOption || { key: null, value: null };
@ -108,12 +112,13 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
public readonly track: TrackByService, public readonly track: TrackByService,
protected configState: ConfigStateService, protected configState: ConfigStateService,
groupDirective: FormGroupDirective, groupDirective: FormGroupDirective,
private injector: Injector
) { ) {
this.form = groupDirective.form; this.form = groupDirective.form;
} }
private getTypeaheadControls() { private getTypeaheadControls() {
const { name } = this.prop; const {name} = this.prop;
const extraPropName = `${EXTRA_PROPERTIES_KEY}.${name}`; const extraPropName = `${EXTRA_PROPERTIES_KEY}.${name}`;
const keyControl = const keyControl =
this.form.get(addTypeaheadTextSuffix(extraPropName)) || this.form.get(addTypeaheadTextSuffix(extraPropName)) ||
@ -133,6 +138,9 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
} }
getComponent(prop: FormProp): string { getComponent(prop: FormProp): string {
if (prop.template) {
return 'template'
}
switch (prop.type) { switch (prop.type) {
case ePropType.Boolean: case ePropType.Boolean:
return 'checkbox'; return 'checkbox';
@ -173,13 +181,24 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
} }
} }
ngOnChanges({ prop }: SimpleChanges) { ngOnChanges({prop}: SimpleChanges) {
const currentProp = prop?.currentValue; const currentProp = prop?.currentValue as FormProp;
const { options, readonly, disabled, validators } = currentProp || {}; const {options, readonly, disabled, validators, template} = currentProp || {};
if (template) {
this.injectorForCustomComponent = Injector.create({
providers: [
{
provide: FORM_PROP_DATA_STREAM,
useValue: currentProp
}
],
parent: this.injector,
});
}
if (options) this.options$ = options(this.data); if (options) this.options$ = options(this.data);
if (readonly) this.readonly = readonly(this.data); if (readonly) this.readonly = readonly(this.data);
if (disabled) { if (disabled) {
this.disabledFn = disabled; this.disabledFn = disabled;
} }

@ -158,15 +158,16 @@ export class ExtensibleTableComponent<R = any> implements OnChanges {
value, value,
}; };
if (prop.value.component) { if (prop.value.component) {
const injector = Injector.create(
[ const injector = Injector.create({
providers: [
{ {
provide: PROP_DATA_STREAM, provide: PROP_DATA_STREAM,
useValue: value, useValue: value,
}, },
], ],
this.injector, parent: this.injector,
); });
record[propKey].injector = injector; record[propKey].injector = injector;
record[propKey].component = prop.value.component; record[propKey].component = prop.value.component;
} }

@ -38,6 +38,7 @@ export class FormProp<R = any> extends Prop<R> {
readonly defaultValue: boolean | number | string | Date; readonly defaultValue: boolean | number | string | Date;
readonly options: PropCallback<R, Observable<ABP.Option<any>[]>> | undefined; readonly options: PropCallback<R, Observable<ABP.Option<any>[]>> | undefined;
readonly id: string | undefined; readonly id: string | undefined;
readonly template? : Type<any>
constructor(options: FormPropOptions<R>) { constructor(options: FormPropOptions<R>) {
super( super(
@ -47,6 +48,7 @@ export class FormProp<R = any> extends Prop<R> {
options.permission, options.permission,
options.visible, options.visible,
options.isExtra, options.isExtra,
options.template
); );
this.asyncValidators = options.asyncValidators || (_ => []); this.asyncValidators = options.asyncValidators || (_ => []);

@ -33,6 +33,7 @@ export abstract class Prop<R = any> {
public readonly permission: string, public readonly permission: string,
public readonly visible: PropPredicate<R> = _ => true, public readonly visible: PropPredicate<R> = _ => true,
public readonly isExtra = false, public readonly isExtra = false,
public readonly template? : Type<any>
) { ) {
this.displayName = this.displayName || this.name; this.displayName = this.displayName || this.name;
} }

@ -3,6 +3,7 @@ import { ActionCallback, ReadonlyActionData as ActionData } from '../models/acti
import { ExtensionsService } from '../services/extensions.service'; import { ExtensionsService } from '../services/extensions.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ePropType } from '../enums/props.enum'; import { ePropType } from '../enums/props.enum';
import {FormProp} from "../models/form-props";
export const EXTENSIONS_IDENTIFIER = new InjectionToken<string>('EXTENSIONS_IDENTIFIER'); export const EXTENSIONS_IDENTIFIER = new InjectionToken<string>('EXTENSIONS_IDENTIFIER');
export type ActionKeys = Extract<'entityActions' | 'toolbarActions', keyof ExtensionsService>; export type ActionKeys = Extract<'entityActions' | 'toolbarActions', keyof ExtensionsService>;
@ -24,3 +25,5 @@ export const ENTITY_PROP_TYPE_CLASSES = new InjectionToken<EntityPropTypeClass>(
factory: () => ({} as EntityPropTypeClass), factory: () => ({} as EntityPropTypeClass),
}, },
); );
export const FORM_PROP_DATA_STREAM = new InjectionToken<FormProp>('FORM_PROP_DATA_STREAM');

Loading…
Cancel
Save