From ee54ed7763c8431d5e4ad9f94ec1b94f2616efb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Levi=20=28Nguy=E1=BB=85n=20L=C6=B0=C6=A1ng=20Huy=29?= Date: Thu, 4 Aug 2022 11:29:07 +0530 Subject: [PATCH 1/7] fix(useTitle): improve types --- packages/core/useTitle/index.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/useTitle/index.ts b/packages/core/useTitle/index.ts index bc9c7f16519..ab05c477c75 100644 --- a/packages/core/useTitle/index.ts +++ b/packages/core/useTitle/index.ts @@ -1,10 +1,10 @@ -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 { /** @@ -22,14 +22,14 @@ export interface UseTitleOptions extends ConfigurableDocument { } 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. @@ -48,7 +48,7 @@ export function useTitle( 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) { From 120e3814bc868cf30005a3bfd9505725549b8793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Levi=20=28Nguy=E1=BB=85n=20L=C6=B0=C6=A1ng=20Huy=29?= Date: Fri, 5 Aug 2022 16:02:27 +0530 Subject: [PATCH 2/7] fix(useTitle): prevent title from being updated infinitely --- packages/core/useTitle/index.md | 27 ++++++++++++++++++++++----- packages/core/useTitle/index.ts | 14 ++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/core/useTitle/index.md b/packages/core/useTitle/index.md index 23d71e40555..ddb00ac5157 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,25 @@ 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 +When setting `observe` to `true`, the `titleTemplate` must return the exact same value as the input title. +Otherwise, the document title will not be updated. +::: + +```js +// this will work +const title = useTitle('New Title', { observe: true, titleTemplate: '%s' }) // default value +// this will work +const title = useTitle('New Title', { observe: true, titleTemplate: title => title }) + +// this won't work +const title = useTitle('New Title', { observe: true, titleTemplate: '%s - %s' }) +// this won't work +const title = useTitle('New Title', { observe: true, titleTemplate: title => `${title} - modified` }) +``` diff --git a/packages/core/useTitle/index.ts b/packages/core/useTitle/index.ts index ab05c477c75..b14e81e91ec 100644 --- a/packages/core/useTitle/index.ts +++ b/packages/core/useTitle/index.ts @@ -50,23 +50,29 @@ export function useTitle( const title: WritableComputedRef = resolveRef(newTitle ?? document?.title ?? null) const isReadonly = newTitle && isFunction(newTitle) + const hasModifiedTitle = isFunction(titleTemplate) ? titleTemplate('') !== '' : titleTemplate !== '%s' function format(t: string) { return isFunction(titleTemplate) ? titleTemplate(t) - : unref(titleTemplate).replace('%s', t) + : unref(titleTemplate).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) { + /* + `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. + therefore, `observe` should be ignored in this case. + */ + if (observe && document && !isReadonly && !hasModifiedTitle) { useMutationObserver( document.head?.querySelector('title'), () => { From 65bea57510f41cfe96be9f628507ff3ff269b56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Levi=20=28Nguy=E1=BB=85n=20L=C6=B0=C6=A1ng=20Huy=29?= Date: Fri, 26 Aug 2022 10:11:16 +0700 Subject: [PATCH 3/7] fix(useTitle): prevent using `observe` with `titleTemplate` --- packages/core/useTitle/index.md | 16 +++++++--------- packages/core/useTitle/index.ts | 20 ++++++++++++-------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/core/useTitle/index.md b/packages/core/useTitle/index.md index ddb00ac5157..ae6148f1981 100644 --- a/packages/core/useTitle/index.md +++ b/packages/core/useTitle/index.md @@ -48,18 +48,16 @@ const title = useTitle('New Title', { titleTemplate: '%s | My Awesome Website' } ``` ::: warning -When setting `observe` to `true`, the `titleTemplate` must return the exact same value as the input title. -Otherwise, the document title will not be updated. +`observe` is incompatible with `titleTemplate`. ::: ```js -// this will work -const title = useTitle('New Title', { observe: true, titleTemplate: '%s' }) // default value -// this will work -const title = useTitle('New Title', { observe: true, titleTemplate: title => title }) +/* ✅ Will work */ +const title = useTitle('New Title', { observe: true }) -// this won't work +/* ✅ Will work */ +const title = useTitle('New Title', { titleTemplate: '%s - %s' }) + +/* ❌ Will throw an error */ const title = useTitle('New Title', { observe: true, titleTemplate: '%s - %s' }) -// this won't work -const title = useTitle('New Title', { observe: true, titleTemplate: title => `${title} - modified` }) ``` diff --git a/packages/core/useTitle/index.ts b/packages/core/useTitle/index.ts index b14e81e91ec..39b6081fab2 100644 --- a/packages/core/useTitle/index.ts +++ b/packages/core/useTitle/index.ts @@ -9,12 +9,14 @@ import { defaultDocument } from '../_configurable' export interface UseTitleOptions extends ConfigurableDocument { /** * 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' */ @@ -22,7 +24,7 @@ export interface UseTitleOptions extends ConfigurableDocument { } export function useTitle( - newTitle?: MaybeReadonlyRef, + newTitle: MaybeReadonlyRef, options?: UseTitleOptions, ): ComputedRef @@ -42,6 +44,14 @@ 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`. + */ + if (options.observe && options.titleTemplate) + throw new Error('Cannot use `observe` and `titleTemplate` together.') + const { document = defaultDocument, observe = false, @@ -50,7 +60,6 @@ export function useTitle( const title: WritableComputedRef = resolveRef(newTitle ?? document?.title ?? null) const isReadonly = newTitle && isFunction(newTitle) - const hasModifiedTitle = isFunction(titleTemplate) ? titleTemplate('') !== '' : titleTemplate !== '%s' function format(t: string) { return isFunction(titleTemplate) @@ -67,12 +76,7 @@ export function useTitle( { immediate: true }, ) - /* - `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. - therefore, `observe` should be ignored in this case. - */ - if (observe && document && !isReadonly && !hasModifiedTitle) { + if (observe && document && !isReadonly) { useMutationObserver( document.head?.querySelector('title'), () => { From d78bb340a0ebe022af7f9055853751ed7b10a4e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Levi=20=28Nguy=E1=BB=85n=20L=C6=B0=C6=A1ng=20Huy=29?= Date: Fri, 26 Aug 2022 10:12:28 +0700 Subject: [PATCH 4/7] test(useTitle): add unit tests --- packages/core/useTitle/index.test.ts | 91 ++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 packages/core/useTitle/index.test.ts diff --git a/packages/core/useTitle/index.test.ts b/packages/core/useTitle/index.test.ts new file mode 100644 index 00000000000..6f9e22ef51b --- /dev/null +++ b/packages/core/useTitle/index.test.ts @@ -0,0 +1,91 @@ +import { computed, ref } from 'vue-demi' +import { useTitle } from '.' + +describe('useTitle', () => { + it('throws error when using both `observe` and `titleTemplate`', () => { + expect(() => { + useTitle(null, { observe: true, titleTemplate: '%s' }) + }).toThrowErrorMatchingInlineSnapshot('"Cannot use `observe` and `titleTemplate` together."') + }) + + 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') + // title.value = '' // typed error + }) + + it('function', () => { + const target = () => 'new title' + const title = useTitle(target) + expect(title.value).toEqual('new title') + // title.value = '' // typed error + }) + }) +}) From cda76580492f4101b13aef6ef1a122fbcea4e4af Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 17 Oct 2022 07:02:48 +0800 Subject: [PATCH 5/7] chore: update --- packages/core/useTitle/index.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/core/useTitle/index.ts b/packages/core/useTitle/index.ts index 39b6081fab2..4eefd27d90e 100644 --- a/packages/core/useTitle/index.ts +++ b/packages/core/useTitle/index.ts @@ -6,7 +6,8 @@ import { useMutationObserver } from '../useMutationObserver' import type { ConfigurableDocument } from '../_configurable' import { defaultDocument } from '../_configurable' -export interface UseTitleOptions extends ConfigurableDocument { +export type UseTitleOptionsBase = +{ /** * Observe `document.title` changes using MutationObserve * Cannot be used together with `titleTemplate` option. @@ -14,6 +15,8 @@ export interface UseTitleOptions extends ConfigurableDocument { * @default false */ observe?: boolean +} +| { /** * The template string to parse the title (e.g., '%s | My Website') * Cannot be used together with `observe` option. @@ -23,6 +26,8 @@ export interface UseTitleOptions extends ConfigurableDocument { titleTemplate?: MaybeRef | ((title: string) => string) } +export type UseTitleOptions = ConfigurableDocument & UseTitleOptionsBase + export function useTitle( newTitle: MaybeReadonlyRef, options?: UseTitleOptions, @@ -49,22 +54,20 @@ export function useTitle( the `document.title` to be different from the `title.value`, causing the title to update infinitely if `observe` is set to `true`. */ - if (options.observe && options.titleTemplate) - throw new Error('Cannot use `observe` and `titleTemplate` together.') - const { document = defaultDocument, - observe = false, - titleTemplate = '%s', } = options 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/g, t) + if (!('titleTemplate' in options)) + return t + const template = options.titleTemplate || '%s' + return isFunction(template) + ? template(t) + : unref(template).replace(/%s/g, t) } watch( @@ -76,7 +79,7 @@ export function useTitle( { immediate: true }, ) - if (observe && document && !isReadonly) { + if ((options as any).observe && !(options as any).titleTemplate && document && !isReadonly) { useMutationObserver( document.head?.querySelector('title'), () => { From d9b6131b47716241f0edd5960b9604b13d8144df Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 17 Oct 2022 07:05:39 +0800 Subject: [PATCH 6/7] Update packages/core/useTitle/index.md --- packages/core/useTitle/index.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/core/useTitle/index.md b/packages/core/useTitle/index.md index ae6148f1981..f124188852b 100644 --- a/packages/core/useTitle/index.md +++ b/packages/core/useTitle/index.md @@ -51,13 +51,3 @@ const title = useTitle('New Title', { titleTemplate: '%s | My Awesome Website' } `observe` is incompatible with `titleTemplate`. ::: -```js -/* ✅ Will work */ -const title = useTitle('New Title', { observe: true }) - -/* ✅ Will work */ -const title = useTitle('New Title', { titleTemplate: '%s - %s' }) - -/* ❌ Will throw an error */ -const title = useTitle('New Title', { observe: true, titleTemplate: '%s - %s' }) -``` From 34ad2a298661c3c9ad8c863089654e2a936ecf01 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 17 Oct 2022 07:20:37 +0800 Subject: [PATCH 7/7] chore: fix tests --- packages/core/useTitle/index.test.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/core/useTitle/index.test.ts b/packages/core/useTitle/index.test.ts index 6f9e22ef51b..e5d5acae5e5 100644 --- a/packages/core/useTitle/index.test.ts +++ b/packages/core/useTitle/index.test.ts @@ -2,12 +2,6 @@ import { computed, ref } from 'vue-demi' import { useTitle } from '.' describe('useTitle', () => { - it('throws error when using both `observe` and `titleTemplate`', () => { - expect(() => { - useTitle(null, { observe: true, titleTemplate: '%s' }) - }).toThrowErrorMatchingInlineSnapshot('"Cannot use `observe` and `titleTemplate` together."') - }) - it('without param', () => { const title = useTitle() expect(title.value).toEqual('') @@ -78,14 +72,16 @@ describe('useTitle', () => { expect(title.value).toEqual('old title') condition.value = true expect(title.value).toEqual('new title') - // title.value = '' // typed error + // @ts-expect-error readonly + title.value = '' }) it('function', () => { const target = () => 'new title' const title = useTitle(target) expect(title.value).toEqual('new title') - // title.value = '' // typed error + // @ts-expect-error readonly + title.value = '' }) }) })