Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(utils): inject getId to allow for custom id generation #1836

Merged
merged 4 commits into from
Apr 14, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/bootstrap-vue-next/src/BootstrapVue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {Plugin} from 'vue'
import type {BootstrapVueOptions, ComponentType, DirectiveType} from './types'
import toastPlugin from './plugins/toastPlugin'
import breadcrumbPlugin from './plugins/breadcrumbPlugin'
import idPlugin from './plugins/idPlugin'
import modalControllerPlugin from './plugins/modalControllerPlugin'
import modalManagerPlugin from './plugins/modalManagerPlugin'
import rtlPlugin from './plugins/rtlPlugin'
Expand Down Expand Up @@ -146,6 +147,9 @@ export const createBootstrap = ({
if (plugins?.breadcrumb ?? true === true) {
app.use(breadcrumbPlugin)
}
if ((plugins?.id ?? true === true) || typeof plugins.id === 'object') {
app.use(idPlugin, plugins)
}
if (plugins?.modalController ?? true === true) {
app.use(modalControllerPlugin)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
<script lang="ts">
import {useAriaInvalid, useId, useStateClass} from '../../composables'
import {RX_SPACE_SPLIT} from '../../constants/regex'
import {
attemptFocus,
getId,
IS_BROWSER,
isVisible,
normalizeSlot,
suffixPropName,
} from '../../utils'
import {attemptFocus, IS_BROWSER, isVisible, normalizeSlot, suffixPropName} from '../../utils'
import {computed, defineComponent, h, nextTick, onMounted, type PropType, ref, watch} from 'vue'
import BCol from '../BCol.vue'
import BFormInvalidFeedback from '../BForm/BFormInvalidFeedback.vue'
Expand Down Expand Up @@ -224,7 +217,7 @@ export default defineComponent({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let $label: any = null
const labelContent = normalizeSlot(SLOT_NAME_LABEL, {}, slots) || props.label
const labelId = labelContent ? getId('_BV_label_') : null
const labelId = labelContent ? useId(undefined, '_BV_label_').value : null
VividLemon marked this conversation as resolved.
Show resolved Hide resolved

if (labelContent || this.isHorizontal) {
const labelTag: 'legend' | 'label' = isFieldset ? 'legend' : 'label'
Expand Down Expand Up @@ -277,7 +270,9 @@ export default defineComponent({
let $invalidFeedback = null
const invalidFeedbackContent =
normalizeSlot(SLOT_NAME_INVALID_FEEDBACK, {}, slots) || this.invalidFeedback
const invalidFeedbackId = invalidFeedbackContent ? getId('_BV_feedback_invalid_') : undefined
const invalidFeedbackId = invalidFeedbackContent
? useId(undefined, '_BV_feedback_invalid_').value
: undefined

if (invalidFeedbackContent) {
$invalidFeedback = h(
Expand All @@ -295,7 +290,9 @@ export default defineComponent({
let $validFeedback = null
const validFeedbackContent =
normalizeSlot(SLOT_NAME_VALID_FEEDBACK, {}, slots) || this.validFeedback
const validFeedbackId = validFeedbackContent ? getId('_BV_feedback_valid_') : undefined
const validFeedbackId = validFeedbackContent
? useId(undefined, '_BV_feedback_valid_').value
: undefined

if (validFeedbackContent) {
$validFeedback = h(
Expand All @@ -313,7 +310,9 @@ export default defineComponent({

let $description = null
const descriptionContent = normalizeSlot(SLOT_NAME_DESCRIPTION, {}, slots) || this.description
const descriptionId = descriptionContent ? getId('_BV_description_') : undefined
const descriptionId = descriptionContent
? useId(undefined, '_BV_description_').value
: undefined
if (descriptionContent) {
$description = h(
BFormText,
Expand Down
2 changes: 1 addition & 1 deletion packages/bootstrap-vue-next/src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export {
} from './useFormCheck'
export {default as useFormInput} from './useFormInput'
export {default as useFormSelect} from './useFormSelect'
export {default as useId} from './useId'
export {default as useId, getId} from './useId'
export {default as useManualTransition} from './useManualTransition'
export {default as useModal} from './useModal'
export {default as useModalController} from './useModalController'
Expand Down
9 changes: 7 additions & 2 deletions packages/bootstrap-vue-next/src/composables/useId.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {getId} from '../utils'
import {computed, type ComputedRef, type MaybeRefOrGetter, toValue} from 'vue'
import {idPluginKey} from '../utils'
import {computed, type ComputedRef, inject, type MaybeRefOrGetter, toValue} from 'vue'

export default (id?: MaybeRefOrGetter<string | undefined>, suffix?: string): ComputedRef<string> =>
computed(() => toValue(id) || getId(suffix))

export const getId = (suffix = '') => {
const getId = inject(idPluginKey, () => Math.random().toString().slice(2, 8))
return `__BVID__${getId()}___BV_${suffix}__`
}
11 changes: 11 additions & 0 deletions packages/bootstrap-vue-next/src/plugins/idPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {type Plugin} from 'vue'
import type {BootstrapVueOptions} from '../types'
import {idPluginKey} from '../utils'

export default {
install(app, options: BootstrapVueOptions['plugins']) {
if (options?.id instanceof Object && typeof options.id.getId === 'function') {
app.provide(idPluginKey, options.id.getId)
}
},
} satisfies Plugin
17 changes: 17 additions & 0 deletions packages/bootstrap-vue-next/src/types/BootstrapVueOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,23 @@ export interface BootstrapVueOptions {
* @default true
*/
breadcrumb?: boolean
/**
* @default true
*/
id?:
| boolean
/**
* This function is allows users to provide a custom id generator
* as a workaround for the lack of stable SSR IDs in Vue 3.x
*
* This lets the Nuxt module swap in the Nuxt `useId` function
* which is stable across SSR and client.
*
* @default undefined
*/
| {
getId?: () => string
}
/**
* @default true
*/
Expand Down
2 changes: 0 additions & 2 deletions packages/bootstrap-vue-next/src/utils/getId.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/bootstrap-vue-next/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export {default as formatItem} from './formatItem'
export {default as getBreakpointProps} from './getBreakpointProps'
export {default as getClasses} from './getClasses'
export {default as getElement} from './getElement'
export {default as getId} from './getId'
export {default as getSlotElements} from './getSlotElements'
export {default as getTableFieldHeadLabel} from './getTableFieldHeadLabel'
export {default as isLink} from './isLink'
Expand Down
2 changes: 2 additions & 0 deletions packages/bootstrap-vue-next/src/utils/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,5 @@ export const modalManagerPluginKey: InjectionKey<{
pushRegistry: (modal: Readonly<ComponentInternalInstance>) => void
removeRegistry: (modal: Readonly<ComponentInternalInstance>) => void
}> = Symbol('modalManagerPlugin')

export const idPluginKey: InjectionKey<() => string> = Symbol('idPluginKey')
100 changes: 100 additions & 0 deletions packages/bootstrap-vue-next/tests/composables/idPlugin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/* eslint-disable vue/one-component-per-file */
import {mount} from '@vue/test-utils'
import {getId} from '../../src/composables'
import {describe, expect, it} from 'vitest'
import {defineComponent, h, provide} from 'vue'
import {useSetup} from '../utils'
import {idPluginKey} from '../../src/utils'

describe('getId', () => {
it('returns something', () => {
useSetup(() => {
const value = getId()
expect(value).toBeDefined()
})
})

it('returns a string', () => {
useSetup(() => {
const value = getId()
expect(typeof value === 'string').toBe(true)
})
})

it('string contains __BVID__', () => {
useSetup(() => {
const value = getId()
expect(value).toContain('__BVID__')
})
})

it('string contains ___BV_{suffix}__ when suffix is defined', () => {
useSetup(() => {
const value = getId('foobar')
expect(value).toContain('___BV_foobar__')
})
})

it('string contains ___BV___ when not suffix', () => {
useSetup(() => {
const value = getId()
expect(value).toContain('___BV___')
})
})
})

export function useSetupWithProvideGetId<V>(setup: () => V) {
const Comp = defineComponent({
setup,
render() {
return h('div')
},
})

const Provider = defineComponent({
components: {Comp},
setup() {
provide(idPluginKey, () => `${Math.random().toString().slice(2, 8)}__PROVIDED__`)
},
template: '<Comp />',
})

return mount(Provider, {slots: {default: Comp}})
}

describe('provideGetId', () => {
it('returns something', () => {
useSetupWithProvideGetId(() => {
const value = getId()
expect(value).toBeDefined()
})
})

it('returns a string', () => {
useSetupWithProvideGetId(() => {
const value = getId()
expect(typeof value === 'string').toBe(true)
})
})

it('string contains __PROVIDED__', () => {
useSetupWithProvideGetId(() => {
const value = getId()
expect(value).toContain('__PROVIDED__')
})
})

it('string contains __PROVIDED_____BV_{suffix}__ when suffix is defined', () => {
useSetupWithProvideGetId(() => {
const value = getId('foobar')
expect(value).toContain('__PROVIDED_____BV_foobar__')
})
})

it('string contains __PROVIDED_____BV___ when not suffix', () => {
useSetupWithProvideGetId(() => {
const value = getId()
expect(value).toContain('__PROVIDED_____BV___')
})
})
})
21 changes: 14 additions & 7 deletions packages/bootstrap-vue-next/tests/composables/useId.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import {useSetup} from '../../tests/utils'
import {useId} from '../../src/composables'
import {describe, expect, it} from 'vitest'
import {reactive} from 'vue'

describe('useId blackbox test', () => {
it('returns id value when id is defined', () => {
const props = reactive({id: 'foo'})
const value = useId(() => props.id)
expect(value.value).toBe('foo')
useSetup(() => {
const props = reactive({id: 'foo'})
const value = useId(() => props.id)
expect(value.value).toBe('foo')
})
})

it('returns something when id is undefined', () => {
const value = useId()
expect(value.value).toBeDefined()
useSetup(() => {
const value = useId()
expect(value.value).toBeDefined()
})
})

it('something returned when undefined contains suffix when suffix', () => {
const value = useId(undefined, 'foobar')
expect(value.value).toContain('foobar')
useSetup(() => {
const value = useId(undefined, 'foobar')
expect(value.value).toContain('foobar')
})
})
})
13 changes: 13 additions & 0 deletions packages/bootstrap-vue-next/tests/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import {defineComponent, h} from 'vue'
import {mount} from '@vue/test-utils'

export const createContainer = (tag = 'div'): HTMLElement => {
const container = document.createElement(tag)
document.body.appendChild(container)
Expand All @@ -7,3 +10,13 @@ export const waitRAF = (): Promise<number> =>
new Promise((resolve) => requestAnimationFrame(resolve))
export const asyncTimeout = (timeout: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve.bind(null), timeout))
export const useSetup = <V>(setup: () => V) => {
const Comp = defineComponent({
setup,
render() {
return h('div')
},
})

return mount(Comp)
}
29 changes: 0 additions & 29 deletions packages/bootstrap-vue-next/tests/utils/getId.spec.ts

This file was deleted.

7 changes: 6 additions & 1 deletion packages/nuxt/src/runtime/createBootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import {createBootstrap} from 'bootstrap-vue-next'
import {defineNuxtPlugin} from '#imports'
import {defineNuxtPlugin, useId} from '#imports'

export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(
createBootstrap({
components: false,
directives: false,
plugins: {
id: {
getId: () => useId().replace(':', '_'),
reubns marked this conversation as resolved.
Show resolved Hide resolved
},
},
})
)
})