Skip to content

Instantly share code, notes, and snippets.

@loilo
Last active March 13, 2024 20:50
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save loilo/1261d239278b22f10e9d3dad66b77602 to your computer and use it in GitHub Desktop.
Save loilo/1261d239278b22f10e9d3dad66b77602 to your computer and use it in GitHub Desktop.
Vue useQuerySelector composable
import { readonly, ref, watch } from 'vue'
import { useMutationObserver } from '@vueuse/core'
export function useQuerySelector(selector, { root = document } = {}) {
selector = ref(selector)
root = ref(root)
const result = ref(null)
// Find first matching element inside a root element
const findElement = root => root.querySelector(selector.value)
// Check if a given element matches the selector
const matches = element => element.matches(selector.value)
// Check if a given element precedes the current result
const precedes = otherElement =>
Boolean(
!result.value ||
result.value.compareDocumentPosition(otherElement) &
Node.DOCUMENT_POSITION_PRECEDING
)
// Reset the result whenever the selector or the root node changes
watch(
[selector, root],
() => {
result.value = findElement(root.value)
},
{ immediate: true }
)
const observer = useMutationObserver(
root,
mutations => {
for (const mutation of mutations) {
if (mutation.type == 'childList') {
// Handle removed elements
if (result.value) {
for (const removedNode of mutation.removedNodes) {
if (removedNode.contains(result)) {
// If element is removed, a new search is started to find the first matching element
result.value = findElement(root.value)
break
}
}
}
// Handle added elements
for (const addedNode of mutation.addedNodes) {
if (addedNode.nodeType !== Node.ELEMENT_NODE) continue
// If we have an existing result and the newly added element is not preceding it, we can safely ignore it
if (!precedes(addedNode)) continue
// Check the added element itself
if (matches(addedNode)) {
result.value = addedNode
} else {
result.value = findElement(addedNode)
}
}
} else if (mutation.type === 'attributes') {
// Handle changed attributes
if (result.value === mutation.target) {
if (!matches(mutation.target)) {
result.value = findElement(root.value)
}
} else if (matches(mutation.target) && precedes(mutation.target)) {
result.value = mutation.target
}
}
}
},
{
childList: true,
subtree: true,
attributes: true
}
)
return {
stop: observer.stop,
isSupported: observer.isSupported,
element: readonly(result)
}
}

useQuerySelector

A Vue composable which finds the first element matched by a selector, using useMutationObserver (i.e. requires the @vueuse/core package).

While using template refs is the canonical way to access elements in Vue, there may be situations (e.g. when you wrap non-Vue code you have no control over) where using raw DOM access may be needed.

Example

import { watchEffect } from 'vue'
import { useQuerySelector } from './use-query-selector.js'

const { element } = useQuerySelector('.my-selector')

watchEffect(() => {
  console.log('First element matching .my-selector: %o', element.value)
})

Caveats

  • Combinators are not supported in selectors (as they greatly reduce the performance of the MutationObserver).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment