New: Cascader component

Hi all, excited to share the new and improved Cascader component! This new version brings Cascader up to the level of our modern components, with features including:

  • The intuitive GUI from Navigation and Checkbox Group to manually create and organize options list
  • Dynamically map existing data to options, as with Select and Checkbox Group
  • A new dropdown look and user experience, consistent with the Select component
  • New label options (customizable caption, color, icon) to match Select option customization
  • Support for numeric values alongside string for options

If you are looking to upgrade, some key changes from the existing Cascader are:

  • The value of the new Cascader component is the value of the selected option. You can still access the path of values leading to the selected option via the new property valuePath.
    CascaderState
  • Like other modern Retool components, values for options need to be unique.
    • If you use the one click upgrade for Cascaders, duplicate values will have parent values appended to make them unique (i.e. option with duplicate value Cost nested under option Shipping, would have it’s value changed to Shipping-Cost). (Starting in v3.145 on cloud)
  • The format for dynamically mapping option data is different from the existing Cascader. Like Checkbox Group and Navigation, the data source is expected to be an array of objects with a property that points to its parent. Cascader nests options by parent value.
    • Below are two code snippets to help transform existing Cascader data (either string[][] or { value: string, label?: string, children: Option[] }[]) to be compatible with the new component { value: string, label?: string, parentValue?: string }[].

We have launched this feature to 100% of cloud users, and hope to launch it to the next on-prem version. Please share any feedback or questions regarding upgrading you have in this thread!

Happy Building!

// Migrates string[][] to { value: string, label?: string, parentValue?: string }[]
function migrateSimpleStructureToCascader2(structure) {
  let options = []
  const keyToValueMap = new Map()
  const uniqueValues = new Set()
  
  for (const leafNodePath of structure) {
    for (let i = 0; i < leafNodePath.length; i++) {
      const key = leafNodePath.slice(0, i + 1).join('-')
      if (keyToValueMap.has(key)) continue

      // If the value is not unique, we use the key
      const originalValue = leafNodePath[i]
      const isNotUniqueValue = uniqueValues.has(originalValue)
      const value = isNotUniqueValue ? key : originalValue

      // Track the option to its potentially changed value
      keyToValueMap.set(key, value)

      const parentKey = i === 0 ? undefined : leafNodePath.slice(0, i).join('-')
      const parentValue = parentKey ? keyToValueMap.get(parentKey) : undefined

      const option = {
        value,
        label: isNotUniqueValue ? originalValue : undefined, 
        parentValue,
      }
      options.push(option)
      uniqueValues.add(value)
    }
  }

  return options;
}

// Migrates { value: string, label?: string, children: { value: string, label?: string }[] }[] to { value: string, label?: string, parentValue?: string }[]
function migrateComplexStructureToCascader2(structure)
{
  let options = []
  const uniqueValues = new Set()

  function recursivelyCreateOptions(
    originalOption,
    {
      options, // Array to add the new option to
      uniqueValues, // Set to keep track of unique values
      parentValue,
    },
  ) {
    let value = originalOption.value
    if (value === undefined) return { options, uniqueValues }
  
    // If the value is not unique, we make it unique by appending the parent key
    if (uniqueValues.has(value)) {
      value = `${parentValue}-${value}`
    }

    // Use explicit label if it exists. Otherwise, if we altered the value to make it unique, use the original value as the label
    let label = originalOption.label
    if (!label && originalOption.value !== value) {
      label = originalOption.value
    }
  
    const option = {
      value,
      label,
      parentValue,
    }
    options.push(option)
    uniqueValues.add(value)
  
    // Create options for children, this can be recursive
    if (originalOption.children) {
      for (const child of originalOption.children) {
        const recursiveResult = recursivelyCreateOptions(child, { options, uniqueValues, parentValue: value })
        options = recursiveResult.options
        uniqueValues = recursiveResult.uniqueValues
      }
    }
  
    return { options, uniqueValues }
  }

  for (const oldOption of structure) {
    options = recursivelyCreateOptions(oldOption, { options, uniqueValues }).options
  }

  return options
}
6 Likes

Hi @Jynnie_Tang,

This is exciting and it will definitely improve the usability of some forms!

Quick question, I tried creating mapped options with a simple JSON but I was not able to nest options using parentKey. Here's my data source
cascader data source example.json (8.0 KB), would you mind taking a look at it?

Thanks!

Hi @MiguelOrtiz!

If you are using Mapped mode in the new Cascader, children are mapped to their parent option by the parent option's value. So you could either map the options, value to the key and refer to parentKey with your data as is; or change your data so parentKeys refer to the parent option's value.

Let me know if this is confusing or you need more help!

Ah, sorry, I completely missed the "Parent Value" field right below hidden. It all makes sense now, thanks!

@Jynnie_Tang just a quick question. As it happens, the actual data set I'm working with at the moment is provided by State Departmetn (thus out of my control), and there are duplicate values between the parent and child categories (both list of integers), which in this case will cause instances after the first one to be removed.

I understand that under the hood this may be a restyled select component ,which may make it more difficult for new features to be added to this, but if there is a chance to add an option to "allow duplicates" it would be great!

Thanks and great work, I love the interface of this component!

1 Like