Skip to content

Instantly share code, notes, and snippets.

@julianrubisch
Created May 31, 2022 12:23
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save julianrubisch/d5a5ecad48e23c681f605cace127df41 to your computer and use it in GitHub Desktop.
Save julianrubisch/d5a5ecad48e23c681f605cace127df41 to your computer and use it in GitHub Desktop.
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
connect () {
this.element.addEventListener('change', this.handleChange.bind(this))
}
handleChange (event) {
this.traverseDown(event.target, event.target.checked)
this.dispatch('change', {
detail: { element: event.target }
})
}
traverseDown (element, checked) {
let rootElement
// if a <summary> is closer than a <details>, we're in a category summary and need to toggle all children
if (element.closest('details').contains(element.closest('summary'))) {
rootElement = element.closest('details')
}
if (rootElement) {
for (const checkbox of rootElement.querySelectorAll(
':scope > :not(summary) input[type=checkbox]'
)) {
checkbox.checked = checked
checkbox.indeterminate = false
}
}
// else we're in a leaf
this.traverseUp(element)
}
traverseUp (element) {
const closestDetails = element.closest('details')
const childCheckboxes = [
...closestDetails.querySelectorAll(
':scope > :not(summary) input[type=checkbox]'
)
]
const rootCheckbox = closestDetails.querySelector(
':scope > summary input[type=checkbox]'
)
if (childCheckboxes.every(element => element.checked)) {
rootCheckbox.checked = true
rootCheckbox.indeterminate = false
} else if (childCheckboxes.every(element => !element.checked)) {
rootCheckbox.checked = false
rootCheckbox.indeterminate = false
} else {
rootCheckbox.checked = false
rootCheckbox.indeterminate = true
}
const rootDetails =
element.tagName === 'DETAILS'
? element.parentElement.closest('details')
: element.closest('details')
if (rootDetails && rootDetails != element) this.traverseUp(rootDetails)
}
}
@KonnorRogers
Copy link

Huh! Pretty cool use of <details.> to make tree view filters!

on an unrelated note, I've been bitten by this before:

this.element.addEventListener('change', this.handleChange.bind(this))

Calling .bind creates a new function, so if an element connects multiple times, you will have multiple "handleChange" functions firing. Instead you can do one of the following so you have a reference to the newly created bound function:

initialize () {
  this._boundHandleChange = this.handleChange.bind(this)
}
connect () {
  this.element.addEventListener('change', this._boundHandleChange)
}

// OR

connect () {
  this.element.addEventListener('change', this.handleChange)
}

handleChange = (event) => {
  // ...
}

@shanlalit
Copy link

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment