Managing VueJS components and Observers during the rendering process to ensure smooth functionality in a multi-phase environment

Situation:

As part of my development work, I am creating a Vue scroll component that encompasses a variable number of HTML sections. This component dynamically generates vertical page navigation, allowing users to either scroll or jump to specific page locations onScroll.

Specifics:

a. In this scenario, the scroll component wraps around 3 sections. Each section ID follows the format "js-page-section-{{index}}"

b. The goal is to retrieve the list of section nodes and then dynamically construct side navigation based on the number of nodes found in the query matching selector criteria. For example, having three sections will result in three page section navigation items. All side navigation IDs begin with “js-side-nav-{{index}}".

c. After rendering the side navigation, it's essential to query all navigation nodes for managing classes, heights, display, opacity, etc. using

 document.querySelectorAll('*[id^="js-side-nav"]');

MODIFICATION

After conducting some research, here are the potential solutions to my dilemma involving 3-phase DOM state management: STEP 1. Reading all nodes equal to x, STEP 2. Building Side Nav scroll based on n number of nodes in the document, and STEP 3. Reading all nav nodes to synchronize with the document nodes' scroll:

  1. Implementing an event system with $emit() && $on. However, this approach can become cluttered quickly and seems like a subpar solution. I often found myself resorting to $root.
  2. Vuex could be used, but it might be an overcomplication for this task.
  3. Utilizing sync works, but it primarily focuses on parent-child property state management which again requires $emit() && $on.
  4. Introducing a service class based on Promise seems like a suitable solution, although handling multiple promises can get cumbersome.
  5. I experimented with Vue $ref, but it appears more suitable for managing state rather than multi-stage DOM manipulation where an observer event approach excels.
  6. The most effective solution seems to be Vue's $nextTick(), which resembles AngularJS' $digest. Essentially, it functions as a setTimeout()-like mechanism pausing for the next digest cycle. However, there can be instances where the tick doesn't sync with the required time, prompting me to develop a throttle method. Below is the code update for reference.

The revamped watch with nextTick()

        watch: {
            'page.sections':  {
                handler(nodeList, oldNodeList){
                    if (this.isNodeList(nodeList) && _.size(nodeList) && this.sideNavActive) {
                        return this.$nextTick(this.sideNavInit);
                    }
                },
                deep: true
            },
        },

The REVAMPED Vue component

<template>
    <div v-scroll="handleScroll">
        <nav class="nav__wrapper" id="navbar-example">
            <ul class="nav">
                <li role="presentation"
                    :id="sideNavPrefix + '-' + (index + 1)"
                    v-for="(item, key,index) in page.sections">
                    <a :href="'#' + getAttribute(item,'id')">
                    <p class="nav__counter" v-text="('0' + (index + 1))"></p>
                        <h3 class="nav__title" v-text="getAttribute(item,'data-title')"></h3>
                        <p class="nav__body" v-text="getAttribute(item,'data-body')"></p>
                    </a>
                </li>
            </ul>
        </nav>
        <slot></slot>
    </div>
</template>

<script>
    import ScrollPageService from '../services/ScrollPageService.js';

    const _S = "section", _N = "sidenavs";

    export default {
        name: "ScrollSection",
        props: {
            nodeId: {
                type: String,
                required: true
            },
            sideNavActive: {
                type: Boolean,
                default: true,
                required: false
            },
            sideNavPrefix: {
                type: String,
                default: "js-side-nav",
                required: false
            },
            sideNavClass: {
                type: String,
                default: "active",
                required: false
            },
            sectionClass: {
                type: String,
                default: "inview",
                required: false
            }
        },
        directives: {
            scroll: {
                inserted: function (el, binding, vnode) {
                    let f = function(evt) {
                        if (binding.value(evt, el)) {
                            window.removeEventListener('scroll', f);
                        }
                    };
                    window.addEventListener('scroll', f);
                }
            },
        },
        data: function () {
            return {
                scrollService: {},
                page: {
                    sections: {},
                    sidenavs: {}
                }
            }
        },
        methods: {
            getAttribute: function(element, key) {
                return element.getAttribute(key);
            },
            updateViewPort: function() {
                if (this.scrollService.isInCurrent(window.scrollY)) return;

                [this.page.sections, this.page.sidenavs] = this.scrollService.updateNodeList(window.scrollY);

            },
            handleScroll: function(evt, el) {
                if (!(this.isScrollInstance())) {
                    return this.$nextTick(this.inViewportInit);
                }

                this.updateViewPort();
            },
            getNodeList: function(key) {
                this.page[key] = this.scrollService.getNodeList(key);
            },
            isScrollInstance: function() {
                return this.scrollService instanceof ScrollPageService;
            },
            sideNavInit: function() {
                if (this.isScrollInstance() && this.scrollService.navInit(this.sideNavPrefix, this.sideNavClass)) this.getNodeList(_N);
            },
            inViewportInit: function() {
                if (!(this.isScrollInstance()) && ((this.scrollService = new ScrollPageService(this.nodeId, this.sectionClass)) instanceof ScrollPageService)) this.getNodeList(_S);
            },
            isNodeList: function(nodes) {
                return NodeList.prototype.isPrototypeOf(nodes);
            },
        },
        watch: {
            'page.sections':  {
                handler(nodeList, oldNodeList){
                    if (this.isNodeList(nodeList) && _.size(nodeList) && this.sideNavActive) {
                        return this.$nextTick(this.sideNavInit);
                    }
                },
                deep: true
            },
        },
        mounted() {
            return this.$nextTick(this.inViewportInit);
        },
    }

</script>

END EDIT


ORIGINAL POST

Issue & Inquiry:

ISSUE:

The querying of sections and rendering of navs operate correctly. However, the querying of nav elements fails due to incomplete DOM rendering. Consequently, I have resorted to using a setTimeout() function even when employing a watch, requiring a timeout workaround.

INQUIRY:

Is there a promise or observer in Vue or JS that I can utilize to determine when the DOM has completed rendering the nav elements so I can read them? For instance, in AngularJS, we might employ $observe.

HTML EXAMPLE

    <html>
        <head></head>
        <body>
            <scroll-section>
                <div id="js-page-section-1"
                     data-title="One"
                     data-body="One Body">
                </div>
                <div id="js-page-section-2"
                     data-title="Two"
                     data-body="Two Body">
                </div>
                <div id="js-page-section-3"
                     data-title="Three"
                     data-body="THree Body">
                </div>
            </scroll-section>
        </body>
    </html>

Vue Component

<template>
    <div v-scroll="handleScroll">
        <nav class="nav__wrapper" id="navbar-example">
            <ul class="nav">
                <li role="presentation"
                    :id="[idOfSideNav(key)]"
                    v-for="(item, key,index) in page.sections.items">
                        <a :href="getId(item)">
                        <p class="nav__counter">{{key}}</p>
                            <h3 class="nav__title" v-text="item.getAttribute('data-title')"></h3>
                            <p class="nav__body" v-text="item.getAttribute('data-body')"></p>
                        </a>
                </li>
            </ul>
        </nav>

        <slot></slot>

    </div>
</template>

<script>
    export default {
        name: "ScrollSection",

        directives: {
            scroll: {
                inserted: function (el, binding, vnode) {
                    let f = function(evt) {
                        _.forEach(vnode.context.page.sections.items, function (elem,k) {
                            if (window.scrollY >= elem.offsetTop && window.scrollY <= (elem.offsetTop + elem.offsetHeight)) {
                                if (!vnode.context.page.sections.items[k].classList.contains("in-viewport") ) {
                                    vnode.context.page.sections.items[k].classList.add("in-viewport");
                                }
                                if (!vnode.context.page.sidenavs.items[k].classList.contains("active") ) {
                                    vnode.context.page.sidenavs.items[k].classList.add("active");
                                }
                            } else {
                                if (elem.classList.contains("in-viewport") ) {
                                    elem.classList.remove("in-viewport");
                                }
                                vnode.context.page.sidenavs.items[k].classList.remove("active");
                            }
                        });

                        if (binding.value(evt, el)) {
                            window.removeEventListener('scroll', f);
                        }
                    };

                    window.addEventListener('scroll', f);
                },
            },

        },
        data: function () {
            return {
                page: {
                    sections: {},
                    sidenavs: {}
                }
            }
        },
        methods: {
            handleScroll: function(evt, el) {
                // Removed for brevity
            },
            idOfSideNav: function(key) {
                return "js-side-nav-" + (key+1);
            },
            classOfSideNav: function(key) {
                if (key==="0") {return "active"}
            },
            elementsOfSideNav:function() {
                this.page.sidenavs = document.querySelectorAll('*[id^="js-side-nav"]');
            },
            elementsOfSections:function() {
                this.page.sections = document.querySelectorAll('*[id^="page-section"]');
            },

        },
        watch: {
            'page.sections': function (val) {
                if (_.has(val,'items') && _.size(val.items)) {
                    var self = this;
                    setTimeout(function(){
                        self.elementsOfSideNavs();
                    }, 300);
                }
            }
        },
        mounted() {
            this.elementsOfSections();
        },

    }


</script>

Answer №1

I have some insights to share that might be helpful for you. A function created by a friend of mine came to mind as I read your question, which we have used in multiple scenarios.

"Is there a promise or observer in Vue or JS that I can utilize to validate if the DOM has completed rendering the navigation elements so that I can access them?"

The function I am referring to is available here (source). It involves executing a function (observation) and attempting to fulfill it within a specified number of attempts.

You may find this useful during component creation or page initialization; although I must admit, I may not fully grasp your specific situation. Nevertheless, certain aspects of your query immediately brought this functionality to my attention. "...wait for something to happen and then make something else happen."

<> Credits to @Markkop, the creator behind that snippet/function =)

/**
 * Waits for object existence using a function to retrieve its value.
 *
 * @param { function() : T } getValueFunction
 * @param { number } [maxTries=10] - Number of tries before the error catch.
 * @param { number } [timeInterval=200] - Time interval between the requests in milis.
 * @returns { Promise.<T> } Promise of the checked value.
 */
export function waitForExistence(getValueFunction, maxTries = 10, timeInterval = 200) {
  return new Promise((resolve, reject) => {
    let tries = 0
    const interval = setInterval(() => {
      tries += 1
      const value = getValueFunction()
      if (value) {
        clearInterval(interval)
        return resolve(value)
      }

      if (tries >= maxTries) {
        clearInterval(interval)
        return reject(new Error(`Could not find any value using ${tries} tentatives`))
      }
    }, timeInterval)
  })
}

Example

function getPotatoElement () {
  return window.document.querySelector('#potato-scroller')
}

function hasPotatoElement () {
  return Boolean(getPotatoElement())
}

// when something load
window.document.addEventListener('load', async () => {
  // we try sometimes to check if our element exists
  const has = await waitForExistence(hasPotatoElement)
  if (has) {
    // and if it exists, we do this
    doThingThatNeedPotato()
  }

  // or you could use a promise chain
  waitForExistence(hasPotatoElement)
    .then(returnFromWaitedFunction => { /* hasPotatoElement */
       if (has) {
         doThingThatNeedPotato(getPotatoElement())
       }
    }) 
})

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Tips on customizing image borders/masks with hover effects

Is there a React library or a simple CSS trick to create an image's canvas cropping effect on hover? For example, similar to this: Thanks in advance! ...

Having difficulties in storing the checkbox selections

Whenever I switch components and return back, the checkboxes do not persist. I want to ensure that the checked checkboxes stay checked. For more information and code samples, you can visit this CodeSandbox link: CodeSandbox Link. courses.js import React ...

Error message occurred in Laravel, specifically in the foreach loop, when integrating with Vue.js due to incorrect

I need to save my data in the database for two tables: Cashoutdetails and Cashout expenses. While I am able to store data for Cashoutdetails without any issues, I encounter an error when trying to store data for the cashout expenses table. Below is the cod ...

Switching <div></div> to inline-block doesn't seem to have any effect (repl.it provided)

Could someone assist me with changing the div element to an inline block? I've been having trouble with it. For reference, here is my repl.it: ...

Following a Node/Npm Update, Sails.js encounters difficulty locating the 'ini' module

While developing an application in Sails.js, I encountered an authentication issue while trying to create user accounts. Despite my efforts to debug the problem, updating Node and NPM only resulted in a different error. module.js:338 throw err; ...

Pass a URL string to a different script using Node.js

Currently, I am delving into the world of Node.js, utilizing Express with Jade and Mongoose as my primary tools. Originating from a background in PHP, transitioning to Python, and embracing MVC principles through Django, my aspiration is to create a multip ...

Caution: The Vue Class Based Component is signalling that a property is not defined on the instance, yet it is being

I've been experimenting with creating a Vue component using vue-class-component and TypeScript. I referenced the official documentation here: https://github.com/vuejs/vue-class-component. Despite defining the data within the class as shown below, I en ...

Supervising the organization of HTML sections for an offline web application

Currently, I am developing an offline HTML5 application that involves a significant amount of DOM manipulation through the creation of HTML strings within JavaScript functions. For example: var html=''; html+='<div class="main">&apos ...

Access the Vue instance from a different element instance

I've developed a leaflet map using vue.js. There's a method I've created called 'showSubmit' that needs to be triggered on the leaflet marker moveend event. Here's what I'm currently doing: this.map.markers.user.on("move ...

Difficulty installing Vue in Laravel

After attempting to install Vue.js within the Laravel framework, I noticed that it failed to create an asset folder or make any changes to the app.js file located inside the asset folder. Despite running `npm install` and `npm run watch`, when I checked ...

Using 'require' within a nested directive that relies on the parent directive in AngularJS

Implementing a sub directive to be utilized in multiple directives is my current challenge. These parent directives share a common controller that has useful methods for updating scope variables within these directives: (potentially changing controllers ...

React does not allow _id to be used as a unique key

When I retrieve the categories from my allProducts array fetched from the database using redux-toolkit, I filter and then slice the array for pagination before mapping over it. However, I keep encountering a warning: Each child in a list should have a un ...

Starting the download of the canvas featuring a stylish border design

I'm facing an issue where the canvas border is not showing up when I download the canvas with a border. The border appears on the screen, but it doesn't get included in the downloaded canvas. let canvas=qrRef.current.querySelector("canvas&qu ...

Why is a <script> tag placed within a <noscript> tag?

Lately, I've been exploring various websites with unique designs and content by inspecting their source code. One site that caught my attention was Squarespace, which had blocks of <script> tags within a <noscript> tag: <!-- Page is at ...

Having trouble with jQuery's scrollLeft function on elements that are not currently visible

Within a large container DIV that contains numerous other elements and has a scroll bar, an issue arises when determining the value of scrollLeft. When the DIV is visible, the scrollLeft() function returns the correct value, but when the element is hidden, ...

where is the yarn global registry located?

After updating yarn to point to my custom registry and verifying the changes, here is what I found: $yarn config list -g yarn config v1.22.10 info yarn config { 'version-tag-prefix': 'v', 'version-git-tag': true, ' ...

Using Jquery to handle input data in a form

Using jQuery, I have set up an AJAX function to fetch data from a database in real-time as the user fills out a form with 5 input fields. Currently, my code looks like this: $("#searchtype, #searchtext, #searchtag, #daterangefrom, #daterangeto").on("load ...

Guide me on setting up a progress bar using the XHR function to display the upload percentage

let formData2 = new FormData(); formData2.append('_token', vm.response._token); formData2.append('file', vm.response.content[i].path); formData2.append('type', vm.res ...

Execute a specialized function with imported modules and specified parameters

Within an npm project, I am looking to execute a custom function with arguments, or ideally provide it as a script in the package.json file like this: npm run custom-function "Hello, World". Currently, I have a file called src/myFunction.ts: import * as e ...

Preventing CSRF Errors when activating API endpoints in Node Express JS by ensuring that bypassing the error message with next() function is ineffective

In this middleware block, I have implemented a mechanism to render a 404 page when an invalid CSRF token is detected. /* * Middleware for rendering 404 page on invalid csrf token */ this.app.use((err: any, req: Request, res: ...