Perform action after loading all children

(translated from Portuguese to English via google translator)
Good Morning,
I’m learning vuejs and the framework quasar at the same time.

I have a custom slider where I can put numerous slides, inside each slide I can put up to 4 cards and this is variable according to the size of the screen.

Since the number is supposed to be infinite, I do not load all the slides at the same time, because if it is a very high number of cards, it can crash the site.

As a solution to this I carry only 3 slides at a time, the current, the previous and the next because they are involved in the css animation.

Within each slide there are dots that are internal to the q-carousel component of the quasar, they represent the number of slides, but when the screen shrinks or the number of dots generates more than 2 lines the view becomes ugly, so I override the dots and I put text like “1 of n”, and this is working when resizing the screen.

The problem is that at the time of loading I need to do the same check, and the method is running, but because of the dynamic loading of the internal components, when it runs the dots list is still empty and the verification fails.

I’m already using this on the mounted, and I’ve already tried using the nexttick and the same problem occurred. I need to run this after all the children have been loaded, how do I do this?

code:

<template>
  <div class="row">
    <q-window-resize-observable @resize="resize"/>
    <l-slide-cards
      v-if="qtdSlides <= 1"
      :load="true"
      :cards="cardsPart"/>
    <q-carousel v-else infinite arrows dots @slide="swipe" class="carouselCards">
      <l-slide-cards
        slot="slide"
        v-for="i in qtdSlides"
        :load="(slide == i || prev == i || next == i)"
        :cards="cardsPart"/>
      <div v-show="!dots" class="carouselCardsPagination">
        {{slide}} de {{qtdSlides}}
      </div>
    </q-carousel>
  </div>
</template>

<script>
  import {
    QCarousel,
    dom,
    QWindowResizeObservable,
    QPagination
  } from 'quasar'
  import LSlideCards from './slideCards'
  export default {
    components: {
      QCarousel,
      LSlideCards,
      QWindowResizeObservable,
      QPagination
    },
    props: {
      cards: {
        type: Array,
        required: false,
        default: function () {
          return Array(368).fill({})
        }
      }
    },
    data: function () {
      return {
        slide: 1,
        width: dom.width(window),
        dots: true
      }
    },
    computed: {
      qtdSlides: {
        get: function () {
          if (this.width > 991) {
            return Math.ceil(this.cards.length / 4)
          }
          else if (this.width > 767) {
            return Math.ceil(this.cards.length / 2)
          }
          else {
            return this.cards.length
          }
        },
        set: function (width) {
          this.width = width
        }
      },
      cardsPart: function () {
        let cardsPerSlide = Math.ceil(this.cards.length / this.qtdSlides)
        let ini = (this.slide - 1) * cardsPerSlide
        let fim = ((this.slide - 1) + 1) * cardsPerSlide
        return this.cards.slice(ini, fim)
      },
      prev: function () {
        return this.slide === 1 ? this.qtdSlides : (this.slide - 1)
      },
      next: function () {
        return this.slide === this.qtdSlides ? 1 : (this.slide + 1)
      }
    },
    methods: {
      swipe: function (slide) {
        this.slide = slide + 1
      },
      resize: function (size) {
        this.qtdSlides = size.width
        this.changeDots()
      },
      changeDots: function () {
        console.log($('.carouselCards .q-carousel-dots').height() <= 48)
        this.dots = ($('.carouselCards .q-carousel-dots').height() <= 48)
        if (this.dots) {
          $('.carouselCards .q-carousel-toolbar').css('visibility', 'visible')
          $('.carouselCards .q-carousel-toolbar').css('position', 'static')
        }
        else {
          $('.carouselCards .q-carousel-toolbar').css('visibility', 'hidden')
          $('.carouselCards .q-carousel-toolbar').css('position', 'absolute')
        }
      }
    },
    mounted: function () {
      this.changeDots()
    }
  }
</script>

<style lang="stylus">
  @import '~src/themes/app.variables.styl';
  .carouselCards .q-carousel-track{
    height: auto!important;
  }
  .carouselCards .q-carousel-dots .q-icon{
    color: $secondary!important;
    opacity: 1!important;
  }
  @media screen and (max-width: 1199px){
    .carouselCards .q-carousel-toolbar{
      padding-left: 30px;
      padding-right: 30px;
    }
    .carouselCards .carouselCardsPagination{
      padding: 8px 30px!important;
    }
  }
  @media screen and (min-width: 1200px){
    .carouselCards .q-carousel-toolbar{
      padding-left: calc((100% - 1140px)/2)!important;
      padding-left: -webkit-calc((100% - 1140px)/2)!important;
      padding-left: -moz-calc((100% - 1140px)/2)!important;
      padding-left: -o-calc((100% - 1140px)/2)!important;
      padding-left: -ms-calc((100% - 1140px)/2)!important;

      padding-right: calc((100% - 1140px)/2)!important;
      padding-right: -webkit-calc((100% - 1140px)/2)!important;
      padding-right: -moz-calc((100% - 1140px)/2)!important;
      padding-right: -o-calc((100% - 1140px)/2)!important;
      padding-right: -ms-calc((100% - 1140px)/2)!important;
    }
    .carouselCards .carouselCardsPagination{
      padding: 8px calc((100% - 1140px)/2)!important;
      padding: 8px -webkit-calc((100% - 1140px)/2)!important;
      padding: 8px -moz-calc((100% - 1140px)/2)!important;
      padding: 8px -o-calc((100% - 1140px)/2)!important;
      padding: 8px -ms-calc((100% - 1140px)/2)!important;
    }
  }

  @media screen and (max-width: 1219px){
    .carouselCards .q-carousel-left-button .q-icon,
    .carouselCards .q-carousel-right-button .q-icon{
      width: 26px!important;
      height: 26px!important;
      font-size: 18px!important;
    }
  }
  @media screen and (min-width: 1220px) and (max-width: 1283px){
    .carouselCards .q-carousel-left-button .q-icon,
    .carouselCards .q-carousel-right-button .q-icon{
      width: 32px!important;
      height: 32px!important;
      font-size: 21px!important;
    }
  }
  @media screen and (min-width: 1284px){
    .carouselCards .q-carousel-left-button .q-icon,
    .carouselCards .q-carousel-right-button .q-icon{
      width: 42px!important;
      height: 42px!important;
      font-size: 32px!important;
    }
  }

  .carouselCards .q-carousel-left-button,
  .carouselCards .q-carousel-right-button{
    padding: 0!important;
  }
  @media screen and (max-width: 1199px){
    .carouselCards .q-carousel-left-button,
    .carouselCards .q-carousel-right-button{
      max-width: 30px!important;
      width: 30px!important;
    }
  }
  @media screen and (min-width: 1200px) and (max-width: 1283px){
    .carouselCards .q-carousel-left-button,
    .carouselCards .q-carousel-right-button{
      max-width: calc((100% - 1140px)/2)!important;
      max-width: -webkit-calc((100% - 1140px)/2)!important;
      max-width: -moz-calc((100% - 1140px)/2)!important;
      max-width: -o-calc((100% - 1140px)/2)!important;
      max-width: -ms-calc((100% - 1140px)/2)!important;

      width: calc((100% - 1140px)/2)!important;
      width: -webkit-calc((100% - 1140px)/2)!important;
      width: -moz-calc((100% - 1140px)/2)!important;
      width: -o-calc((100% - 1140px)/2)!important;
      width: -ms-calc((100% - 1140px)/2)!important;
    }
  }
  @media screen and (min-width: 1284px){
    .carouselCards .q-carousel-left-button,
    .carouselCards .q-carousel-right-button{
      max-width: 70px!important;
      width: 70px!important;
    }
  }
  .carouselCards .q-carousel-left-button .q-icon,
  .carouselCards .q-carousel-left-button .q-icon:active,
  .carouselCards .q-carousel-left-button .q-icon:visited,
  .carouselCards .q-carousel-left-button .q-icon:focus,
  .carouselCards .q-carousel-right-button .q-icon,
  .carouselCards .q-carousel-right-button .q-icon:active,
  .carouselCards .q-carousel-right-button .q-icon:visited,
  .carouselCards .q-carousel-right-button .q-icon:focus{
    opacity: 1!important;
    padding: 0!important;
    color: $primary!important;
    background-color: $secondary!important;
  }
  .carouselCards .carouselCardsPagination{
    font-family: $fprimary!important;
    font-size: 18px!important;
    color: $tsecondary!important;
    text-align: center!important;
  }
</style>

I’ve been refactoring all my components, now it’s the turn. I’ve learned more about Vuejs and will spare you the trouble of analyzing this huge code.

My question is this:

  1. Is there any way to perform an action with when DOM is fully loaded? (In this case the DOM part of the component)

  2. I need to check this because I’m manipulating a third party component, so I do not want to change the component’s internal code, so I need to execute the action when all child elements complete loading / rendering their respective parts of the DOM. It’s possible?

If you have any examples, you are welcome.

People from the vuejs core or any other experienced here in the forum, please help, already have many days that I have this problem.

mounted() {
  this. $nextTick(function () {
    // put code here
  })
}

Note: I am not native speaker of English I am using google translator.

That’s exactly the problem.
The component in question has many children, who also have children.
I already tried it with $ nextTick too, but it runs a little before the children finish loading.

In my case the problem is not knowing when the Object / component loaded, this I know how to do, I really need to know when the DOM of that component of all the children finished loading.

I depend on this completed load to perform the action I want.
Because it is a visual action.

Detail about the operation:

I have a carousel full of slides, inside these slides there are cards and inside these cards there are buttons and other components.

Each slide is responsive, so in large screens it can hold up to 4 cards, in medium screens 2 and in small screens 1 card.

Who controls how many slides will be required to hold a certain amount of cards is the carousel component in qtdSlides, because this is obviously a computed property.

The amount of slides is represented by dots on the bottom, the problem is that I can put a dynamic amount of cards in that carousel, and I’m testing the worst case with hundreds of slides.

In summary in these cases the amount of points in such cases is so large that it exceeds 2 lines, this is visually ugly, so in these cases I want to show a text with something like “1 of …”.

When the screen resizes this already works, because I have events for this but the main missing is the loading. And this, necessarily depends on the complete loading (visual) of all the children, of that component.

Detail: The carousel is a third-party component, more specifically the quasar framework, I have already customized everything I could, and let’s face it, not very interesting to change things in a framework. I’m asking here, because this is a question related to vuejs, not a problem in the quasar component.

Carousel documentation:
http://quasar-framework.org/components/carousel.html

If there is another way to solve it, I’m also open to suggestions, I’m refactoring this component, so if there are drastic changes this is the time.

For this component I found the variable where I should check this, on mounted I should check the lenght of that variable:

this.$refs.slideCards.$el.children [0].children [3] .children[0].children

or check change in value of this:

this.$refs.slideCards.$el.children[0].children [3].children [0].clientHeight

Both when mounted when nextTick execute these variables have not yet been loaded, so the value returns 1px, because the children (dots) have not yet been loaded.

So I repeat the question, and now even for core people, is there any way to watch the change of these variables or even some low-level event from the vuejs where I can detect that the page was actually rendered?

Note: When I say rendered, I am not referring to the loading of the vue object, I am referring to the changes of the virtual DOM have actually been applied in the real DOM. As with the onload event in pure javascript.

If it exists, please tell me how, for a long time I have this problem, I need to solve it.

Is there an event that runs when the vnode is fully rendered? What?

Component Mounted

You should put the this.$nextTick call in mounted, as @LinusBorg and the docs suggest. This should work for you.

I also tested this before posting the first time, but as I said, the q-carousel is a third-party component (of the quasar framework).

When I add the slots, in this case the l-slide-cards it calculates internally the amount of dots (this is a slider), the amount of slides and dots is equivalent to my qtdSlides variable.

I have already noticed, but the part of the dots, which is inside the q-carousel has no refs, so it does not have access by the standard of the vue. I have jquery installed as well, so I can do this via DOM, the issue is that when mounted or nextTick are executed, this has not yet been applied to the actual DOM and the value in pixels returns me 1, which is not expected.

Note: From the height of this div where the dots are I will determine if I will use the standard q-carousel or textual notation to represent the number of slides.

Due to all that has been reported, there are only two ways I can identify this:

  1. Watch the resize of this div and issue events when this is greater than 1.
  2. Capture some event that assures me that the page has been rendered, this assures me that the height of the div is already calculated. NOTE: onload does not work, because the vuejs can make changes without reloading the page.

Related Topic:
https://forum.vuejs.org/t/how-to-watch-the-height-change-dom-of-an-element-in-vuejs/21290/7

From the topic:

I got the answer that solved the problem.

Code:

<template>
  <div
    class="row justify-center"
  >
    <q-window-resize-observable
      ref="resizeWindow"/>
    <l-slide-cards
      v-if="qtdSlides <= 1"
      :load="true"
      :cards="cardsPart"/>
    <q-carousel
      v-else
      infinite
      arrows
      dots
      @slide="swipe"
      :id='_uid'
      :class="[
        'carouselCards',
        {'hideDots': firstLoad || !dots}
      ]"
    >
      <l-slide-cards
        slot="slide"
        v-for="i in qtdSlides"
        :key="i"
        :load="(slide == i || prev == i || next == i)"
        :cards="cardsPart"/>
      <div
        v-if="!firstLoad && !dots"
        class="carouselCardsPagination"
      >
        {{slide}} de {{qtdSlides}}
      </div>
    </q-carousel>
  </div>
</template>

<script>
  import {
    QCarousel,
    dom,
    QWindowResizeObservable,
    QPagination
  } from 'quasar'
  import LSlideCards from './slideCards'
  export default {
    components: {
      QCarousel,
      LSlideCards,
      QWindowResizeObservable,
      QPagination
    },
    props: {
      cards: {
        type: Array,
        required: false,
        default: function () {
          let c = Array(368).fill(null).map(function () {
            return {}
          })
          c._objPadrao = true
          return c
        }
      }
    },
    data () {
      return {
        slide: 1,
        width: dom.width(window),
        firstLoad: true,
        slideHeight: 0
      }
    },
    computed: {
      normalizedCards: function () {
        let c = []
        c._fill = function (length) {
          let size = length === 0 ? 100 : length
          return Array(size).fill(null).map(function () {
            return {}
          })
        }

        let cards = this.normalizeProp(this.cards, c, true)
        return this.addReactivity(cards)
      },
      qtdSlides: {
        get: function () {
          if (this.width > 991) {
            return Math.ceil(this.normalizedCards.length / 4)
          }
          else if (this.width > 767) {
            return Math.ceil(this.normalizedCards.length / 2)
          }
          else {
            return this.normalizedCards.length
          }
        },
        set: function (width) {
          this.width = width
        }
      },
      cardsPart: function () {
        return this.normalizedCards.slice(this.cardPosIni, this.cardPosFinal)
      },
      cardsPerSlide: function () {
        return Math.ceil(this.normalizedCards.length / this.qtdSlides)
      },
      cardPosIni: function () {
        return (this.slide - 1) * this.cardsPerSlide
      },
      cardPosFinal: function () {
        return ((this.slide - 1) + 1) * this.cardsPerSlide
      },
      prev: function () {
        return this.slide === 1 ? this.qtdSlides : (this.slide - 1)
      },
      next: function () {
        return this.slide === this.qtdSlides ? 1 : (this.slide + 1)
      },
      dots: function () {
        console.log(this.slideHeight <= 48)
        return this.slideHeight <= 48
      }
    },
    methods: {
      swipe: function (slide) {
        this.slide = slide + 1
      },
      resize: function (size) {
        console.log('resize')
        this.qtdSlides = size.width
      },
      changeSlideHeight: async function () {
        await this.$nextTick()
        this.slideHeight = this.$el.lastChild.lastChild.children[3].firstChild.clientHeight
      }
    },
    watch: {
      width: async function () {
        await this.changeSlideHeight()
      },
      qtdSlides: async function () {
        await this.changeSlideHeight()
      }
    },
    mounted: async function () {
      await this.changeSlideHeight()
      this.firstLoad = false
      this.$refs.resizeWindow.$on('resize', this.resize)
    }
  }
</script>

<style lang="stylus">
  @import '~src/themes/app.variables.styl';
  .carouselCards .q-carousel-track{
    height: auto!important;
  }
  .carouselCards .q-carousel-dots .q-icon{
    color: $secondary!important;
  }
  .hideDots .q-carousel-inner .q-carousel-toolbar{
    position: absolute!important;
    visibility: hidden!important;
  }
  .carouselCards .q-carousel-toolbar{
    position: static!important;
    visibility: visible!important;
  }
  .carouselCards .q-carousel-toolbar,
  .carouselCards .q-carousel-dots,
  .carouselCards .q-carousel-dots .q-icon{
    opacity: 1!important;
  }
  @media screen and (max-width: 1199px){
    .carouselCards .q-carousel-toolbar{
      padding-left: 30px;
      padding-right: 30px;
    }
    .carouselCards .carouselCardsPagination{
      padding: 8px 30px!important;
    }
  }
  @media screen and (min-width: 1200px){
    .carouselCards .q-carousel-toolbar{
      padding-left: calc((100% - 1140px)/2)!important;
      padding-left: -webkit-calc((100% - 1140px)/2)!important;
      padding-left: -moz-calc((100% - 1140px)/2)!important;
      padding-left: -o-calc((100% - 1140px)/2)!important;
      padding-left: -ms-calc((100% - 1140px)/2)!important;

      padding-right: calc((100% - 1140px)/2)!important;
      padding-right: -webkit-calc((100% - 1140px)/2)!important;
      padding-right: -moz-calc((100% - 1140px)/2)!important;
      padding-right: -o-calc((100% - 1140px)/2)!important;
      padding-right: -ms-calc((100% - 1140px)/2)!important;
    }
    .carouselCards .carouselCardsPagination{
      padding: 8px calc((100% - 1140px)/2)!important;
      padding: 8px -webkit-calc((100% - 1140px)/2)!important;
      padding: 8px -moz-calc((100% - 1140px)/2)!important;
      padding: 8px -o-calc((100% - 1140px)/2)!important;
      padding: 8px -ms-calc((100% - 1140px)/2)!important;
    }
  }

  @media screen and (max-width: 1219px){
    .carouselCards .q-carousel-left-button .q-icon,
    .carouselCards .q-carousel-right-button .q-icon{
      width: 26px!important;
      height: 26px!important;
      font-size: 18px!important;
    }
  }
  @media screen and (min-width: 1220px) and (max-width: 1283px){
    .carouselCards .q-carousel-left-button .q-icon,
    .carouselCards .q-carousel-right-button .q-icon{
      width: 32px!important;
      height: 32px!important;
      font-size: 21px!important;
    }
  }
  @media screen and (min-width: 1284px){
    .carouselCards .q-carousel-left-button .q-icon,
    .carouselCards .q-carousel-right-button .q-icon{
      width: 42px!important;
      height: 42px!important;
      font-size: 32px!important;
    }
  }

  .carouselCards .q-carousel-left-button,
  .carouselCards .q-carousel-right-button{
    padding: 0!important;
  }
  @media screen and (max-width: 1199px){
    .carouselCards .q-carousel-left-button,
    .carouselCards .q-carousel-right-button{
      max-width: 30px!important;
      width: 30px!important;
    }
  }
  @media screen and (min-width: 1200px) and (max-width: 1283px){
    .carouselCards .q-carousel-left-button,
    .carouselCards .q-carousel-right-button{
      max-width: calc((100% - 1140px)/2)!important;
      max-width: -webkit-calc((100% - 1140px)/2)!important;
      max-width: -moz-calc((100% - 1140px)/2)!important;
      max-width: -o-calc((100% - 1140px)/2)!important;
      max-width: -ms-calc((100% - 1140px)/2)!important;

      width: calc((100% - 1140px)/2)!important;
      width: -webkit-calc((100% - 1140px)/2)!important;
      width: -moz-calc((100% - 1140px)/2)!important;
      width: -o-calc((100% - 1140px)/2)!important;
      width: -ms-calc((100% - 1140px)/2)!important;
    }
  }
  @media screen and (min-width: 1284px){
    .carouselCards .q-carousel-left-button,
    .carouselCards .q-carousel-right-button{
      max-width: 70px!important;
      width: 70px!important;
    }
  }
  .carouselCards .q-carousel-left-button .q-icon,
  .carouselCards .q-carousel-left-button .q-icon:active,
  .carouselCards .q-carousel-left-button .q-icon:visited,
  .carouselCards .q-carousel-left-button .q-icon:focus,
  .carouselCards .q-carousel-right-button .q-icon,
  .carouselCards .q-carousel-right-button .q-icon:active,
  .carouselCards .q-carousel-right-button .q-icon:visited,
  .carouselCards .q-carousel-right-button .q-icon:focus{
    opacity: 0.75!important;
    padding: 0!important;
    color: $primary!important;
    background-color: $secondary!important;
  }
  .carouselCards .q-carousel-left-button .q-icon:hover,
  .carouselCards .q-carousel-right-button .q-icon:hover{
    opacity: 1!important;
  }
  .carouselCards .carouselCardsPagination{
    font-family: $fprimary!important;
    font-size: 18px!important;
    color: $tsecondary!important;
    text-align: center!important;
  }
</style>

Small correction:

I see what you are doing now (I think). Instead of hooking into the DOM, you could use QResizeObservable to get what you want.

This was exactly what I was going to do if the quasar framework staff had left a $ref for dots, so I could add the event to the $on, but intentionally they did not, so I just needed a little hack.

But the problem has spread beyond that, using the debugger I realized that because of the asynchronous nature of the javascript, it is not enough only to identify if the div where the dots are has height, because in the middle of the rendering process the names used to map the font that is used to represent the dots pops up, so I can only guarantee this when the div where the dots are has height and the width of all the dots is 24px.

So I say, if the vuejs had any event that would identify when the browser finishes rendering or reflow, or whatever the technical name of that, it would have good use in applications like that.

In short, until a better solution appears, it works.

Correct me if I was wrong.

I have the same challenge. I need to know when the parent and ALL of it’s children are mounted and so far I can’t find a reliable way. nextTick only works when the app is already spun up and you are navigating your SPA. It does NOT fire as you would expect when you first hit the page.

Im my case I’m wiring up Yotpo reviews and I need to know when all comps are ready before I tell it to refresh them :frowning: . I have a couple children’s children comp that fire mounted() well after the parent.

I would like to see a demo of that, because that’s not really possible. Children always mount before their parent in a sync heinous render.

A delayed render and mount can only happen if those children are rendered conditionally after i.e. some data has been loaded asynchronously. In that case, you will have to provide that information, it’s your business logic tbh at dictates when that moment is actually there.

With a global state like vuex that’s not that hard to do. Without it, it can be hairy.

Yeah we are using vuex and vue-router. And yes technically the children’s children comps are v-if’ed based on data from an api, so I guess that would probably make sense then. The product line comp is rendered first, then initialized, hits api’s via vuex and then those would trigger those children’s children to update since they have real data… ug this is going to be fun.

That would also explain why when you nav around the app the nexttick works fine because the data that the comp needs to render is already in vuex and just gets hydrated by the api. duh… my brain is fried, thanks for making me think a diff angle.