Conditional Tasks in Dynamic Forms
Tags: angular, dynamic, forms, matchers
Published: 2021-05-06 17:32:27

---
series: Dynamic Forms for Angular
---
> **TL;DR**
> We have support for conditional behaviors in `@myndpm/dyn-forms` documented at [mynd.dev](https://mynd.dev/docs/dyn-forms/intro/matchers) 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](https://github.com/myndpm/open-source/blob/master/libs/forms/core/src/control-matchers.types.ts) and this [real use-case demo](https://mynd.dev/demos/dyn-forms/builder).

## 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](https://github.com/myndpm/open-source/blob/master/libs/forms/core/src/tree.types.ts#L12) 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:
```typescript
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:
```typescript
(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:
```typescript
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:
```typescript
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):
```typescript
{
  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:
```typescript
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:
```typescript
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:
```typescript
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:
```typescript
// 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:
```typescript
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](https://github.com/myndpm/open-source/issues/new?assignees=&labels=&template=feature-request.md&title=) and join our [discussions](https://github.com/myndpm/open-source/discussions).

*// PS. We are hiring!*