Skip to content

Commit

Permalink
fix(useTitle): prevent observe and titleTemplate been specified a…
Browse files Browse the repository at this point in the history
…t the same time (#2049)

Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
huynl-96 and antfu committed Oct 16, 2022
1 parent feaa195 commit 8c1ba50
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 22 deletions.
15 changes: 10 additions & 5 deletions packages/core/useTitle/index.md
Expand Up @@ -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
Expand All @@ -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'
Expand All @@ -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`.
:::

87 changes: 87 additions & 0 deletions 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 | string>(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 | string>(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 = ''
})
})
})
47 changes: 30 additions & 17 deletions 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<string> | ((title: string) => string)
}

export type UseTitleOptions = ConfigurableDocument & UseTitleOptionsBase

export function useTitle(
newTitle?: MaybeRef<string | null | undefined>,
newTitle: MaybeReadonlyRef<string | null | undefined>,
options?: UseTitleOptions,
): Ref<string>
): ComputedRef<string | null | undefined>

export function useTitle(
newTitle?: MaybeComputedRef<string | null | undefined>,
newTitle?: MaybeRef<string | null | undefined>,
options?: UseTitleOptions,
): ComputedRef<string>
): Ref<string | null | undefined>

/**
* Reactive document title.
Expand All @@ -42,31 +49,37 @@ export function useTitle(
newTitle: MaybeComputedRef<string | null | undefined> = 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<string>
const title: WritableComputedRef<string | null | undefined> = 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'),
() => {
Expand Down

0 comments on commit 8c1ba50

Please sign in to comment.