Dependent Selector Configurations | Pandium Docs

For the complete documentation index, see llms.txt. This page is also available as Markdown.

This page describes the definitions + init-sync pattern used by manifest version 0.4. On manifest version 1.0, dependent selectors are sourced from tenant metadata via optionsMapPath: '#/metadata/<key>' — still typically populated by an init sync. See Using Metadata to Populate Dynamic Configs.

The last article explained how to make a dynamic configuration, a setting where the options are fetched from an API or database so they change from tenant to tenant.

This article will explain how to make a dependent selector configuration, a setting where the options are fetched from an API or database and vary based on the user's selection of an earlier config (which can also be dynamic).

Here is an example of a dependent selector configuration:

A tenant settings page that allows the user to select their favorite food.

The options for the favorite food will be fetched from an API or database, so they will be different from tenant to tenant.

There will also be a food type configuration.

Here are what the options could look like to different users:

The only food type option for Tenant A are "Sweet" and "Savory"

When the user selects the "Sweet" food type, the options for favorite food are only sweet foods.

When the user selects the "Savory" food type, the options for favorite food are only savory foods.

The only food type option for Tenant B are "Fruits" and "Veggies"

When the user selects the "Fruits" food type, the options for favorite food are only fruits.

When the user selects the "Veggies" food type, the options for favorite food are only vegetables.

A dependent dynamic configuration is implemented by creating a parent selector and a dependent selector:

  1. Create the parent selector: This is the dynamic configuration whose selected value determines the options available for the dependent selector.
    • In this example, the parent selector is the food_type.
  1. Like other dynamic configurations, the schema property for this must have a reference to a list of options in the schema's definitions.

{'const': 1, 'title': 'Fruits'}, {'const': 2, 'title': 'Veggies'}, ] - It could look like this: food_type_options = [

{'const': 3, 'title': 'Sweet'}, {'const': 4, 'title': 'Savory'}, ]

  1. Create a dependent selector: This is the configuration whose options will depend on the parent selector and the values fetched from a database/API.

    • In this example, the dependent selector is favorite_food.
    • Here is how a dependent selector is set up:
  2. Like a normal configuration, the schema property for this must have a type. The type for a dependent selector should be string.

    1. It must have two additional properties:
  3. optionsMapPath: This must reference the schema definition that has the options for the dependent selector.

  1. parentField: This must reference the schema property for the parent selector which controls the options that will be present.
  1. It's value must be a map of the option type IDs (from the parent selector) to a list of the options for that type. 2. All the options for every list must be either entirely string or entirely oneOf (an objects with a title and const). 3. In this example, each option for every list will be a string. 4. In this example, the standard out of an init sync must include food_options.

1: ['Strawberry', 'Banana', 'Kiwi'], 2: ['Carrot', 'Broccoli', 'Pepper'] }

3: ['Chocolate', 'Caramel', 'Jello'], 4: ['Chips', 'Pizza', 'Bread'], }

Putting it all together

The configs section of the PANDIUM.yaml would include this:

configs:
  schema:
    definitions:
      food_type_options:
        oneOf:
          - const: Placeholder
            title: Placeholder
      food_options:
        type: object
    properties:
      food_type:
        $ref: "#/definitions/food_type_options"
      favorite_food:
        type: string
        optionsMapPath: '#/definitions/food_options'
        parentField: '#/properties/food_type'

uischema:
    type: VerticalLayout
    elements:
      # Parent
      - label: Food Type
        scope: '#/properties/food_type'
        type: Control
      # Dependent
      - label: Favorite Food
        scope: '#/properties/favorite_food'
        type: Control

A TypeScript implementation of the init sync of the integration could look like this:

if (pandium.context.runMode === 'init') {
    const foods = await foodClient.getMany('food');
    /* foods is a list that could look like this:
    [
     { type: 'Sweet', name: 'Chocolate' },
     { type: 'Sweet', name: 'Caramel' },
     { type: 'Sweet', name: 'Jello' },
     { type: 'Savory', name: 'Chips' },
     { type: 'Savory', name: 'Pizza' },
     { type: 'Savory', name: 'Bread' },
    ]
    */

// Loop over the foods to identify the food types.
    const uniqueFoodTypes = foods.reduce((foodTypesList, currentFood) => {
      if(!foodTypesList.includes(currentFood.type)){
        foodTypesList.push(currentFood.type)
      }
      return foodTypesList
    }, [] )

// Map each food type to the oneOf format
    const foodTypes = uniqueFoodTypes.map((type, index) => {
      return {
        const: index + 1,
        title: type
      }
    })

// Create a map from food type to ID, which can be used to organize foods into lists by type.
    const foodTypeToIdMap = foodTypes.reduce((typeToIdMap, foodType) => {
      typeToIdMap[foodType.title] = foodType.const
      return typeToIdMap
    }, {})

// Organize the foods into lists of their types
    foodListByTypeId = foods.reduce((foodListByType,currentFood) => {
      const typeId = foodTypeToIdMap[currentFood.type]
      if(!foodListByType[typeId]) {
        foodListByType[typeId] = []
      }
      foodListByType[typeId].push(currentFood.name)
      return foodListByType
    }, {} )

const stdout = {
      food_type_options: foodTypes,
      food_options: foodListByTypeId
    };
    // Print json string to stdout and end the script.
    console.log(JSON.stringify(stdout));
    return
}

Another example

This example has a static parent selector and the options for the dependent selector will be oneOf rather than string.

The configs section of the PANDIUM.yaml would include this:

configs:
  schema:
    type: object
    definitions:
      pokemon_types:
        oneOf:
          - const: Fire
            title: Fire
          - const: Water
            title: Water
          - const: Rock
            title: Rock
          - const: Flying
            title: Flying
      pokemon_options:
        type: object
    properties:
      pokemon_type:
        $ref: "#/definitions/pokemon_types"
        type: string
      pokemon_1:
        type: string
        optionsMapPath: "#/definitions/pokemon_options"
        parentField: "#/properties/pokemon_type"
      pokemon_2:
        type: string
        optionsMapPath: "#/definitions/pokemon_options"
        parentField: "#/properties/pokemon_type"

uischema:
    type: VerticalLayout
    elements:
      - label: Pokemon Type
        scope: "#/properties/pokemon_type"
        type: Control
      - type: Section
        label: Pokemon Selection
        hintText: Choose your Pokemon
        elements:
          - label: First Pokemon
            scope: "#/properties/pokemon_1"
            type: Control
          - label: Second Pokemon
            scope: "#/properties/pokemon_2"
            type: Control

A TypeScript implementation of the init sync of this integration could look like this:

if (pandium.context.runMode === 'init') {
    const pokemon = await pokeClient.getMany('pokemon');
    /* pokemon is a list that could look like this:
    [
     { type: 'Fire', name: 'Charmander', id: 1 },
     { type: 'Fire', name: 'Vulpix', id: 2  },
     { type: 'Water', name: 'Squirtle', id: 3  },
     { type: 'Water', name: 'Poliwag', id: 4  },
     { type: 'Rock', name: 'Geodude', id: 5  },
     { type: 'Rock', name: 'Onix', id: 6  },
     { type: 'Flying', name: 'Butterfree', id: 7 },
     { type: 'Flying', name: 'Pidgeot', id: 8  },
    ]
    */

const pokemonOptions = {
      Fire:[],
      Water:[],
      Rock: [],
      Flying: [],
    }

// Add each pokemon to the list for the appropriate type.
    for (const currentPokemon of pokemon) {
      pokemonOptions[currentPokemon.type].push({
        title: currentPokemon.name,
        const: currentPokemon.id
      })
    }

const stdout = {
      pokemon_options: pokemonOptions
    };
    // Print json string to stdout and end the script.
    console.log(JSON.stringify(stdout));
    return
}

Dependent Selectors in Arrays

Dependent selectors can be used in arrays. The selector will look first for its parent in its own object, and if it doesn't find it there it will look up the next level in the schema.