Representation of a Query Builder

I’m trying to represent a “query builder” in json forms that represents the following concepts:

  • a collection of query groups joined by an operator (And / OR) that contain:
    • a collection of queries where each query is joined by an operator (AND / OR) (which I forgot to include in my example below but isn’t a major concern):
      • a field to select a Property name (an enum of values)
      • 1 or more fields that are variable based on the selected Property name

As inspiration, an existing control I have that I’d like to migrate to JSON Forms looks like the following:

By combining the nestedArrays and conditional-schema-compositions (examples located in github, links removed due to new user restrictions) examples I think I got pretty much all the pieces together.

Can see the stackblitz example: JSON Forms Query Builder Example - StackBlitz

schema
{
  "definitions": {
    "query": {
      "type": "object",
      "properties": {
        "propertyName": {
          "type": "string",
          "enum": ["connectionStatus", "description"]
        }
      },
      "anyOf": [
        {
          "if": {
            "properties": { "propertyName": { "const": "connectionStatus" } }
          },
          "then": {
            "properties": {
              "connectionStatusOperator": {
                "type": "string",
                "enum": ["equals", "doesNotEqual"]
              },
              "connectionStatusValue": {
                "type": "string",
                "enum": ["approved", "disconnected", "connected"]
              }
            }
          }
        },
        {
          "if": {
            "properties": { "propertyName": { "const": "description" } }
          },
          "then": {
            "properties": {
              "descriptionOperator": {
                "type": "string",
                "enum": ["equals", "doesNotEqual"]
              },
              "descriptionValue": { "type": "string" }
            }
          }
        }
      ]
    },
    "queryGroup": {
      "type": "object",
      "properties": {
        "queryGroupsOperator": { "type": "string", "enum": ["AND", "OR"] },
        "queries": {
          "type": "array",
          "items": { "$ref": "#/definitions/query" }
        }
      }
    }
  },
  "type": "object",
  "properties": {
    "queryGroups": {
      "type": "array",
      "items": { "$ref": "#/definitions/queryGroup" }
    }
  }
}
uischema.json
{
  "type": "HorizontalLayout",
  "elements": [
    {
      "type": "Control",
      "label": { "text": "Query Groups", "show": true },
      "scope": "#/properties/queryGroups",
      "options": {
        "showSortButtons": true,
        "detail": {
          "type": "VerticalLayout",
          "elements": [
            {
              "type": "Control",
              "scope": "#/properties/queries",
              "options": {
                "showSortButtons": true,
                "detail": {
                  "type": "HorizontalLayout",
                  "elements": [
                    { "type": "Control", "scope": "#/properties/propertyName" },
                    {
                      "type": "Control",
                      "scope": "#/properties/connectionStatusOperator",
                      "rule": {
                        "effect": "SHOW",
                        "condition": {
                          "scope": "#/properties/propertyName",
                          "schema": { "const": "connectionStatus" }
                        }
                      }
                    },
                    {
                      "type": "Control",
                      "scope": "#/properties/connectionStatusValue",
                      "rule": {
                        "effect": "SHOW",
                        "condition": {
                          "scope": "#/properties/propertyName",
                          "schema": { "const": "connectionStatus" }
                        }
                      }
                    },
                    {
                      "type": "Control",
                      "scope": "#/properties/descriptionOperator",
                      "rule": {
                        "effect": "SHOW",
                        "condition": {
                          "scope": "#/properties/propertyName",
                          "schema": { "const": "description" }
                        }
                      }
                    },
                    {
                      "type": "Control",
                      "scope": "#/properties/descriptionValue",
                      "rule": {
                        "effect": "SHOW",
                        "condition": {
                          "scope": "#/properties/propertyName",
                          "schema": { "const": "description" }
                        }
                      }
                    }
                  ]
                }
              }
            },
            { "type": "Control", "scope": "#/properties/queryGroupsOperator" }
          ]
        }
      }
    }
  ]
}

My questions are:

  • Does this look like a reasonable approach to represent the “query builder” pattern? Anyone have alternative examples?

  • The pattern shown is trying to associate a specific Property Name configuration with a specific set of additional fields. However the way it is written potentially results in extra fields not associated with the current selected Property Name.

    For example:

    • Select Property Name of connectionStatus
    • Choose a connection status operator
    • Switch Property Name to description
    • Notice the connectionStatusOperator is still in the data: { "queryGroups": [ { "queries": [ { "propertyName": "description", "connectionStatusOperator": "equals" } ] } ] }

    Any ideas on better ways to represent the intended behavior to avoid the unrelated fields in the data for the selected Property Name?

  • A limitation of the approach of this pattern is you kind of need to make sure all the possible fields associated with each different Property Name configuration are mutually exclusive / globally unique since they all can end up in the same object. In the example I’m prefixing the names with the Property Name they are associated with. Any ideas on representing that better / avoiding having to figure out patterns to uniquify the names?

Hi @rajsite,

Personally I like to model distinct choices as an array of oneOf, each entry modeling one of the options. This feels a bit more “natural” than using if/then/else rules. It’s also easier for rendering, as each entry may serve as a (sub) form. Leveraging this it becomes easy to determine which UI controls to show without needing to replicate the JSON Schema if/then/else constructions with SHOW/HIDE rules in the UI Schema.

All oneOf entries should share a common property which serves as the “identifier”, so that they can be differentiated. In your case that’s the propertyName.

The use case could be modeled like this:

{
  "definitions": {
    "query": {
      "type": "object",
      "required": ["propertyName"],
      "default": {
        "propertyName": "connectionStatus" // indicating the default choice
      },
      "oneOf": [
        {
          "title": "Connection Status",
          "properties": {
            "propertyName": {
              "const": "connectionStatus",
              "default": "connectionStatus" // making sure the data is automatically filled in
            },
            "connectionStatusOperator": {
              "type": "string",
              "enum": ["equals", "doesNotEqual"]
            },
            "connectionStatusValue": {
              "type": "string",
              "enum": ["approved", "disconnected", "connected"]
            }
          },
          "required": ["connectionStatusOperator", "connectionStatusValue"]
        },
        {
          "title": "Description",
          "properties": {
            "propertyName": {
              "const": "description",
              "default": "description" // making sure the data data is automatically filled in
            },
            "descriptionOperator": {
              "type": "string",
              "enum": ["equals", "doesNotEqual"]
            },
            "descriptionValue": {
              "type": "string"
            }
          },
          "required": ["descriptionOperator", "descriptionValue"]
        }
      ]
    },
    "queryGroup": {
      "type": "object",
      "properties": {
        "queryGroupsOperator": {
          "type": "string",
          "enum": ["AND", "OR"]
        },
        "queries": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/query"
          },
          "default": []
        }
      },
      "required": ["queryGroupsOperator", "queries"]
    }
  },
  "type": "object",
  "properties": {
    "queryGroups": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/queryGroup"
      }
    }
  },
  "required": ["queryGroups"]
}

This JSON Schema already works great in the React Material renderers without any UI Schema or customization.

QueryBuilderExample

However the Angular Material renderers don’t have such a “OneOf” renderer yet. So you would need to implement one. Here you can find the one React Material.

1 Like

Thanks @sdirix! That does seem like a nicer approach to the schema and better represents what was trying to be conveyed.

Do you forsee any complications to the angular-material oneOf support as to why that behavior isn’t implemented yet or is it currently just an uninvestigated feature gap?

Hi @rajsite,

It’s definitely possible to implement this in Angular.

All our renderer sets are very different capable, depending on how much interest there are in them from the community, our professional support clients and where we use them ourselves.

If you implement such a renderer, then it would be great if you could contribute it back to the Angular Material renderer set.

1 Like