diff --git a/packages/core/useTitle/index.md b/packages/core/useTitle/index.md index 23d71e40555..f124188852b 100644 --- a/packages/core/useTitle/index.md +++ b/packages/core/useTitle/index.md @@ -7,7 +7,8 @@ category: Browser Reactive document title. ::: tip -When using with Nuxt 3, this functions will **NOT** be auto imported in favor of Nuxt's built-in `useTitle()`. Use explicit import if you want to use the function from VueUse. +When using with Nuxt 3, this functions will **NOT** be auto imported in favor of Nuxt's built-in `useTitle()`. +Use explicit import if you want to use the function from VueUse. ::: ## Usage @@ -20,13 +21,13 @@ console.log(title.value) // print current title title.value = 'Hello' // change current title ``` -Set initial title immediately +Set initial title immediately: ```js const title = useTitle('New Title') ``` -Pass a `ref` and the title will be updated when the source ref changes +Pass a `ref` and the title will be updated when the source ref changes: ```js import { useTitle } from '@vueuse/core' @@ -40,9 +41,13 @@ const title = computed(() => { useTitle(title) // document title will match with the ref "title" ``` -Pass an optional template tag [Vue Meta Title Template](https://vue-meta.nuxtjs.org/guide/metainfo.html) -to update the title to be injected into this template: +Pass an optional template tag [Vue Meta Title Template](https://vue-meta.nuxtjs.org/guide/metainfo.html) to update the title to be injected into this template: ```js const title = useTitle('New Title', { titleTemplate: '%s | My Awesome Website' }) ``` + +::: warning +`observe` is incompatible with `titleTemplate`. +::: + diff --git a/packages/core/useTitle/index.test.ts b/packages/core/useTitle/index.test.ts new file mode 100644 index 00000000000..e5d5acae5e5 --- /dev/null +++ b/packages/core/useTitle/index.test.ts @@ -0,0 +1,87 @@ +import { computed, ref } from 'vue-demi' +import { useTitle } from '.' + +describe('useTitle', () => { + it('without param', () => { + const title = useTitle() + expect(title.value).toEqual('') + title.value = 'new title' + expect(title.value).toEqual('new title') + }) + + describe('with writable param', () => { + it('string', () => { + const title = useTitle('old title') + expect(title.value).toEqual('old title') + title.value = 'new title' + expect(title.value).toEqual('new title') + }) + + it('null', () => { + const title = useTitle(null) + expect(title.value).toEqual('') + title.value = 'new title' + expect(title.value).toEqual('new title') + }) + + it('undefined', () => { + const title = useTitle(undefined) + expect(title.value).toEqual('') + title.value = 'new title' + expect(title.value).toEqual('new title') + }) + + describe('ref param', () => { + it('string', () => { + const targetRef = ref('old title') + const title = useTitle(targetRef) + expect(title.value).toEqual('old title') + targetRef.value = 'new title' + expect(title.value).toEqual('new title') + title.value = 'latest title' + expect(title.value).toEqual('latest title') + }) + + it('null', () => { + const targetRef = ref(null) + const title = useTitle(targetRef) + expect(title.value).toEqual(null) + targetRef.value = 'new title' + expect(title.value).toEqual('new title') + title.value = 'latest title' + expect(title.value).toEqual('latest title') + }) + + it('undefined', () => { + const targetRef = ref(undefined) + const title = useTitle(targetRef) + expect(title.value).toEqual(undefined) + targetRef.value = 'new title' + expect(title.value).toEqual('new title') + title.value = 'latest title' + expect(title.value).toEqual('latest title') + }) + }) + }) + + describe('with readonly param', () => { + it('computed', () => { + const condition = ref(false) + const target = computed(() => condition.value ? 'new title' : 'old title') + const title = useTitle(target) + expect(title.value).toEqual('old title') + condition.value = true + expect(title.value).toEqual('new title') + // @ts-expect-error readonly + title.value = '' + }) + + it('function', () => { + const target = () => 'new title' + const title = useTitle(target) + expect(title.value).toEqual('new title') + // @ts-expect-error readonly + title.value = '' + }) + }) +}) diff --git a/packages/core/useTitle/index.ts b/packages/core/useTitle/index.ts index bc9c7f16519..4eefd27d90e 100644 --- a/packages/core/useTitle/index.ts +++ b/packages/core/useTitle/index.ts @@ -1,35 +1,42 @@ -import type { MaybeComputedRef, MaybeRef } from '@vueuse/shared' +import type { MaybeComputedRef, MaybeReadonlyRef, MaybeRef } from '@vueuse/shared' import { isFunction, isString, resolveRef } from '@vueuse/shared' -import type { ComputedRef, Ref } from 'vue-demi' +import type { ComputedRef, Ref, WritableComputedRef } from 'vue-demi' import { unref, watch } from 'vue-demi' +import { useMutationObserver } from '../useMutationObserver' import type { ConfigurableDocument } from '../_configurable' import { defaultDocument } from '../_configurable' -import { useMutationObserver } from '../useMutationObserver' -export interface UseTitleOptions extends ConfigurableDocument { +export type UseTitleOptionsBase = +{ /** * Observe `document.title` changes using MutationObserve + * Cannot be used together with `titleTemplate` option. * * @default false */ observe?: boolean +} +| { /** * The template string to parse the title (e.g., '%s | My Website') + * Cannot be used together with `observe` option. * * @default '%s' */ titleTemplate?: MaybeRef | ((title: string) => string) } +export type UseTitleOptions = ConfigurableDocument & UseTitleOptionsBase + export function useTitle( - newTitle?: MaybeRef, + newTitle: MaybeReadonlyRef, options?: UseTitleOptions, -): Ref +): ComputedRef export function useTitle( - newTitle?: MaybeComputedRef, + newTitle?: MaybeRef, options?: UseTitleOptions, -): ComputedRef +): Ref /** * Reactive document title. @@ -42,31 +49,37 @@ export function useTitle( newTitle: MaybeComputedRef = null, options: UseTitleOptions = {}, ) { + /* + `titleTemplate` that returns the modified input string will make + the `document.title` to be different from the `title.value`, + causing the title to update infinitely if `observe` is set to `true`. + */ const { document = defaultDocument, - observe = false, - titleTemplate = '%s', } = options - const title = resolveRef(newTitle ?? document?.title ?? null) as Ref + const title: WritableComputedRef = resolveRef(newTitle ?? document?.title ?? null) const isReadonly = newTitle && isFunction(newTitle) function format(t: string) { - return isFunction(titleTemplate) - ? titleTemplate(t) - : unref(titleTemplate).replace('%s', t) + if (!('titleTemplate' in options)) + return t + const template = options.titleTemplate || '%s' + return isFunction(template) + ? template(t) + : unref(template).replace(/%s/g, t) } watch( title, (t, o) => { - if (isString(t) && t !== o && document) - document.title = format(t) + if (t !== o && document) + document.title = format(isString(t) ? t : '') }, { immediate: true }, ) - if (observe && document && !isReadonly) { + if ((options as any).observe && !(options as any).titleTemplate && document && !isReadonly) { useMutationObserver( document.head?.querySelector('title'), () => {