Mateo Tibaquirá
Posted on May 6, 2021
TL;DR
We have support for conditional behaviors in@myndpm/dyn-forms
documented at mynd.dev and we are able to provide them easily like the Controls, Validators, AsyncValidators, etc.
In complex forms use-cases, some controls directly depend on the value or status of some other form control. Then we implement custom behaviors, like hiding
a field when another control has some value, or disabling
it depending on a complex condition, etc.
To support this, we've added Matchers
and Conditions
, which can be provided just like the Validators
and AsyncValidators
as we saw in the previous chapter of this series. If you want to get an initial idea from the code, you can check this source file and this real use-case demo.
DynTreeNode
Each dynamic control has a composed Node service instance which holds the data of this point in the form hierarchy. It provides the API and the data to manipulate the form in a customized way when needed.
The node has the control
Form Instance, the params
object, some utility methods to query
or select
parent and child controls, manipulate the visibility, etc. We will use this node inside the Conditions, Matchers and any other custom Handlers.
Conditions
To match a special requirement, we need to define one or more conditions, so when all (AND
) or one (OR
) of them are fulfilled we perform a particular action. The Condition Function type consists of:
interface DynControlConditionFn {
(node: DynTreeNode): Observable<any>;
}
it streams a truthy value whenever the condition is fulfilled or not, for example, we could check if a specific control has the expected value:
(node: DynTreeNode) => {
return node.query('specific.control').valueChanges.pipe(
map(controlValue => controlValue === 'xValue'),
);
}
we can join these conditions with the required operator (AND | OR
) for our use-case, and then evaluate the action to execute in the specific Matcher
.
Matchers
We define our requirement with the Matchers that we want to run when
all or a single condition is 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 DISABLE
matcher is included in the library with ENABLE
, SHOW
, HIDE
(display: none) and INVISIBLE
(visibility: hidden).
One matcher consists of a function which performs a task in the form hierarchy; to do so, it receives the DynTreeNode
instance:
interface DynControlMatcherFn {
(args: {
node: DynTreeNode;
hasMatch: boolean;
firstTime: boolean;
results: any[];
}): void;
}
So, for example the DISABLE
matcher operates into the form control when the specified conditions are fulfilled (has match):
{
id: 'DISABLE',
fn: (): DynControlMatcherFn => {
return ({ node , hasMatch }) => {
hasMatch ? node.control.disable() : node.control.enable();
}
}
},
Advanced Stuff
This conditional processing enables us to do some additional logical operations, like negate
the result of one or all the conditions, so we are able to play with conditions upside down and have the simplest specification of our requirements.
Matcher Example
For example, if we want to run a Matcher for all the options of a SELECT except a few of them, OR
without another condition, we can define that requirement with the few known values instead listing all the other values (which can be a long list), and negate the matcher input:
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
}
the Matcher will receive hasMatch: true
when the selector has a value NOT in the provided list.
Also note that you can provide your Matcher factories with a custom id
like 'MyMatcherID'
just like we will do with conditions in the following section.
Condition Factory
We can register Factories with an id
and a fn
as we do with Validators, and parametrize them in the Config Object:
export interface DynControlCondition {
id: string;
fn: (...args: any[]) => DynControlConditionFn;
}
Remember that DynControlConditionFn
returns an Observable<any>
so you can implement and provide your custom conditions like:
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
You can use your custom conditions in these ways:
// 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 last notation the whole config object is passed to the Factory, that's how the DEFAULT
condition handler receives the path
, value
and negate
config values.
Note: If no value
is configured, the DEFAULT
handler emits true
everytime the configured path
control value changes:
id: 'DEFAULT',
fn: ({ path, value, negate }): DynControlConditionFn => {
return (node: DynTreeNode): Observable<boolean> => {
if (value === undefined) {
return node.query(path).valueChanges.pipe(mapTo(true));
}
...
}
}
Conclusion
We have covered most of the details of Matchers
and Conditions
, and how one or many conditions can be configured so when one or all of them are fulfilled, they trigger a matcher which can modify the state of the form hierarchy via the DynTreeNode
API.
If you have an idea after this reading, or after using this library in an Angular App, please share it with us! :)
You can request features and join our discussions.
// PS. We are hiring!
Posted on May 6, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.