Broadleaf Microservices

Conditionals

Overview

Conditionals are used throughout the admin metadata to gate certain components and functionality. Form components are able to use conditionals to only render when the form state matches a certain state. Request mappings are able to use conditionals to only map or transform data when the data matches a certain state.

Property Conditionals

Property conditionals can be used to condition functionality based on the state of properties.

Property conditionals include the following properties:

type

The type of the conditional. PROPERTY for property conditionals.

field

The name of the property the conditional targets.

operator

The operator which decides how to evaluate the targeted field. For example, == would mean an equality check.

value

The value to compare the targeted field against. operator enumerations like EXISTS do not require this.

Let’s take a look at an example of a property conditional which matches when evaluated against data with a type property equal to PRODUCT in the Java DSL and JSON formats:

Conditionals.when("type").equalTo("PRODUCT");
{
  "type": "PROPERTY",
  "field": "type",
  "operator": "==",
  "value": "PRODUCT"
}

If we evaluated this conditional against the following JSON, we would expect it to match:

{
  "type": "PRODUCT"
}

Alternatively, if we evaluated this conditional against the following JSON, we would expect it to not match:

{
  "type": "VARIANT"
}

Property conditionals can be a useful way to conditionally render fields within a form, for example:

form.addField("description", Fields.string()
        .label("Description")
        // only renders when "hasDescription" is true
        .conditional(Conditionals.when("hasDescription").equalTo(true)))

Logical Conditionals

Logical conditionals can be used to provide logical operators between a set of condiionals.

Logical conditionals include the following properties:

type

The type of the conditional. LOGICAL for logical conditionals.

operator

The operator of the conditional. One of AND, OR, or NOT.

conditionals

The set of conditionals to evaluate and apply the logical operator between.

Let’s take a look at an example of a logical conditional which matches when either type is PRODUCT or VARIANT:

Conditionals.or(
        Conditionals.when('type').equalTo('PRODUCT'),
        Conditionals.when('type').equalTo('VARIANT')
);
{
  "type": "LOGICAL",
  "operator": "OR",
  "conditionals": [
    {
      "type": "PROPERTY",
      "field": "type",
      "operator": "==",
      "value": "PRODUCT"
    },
    {
      "type": "PROPERTY",
      "field": "type",
      "operator": "==",
      "value": "VARIANT"
    }
  ]
}

If we evaluated this conditional against the following JSON, we would expect it to match:

{
  "type": "PRODUCT"
}

Alternatively, if we evaluated this conditional against the following JSON, we would expect it to not match:

{
  "type": "CATEGORY"
}

Collection Conditionals

Collection conditionals can be used to condition functionality based on the state of members of a collection property.

Collection conditionals include the following properties:

type

The type of the conditional. COLLECTION for colleection conditionals.

field

The name of the collection property the conditional targets.

conditionals

The set of conditionals to evaluate against each collection member.

matchStrategy

The strategy used to determine how matches of collection members affect the final match. For the ALL strategy, all members must match the conditionals. For the ANY strategy, only one member must match the conditionals.

matchIfEmpty

Whether an empty collection should be considered a match. If true, a null, undefined, or empty collection will be considered a match.

Let’s take a look at an example of a collection conditional which matches when all collection members include a type property equal to PRODUCT in the Java DSL and JSON formats:

Conditionals.whenCollection("choices")
    .allMatch()
    .notMatchIfEmpty()
    .conditional(Conditionals.when("type").equalTo("PRODUCT"));
{
  "type": "COLLECTION",
  "field": "choices",
  "matchStrategy": "ALL",
  "matchIfEmpty": false,
  "conditionals": [
    {
      "type": "PROPERTY",
      "field": "type",
      "operator": "==",
      "value": "PRODUCT"
    }
  ]
}

If we evaluated this conditional against the following JSON, we would expect it to match:

{
  "choices": [
    {
      "type": "PRODUCT"
    }
  ]
}

Alternatively, if we evaluated this conditional against the following JSON, we would expect it to not match:

{
  "choices": [
    {
      "type": "VARIANT"
    }
  ]
}

Collection conditionals can be a useful way to conditionally render fields within a form, for example:

form.addField("defaultProductId", createProductLookup()
        // only renders when each "choice" has a "type" of "PRODUCT"
        .conditional(Conditionals.whenCollection("choices")
            .allMatch()
            .notMatchIfEmpty()
            .conditional(Conditionals.when("type").equalTo("PRODUCT")))

New Conditionals Types

The admin supports the introduction of new conditional types if the out-of-box ones do not support a use case.

The first step to adding a new conditional type is within the frontend application. We will need to import the conditional helpers and register a new conditional provider within our frontend code. Let’s take a look at a conditional type that matches when a targeted field is lower-case:

import { services } from '@broadleaf/admin-components';
import { get } from 'lodash';

services.ConditionalHelper.registerConditionalProvider({
  order: 5000,

  canHandle(conditional) {
    // if type matches "IS_LOWERCASE", this provider will be engaged
    return conditional.type === 'IS_LOWERCASE';
  },

  evaluate(conditional, values, helper) {
    const value = get(values, conditional.field);
    // if the targeted value is a "string" and equals itself lower-cased, we consider it a match
    return typeof value === 'string' && value === value.toLowerCase();
  }
});

The second step to adding a new conditional type is within the metadata DSL. We will need to create a new interface and implementation class for the new conditional type. Let’s take a look at doing this for our previous IS_LOWERCASE example. First, we need to create an interface IsLowercaseConditional:

// IsLowercaseConditional.java
public interface IsLowercaseConditional<C extends IsLowercaseConditional<C>> extends Conditional<C> {
    String TYPE = "IS_LOWERCASE";

    @Override
    default String getType() {
        return TYPE;
    }

    String getField();

    void setField(String field);

    default C field(String field) {
        setField(field);
        return self();
    }
}

Next, we need to create an implementation, DefaultIsLowercaseConditional:

@Getter
@Setter
@EqualsAndHashCode
@JsonInclude(JsonInclude.Include.NON_NULL)
public class DefaultIsLowercaseConditional implements Serializable,
        IsLowercaseConditional<DefaultIsLowercaseConditional> {
    private static final long serialVersionUID = 1L;

    /**
     * Field name target for this conditional
     */
    private String field;

    public DefaultIsLowercaseConditional(String field) {
        setField(field);
    }

    public DefaultIsLowercaseConditional(IsLowercaseConditional<?> conditional) {
        setField(conditional.getField());
    }

    @Override
    public DefaultIsLowercaseConditional copy() {
        return new DefaultIsLowercaseConditional(this);
    }
}
Note

We create both an interface and implementation class to ensure our new conditional type is extensible in the event we want a different implementation.

Finally, since we have the frontend code and DSL complete, we can make use of our new conditional:

form.addField("description", Fields.string()
        .label("Description")
        // only render this field when "name" is lower-case
        .conditional(new DefaultIsLowercaseConditional("name")));