Dynamic Form (or Form Prop) Extensions for Angular UI
Introduction
Form prop extension system allows you to add a new field to the create and/or edit forms for a form or change/remove an already existing one. A "Date of Birth" field was added to the user management page below:
You can validate the field, perform visibility checks, and do more. You will also have access to the current entity when creating a contributor for an edit form.
How to Set Up
In this example, we will add a "Date of Birth" field in the user management page of the Identity Module and validate it.
Step 1. Create Form Prop Contributors
The following code prepares two constants named identityCreateFormPropContributors
and identityEditFormPropContributors
, ready to be imported and used in your root module:
// src/app/form-prop-contributors.ts
import {
eIdentityComponents,
IdentityCreateFormPropContributors,
} from '@abp/ng.identity';
import { IdentityUserDto } from '@abp/ng.identity/proxy';
import { ePropType, FormProp, FormPropList } from '@abp/ng.components/extensible';
import { Validators } from '@angular/forms';
const birthdayProp = new FormProp<IdentityUserDto>({
type: ePropType.Date,
name: 'birthday',
displayName: 'AbpIdentity::Birthday',
validators: () => [Validators.required],
});
export function birthdayPropContributor(propList: FormPropList<IdentityUserDto>) {
propList.addByIndex(birthdayProp, 4);
}
export const identityCreateFormPropContributors: IdentityCreateFormPropContributors = {
// enum indicates the page to add contributors to
[eIdentityComponents.Users]: [
birthdayPropContributor,
// You can add more contributors here
],
};
export const identityEditFormPropContributors = identityCreateFormPropContributors;
// you may define different contributors for edit form if you like
The list of props, conveniently named as propList
, is a doubly linked list. That is why we have used the addByIndex
method, which adds the given value to the specified index of the list. You may find all available methods here.
Step 2. Import and Use Form Prop Contributors
Import identityCreateFormPropContributors
and identityEditFormPropContributors
in your routing module and pass it to the static forLazy
method of IdentityModule
as seen below:
// src/app/app-routing.module.ts
// other imports
import {
identityCreateFormPropContributors,
identityEditFormPropContributors,
} from './form-prop-contributors';
const routes: Routes = [
// other routes
{
path: 'identity',
loadChildren: () =>
import('@abp/ng.identity').then(m =>
m.IdentityModule.forLazy({
createFormPropContributors: identityCreateFormPropContributors,
editFormPropContributors: identityEditFormPropContributors,
})
),
},
// other routes
];
That is it, birthdayProp
form prop will be added, and you will see the datepicker for the "Date of Birth" field right before the "Email address" in the forms of the users page in the IdentityModule
.
Object Extensions
Extra properties defined on an existing entity will be included in the create and edit forms and validated based on their configuration. The form values will also be mapped to and from extraProperties
automatically. They are available when defining custom contributors, so you can drop, modify, or reorder them. The isExtra
identifier will be set to true
for these properties and will define this automatic behavior.
API
PropData<R = any>
PropData
is the shape of the parameter passed to all callbacks or predicates in a FormProp
.
It has the following properties:
getInjected is the equivalent of Injector.get. You can use it to reach injected dependencies of
ExtensibleFormPropComponent
, including, but not limited to, its parent components.{ type: ePropType.Enum, name: 'myField', options: data => { const restService = data.getInjected(RestService); const usersComponent = data.getInjected(UsersComponent); // Use restService and usersComponent public props and methods here } },
record is the row data, i.e. current value of the selected item to edit. This property is available only on edit forms.
{ type: ePropType.String, name: 'myProp', readonly: data => data.record.someOtherProp, }
PropCallback<T, R = any>
PropCallback
is the type of the callback function that can be passed to a FormProp
as prop
parameter. A prop callback gets a single parameter, the PropData
. The return type may be anything, including void
. Here is a simplified representation:
type PropCallback<T, R = any> = (data?: PropData<T>) => R;
PropPredicate<T>
PropPredicate
is the type of the predicate function that can be passed to a FormProp
as visible
parameter. A prop predicate gets a single parameter, the PropData
. The return type must be boolean
. Here is a simplified representation:
type PropPredicate<T> = (data?: PropData<T>) => boolean;
FormPropOptions<R = any>
FormPropOptions
is the type that defines required and optional properties you have to pass in order to create a form prop.
Its type definition is as follows:
type FormPropOptions<R = any> = {
type: ePropType;
name: string;
displayName?: string;
id?: string;
permission?: string;
visible?: PropPredicate<R>;
readonly?: PropPredicate<R>;
disabled?: PropPredicate<R>;
validators?: PropCallback<R, ValidatorFn[]>;
asyncValidators?: PropCallback<R, AsyncValidatorFn[]>;
defaultValue?: boolean | number | string | Date;
options?: PropCallback<R, Observable<ABP.Option<any>[]>>;
autocomplete?: string;
isExtra? boolean;
};
As you see, passing type
and name
is enough to create a form prop. Here is what each property is good for:
- type is the type of the prop value. It defines which input is rendered for the prop in the form. (required)
- name is the property name (or key) which will be used to read the value of the prop. (required)
- displayName is the name of the property which will be localized and shown as column header. (default:
options.name
) - id will be set as the
for
attribute of the label and theid
attribute of the input for the field. (default:options.name
) - permission is the permission context which will be used to decide if a column for this form prop should be displayed to the user or not. (default:
undefined
) - visible is a predicate that will be used to decide if this prop should be displayed on the form or not. (default:
() => true
) - readonly is a predicate that will be used to decide if this prop should be readonly or not. (default:
() => false
) - disabled is a predicate that will be used to decide if this prop should be disabled or not. (default:
() => false
) - validators is a callback that returns validators for the prop. (default:
() => []
) - asyncValidators is a callback that returns async validators for the prop. (default:
() => []
) - defaultValue is the initial value the field will have. (default:
null
) - options is a callback that is called when a dropdown is needed. It must return an observable. (default:
undefined
) - autocomplete will be set as the
autocomplete
attribute of the input for the field. Please check possible values. (default:'off'
) - isExtra indicates this prop is an object extension. When
true
, the value of the field will be mapped from and toextraProperties
of the entity. (default:undefined
)
Important Note: Do not use
record
property ofPropData
in create form predicates and callbacks, because it will beundefined
. You can use it on edit form contributors though.
You may find a full example below.
FormProp<R = any>
FormProp
is the class that defines your form props. It takes a FormPropOptions
and sets the default values to the properties, creating a form prop that can be passed to a form contributor.
const options: FormPropOptions<IdentityUserDto> = {
type: ePropType.Enum,
name: 'myProp',
displayName: 'Default::MyPropName',
id: 'my-prop',
permission: 'AbpIdentity.Users.ReadSensitiveData', // hypothetical
visible: data => {
const store = data.getInjected(Store);
const selectSensitiveDataVisibility = ConfigState.getSetting(
'Abp.Identity.IsSensitiveDataVisible' // hypothetical
);
return store.selectSnapshot(selectSensitiveDataVisibility).toLowerCase() === 'true';
},
readonly: data => data.record.someProp,
disabled: data => data.record.someOtherProp,
validators: () => [Validators.required],
asyncValidators: data => {
const http = data.getInjected(HttpClient);
function validate(control: AbstractControl): Observable<ValidationErrors | null> {
if (control.pristine) return of(null);
return http
.get('https://api.my-brand.io/hypothetical/endpoint/' + control.value)
.pipe(map(response => (response.valid ? null : { invalid: true })));
}
return [validate];
},
defaultValue: 0,
options: data => {
const service = data.getInjected(MyIdentityService);
return service.getMyPropOptions()
.pipe(
map(({items}) => items.map(
item => ({key: item.name, value: item.id })
)),
);
},
autocomplete: 'off',
isExtra: true,
template: undefined | Type<any> // Custom angular component
};
const prop = new FormProp(options);
FormProp has the template option since version 6.0. it can accept custom angular component. The component can access PropData and Prop. Example of the custom prop component.
import {
EXTENSIBLE_FORM_VIEW_PROVIDER,
EXTENSIONS_FORM_PROP,
EXTENSIONS_FORM_PROP_DATA,
} from '@abp/ng.components/extensible';
@Component({
selector: 'my-custom-custom-prop',
templateUrl: './my-custom-custom-prop.component.html',
viewProviders: [EXTENSIBLE_FORM_VIEW_PROVIDER], //you should add this, otherwise form-group doesn't work.
})
export class MyCustomPropComponent {
constructor(
@Inject(EXTENSIONS_FORM_PROP) private formProp: FormProp,
@Inject(EXTENSIONS_FORM_PROP_DATA) private propData: ProfileDto,
...)
...
}
It also has two static methods to create its instances:
- FormProp.create<R = any>(options: FormPropOptions<R>) is used to create an instance of
FormProp
.const prop = FormProp.create(options);
- FormProp.createMany<R = any>(options: FormPropOptions<R>[]) is used to create multiple instances of
FormProp
with given array ofFormPropOptions
.const props = FormProp.createMany(optionsArray);
FormPropList<R = any>
FormPropList
is the list of props passed to every prop contributor callback as the first parameter named propList
. It is a doubly linked list. You may find all available methods here.
The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this:
export function reorderUserContributors(
propList: FormPropList<IdentityUserDto>,
) {
// drop email node
const emailPropNode = propList.dropByValue(
'AbpIdentity::EmailAddress',
(prop, displayName) => prop.displayName === displayName,
);
// add it back after phoneNumber
propList.addAfter(
emailPropNode.value,
'phoneNumber',
(value, name) => value.name === name,
);
}
CreateFormPropContributorCallback<R = any>
CreateFormPropContributorCallback
is the type that you can pass as create form prop contributor callbacks to static forLazy
methods of the modules.
export function myPropCreateContributor(
propList: FormPropList<IdentityUserDto>,
) {
// add myProp as 2nd field from the start
propList.add(myProp).byIndex(1);
}
export const identityCreateFormPropContributors = {
[eIdentityComponents.Users]: [myPropCreateContributor],
};
EditFormPropContributorCallback<R = any>
EditFormPropContributorCallback
is the type that you can pass as edit form prop contributor callbacks to static forLazy
methods of the modules.
export function myPropEditContributor(
propList: FormPropList<IdentityUserDto>,
) {
// add myProp as 2nd field from the end
propList.add(myProp).byIndex(-1);
}
export const identityEditFormPropContributors = {
[eIdentityComponents.Users]: [myPropEditContributor],
};