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 { eAccountComponents } from './enums/components';
import { AuthenticationFlowGuard } from './guards/authentication-flow.guard';
import { AccountExtensionsGuard } from './guards';
const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'login' },
@ -68,7 +70,7 @@ const routes: Routes = [
{
path: 'manage',
component: ReplaceableRouteContainerComponent,
canActivate: [AuthGuard],
canActivate: [AuthGuard, AccountExtensionsGuard],
data: {
replaceableComponent: {
key: eAccountComponents.ManageProfile,

@ -15,6 +15,11 @@ import { accountConfigOptionsFactory } from './utils/factory-utils';
import { AuthenticationFlowGuard } from './guards/authentication-flow.guard';
import { ForgotPasswordComponent } from './components/forgot-password/forgot-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 = [
LoginComponent,
@ -27,13 +32,14 @@ const declarations = [
];
@NgModule({
declarations: [...declarations],
declarations: [...declarations, ExtensiblePersonalSettingsComponent, HelloComponent],
imports: [
CoreModule,
AccountRoutingModule,
ThemeSharedModule,
NgbDropdownModule,
NgxValidateCoreModule,
UiExtensionsModule,
],
exports: [...declarations],
})
@ -49,6 +55,11 @@ export class AccountModule {
useFactory: accountConfigOptionsFactory,
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 './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 {
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 './extensions.token';

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

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

@ -158,15 +158,16 @@ export class ExtensibleTableComponent<R = any> implements OnChanges {
value,
};
if (prop.value.component) {
const injector = Injector.create(
[
const injector = Injector.create({
providers: [
{
provide: PROP_DATA_STREAM,
useValue: value,
},
],
this.injector,
);
parent: this.injector,
});
record[propKey].injector = injector;
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 options: PropCallback<R, Observable<ABP.Option<any>[]>> | undefined;
readonly id: string | undefined;
readonly template? : Type<any>
constructor(options: FormPropOptions<R>) {
super(
@ -47,6 +48,7 @@ export class FormProp<R = any> extends Prop<R> {
options.permission,
options.visible,
options.isExtra,
options.template
);
this.asyncValidators = options.asyncValidators || (_ => []);

@ -33,6 +33,7 @@ export abstract class Prop<R = any> {
public readonly permission: string,
public readonly visible: PropPredicate<R> = _ => true,
public readonly isExtra = false,
public readonly template? : Type<any>
) {
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 { Observable } from 'rxjs';
import { ePropType } from '../enums/props.enum';
import {FormProp} from "../models/form-props";
export const EXTENSIONS_IDENTIFIER = new InjectionToken<string>('EXTENSIONS_IDENTIFIER');
export type ActionKeys = Extract<'entityActions' | 'toolbarActions', keyof ExtensionsService>;
@ -24,3 +25,5 @@ export const ENTITY_PROP_TYPE_CLASSES = new InjectionToken<EntityPropTypeClass>(
factory: () => ({} as EntityPropTypeClass),
},
);
export const FORM_PROP_DATA_STREAM = new InjectionToken<FormProp>('FORM_PROP_DATA_STREAM');

Loading…
Cancel
Save