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

Express fails to handle the POST request

Using ejs, express, nodeJS and mySQL has been great so far. However, I'm facing an error with this code: Cannot POST /search. I believe the index.ejs and app.js files are okay, but I suspect there's a problem with the searchRouter... app.js cons ...

Acquiring the console log data from Firefox version 43 using Selenium, all without the use of any

Can Selenium retrieve the console.log from browsers like Firefox 43? If so, how can it be done? My settings are as follows: DesiredCapabilities capabilities = DesiredCapabilities.firefox(); LoggingPreferences logs = new LoggingPreferences(); logs.enable( ...

Is there a way to identify the index of user input when using the .map method?

I'm currently utilizing the Array.prototype.map() method to present an array within a table. Each row in this table includes both an input field and a submit button that corresponds to each element from the Array.prototype.map() function. Is there a w ...

Saving a picture to local storage with the file input type in ReactJS

I am attempting to save an image in the browser storage once a user selects an image from their computer. <div className="add_grp_image_div margin_bottom"> <img src={img_upload} className="add_grp_image"/> <input type="file" class ...

PHP Troubleshooting: Resolving Ajax Problems in Symfony 4

I am currently learning Symfony and attempting to integrate Ajax with Symfony. I have placed the Ajax code within a javascript block in Twig and added a simple function in the controller file to test its functionality. However, it seems that the Ajax is no ...

Retrieve user input from an HTML form and pass it as a parameter in a jQuery AJAX request

Is there a way to pass a value from a user input in an HTML file to jQuery.ajax? Take a look at the code snippet from my JS file: jQuery(document).ready(function() { jQuery.ajax({ type: 'POST', url: 'myurl.asp ...

Is there a way to allow only the block code to shift while keeping the other span tags stationary?

Is it possible to change the text without affecting other span tags in the code? I want to make sure only this specific text is updated. How can I achieve that? var para_values = [ { content: "BRAND " }, { content: "MISSION" } ]; functi ...

Is it possible to trigger a change or click event based on a condition defined by a v-select component?

I have a v-select field that allows users to select their user type. When a user selects a type, the v-model is updated accordingly. I want to implement a method that triggers a message when a specific user type is chosen, but I don't want the message ...

Changing the placeholder text in a text input field based on the value of the v-model in Vue

I am looking to update the placeholder of a text input using Vue.js data binding. Below is my code snippet. <select2 :options="power_options" v-model="power"> <option selected value="hp">hp</option> & ...

What is causing Puppeteer to not wait?

It's my understanding that in the code await Promise.all(...), the sequence of events should be: First console.log is printed 9-second delay occurs Last console.log is printed How can I adjust the timing of the 3rd print statement to be displayed af ...

What is the best way to set up a task scheduler using node-cron in a Vue.js

Following the documentation in Node.js, each * symbol has a specific meaning. cron.schedule('* * * * *', () => { console.log('running a task every minute'); }); # ┌────────────── second (optional) # ...

angular ensuring seamless synchronization of objects across the application

This question pertains to both angular and javascript. In our angular app, we have numerous objects from the backend that need to remain synchronized. I am facing challenges in establishing efficient data bindings to ensure this synchronization throughout ...

effective method for iterating through JSON data using JavaScript or jQuery

Upon receiving a JSON object from the server, it looks like this: { "0": { "id": "1252380", "text": "This whole #BundyRanch thing stinks to high hell. Can it be a coincidence that Harry Reid n his son have a financial interest in this land?", ...

How to incorporate "selectAllow" when dealing with dates in FullCalendar

I've been attempting to restrict user selection between two specific dates, but so far I haven't been able to make it work. The only way I have managed to do it is by following the routine specified in the businessHours documentation. Is there a ...

Executing Java Script on several identical elements

Currently, I am facing an issue with my JavaScript function that is supposed to toggle the display of titles within elements. The function works perfectly fine on the first element, but it does not work on the other elements. Here is the code snippet: ...

Firefox won't trigger the `beforeunload` event unless I interact with the webpage by clicking on it

In my quest to handle the beforeunload event in Firefox, I've encountered a small hurdle. It seems to be working smoothly, but only if the user physically interacts with the page by clicking on it or entering text into an input field. Below is the co ...

Retrieving properties from video element following webpage loading

I am trying to access the 'currentSrc' value from a video object in my code. Here is what I have: mounted: function () { this.$nextTick(function () { console.log(document.getElementById('video').currentSrc) }); }, Despi ...

Using AJAX in Laravel Blade to bypass the specified div class

As a beginner in AJAX JavaScript, I have been trying to filter data in Laravel 10 without refreshing the page using AJAX, but so far I haven't had much success. Below is the code snippet from my blade view: <script src="https://code.jquery.co ...

Display Image based on AngularJS value

Within my data, there exists a value {{catadata2.EndorsementList['0'].Rating}}. This value can be either 3, 4, or 5. Based on this value, I am looking to display the image <img src="/assets/img/rating.png" /> a certain number of times. For ...

Error: jQuery is unable to access the property 'xxx' because it is undefined

While attempting to make a post request from the site to the server with user input data, I encountered an error message saying TypeError: Cannot read property 'vehicle' of undefined as the response. Here is the HTML and script data: <!DOCTY ...