From df7b088bdcc855b5b5554949cb0794931cd47de3 Mon Sep 17 00:00:00 2001 From: reubns <36566445+reubns@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:51:36 +0100 Subject: [PATCH 1/4] feat(utils): inject getId to allow for custom id generation --- .../src/utils/exports/index.ts | 2 +- .../bootstrap-vue-next/src/utils/getId.ts | 19 +++++- .../bootstrap-vue-next/src/utils/index.ts | 2 +- .../tests/composables/useId.spec.ts | 21 ++++--- packages/bootstrap-vue-next/tests/utils.ts | 13 ++++ .../tests/utils/getId.spec.ts | 31 +++++++--- .../tests/utils/provideId.spec.ts | 61 +++++++++++++++++++ packages/nuxt/src/module.ts | 6 ++ 8 files changed, 134 insertions(+), 21 deletions(-) create mode 100644 packages/bootstrap-vue-next/tests/utils/provideId.spec.ts diff --git a/packages/bootstrap-vue-next/src/utils/exports/index.ts b/packages/bootstrap-vue-next/src/utils/exports/index.ts index 838e908d8..8393b1399 100644 --- a/packages/bootstrap-vue-next/src/utils/exports/index.ts +++ b/packages/bootstrap-vue-next/src/utils/exports/index.ts @@ -1 +1 @@ -export {BvEvent, BvTriggerableEvent, BvCarouselEvent} from '..' +export {BvEvent, BvTriggerableEvent, BvCarouselEvent, provideGetId} from '..' diff --git a/packages/bootstrap-vue-next/src/utils/getId.ts b/packages/bootstrap-vue-next/src/utils/getId.ts index 0b85fd498..b4b06ad2c 100644 --- a/packages/bootstrap-vue-next/src/utils/getId.ts +++ b/packages/bootstrap-vue-next/src/utils/getId.ts @@ -1,2 +1,17 @@ -export default (suffix = ''): string => - `__BVID__${Math.random().toString().slice(2, 8)}___BV_${suffix}__` +import {inject, type InjectionKey, provide} from 'vue' + +const GET_ID: InjectionKey<() => string> = Symbol('bootstrap-vue-next.getId') + +export default (suffix = '') => { + const getId = inject(GET_ID, () => Math.random().toString().slice(2, 8)) + return `__BVID__${getId()}___BV_${suffix}__` +} + +/** + * 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 users of Nuxt swap in the Nuxt `useId` function + * which is stable across SSR and client. + */ +export const provideGetId = (fn: () => string) => provide(GET_ID, fn) diff --git a/packages/bootstrap-vue-next/src/utils/index.ts b/packages/bootstrap-vue-next/src/utils/index.ts index 9ed5acb53..b51c8c85c 100644 --- a/packages/bootstrap-vue-next/src/utils/index.ts +++ b/packages/bootstrap-vue-next/src/utils/index.ts @@ -7,7 +7,7 @@ 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 getId, provideGetId} from './getId' export {default as getSlotElements} from './getSlotElements' export {default as getTableFieldHeadLabel} from './getTableFieldHeadLabel' export {default as isLink} from './isLink' diff --git a/packages/bootstrap-vue-next/tests/composables/useId.spec.ts b/packages/bootstrap-vue-next/tests/composables/useId.spec.ts index 4337b89ef..998e964e7 100644 --- a/packages/bootstrap-vue-next/tests/composables/useId.spec.ts +++ b/packages/bootstrap-vue-next/tests/composables/useId.spec.ts @@ -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') + }) }) }) diff --git a/packages/bootstrap-vue-next/tests/utils.ts b/packages/bootstrap-vue-next/tests/utils.ts index a6bf0325d..eef668bdb 100644 --- a/packages/bootstrap-vue-next/tests/utils.ts +++ b/packages/bootstrap-vue-next/tests/utils.ts @@ -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) @@ -7,3 +10,13 @@ export const waitRAF = (): Promise => new Promise((resolve) => requestAnimationFrame(resolve)) export const asyncTimeout = (timeout: number): Promise => new Promise((resolve) => setTimeout(resolve.bind(null), timeout)) +export const useSetup = (setup: () => V) => { + const Comp = defineComponent({ + setup, + render() { + return h('div') + }, + }) + + return mount(Comp) +} diff --git a/packages/bootstrap-vue-next/tests/utils/getId.spec.ts b/packages/bootstrap-vue-next/tests/utils/getId.spec.ts index 2c54565c3..d23c2a2e0 100644 --- a/packages/bootstrap-vue-next/tests/utils/getId.spec.ts +++ b/packages/bootstrap-vue-next/tests/utils/getId.spec.ts @@ -1,29 +1,40 @@ +import {useSetup} from '../../tests/utils' import {getId} from '../../src/utils' import {describe, expect, it} from 'vitest' describe('getId', () => { it('returns something', () => { - const value = getId() - expect(value).toBeDefined() + useSetup(() => { + const value = getId() + expect(value).toBeDefined() + }) }) it('returns a string', () => { - const value = getId() - expect(typeof value === 'string').toBe(true) + useSetup(() => { + const value = getId() + expect(typeof value === 'string').toBe(true) + }) }) it('string contains __BVID__', () => { - const value = getId() - expect(value).toContain('__BVID__') + useSetup(() => { + const value = getId() + expect(value).toContain('__BVID__') + }) }) it('string contains ___BV_{suffix}__ when suffix is defined', () => { - const value = getId('foobar') - expect(value).toContain('___BV_foobar__') + useSetup(() => { + const value = getId('foobar') + expect(value).toContain('___BV_foobar__') + }) }) it('string contains ___BV___ when not suffix', () => { - const value = getId() - expect(value).toContain('___BV___') + useSetup(() => { + const value = getId() + expect(value).toContain('___BV___') + }) }) }) diff --git a/packages/bootstrap-vue-next/tests/utils/provideId.spec.ts b/packages/bootstrap-vue-next/tests/utils/provideId.spec.ts new file mode 100644 index 000000000..609892a25 --- /dev/null +++ b/packages/bootstrap-vue-next/tests/utils/provideId.spec.ts @@ -0,0 +1,61 @@ +/* eslint-disable vue/one-component-per-file */ +import {mount} from '@vue/test-utils' +import getId, {provideGetId} from '../../src/utils/getId' +import {describe, expect, it} from 'vitest' +import {defineComponent, h} from 'vue' + +export function useSetupWithProvideGetId(setup: () => V) { + const Comp = defineComponent({ + setup, + render() { + return h('div') + }, + }) + + const Provider = defineComponent({ + components: {Comp}, + setup() { + provideGetId(() => `${Math.random().toString().slice(2, 8)}__PROVIDED__`) + }, + template: '', + }) + + 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___') + }) + }) +}) diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index a43479200..8b3c1b66d 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -86,5 +86,11 @@ export default defineNuxtModule({ }) ) } + + // Add workaround for the lack of stable SSR IDs in Vue 3.x + addImports({ + from: 'bootstrap-vue-next', + name: 'provideGetId', + }) }, }) From 9326d3bced9e8c8debef12328c5081e0580436d8 Mon Sep 17 00:00:00 2001 From: reubns <36566445+reubns@users.noreply.github.com> Date: Thu, 4 Apr 2024 14:29:20 +0100 Subject: [PATCH 2/4] refactor(useId): use plugin for injecting id generator function --- .../bootstrap-vue-next/src/BootstrapVue.ts | 4 ++ .../src/components/BFormGroup/BFormGroup.vue | 23 +++++----- .../src/composables/index.ts | 2 +- .../src/composables/useId.ts | 9 +++- .../src/plugins/idPlugin.ts | 11 +++++ .../src/types/BootstrapVueOptions.ts | 17 +++++++ .../src/utils/exports/index.ts | 2 +- .../bootstrap-vue-next/src/utils/getId.ts | 17 ------- .../bootstrap-vue-next/src/utils/index.ts | 1 - packages/bootstrap-vue-next/src/utils/keys.ts | 2 + .../idPlugin.spec.ts} | 45 +++++++++++++++++-- .../tests/utils/getId.spec.ts | 40 ----------------- packages/nuxt/src/module.ts | 6 --- packages/nuxt/src/runtime/createBootstrap.ts | 7 ++- 14 files changed, 102 insertions(+), 84 deletions(-) create mode 100644 packages/bootstrap-vue-next/src/plugins/idPlugin.ts delete mode 100644 packages/bootstrap-vue-next/src/utils/getId.ts rename packages/bootstrap-vue-next/tests/{utils/provideId.spec.ts => composables/idPlugin.spec.ts} (56%) delete mode 100644 packages/bootstrap-vue-next/tests/utils/getId.spec.ts diff --git a/packages/bootstrap-vue-next/src/BootstrapVue.ts b/packages/bootstrap-vue-next/src/BootstrapVue.ts index 53fb2a941..051fff80f 100644 --- a/packages/bootstrap-vue-next/src/BootstrapVue.ts +++ b/packages/bootstrap-vue-next/src/BootstrapVue.ts @@ -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' @@ -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) } diff --git a/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue b/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue index b99f7134a..2847938e2 100644 --- a/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue +++ b/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue @@ -1,14 +1,7 @@