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

Offcanvas responsive issues #1153

Open
VividLemon opened this issue May 30, 2023 · 9 comments
Open

Offcanvas responsive issues #1153

VividLemon opened this issue May 30, 2023 · 9 comments
Labels
bug Something isn't working stale-exempt Use this to prevent auto-stalling of an issue

Comments

@VividLemon
Copy link
Member

While developing the documentation, it became apparent that there is an issue with attempting to use responsive, and model value together can cause hydration issues. Specifically if model value is true, and then you try to use responsive at the same time.

So, we need to think about some way to fix all problems at once while retaining the model value, and SSR compatibility.

@VividLemon VividLemon added bug Something isn't working stale-exempt Use this to prevent auto-stalling of an issue labels May 30, 2023
@phlegx
Copy link
Contributor

phlegx commented Jun 8, 2023

Hi @VividLemon!

I get a Vue warn with b-offcanvas in the client console:

[Vue warn]: Hydration node mismatch:
- Client vnode: div 
- Server rendered DOM: #text "\n    " (text) 
  at <BaseTransition mode=undefined onBeforeEnter=fn<onBeforeEnter> onAfterEnter=fn<x>  ... > 
  at <Transition mode=undefined css=true name=""  ... > 
  at <BTransition no-fade=true trans-props= 
Object { enterToClass: "showing", enterFromClass: "", leaveToClass: "hiding show", leaveFromClass: "show" }
 onBeforeEnter=fn<O>  ... > 
  at <BOffcanvas class="lb-header-oc" id="lb-header-oc" modelValue=true  ... >
  ...

and

[Vue warn]: Hydration children mismatch in <body>: server rendered element contains fewer child nodes than client vdom. 
  at <BOffcanvas class="lb-header-oc" id="lb-header-oc" modelValue=true  ... >
  ...

Can this help to find a fix?

@phlegx
Copy link
Contributor

phlegx commented Jun 8, 2023

Could an own teleport wrapper node be a solution instead of teleport to body?

<body>
  <div id="app">...</div>
  <div id="teleports"><!-- target teleports here --></div>
</body>

vuejs/core#5242 (comment)

@phlegx
Copy link
Contributor

phlegx commented Jun 9, 2023

@VividLemon like described in the official documentation of vue, we should consider to use an own DOM node:

Avoid targeting body when using Teleports and SSR together - usually, will contain other server-rendered content which makes it impossible for Teleports to determine the correct starting location for hydration. Instead, prefer a dedicated container, e.g. <div id="teleported"></div> which contains only teleported content.

Every bootstrap-vue component, that uses teleport, can be configured (:to=). Default is body and SSR deployments need to set teleport :to= to #teleported.

We have two possible solutions:

  1. Teleport components are rendered only on client side (no hydration node mismatch).
<ClientOnly>
  <Teleport to="body">
    ...
  </Teleport>
</ClientOnly> 
  1. Required SSR rendering of teleport components, by using a unique DOM node outside app node (solves hydration node mismatch).
<Teleport to="#teleported">
  ...
</Teleport>

with index.html like:

...
<body>
  <div id="app"></div>
  <div id="teleported"></div>
</body>

SSR rendering does something like:

teleported = context.teleports['#teleported']
...
// Replace <div id="teleported"> with <div id="teleported" data-server-rendered="true">${teleported}

@VividLemon
Copy link
Member Author

@VividLemon like described in the official documentation of vue, we should consider to use an own DOM node:

Avoid targeting body when using Teleports and SSR together - usually, will contain other server-rendered content which makes it impossible for Teleports to determine the correct starting location for hydration. Instead, prefer a dedicated container, e.g. <div id="teleported"></div> which contains only teleported content.

Every bootstrap-vue component, that uses teleport, can be configured (:to=). Default is body and SSR deployments need to set teleport :to= to #teleported.

We have two possible solutions:

1. Teleport components are rendered only on client side (no hydration node mismatch).
<ClientOnly>
  <Teleport to="body">
    ...
  </Teleport>
</ClientOnly> 
3. Required SSR rendering of teleport components, by using a unique DOM node outside app node (solves hydration node mismatch).
<Teleport to="#teleported">
  ...
</Teleport>

with index.html like:

...
<body>
  <div id="app"></div>
  <div id="teleported"></div>
</body>

SSR rendering does something like:

teleported = context.teleports['#teleported']
...
// Replace <div id="teleported"> with <div id="teleported" data-server-rendered="true">${teleported}

The issue described here is not related to the teleport. Nor will the teleport not work as intended. I'm not sure why the Vuejs documentation recommends that solution, as it will not work, period.

The issue with teleports, in general, not just in SSR, is that the teleport can't grab an item that doesn't exist. Normally what will occur when trying to teleport to a VNode is that it will fail because it can't find it.

Nuxt equally contains this issue as it can't teleport to something that doesn't exist yet. In Vue's case, there has been some discussion about bypassing this issue by passively waiting for the mounting point before triggering the teleport - ie it will detect when the mount point is available, then proceed to teleport to it. However, this was discussed many, many months ago, and has seen to action on it's delivery.

In regards to the Vuejs documentation, like I said, it's completely opposite to what Nuxt will actually recommend

The component teleports a component to a different location in the DOM.

The to target of expects a CSS selector string or an actual DOM node. Nuxt currently has SSR support for teleports to body only, with client-side support for other targets using a wrapper.

Despite all this, this is not the issue that is described here. The issue described here relates to the following flow:

  1. You want to use props.responsive, the behavior demonstrated here https://getbootstrap.com/docs/5.3/components/offcanvas/#responsive

For this behavior, if you are ABOVE a breakpoint threshold, the content will always be rendered. If you are below the threshold, the content will appear through a toggle button -- our vmodel.

  1. In order to get the content to be rendered when it's above the breakpoint, we need to add a check so
    <template v-if="lazyShowing"> => <template v-if="lazyShowing || isInResponsiveBreakpoint">

  2. In addition to this, the current functionality needs to be preserved. The current code is what is required for when you are below a breakpoint, while above the breakpoint, you need to always display the content.

However, this has massive ramifications for SSR.

Since the server does not have a viewport, and thus doesn't have a breakpoint, if you render the content on a large screen, the node will look like this:

<div
        v-show="modelValue"
        ref="element"
        aria-modal="true"
        role="dialog"
        :class="computedClasses"
        tabindex="-1"
        aria-labelledby="offcanvasLabel"
        data-bs-backdrop="false"
        v-bind="$attrs"
        @keyup.esc="hide('esc')"
      >
     <-- --!>
     </div>

Like I said, the viewport is 0 on the server, so the content is false.

However, the client has a viewport above the threshold! So it will render it's content!

<div
        v-show="modelValue"
        ref="element"
        aria-modal="true"
        role="dialog"
        :class="computedClasses"
        tabindex="-1"
        aria-labelledby="offcanvasLabel"
        data-bs-backdrop="false"
        v-bind="$attrs"
        @keyup.esc="hide('esc')"
      >
     CONTENT!
     </div>

So, if we make this change, there will be a hydration mismatch. The only solution is to always use client-only with this component. Which is not ideal as, specifically for SSR, a lot of the time you use an offcanvas solution for nav,
image
And putting your nav in a client-only eliminates a huge bonus from using SSR, as web crawlers not able to traverse JS won't be able to index your site correctly.

So, the issue here does not have to do with the process of teleporting. It has to do with displaying content when there is a breakpoint, and when there is not. If you're getting a hydration mismatch, perhaps you should open a new issue with some examples.

@StirStudios
Copy link
Contributor

StirStudios commented Jun 18, 2023

As always @VividLemon you describe the issue in great detail so we can understand the issue at hand!

Is there anything here that sparks a solution:
react-bootstrap/react-bootstrap@ee779c7
react-bootstrap/react-bootstrap#6380

@VividLemon
Copy link
Member Author

I believe I may have a solution

@StirStudios
Copy link
Contributor

Of course you have, you are a rockstar!

@VividLemon
Copy link
Member Author

VividLemon commented Jun 18, 2023

I won't be able to release any of my fixes until qmhc/vite-plugin-dts#218 (comment) and my branch are fixed.

@StirStudios
Copy link
Contributor

Got it, regardless, progress is key, and awesome to here. I just found those links and thought they might be useful!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working stale-exempt Use this to prevent auto-stalling of an issue
Projects
None yet
Development

No branches or pull requests

3 participants