Skip to content

Commit

Permalink
fix(useTitle): prevent using observe with titleTemplate
Browse files Browse the repository at this point in the history
  • Loading branch information
huynl-96 committed Aug 26, 2022
1 parent 120e381 commit 53cf00f
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 17 deletions.
16 changes: 7 additions & 9 deletions packages/core/useTitle/index.md
Expand Up @@ -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` })
```
85 changes: 85 additions & 0 deletions packages/core/useTitle/index.test.ts
@@ -0,0 +1,85 @@
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')
// title.value = '' // typed error
})

it('function', () => {
const target = () => 'new title'
const title = useTitle(target)
expect(title.value).toEqual('new title')
// title.value = '' // typed error
})
})
})
20 changes: 12 additions & 8 deletions packages/core/useTitle/index.ts
Expand Up @@ -9,20 +9,22 @@ 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'
*/
titleTemplate?: MaybeRef<string> | ((title: string) => string)
}

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

Expand All @@ -42,6 +44,14 @@ 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`.
*/
if (options.observe && options.titleTemplate)
throw new Error('Cannot use `observe` and `titleTemplate` together.')

const {
document = defaultDocument,
observe = false,
Expand All @@ -50,7 +60,6 @@ export function useTitle(

const title: WritableComputedRef<string | null | undefined> = resolveRef(newTitle ?? document?.title ?? null)
const isReadonly = newTitle && isFunction(newTitle)
const hasModifiedTitle = isFunction(titleTemplate) ? titleTemplate('') !== '' : titleTemplate !== '%s'

function format(t: string) {
return isFunction(titleTemplate)
Expand All @@ -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'),
() => {
Expand Down

0 comments on commit 53cf00f

Please sign in to comment.