diff --git a/packages/core/src/head.ts b/packages/core/src/head.ts index ff00b9e74..5e5d3c051 100644 --- a/packages/core/src/head.ts +++ b/packages/core/src/head.ts @@ -1,5 +1,7 @@ import debounce from './debounce' +const INERTIA_ATTRIBUTES = ['data-inertia', 'inertia'] + const Renderer = { buildDOMElement(tag: string): ChildNode { const template = document.createElement('template') @@ -20,13 +22,19 @@ const Renderer = { }, isInertiaManagedElement(element: Element): boolean { - return element.nodeType === Node.ELEMENT_NODE && element.getAttribute('inertia') !== null + return ( + element.nodeType === Node.ELEMENT_NODE && + INERTIA_ATTRIBUTES.some((attribute) => element.getAttribute(attribute) !== null) + ) }, findMatchingElementIndex(element: Element, elements: Array): number { - const key = element.getAttribute('inertia') - if (key !== null) { - return elements.findIndex((element) => element.getAttribute('inertia') === key) + for (const attr of INERTIA_ATTRIBUTES) { + const key = element.getAttribute(attr) + + if (key !== null) { + return elements.findIndex((el) => el.getAttribute(attr) === key) + } } return -1 @@ -64,8 +72,21 @@ export default function createHeadManager( createProvider: () => { update: (elements: string[]) => void disconnect: () => void + preferredAttribute: () => string } } { + // Detect which attribute to use based on existing elements + let preferredAttribute = 'inertia' + + if (!isServer) { + const hasLegacyAttribute = document.querySelector('[inertia]') + const hasDataAttribute = document.querySelector('[data-inertia]') + + if (!hasLegacyAttribute && hasDataAttribute) { + preferredAttribute = 'data-inertia' + } + } + const states: Record> = {} let lastProviderId = 0 @@ -102,7 +123,7 @@ export default function createHeadManager( const title = titleCallback('') const defaults: Record = { - ...(title ? { title: `${title}` } : {}), + ...(title ? { title: `${title}` } : {}), } const elements = Object.values(states) @@ -118,10 +139,19 @@ export default function createHeadManager( return carry } - const match = element.match(/ inertia="[^"]+"/) - if (match) { - carry[match[0]] = element - } else { + let matchFound = false + + for (const attr of INERTIA_ATTRIBUTES) { + const match = element.match(new RegExp(` ${attr}="[^"]+"`)) + + if (match) { + carry[match[0]] = element + matchFound = true + break + } + } + + if (!matchFound) { carry[Object.keys(carry).length] = element } @@ -148,6 +178,7 @@ export default function createHeadManager( reconnect: () => reconnect(id), update: (elements) => update(id, elements), disconnect: () => disconnect(id), + preferredAttribute: () => preferredAttribute, } }, } diff --git a/packages/react/src/Head.ts b/packages/react/src/Head.ts index bf844d36d..fa7090b1d 100644 --- a/packages/react/src/Head.ts +++ b/packages/react/src/Head.ts @@ -81,7 +81,7 @@ const Head: InertiaHead = function ({ children, title }) { function ensureNodeHasInertiaProp(node) { return React.cloneElement(node, { - inertia: node.props['head-key'] !== undefined ? node.props['head-key'] : '', + [provider.preferredAttribute()]: node.props['head-key'] !== undefined ? node.props['head-key'] : '', }) } @@ -94,7 +94,7 @@ const Head: InertiaHead = function ({ children, title }) { .filter((node) => node) .map((node) => renderNode(node)) if (title && !computed.find((tag) => tag.startsWith('${title}`) + computed.push(`${title}`) } return computed } diff --git a/packages/react/test-app/index.html b/packages/react/test-app/index.html index c6bcfc7eb..67f4be4d1 100644 --- a/packages/react/test-app/index.html +++ b/packages/react/test-app/index.html @@ -2,7 +2,7 @@ - Inertia React - Testing Environment + Inertia React - Testing Environment diff --git a/packages/svelte/test-app/index.html b/packages/svelte/test-app/index.html index 6c247ed86..0c011483e 100644 --- a/packages/svelte/test-app/index.html +++ b/packages/svelte/test-app/index.html @@ -2,7 +2,7 @@ - Inertia Svelte - Testing Environment + Inertia Svelte - Testing Environment diff --git a/packages/vue3/src/head.ts b/packages/vue3/src/head.ts index 91690b64a..a577757e5 100644 --- a/packages/vue3/src/head.ts +++ b/packages/vue3/src/head.ts @@ -44,7 +44,7 @@ const Head: InertiaHead = defineComponent({ }, renderTagStart(node) { node.props = node.props || {} - node.props.inertia = node.props['head-key'] !== undefined ? node.props['head-key'] : '' + node.props[this.provider.preferredAttribute()] = node.props['head-key'] !== undefined ? node.props['head-key'] : '' const attrs = Object.keys(node.props).reduce((carry, name) => { const value = String(node.props[name]) if (['key', 'head-key'].includes(name)) { @@ -96,7 +96,7 @@ const Head: InertiaHead = defineComponent({ }, addTitleElement(elements) { if (this.title && !elements.find((tag) => tag.startsWith('${this.title}`) + elements.push(`${this.title}`) } return elements }, diff --git a/packages/vue3/test-app/index.html b/packages/vue3/test-app/index.html index 2f5b89ad7..633ddd31b 100644 --- a/packages/vue3/test-app/index.html +++ b/packages/vue3/test-app/index.html @@ -2,7 +2,7 @@ - Inertia Vue - Testing Environment + Inertia Vue - Testing Environment diff --git a/tests/app/helpers.js b/tests/app/helpers.js index 7dc54cb48..3efddf3f0 100644 --- a/tests/app/helpers.js +++ b/tests/app/helpers.js @@ -5,7 +5,7 @@ const package = process.env.PACKAGE || 'vue3' module.exports = { package, - render: (req, res, data) => { + render: (req, res, data, titleAttribute = '') => { data = { component: req.path .slice(1) @@ -46,7 +46,8 @@ module.exports = { fs .readFileSync(path.resolve(__dirname, '../../packages/', package, 'test-app/dist/index.html')) .toString() - .replace("'{{ placeholder }}'", JSON.stringify(data)), + .replace("'{{ placeholder }}'", JSON.stringify(data)) + .replace('{{ titleAttribute }}', titleAttribute), ) }, location: (res, href) => res.status(409).header('X-Inertia-Location', href).send(''), diff --git a/tests/app/server.js b/tests/app/server.js index f725d6556..ea0815755 100644 --- a/tests/app/server.js +++ b/tests/app/server.js @@ -109,6 +109,8 @@ app.get('/client-side-visit', (req, res) => }), ) +app.get('/head/:attribute', (req, res) => inertia.render(req, res, { component: 'Head' }, req.params.attribute)) + app.get('/visits/partial-reloads', (req, res) => inertia.render(req, res, { component: 'Visits/PartialReloads', diff --git a/tests/head.spec.ts b/tests/head.spec.ts index 154dc74f9..d513d16ba 100644 --- a/tests/head.spec.ts +++ b/tests/head.spec.ts @@ -1,19 +1,29 @@ import { expect, test } from '@playwright/test' -test('renders the title tag and children', async ({ page }) => { - test.skip(process.env.PACKAGE === 'svelte', 'Svelte adapter has no Head component') +const supportedAttributes = [undefined, 'inertia', 'data-inertia'] - await page.goto('/head') +supportedAttributes.forEach((attribute) => { + test(`renders the title tag and children with ${attribute ?? 'no'} attribute`, async ({ page }) => { + test.skip(process.env.PACKAGE === 'svelte', 'Svelte adapter has no Head component') - const inertiaTitle = await page.evaluate(() => document.querySelector('title[inertia]')?.textContent) + await page.goto(attribute ? `/head/${attribute}` : '/head') - await expect(inertiaTitle).toBe('Test Head Component') - await expect(page.locator('meta[name="viewport"]')).toHaveAttribute('content', 'width=device-width, initial-scale=1') - await expect(page.locator('meta[name="description"]')).toHaveAttribute('content', 'This is an "escape" example') - await expect(page.locator('meta[name="number"]')).toHaveAttribute('content', '0') - await expect(page.locator('meta[name="boolean"]')).toHaveAttribute('content', 'true') - await expect(page.locator('meta[name="false"]')).toHaveAttribute('content', 'false') - await expect(page.locator('meta[name="null"]')).toHaveAttribute('content', 'null') - await expect(page.locator('meta[name="undefined"]')).toHaveAttribute('content', 'undefined') - await expect(page.locator('meta[name="float"]')).toHaveAttribute('content', '3.14') + const inertiaTitle = await page.evaluate( + (attribute) => document.querySelector(`title[${attribute}]`)?.textContent, + attribute ?? 'inertia', // it defaults to 'inertia' if no attribute is provided + ) + + await expect(inertiaTitle).toBe('Test Head Component') + await expect(page.locator('meta[name="viewport"]')).toHaveAttribute( + 'content', + 'width=device-width, initial-scale=1', + ) + await expect(page.locator('meta[name="description"]')).toHaveAttribute('content', 'This is an "escape" example') + await expect(page.locator('meta[name="number"]')).toHaveAttribute('content', '0') + await expect(page.locator('meta[name="boolean"]')).toHaveAttribute('content', 'true') + await expect(page.locator('meta[name="false"]')).toHaveAttribute('content', 'false') + await expect(page.locator('meta[name="null"]')).toHaveAttribute('content', 'null') + await expect(page.locator('meta[name="undefined"]')).toHaveAttribute('content', 'undefined') + await expect(page.locator('meta[name="float"]')).toHaveAttribute('content', '3.14') + }) })