Chained Selects
DotSelect supports declarative parent-child select relationships where the options in a child select depend on the selected value of its parent. This is configured entirely through HTML attributes.
How Chaining Works
- A group of selects is linked by a shared
data-chain-groupname - Each select in the chain is assigned an index with
data-chained-index(starting at0) - When a parent select's value changes, all child selects reload their options based on the parent's selection
- Empty levels are automatically skipped
Static Chained Selects
For static data, use data-chained-child-data to provide a JSON mapping of parent values to child options:
<!-- Parent: Country -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="0"
data-placeholder="Select a country..."
>
<option value="">Select a country...</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
</select>
<!-- Child: State/Province -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="1"
data-placeholder="Select a state..."
data-chained-child-data='{
"us": [
{"id": "ca", "text": "California"},
{"id": "ny", "text": "New York"},
{"id": "tx", "text": "Texas"}
],
"ca": [
{"id": "on", "text": "Ontario"},
{"id": "bc", "text": "British Columbia"},
{"id": "qc", "text": "Quebec"}
]
}'
>
</select>
<!-- Grandchild: City -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="2"
data-placeholder="Select a city..."
data-chained-child-data='{
"ca": [
{"id": "la", "text": "Los Angeles"},
{"id": "sf", "text": "San Francisco"}
],
"ny": [
{"id": "nyc", "text": "New York City"},
{"id": "buf", "text": "Buffalo"}
],
"tx": [
{"id": "hou", "text": "Houston"},
{"id": "dal", "text": "Dallas"}
],
"on": [
{"id": "tor", "text": "Toronto"},
{"id": "ott", "text": "Ottawa"}
],
"bc": [
{"id": "van", "text": "Vancouver"},
{"id": "vic", "text": "Victoria"}
],
"qc": [
{"id": "mtl", "text": "Montreal"},
{"id": "qcc", "text": "Quebec City"}
]
}'
>
</select>import { DotSelect } from 'dot-select'
const ds = new DotSelect('select[data-dot-select]')
// API
ds.getValue()
ds.setValue(['value'])
ds.clear()
ds.destroy()import { useEffect, useRef } from 'react'
import { DotSelect } from 'dot-select'
import 'dot-select/css'
export default function Select() {
const ref = useRef(null)
useEffect(() => {
const ds = new DotSelect(ref.current)
return () => ds.destroy()
}, [])
return (
<!-- Parent: Country -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="0"
data-placeholder="Select a country..."
>
<option value="">Select a country...</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
</select>
<!-- Child: State/Province -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="1"
data-placeholder="Select a state..."
data-chained-child-data='{
"us": [
{"id": "ca", "text": "California"},
{"id": "ny", "text": "New York"},
{"id": "tx", "text": "Texas"}
],
"ca": [
{"id": "on", "text": "Ontario"},
{"id": "bc", "text": "British Columbia"},
{"id": "qc", "text": "Quebec"}
]
}'
>
</select>
<!-- Grandchild: City -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="2"
data-placeholder="Select a city..."
data-chained-child-data='{
"ca": [
{"id": "la", "text": "Los Angeles"},
{"id": "sf", "text": "San Francisco"}
],
"ny": [
{"id": "nyc", "text": "New York City"},
{"id": "buf", "text": "Buffalo"}
],
"tx": [
{"id": "hou", "text": "Houston"},
{"id": "dal", "text": "Dallas"}
],
"on": [
{"id": "tor", "text": "Toronto"},
{"id": "ott", "text": "Ottawa"}
],
"bc": [
{"id": "van", "text": "Vancouver"},
{"id": "vic", "text": "Victoria"}
],
"qc": [
{"id": "mtl", "text": "Montreal"},
{"id": "qcc", "text": "Quebec City"}
]
}'
>
</select>
)
}<template>
<!-- Parent: Country -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="0"
data-placeholder="Select a country..."
>
<option value="">Select a country...</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
</select>
<!-- Child: State/Province -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="1"
data-placeholder="Select a state..."
data-chained-child-data='{
"us": [
{"id": "ca", "text": "California"},
{"id": "ny", "text": "New York"},
{"id": "tx", "text": "Texas"}
],
"ca": [
{"id": "on", "text": "Ontario"},
{"id": "bc", "text": "British Columbia"},
{"id": "qc", "text": "Quebec"}
]
}'
>
</select>
<!-- Grandchild: City -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="2"
data-placeholder="Select a city..."
data-chained-child-data='{
"ca": [
{"id": "la", "text": "Los Angeles"},
{"id": "sf", "text": "San Francisco"}
],
"ny": [
{"id": "nyc", "text": "New York City"},
{"id": "buf", "text": "Buffalo"}
],
"tx": [
{"id": "hou", "text": "Houston"},
{"id": "dal", "text": "Dallas"}
],
"on": [
{"id": "tor", "text": "Toronto"},
{"id": "ott", "text": "Ottawa"}
],
"bc": [
{"id": "van", "text": "Vancouver"},
{"id": "vic", "text": "Victoria"}
],
"qc": [
{"id": "mtl", "text": "Montreal"},
{"id": "qcc", "text": "Quebec City"}
]
}'
>
</select>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { DotSelect } from 'dot-select'
import 'dot-select/css'
const selectRef = ref(null)
let ds = null
onMounted(() => {
ds = new DotSelect(selectRef.value)
})
onBeforeUnmount(() => {
ds?.destroy()
})
</script>AJAX Chained Selects
For dynamic data, use data-chained-child-ajax-data to specify the AJAX URL with a token for the parent value:
<!-- Parent: Country -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="0"
data-ajax-url="/api/countries?q={@term}"
data-searchable="true"
data-placeholder="Select a country..."
>
</select>
<!-- Child: State/Province -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="1"
data-chained-child-ajax-data="/api/states?country={@val}&q={@term}"
data-searchable="true"
data-placeholder="Select a state..."
>
</select>
<!-- Grandchild: City -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="2"
data-chained-child-ajax-data="/api/cities?state={@val}&q={@term}"
data-searchable="true"
data-placeholder="Select a city..."
>
</select>import { DotSelect } from 'dot-select'
const ds = new DotSelect('select[data-dot-select]')
// API
ds.getValue()
ds.setValue(['value'])
ds.clear()
ds.destroy()import { useEffect, useRef } from 'react'
import { DotSelect } from 'dot-select'
import 'dot-select/css'
export default function Select() {
const ref = useRef(null)
useEffect(() => {
const ds = new DotSelect(ref.current)
return () => ds.destroy()
}, [])
return (
<!-- Parent: Country -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="0"
data-ajax-url="/api/countries?q={@term}"
data-searchable="true"
data-placeholder="Select a country..."
>
</select>
<!-- Child: State/Province -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="1"
data-chained-child-ajax-data="/api/states?country={@val}&q={@term}"
data-searchable="true"
data-placeholder="Select a state..."
>
</select>
<!-- Grandchild: City -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="2"
data-chained-child-ajax-data="/api/cities?state={@val}&q={@term}"
data-searchable="true"
data-placeholder="Select a city..."
>
</select>
)
}<template>
<!-- Parent: Country -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="0"
data-ajax-url="/api/countries?q={@term}"
data-searchable="true"
data-placeholder="Select a country..."
>
</select>
<!-- Child: State/Province -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="1"
data-chained-child-ajax-data="/api/states?country={@val}&q={@term}"
data-searchable="true"
data-placeholder="Select a state..."
>
</select>
<!-- Grandchild: City -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="2"
data-chained-child-ajax-data="/api/cities?state={@val}&q={@term}"
data-searchable="true"
data-placeholder="Select a city..."
>
</select>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { DotSelect } from 'dot-select'
import 'dot-select/css'
const selectRef = ref(null)
let ds = null
onMounted(() => {
ds = new DotSelect(selectRef.value)
})
onBeforeUnmount(() => {
ds?.destroy()
})
</script>TIP
In chained AJAX URLs, {@val} refers to the parent select's current value, not the current select's value. This is what creates the parent-child relationship.
Chaining Configuration
| Attribute | Description | Example |
|---|---|---|
data-chain-group | Name that links selects into a chain | "location" |
data-chained-index | Position in the chain (0-based) | 0, 1, 2 |
data-chained-child-data | JSON map of parent values to child options | '{"us": [...]}' |
data-chained-child-ajax-data | AJAX URL template for loading child options | "/api/states?country={@val}" |
data-chained-disabled-on-empty | Disable the select when parent has no value | "true" |
data-chained-clear-on-parent-change | Clear value when parent changes | "true" |
Auto-Skip Empty Levels
If a parent value results in no options for a child, DotSelect can automatically skip that level and move to the next child that has options:
<select
data-dot-select
data-chain-group="location"
data-chained-index="1"
data-chained-child-ajax-data="/api/regions?country={@val}"
data-chained-skip-empty="true"
data-placeholder="Select a region..."
>
</select>import { DotSelect } from 'dot-select'
const ds = new DotSelect('select[data-dot-select]')
// API
ds.getValue()
ds.setValue(['value'])
ds.clear()
ds.destroy()import { useEffect, useRef } from 'react'
import { DotSelect } from 'dot-select'
import 'dot-select/css'
export default function Select() {
const ref = useRef(null)
useEffect(() => {
const ds = new DotSelect(ref.current)
return () => ds.destroy()
}, [])
return (
<select
data-dot-select
data-chain-group="location"
data-chained-index="1"
data-chained-child-ajax-data="/api/regions?country={@val}"
data-chained-skip-empty="true"
data-placeholder="Select a region..."
>
</select>
)
}<template>
<select
data-dot-select
data-chain-group="location"
data-chained-index="1"
data-chained-child-ajax-data="/api/regions?country={@val}"
data-chained-skip-empty="true"
data-placeholder="Select a region..."
>
</select>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { DotSelect } from 'dot-select'
import 'dot-select/css'
const selectRef = ref(null)
let ds = null
onMounted(() => {
ds = new DotSelect(selectRef.value)
})
onBeforeUnmount(() => {
ds?.destroy()
})
</script>When data-chained-skip-empty="true" is set and the AJAX response returns an empty array, DotSelect hides the current select and passes the parent value directly to the next child in the chain.
Disabling Children Until Parent is Selected
By default, child selects are disabled until their parent has a value:
<!-- This select will be disabled until the parent (index 0) has a value -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="1"
data-chained-disabled-on-empty="true"
data-placeholder="First select a country..."
>
</select>import { DotSelect } from 'dot-select'
const ds = new DotSelect('select[data-dot-select]')
// API
ds.getValue()
ds.setValue(['value'])
ds.clear()
ds.destroy()import { useEffect, useRef } from 'react'
import { DotSelect } from 'dot-select'
import 'dot-select/css'
export default function Select() {
const ref = useRef(null)
useEffect(() => {
const ds = new DotSelect(ref.current)
return () => ds.destroy()
}, [])
return (
<!-- This select will be disabled until the parent (index 0) has a value -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="1"
data-chained-disabled-on-empty="true"
data-placeholder="First select a country..."
>
</select>
)
}<template>
<!-- This select will be disabled until the parent (index 0) has a value -->
<select
data-dot-select
data-chain-group="location"
data-chained-index="1"
data-chained-disabled-on-empty="true"
data-placeholder="First select a country..."
>
</select>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { DotSelect } from 'dot-select'
import 'dot-select/css'
const selectRef = ref(null)
let ds = null
onMounted(() => {
ds = new DotSelect(selectRef.value)
})
onBeforeUnmount(() => {
ds?.destroy()
})
</script>JavaScript Chain API
For advanced use cases, you can interact with chains programmatically:
// Initialize a chain group
const chain = DotSelect.chain('location');
// Get all current values in the chain
const values = chain.getValues();
// { 0: "us", 1: "ca", 2: "sf" }
// Get a specific level's value
const state = chain.getValue(1);
// Listen for changes at any level
chain.onChange((index, value, instance) => {
console.log(`Level ${index} changed to ${value}`);
});
// Wait for the chain to be fully initialized
chain.whenReady(() => {
console.log('All chain members are ready');
});
// Get all DotSelect instances in the chain
const members = chain.getMembers();
// Destroy the chain
chain.destroy();Pre-Selected Chain Values
You can pre-select values across the entire chain. DotSelect resolves them in order:
<select data-dot-select data-chain-group="location" data-chained-index="0">
<option value="us" selected>United States</option>
<option value="ca">Canada</option>
</select>
<select data-dot-select data-chain-group="location" data-chained-index="1"
data-chained-child-ajax-data="/api/states?country={@val}"
data-selected="ca"
data-ajax-resolve-url="/api/states/resolve?ids={@selected}"
>
</select>
<select data-dot-select data-chain-group="location" data-chained-index="2"
data-chained-child-ajax-data="/api/cities?state={@val}"
data-selected="sf"
data-ajax-resolve-url="/api/cities/resolve?ids={@selected}"
>
</select>import { DotSelect } from 'dot-select'
const ds = new DotSelect('select[data-dot-select]')
// API
ds.getValue()
ds.setValue(['value'])
ds.clear()
ds.destroy()import { useEffect, useRef } from 'react'
import { DotSelect } from 'dot-select'
import 'dot-select/css'
export default function Select() {
const ref = useRef(null)
useEffect(() => {
const ds = new DotSelect(ref.current)
return () => ds.destroy()
}, [])
return (
<select data-dot-select data-chain-group="location" data-chained-index="0">
<option value="us" selected>United States</option>
<option value="ca">Canada</option>
</select>
<select data-dot-select data-chain-group="location" data-chained-index="1"
data-chained-child-ajax-data="/api/states?country={@val}"
data-selected="ca"
data-ajax-resolve-url="/api/states/resolve?ids={@selected}"
>
</select>
<select data-dot-select data-chain-group="location" data-chained-index="2"
data-chained-child-ajax-data="/api/cities?state={@val}"
data-selected="sf"
data-ajax-resolve-url="/api/cities/resolve?ids={@selected}"
>
</select>
)
}<template>
<select data-dot-select data-chain-group="location" data-chained-index="0">
<option value="us" selected>United States</option>
<option value="ca">Canada</option>
</select>
<select data-dot-select data-chain-group="location" data-chained-index="1"
data-chained-child-ajax-data="/api/states?country={@val}"
data-selected="ca"
data-ajax-resolve-url="/api/states/resolve?ids={@selected}"
>
</select>
<select data-dot-select data-chain-group="location" data-chained-index="2"
data-chained-child-ajax-data="/api/cities?state={@val}"
data-selected="sf"
data-ajax-resolve-url="/api/cities/resolve?ids={@selected}"
>
</select>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { DotSelect } from 'dot-select'
import 'dot-select/css'
const selectRef = ref(null)
let ds = null
onMounted(() => {
ds = new DotSelect(selectRef.value)
})
onBeforeUnmount(() => {
ds?.destroy()
})
</script>WARNING
When pre-selecting values in an AJAX chain, make sure each child level has data-ajax-resolve-url configured so it can resolve the selected ID into display text.
Country, State, City Example
A complete, copy-paste ready example:
<div class="location-selector">
<label>Country</label>
<select
data-dot-select
data-chain-group="address"
data-chained-index="0"
data-searchable="true"
data-ajax-url="/api/countries?q={@term}&page={@page}"
data-pagination="true"
data-placeholder="Select a country..."
data-allow-clear="true"
>
</select>
<label>State / Province</label>
<select
data-dot-select
data-chain-group="address"
data-chained-index="1"
data-searchable="true"
data-chained-child-ajax-data="/api/states?country={@val}&q={@term}&page={@page}"
data-pagination="true"
data-chained-disabled-on-empty="true"
data-chained-clear-on-parent-change="true"
data-placeholder="Select a state..."
data-allow-clear="true"
>
</select>
<label>City</label>
<select
data-dot-select
data-chain-group="address"
data-chained-index="2"
data-searchable="true"
data-chained-child-ajax-data="/api/cities?state={@val}&q={@term}&page={@page}"
data-pagination="true"
data-chained-disabled-on-empty="true"
data-chained-clear-on-parent-change="true"
data-placeholder="Select a city..."
data-allow-clear="true"
>
</select>
</div>import { DotSelect } from 'dot-select'
const ds = new DotSelect('select[data-dot-select]')
// API
ds.getValue()
ds.setValue(['value'])
ds.clear()
ds.destroy()import { useEffect, useRef } from 'react'
import { DotSelect } from 'dot-select'
import 'dot-select/css'
export default function Select() {
const ref = useRef(null)
useEffect(() => {
const ds = new DotSelect(ref.current)
return () => ds.destroy()
}, [])
return (
<div class="location-selector">
<label>Country</label>
<select
data-dot-select
data-chain-group="address"
data-chained-index="0"
data-searchable="true"
data-ajax-url="/api/countries?q={@term}&page={@page}"
data-pagination="true"
data-placeholder="Select a country..."
data-allow-clear="true"
>
</select>
<label>State / Province</label>
<select
data-dot-select
data-chain-group="address"
data-chained-index="1"
data-searchable="true"
data-chained-child-ajax-data="/api/states?country={@val}&q={@term}&page={@page}"
data-pagination="true"
data-chained-disabled-on-empty="true"
data-chained-clear-on-parent-change="true"
data-placeholder="Select a state..."
data-allow-clear="true"
>
</select>
<label>City</label>
<select
data-dot-select
data-chain-group="address"
data-chained-index="2"
data-searchable="true"
data-chained-child-ajax-data="/api/cities?state={@val}&q={@term}&page={@page}"
data-pagination="true"
data-chained-disabled-on-empty="true"
data-chained-clear-on-parent-change="true"
data-placeholder="Select a city..."
data-allow-clear="true"
>
</select>
</div>
)
}<template>
<div class="location-selector">
<label>Country</label>
<select
data-dot-select
data-chain-group="address"
data-chained-index="0"
data-searchable="true"
data-ajax-url="/api/countries?q={@term}&page={@page}"
data-pagination="true"
data-placeholder="Select a country..."
data-allow-clear="true"
>
</select>
<label>State / Province</label>
<select
data-dot-select
data-chain-group="address"
data-chained-index="1"
data-searchable="true"
data-chained-child-ajax-data="/api/states?country={@val}&q={@term}&page={@page}"
data-pagination="true"
data-chained-disabled-on-empty="true"
data-chained-clear-on-parent-change="true"
data-placeholder="Select a state..."
data-allow-clear="true"
>
</select>
<label>City</label>
<select
data-dot-select
data-chain-group="address"
data-chained-index="2"
data-searchable="true"
data-chained-child-ajax-data="/api/cities?state={@val}&q={@term}&page={@page}"
data-pagination="true"
data-chained-disabled-on-empty="true"
data-chained-clear-on-parent-change="true"
data-placeholder="Select a city..."
data-allow-clear="true"
>
</select>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { DotSelect } from 'dot-select'
import 'dot-select/css'
const selectRef = ref(null)
let ds = null
onMounted(() => {
ds = new DotSelect(selectRef.value)
})
onBeforeUnmount(() => {
ds?.destroy()
})
</script>