inertia icon indicating copy to clipboard operation
inertia copied to clipboard

createInertiaApp not using "id" option for Vue root element

Open swifthand opened this issue 1 year ago • 8 comments

Version:

  • nodejs version: 18.12.1

  • @inertiajs/vue3 version: 3.2.45

  • @inertiajs/inertia version: 0.11.1

  • @inertiajs/inertia-vue3 version: 0.6.0

  • ruby version: 3.1.2

  • rails version: 7.0.4

Describe the problem:

Per the documentation page "Client-side setup", a custom element ID can be specified for the root of the application:

By default, Inertia assumes that you have a root element with an id of app. If different, you can change this using the id property.

createInertiaApp({
 id: 'my-app',
 // ...
})

Something, somewhere is dropping the ball when attempting to use this capability. If an element with the ID exists named my-vue-root, it is not used when passed as the id option/argument to createInertiaApp.

I have tested this in a clean install, with the versions described above, using the simplest-possible example: a single component/page application with a stripped-down layout template, and bare minimum setup for Inertia/Vue. Different amounts of fiddling and banging has led to one of two main outcomes:

  1. A new <div> element is created within the intended root element with and id of app, ignoring the intended purpose.
  2. An error Uncaught (in promise) SyntaxError: "undefined" is not valid JSON in createInertiaApp.js:7:36, because this root element does not have the value data-page, or if I add data-page="" to the element, it fails for other reasons.

Reading the source of createInertiaApp.js I see that one can pass a page argument to createInertiaApp(). It is not documented in that file, but it seems one can use this by passing page as an object with properties component, props and url. This can "force" an initial page load to boot up with a particular component. However, the component does not load any data from the server (regardless of the url property in the page object), and worse, doing so causes a page refresh to leave the current route and re-render this initial page/component.

It "works" in that the specified component will mount within the element with id="my-app" instead of make a nested <div id="app">. So success on that, but then breaks routing on reloads. For example, if the page property in the argument is { component: "Home", url: "/" }, then one navigates to a Foo.vue page at /foo, and then reloads, the page does not return to /foo. Instead it is reset to show Home and the route is reset to /. There is a logic to that, but it is clearly not a desirable tradeoff just to force the proper use of a DOM element by id.

Steps to reproduce:

First, the simplest possible combination:

app/views/layouts/application.html.erb:

<!DOCTYPE html>
<html>
  ...
  <body>
    <div id="my-vue-root">
      <%= yield %>
    </div>
  </body>
</html>

app/frontend/entrypoints/application.js:

import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/inertia-vue3';

const pages = import.meta.globEagerDefault('../Pages/**/*.vue');

createInertiaApp({
  id: 'my-vue-root',
  resolve: (name) => {
    return pages[`../Pages/${name}.vue`];
  },
  setup({ el, App, props, plugin }) {
    const vueApp = createApp({ render: () => h(App, props), });
    vueApp.use(plugin).mount(el);
  },
});

app/frontend/Pages/Home.vue:

<template>
  <div>
    <h1>Home Page</h1>
  </div>
</template>

Even though <div id="my-vue-root"> clearly exists, this produces the JavaScript error:

Uncaught (in promise) SyntaxError: "undefined" is not valid JSON
    at JSON.parse (<anonymous>)
    at exports.createInertiaApp (createInertiaApp.js:7:36)
    at application.js:56:1

Stepping through in the debugger, the createInertiaApp function is receiving the id property from the argument, but is trying to catch some data parameter page that does not exist yet, because the app is not yet created. Obviously the app and its page data cannot be created yet, as this is the create function itself!
(From packages/vue3/src/createInertiaApp.js):

export default async function createInertiaApp({ id = 'app', resolve, setup, title, page, render }) {
  const isServer = typeof window === 'undefined'
  const el = isServer ? null : document.getElementById(id)
  const initialPage = page || JSON.parse(el.dataset.page) // Line 7, where the error occurs
  ...
}

In an even stranger case, and I apologize for not isolating this better, I have managed to get a halfway failure, where this SyntaxError: "undefined" is not valid JSON error occurs, but the DOM itself appears to have created the <div id="app"> nested within my specified <div id="my-vue-root"> element, before attempting to use that element and failing anyway. The DOM in such an occasion looks like this:

<div id="my-vue-root">
  <div id="app" data-page="{'component':'Home','props':{},'url':'/','version':null}"></div>
</div>

Again, I can "force" the root to attach to <div id="my-vue-root"> if I instead initialize with a page property in the object argument:

createInertiaApp({
  id: 'my-vue-root',
  page: {
    component: "Home",
    url: "/",
  },
  resolve: ..., // same as above
  setup({ el, App, props, plugin }) { ... }, // same as above
});

However, again, doing this causes any and all reloads of the application, at any route (e.g. after navigating elsewhere) to reset and load Home at the route /. I feel like I understand why, but it makes me feel like this page option must truly be not intended for use in this way.

I am left wondering, based on the documentation, whether this is a bug, or if I am radically misunderstanding something about this id option for createInertiaApp?

Why this matters

I ask this because, in a more complex, real-world use, I wish to point Inertia at different root elements by id, in different situations. If this is not how the id option is intended to be used, I kindly request some documentation explaining what steps are required to properly use this id option for createInertiaApp.

swifthand avatar Dec 08 '22 08:12 swifthand

This just happened to me too.

Uncaught (in promise) SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data

app.blade.php

<!DOCTYPE html>
<html lang="en" data-bs-theme="dark" class="w-100 h-100">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
    <title>Document</title>
    @vite(['resources/js/app.js'])
    @inertiaHead
</head>
<body id="my-app" class="w-100 h-100">
</body>
</html>

app.js

...
const inertiaData = {
    id: 'my-app',
    resolve: name => {
        const pages = import.meta.glob('./Pages/**/*.vue', {eager: true})
        let page = pages[`./Pages/${name}.vue`]
        page.default.layout = page.default.layout || AppLayout
        return page
    },
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .mount(el)
    },
};
createInertiaApp(inertiaData);

For the "createInertiaApp" portion:

export default async function createInertiaApp({ id = 'app', resolve, setup, title, page, render }) {
  const isServer = typeof window === 'undefined'
  const el = isServer ? null : document.getElementById(id)
  const initialPage = page || JSON.parse(el.dataset.page) // Line 7, where the error occurs
  ...
}

It gets compiled to this JS:

let m = typeof window > "u", ... // Is this a valid "undefined" check? This isn't returning true or false, but stays undefined.

By this being set to undefined the JSON parse seems to just not go through.

I can provide more details, and this would be awesome to be fixed.

PerikiyoXD avatar Mar 31 '23 18:03 PerikiyoXD

update your blade.php : @inertia('my-app')

zxdstyle avatar Apr 21 '23 11:04 zxdstyle

I'm experiencing the same issue in React. Is there any solution to this or is it just a bug.

reinvanimschoot avatar Jun 30 '23 09:06 reinvanimschoot

Yeah, the docs are incomplete on this. What's described in the docs only changes the client-side, server-side the rendered div also has to be changed for it to even work. For Laravel you'd have to use the approach @zxdstyle mentioned (@inertia('my-app') / https://legacy.inertiajs.com/releases/inertia-laravel-0.5.0-2022-01-07). It looks like for the Rails adapter there is no such option (https://github.com/inertiajs/inertia-rails/blob/master/app/views/inertia.html.erb), perhaps @BrandonShar can shine some light on this?

RobertBoes avatar Jun 30 '23 09:06 RobertBoes

Then, a solution would be updating the docs indicating the proper way of initializing inertia using the @inertia('<id>')

PerikiyoXD avatar Jul 17 '23 07:07 PerikiyoXD

Hey! Thanks so much for your interest in Inertia.js and for sharing this issue/suggestion.

In an attempt to get on top of the issues and pull requests on this project I am going through all the older issues and PRs and closing them, as there's a decent chance that they have since been resolved or are simply not relevant any longer. My hope is that with a "clean slate" me and the other project maintainers will be able to better keep on top of issues and PRs moving forward.

Of course there's a chance that this issue is still relevant, and if that's the case feel free to simply submit a new issue. The only thing I ask is that you please include a super minimal reproduction of the issue as a Git repo. This makes it much easier for us to reproduce things on our end and ultimately fix it.

Really not trying to be dismissive here, I just need to find a way to get this project back into a state that I am able to maintain it. Hope that makes sense! ❤️

reinink avatar Jul 28 '23 01:07 reinink

Add @inertia(<id>) as a possible initialization to the docs

PerikiyoXD avatar Aug 25 '23 05:08 PerikiyoXD

Add @inertia(<id>) as a possible initialization to the docs

Yup, exactly, this works!

@inertia('my-custom-id')

And I agree, this should get added to the server-side setup page: https://inertiajs.com/server-side-setup

Going to open this issue as a reminder to do that 👍

reinink avatar Aug 25 '23 11:08 reinink