TL;DR
The@myndpm/dyn-formspackage includes support for conditional logic, with documentation available at mynd.dev. These behaviors can be supplied just as easily as Controls, Validators, AsyncValidators, and similar features.
In advanced form scenarios, certain controls often rely on the value or state of other controls. This leads to custom logic such as hiding a field when another control holds a specific value, or disabling it based on a more intricate condition.
To address this, we introduced Matchers and Conditions, which can be provided in the same manner as Validators and AsyncValidators covered earlier in this series. For a quick look at the code, see this source file and this practical demo.
DynTreeNode
Every dynamic control is backed by a composed Node service instance that stores the data for that specific point in the form's hierarchy. This service exposes the API and the necessary data for manipulating the form in a bespoke way when required.
This node includes the control Form Instance, the params object, and utility methods for querying or selecting parent or child controls, toggling visibility, and more. The node is the primary tool we'll use within Conditions, Matchers, and any other custom Handlers.
Conditions
To fulfill a specific requirement, one or more conditions must be defined. A particular task is executed when all (using AND) or just one (using OR) of these conditions are met. The Condition Function type is outlined as follows:
interface DynControlConditionFn {
(node: DynTreeNode): Observable<any>;
}
It emits a truthy value whenever the condition is either satisfied or not. For instance, we could verify whether a specific control contains an expected value:
(node: DynTreeNode) => {
return node.query('specific.control').valueChanges.pipe(
map(controlValue => controlValue === 'xValue'),
);
}
These conditions can be combined with the necessary operator (AND | OR) to fit our scenario, and then the corresponding action is determined within the specific Matcher.
Matchers
Our requirements are defined using Matchers that execute when all conditions or a single condition are satisfied:
match: [{
matchers: ['DISABLE'], // one or more matchers
when: [{
// the library provides a DEFAULT condition handler
// to process path, value and negation
path: 'other.field',
value: 'expectedValue'
}]
}]
The library ships with the DISABLE matcher, alongside ENABLE, SHOW, HIDE (which applies display: none) and INVISIBLE (which applies visibility: hidden).
A matcher is essentially a function that carries out a task within the form hierarchy. To achieve this, it accepts the DynTreeNode instance:
interface DynControlMatcherFn {
(args: {
node: DynTreeNode;
hasMatch: boolean;
firstTime: boolean;
results: any[];
}): void;
}
So, for instance, the DISABLE matcher acts on the form control when the specified conditions are met (a match is found):
{
id: 'DISABLE',
fn: (): DynControlMatcherFn => {
return ({ node , hasMatch }) => {
hasMatch ? node.control.disable() : node.control.enable();
}
}
},
Advanced Stuff
This conditional processing opens up possibilities for additional logical operations, such as using negate to reverse the result of one or all conditions. This allows us to work with conditions in an inverted way and keep our requirement specifications as simple as possible.
Matcher Example
Consider a scenario where we want a Matcher to apply to all options in a SELECT except for a handful of them, without needing a separate OR condition. We can define the requirement using just those few known values, negating the matcher input instead of listing out every other (potentially long) value:
match: {
matchers: ['MyMatcherID'],
operator: 'OR', // the operator is AND by default
when: [
{
path: 'selectorName',
value: ['A', 'B', 'C'] // this will check if selectorName.value is IN this array
},
{
path: 'other.control',
value: 'anotherValue'
},
],
negate: true
}
In this case, the Matcher receives hasMatch: true when the selector's value is NOT present in the provided list.
It's also worth noting that you can supply your own Matcher factories with a custom id, such as 'MyMatcherID', in a manner similar to how we'll handle conditions in the next section.
Condition Factory
Factories can be registered with an id and a fn, just like Validators, and then parameterized within the Config Object:
export interface DynControlCondition {
id: string;
fn: (...args: any[]) => DynControlConditionFn;
}
Keep in mind that DynControlConditionFn returns an Observable<boolean>, which means you can implement and provide custom conditions in the following way:
const conditions = [{
id: 'MyConditionId',
fn: (...args: any[]) => { // Factory
return (node: DynTreeNode) => { // Condition
return node.control.valueChanges.pipe(map(...));
}
}
}];
@NgModule({
imports: [
DynFormsModule.forFeature({ conditions });
Conditions Config
Your custom conditions can be utilized in the following configurations:
// inline function
when: [
(node: DynTreeNode) => {
// manipulate the form via DynTreeNode
}
]
// factory ID without arguments
when: [
'MyConditionId',
]
// parametrized factory
when: [
['MyConditionId', args],
]
// or declarative inline config
when: [
{
condition: 'MyConditionId',
path: 'other.control', // path is the only mandatory field in this format,
param1: 'anyValue', // the whole object will be passed to your DynControlConditionFn
},
]
In the final notation, the entire config object is passed to the Factory. This mechanism is how the DEFAULT condition handler receives its configuration values for path, value, and negate.
Note: If no value is specified, the DEFAULT handler emits true each time the value of the control at the configured path changes:
id: 'DEFAULT',
fn: ({ path, value, negate }): DynControlConditionFn => {
return (node: DynTreeNode): Observable<boolean> => {
if (value === undefined) {
return node.query(path).valueChanges.pipe(mapTo(true));
}
...
}
}
Wrapping Up
Throughout this guide, we have delved into the intricacies of Matchers and Conditions. We have seen how you can set up one or many conditions, and configure them to trigger a matcher when all or some of them are satisfied. This mechanism leverages the DynTreeNode API to effectively alter the state of the form tree.
Should a new idea occur to you after reading or while integrating this library into your Angular project, don't hesitate to tell us about it.
You are welcome to submit feature requests and take part in our community discussions.
// PS. We are hiring!
