Skip to content

Commit

Permalink
feat(ui): new composable - useId() #16792
Browse files Browse the repository at this point in the history
  • Loading branch information
rstoenescu committed Mar 11, 2024
1 parent 05ec5b6 commit 072482b
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 46 deletions.
5 changes: 5 additions & 0 deletions docs/src/assets/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,11 @@ export default [
badge: 'new',
path: 'use-hydration'
},
{
name: 'useId',
badge: 'new',
path: 'use-id'
},
{
name: 'useRenderCache',
badge: 'new',
Expand Down
63 changes: 63 additions & 0 deletions docs/src/pages/vue-composables/use-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: useId composable
desc: What is useId() composable and how you can use it
keys: useId
badge: Quasar v2.15+
---

The `useId()` composable returns an "id" which is a ref() that can be used as a unique identifier to apply to a DOM node attribute.

Should you supply a function (`getValue` from the typing below) to get the value that the id might have, it will make sure to keep it updated.

On SSR, it takes into account the process of hydration so that your component won't generate any such errors.

## Syntax

```js
import { useId } from 'quasar'

setup () {
const { id } = useId()
// ...
}
```

```js
function useId(
opts?: {
getValue?: () => string | null | undefined,
required?: boolean // default: true
}
): {
id: Ref<string>;
};
```

## Example

```html
<template>
<div :id="id">
Some component
</div>
</template>

<script>
import { useId } from 'quasar'
export default {
props: {
for: String
},
setup () {
const { id } = useId({
getValue: () => props.for,
required: true
})
return { id }
}
}
</script>
```
2 changes: 1 addition & 1 deletion ui/src/components/btn-dropdown/QBtnDropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import QBtnGroup from '../btn-group/QBtnGroup.js'
import QMenu from '../menu/QMenu.js'

import { getBtnDesignAttr, useBtnProps } from '../btn/use-btn.js'
import useId from '../../composables/private/use-id.js'
import useId from '../../composables/use-id.js'
import { useTransitionProps } from '../../composables/private/use-transition.js'

import { createComponent } from '../../utils/private/create.js'
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/expansion-item/QExpansionItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import QSlideTransition from '../slide-transition/QSlideTransition.js'
import QSeparator from '../separator/QSeparator.js'

import useDark, { useDarkProps } from '../../composables/private/use-dark.js'
import useId from '../../composables/private/use-id.js'
import useId from '../../composables/use-id.js'
import { useRouterLinkProps } from '../../composables/private/use-router-link.js'
import useModelToggle, { useModelToggleProps, useModelToggleEmits } from '../../composables/private/use-model-toggle.js'

Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/fab/QFab.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import QBtn from '../btn/QBtn.js'
import QIcon from '../icon/QIcon.js'

import useFab, { useFabProps } from './use-fab.js'
import useId from '../../composables/private/use-id.js'
import useId from '../../composables/use-id.js'
import useModelToggle, { useModelToggleProps, useModelToggleEmits } from '../../composables/private/use-model-toggle.js'

import { createComponent } from '../../utils/private/create.js'
Expand Down
2 changes: 2 additions & 0 deletions ui/src/composables.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import useMeta from './composables/use-meta.js'
import useQuasar from './composables/use-quasar.js'

import useHydration from './composables/use-hydration.js'
import useId from './composables/use-id.js'
import useRenderCache from './composables/use-render-cache.js'
import useSplitAttrs from './composables/use-split-attrs.js'
import useTick from './composables/use-tick.js'
Expand All @@ -16,6 +17,7 @@ export {
useQuasar,

useHydration,
useId,
useRenderCache,
useSplitAttrs,
useTick,
Expand Down
15 changes: 6 additions & 9 deletions ui/src/composables/private/use-field.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { h, ref, computed, watch, Transition, nextTick, onActivated, onDeactivated, onBeforeUnmount, onMounted, getCurrentInstance } from 'vue'
import { h, ref, computed, Transition, nextTick, onActivated, onDeactivated, onBeforeUnmount, onMounted, getCurrentInstance } from 'vue'

import QIcon from '../../components/icon/QIcon.js'
import QSpinner from '../../components/spinner/QSpinner.js'

import useDark, { useDarkProps } from '../../composables/private/use-dark.js'
import useId, { getId } from './use-id.js'
import useId from '../use-id.js'
import useValidate, { useValidateProps } from './use-validate.js'
import useSplitAttrs from '../use-split-attrs.js'

Expand Down Expand Up @@ -72,7 +72,10 @@ export function useFieldState ({ requiredForAttr = true, tagProp } = {}) {
const { props, proxy } = getCurrentInstance()

const isDark = useDark(props, proxy.$q)
const targetUid = useId(props.for, requiredForAttr)
const targetUid = useId({
required: requiredForAttr,
getValue: () => props.for
})

return {
requiredForAttr,
Expand Down Expand Up @@ -256,12 +259,6 @@ export default function (state) {
return acc
})

watch(() => props.for, val => {
// don't transform targetUid into a computed
// prop as it will break SSR
state.targetUid.value = getId(val, state.requiredForAttr)
})

function focusHandler () {
const el = document.activeElement
let target = state.targetRef !== void 0 && state.targetRef.value
Expand Down
34 changes: 0 additions & 34 deletions ui/src/composables/private/use-id.js

This file was deleted.

50 changes: 50 additions & 0 deletions ui/src/composables/use-id.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ref, computed, watch, onMounted } from 'vue'

import uid from '../utils/uid.js'

import { isRuntimeSsrPreHydration } from '../plugins/Platform.js'

function parseValue (val) {
return val === void 0 || val === null
? null
: val
}

function getId (val, required) {
return val === void 0 || val === null
? (required === true ? `f_${ uid() }` : null)
: val
}

/**
* Returns an "id" which is a ref() that can be used as
* a unique identifier to apply to a DOM node attribute.
*
* On SSR, it takes care of generating the id on the client side (only) to
* avoid hydration errors.
*/
export default function ({ getValue, required = true } = {}) {
if (isRuntimeSsrPreHydration.value === true) {
const id = getValue !== void 0
? ref(parseValue(getValue()))
: ref(null)

if (required === true && id.value === null) {
onMounted(() => {
id.value = `f_${ uid() }` // getId(null, true)
})
}

if (getValue !== void 0) {
watch(getValue, newId => {
id.value = getId(newId, required)
})
}

return id
}

return getValue !== void 0
? computed(() => getId(getValue(), required))
: ref(`f_${ uid() }`) // getId(null, true)
}
7 changes: 7 additions & 0 deletions ui/types/composables.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ export function useHydration(): {
isHydrated: Ref<boolean>;
};

export function useId(opts?: {
getValue?: () => string | null | undefined;
required?: boolean;
}): {
id: Ref<string>;
};

export function useMeta(options: MetaOptions | (() => MetaOptions)): void;

export function useQuasar(): QVueGlobals;
Expand Down

0 comments on commit 072482b

Please sign in to comment.