ionic-team / ionic-framework

A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.
https://ionicframework.com
MIT License
50.93k stars 13.52k forks source link

bug: vue, elements not shown at the moment did enter lifecycle is fired #24434

Open honghuangdc opened 2 years ago

honghuangdc commented 2 years ago

Prerequisites

Ionic Framework Version

Current Behavior

create a reference in dom, which is inside Icon-vue component, then get the config "offsetHeight", "offsetWidth", "clientHeight", "clientWidth" of the dom in lifecycle onMouted, but their value are all zero, and when I add setTimeout function to get config, it will get real value.

Expected Behavior

it can get the real value of config "offsetHeight", "offsetWidth", "clientHeight", "clientWidth" of the dom reference in lifecycle onMouted.

Steps to Reproduce

  1. set dom reference inside ionic-vue component
  2. in lifecycle onMouted, get the config "offsetHeight", "offsetWidth", "clientHeight", "clientWidth" value

here is the code:

<template>
  <ion-page>
    <div ref="domRef">dom test</div>
  </ion-page>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue';
import { IonPage } from '@ionic/vue';

export default defineComponent({
  name: 'App',
  components: {
    IonPage
  },
  setup() {
    const domRef = ref<HTMLElement | null>(null)

    onMounted(() => {
      console.log(domRef.value) // it can get dom reference
      console.log(domRef.value?.offsetHeight) // but the value is zero
      console.log(domRef.value?.offsetWidth)
      console.log(domRef.value?.clientHeight)
      console.log(domRef.value?.clientWidth)
    })

    return {
      domRef
    }
  }
});
</script>
<style scoped></style>

Code Reproduction URL

No response

Ionic Info

No response

Additional Information

No response

ionitron-bot[bot] commented 2 years ago

Thanks for the issue! This issue has been labeled as holiday triage. With the winter holidays quickly approaching, much of the Ionic Team will soon be taking time off. During this time, issue triaging and PR review will be delayed until the team begins to return. After this period, we will work to ensure that all new issues are properly triaged and that new PRs are reviewed.

In the meantime, please read our Winter Holiday Triage Guide for information on how to ensure that your issue is triaged correctly.

Thank you!

sean-perkins commented 2 years ago

Hello @honghuangdc thank for the issue!

Excuse my lack of familiarity with Vue, but I don't believe onMounted is guaranteed to execute after the view has completed rendering. The DOM reference values you are accessing won't be populated until the element has completed reflow & repaint.

Looking at the Vue docs, they recommend using $nextTick in onMounted when needing to run code after render: https://v3.vuejs.org/api/options-lifecycle-hooks.html#mounted

this.$nextTick(function () {
  // Code that will run only after the
  // entire view has been rendered
})

Can you let me know if this resolves your issue or if I am incorrect in my understanding of your issue? Thanks!

honghuangdc commented 2 years ago

thanks your reply.if add nextTick function in lifecycle onMouted, it still has the same problem. It has nothing to do with vue, because there is no such problem in a vue project without ionic-vue. for example, create a dom reference which is the child element of body, but outside the ionic-vue component, those config values can be normally getted in lifecycle onMouted.

honghuangdc commented 2 years ago

this problem bothers me so long, because some vue plugins which need dom reference to get those config values won’t run normally

sean-perkins commented 2 years ago

Hello @honghuangdc thanks for your patience. Followed up with Liam to understand more about this issue.

In your example, since you are attaching to a div, you will need to use the onIonViewDidEnter lifecycle hook. The hook is fired at the same time the element is shown, so you will need to wait a frame for the browser to re-render the contents.

Let me know if that information helps or if you have additional questions. Thanks!

honghuangdc commented 2 years ago

oh,when I met this problem, I already tried in this way, it still had the same problem.

honghuangdc commented 2 years ago

and I did the same action in @ionic/react, it didn't have the problem.

liamdebeasi commented 2 years ago

We can probably improve this behavior. It seems that the page elements are not shown at the moment the "did enter" lifecycle is fired: https://github.com/ionic-team/ionic-framework/blob/main/packages/vue/src/components/IonRouterOutlet.ts#L347

They are shown 1-2 frames after which is why a setTimeout works.

Firing this in a requestAnimationFrame or after components.value is set (or both) may help.

aparajita commented 6 months ago

@liamdebeasi boy it would be great if this were addressed at the framework level. I have to jump through so many hoops and end up with brittle code because even when the component is present in the dom, its shadow dom may not be rendered. In addition, if we are showing a modal, onIonViewDidEnter is not even fired.

Relying on timeouts or an indeterminate number of animation frames is, shall we say, less than optimal.

agileago commented 2 months ago

same situation +1

agileago commented 1 month ago

now, my solution is

const PageRouteWrapper = defineComponent((props, ctx) => {
  const show = ref(false)

  onIonViewDidEnter(() => {
    setTimeout(() => (show.value = true), 50)
  }, getCurrentInstance()?.parent)

  return () => (
    <IonPage>
      <IonContent>{show.value ? ctx.slots.default?.() : null}</IonContent>
    </IonPage>
  )
})

const PageRoute = defineComponent(() => {
  return () => (
    <PageRouteWrapper>
      <div>111111</div>
    </PageRouteWrapper>
  )
})

and i find setTimeout 0 is not work. it is better to set 50ms

aparajita commented 1 month ago

Yup, but timeouts are inherently brittle. On a slow Android phone 50 ms may not be sufficient.

agileago commented 1 month ago

@aparajita Is there a precise timing for judgment ?

aparajita commented 1 month ago

It's impossible to know what the correct timing is precisely. That's why we need it to be fixed at the framework level.

agileago commented 1 month ago

@sean-perkins @liamdebeasi

Can frameworks ensure a precise lifecycle for DOM elements to obtain their size information? Many UI frameworks like vant depend on the onMounted event to dynamically calculate additional logic based on the DOM's size.

agileago commented 1 month ago

@aparajita How did you handle it in the end?

aparajita commented 1 month ago

@aparajita How did you handle it in the end?

I tried various solutions, none of which were robust. Now I just try to avoid it as much as possible, and if I can't, I use a timeout.

agileago commented 1 month ago

How long did you use the timeout @aparajita

liamdebeasi commented 1 month ago

@sean-perkins @liamdebeasi

Can frameworks ensure a precise lifecycle for DOM elements to obtain their size information? Many UI frameworks like vant depend on the onMounted event to dynamically calculate additional logic based on the DOM's size.

Hi,

I don't work at Ionic anymore, so I'm not able to help prioritize this bug fix.

If anyone is interested in trying to contribute their own fix, this line of code may be a good place to start your investigation. This code was added to hide a layout shift. However, I believe we've improved tabs performance such that this fix may no longer be needed. You can probably try the reproduction steps in https://github.com/ionic-team/ionic-framework/issues/22052 to verify with the linked line of code removed.

The main issue here is that elements are being shown after onMounted, so the bounding box will be 0x0 at the time that onMounted fires.

aparajita commented 1 month ago

I don't work at Ionic anymore

@liamdebeasi major bummer! Thanks for your fantastic support while you were at Ionic. Hope you got a better gig.

agileago commented 1 month ago

@aparajita I find a perfect way to resolve this situation. we can use resizeObserver

import { useResizeObserver } from '@vueuse/core'

const IonicWrapper = defineComponent((props, ctx) => {
  const contentRef = ref()
  const showContent = ref(false)
  let now = 0

  onMounted(() => (now = Date.now()))

  const rob = useResizeObserver(contentRef, entries => {
    const entry = entries[0]
    const { width } = entry.contentRect
    if (showContent.value) return
    if (width !== 0) {
      console.log('rect gap time', Date.now() - now)
      showContent.value = true
      rob.stop()
    }
  })
  if (!rob.isSupported.value) {
    onMounted(() => setTimeout(() => (showContent.value = true), 50))
  }

  return () => (
    <IonPage>
      <IonContent>
        <div ref={contentRef} class={'h-full'}>{showContent.value ? ctx.slots.default?.() : null}</div>
      </IonContent>
    </IonPage>
  )
})

const PageRoute = defineComponent(() => {
  return () => (
    <IonicWrapper>
      <div>this child is main logic and other component</div>
    </IonicWrapper>
  )
})

i test it. it works perfect !

aparajita commented 1 month ago

@aparajita I find a perfect way to resolve this situation. we can use resizeObserver

Very clever solution!

HekunX commented 2 weeks ago

I have also encountered this problem when i use IonRouterOutlet component recently.I debbuged the source code and find it may be ralated to the lifecircle of ionic web components.the ionic use stenciljs to render web components,It executes renrder after the completion of connectedCallback event,then child element will be displayed.but vue exucute OnMounted after connectedCallback immediately.so it may be not display.i did this in order vue compoenent work will.

import { IonRouterOutlet as R } from "@ionic/core/components/ion-router-outlet.js";

let source = (R!.prototype as any).connectedCallback;
(R!.prototype as any).connectedCallback = async function () {
    var th = this as any;
    th.shadowRoot!.innerHTML = "<slot></slot>";
    source.apply(this);
}

i set slot when connectedCallback executing so that child can be display immediately .