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

fix(useTitle): prevent observe and titleTemplate been specified at the same time #2049

Merged
merged 8 commits into from Oct 16, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
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