4

I have a function that queries the DOM and returns an array of data attributes.

var getUIDs = function () {

    $list = $('.foo');

    if ( $list.length ) {

        // loop through all the .foos and build an array
        return fooArray;

    } else {

        setTimeout(function(){
            getUIDs()
        }, 500);

    }

}

This function can sometimes be called prior to .foo being present in the DOM. That's why if I check every half second or so, within a few seconds the array will exist and I could send it back.

My question is, is there an established pattern I should follow that allows a function to be called, but will not receive a return value until there is one?

brianrhea
  • 3,674
  • 3
  • 34
  • 57
  • 3
    use callbacks or promises – Jaromanda X Jun 08 '17 at 01:31
  • For your specific use case document ready is what you need (https://learn.jquery.com/using-jquery-core/document-ready/). If you need to wait until an asynchronous function has data, callbacks or promises are used to return the data back once it's ready. – Lance Whatley Jun 08 '17 at 01:36
  • 2
    @LanceWhatley `ready` doesn’t necessarily solve this. `.foo` elements could still be added dynamically at a later time. – Sebastian Simon Jun 08 '17 at 01:37
  • Basically the same thing as Ajax call: https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call – epascarello Jun 08 '17 at 01:40
  • @Xufox, his question says nothing about elements being dynamically added or removed, only that the elements aren't present in the DOM yet when the function is called, which is the point of `ready`. If `.foo`s are being added or removed dynamically and we need to know when this happens, then use event handlers to fire when these elements are added or removed and update the array at that time. – Lance Whatley Jun 08 '17 at 01:40
  • @Xufox is right, the elements appear after document is ready because they are added dynamically – brianrhea Jun 08 '17 at 01:40
  • Which is where my answer comes in :) – Clonkex Jun 08 '17 at 01:43
  • 1
    Your current logic is severely broken, because the value returned from `getUIDs` when it is invoked via `setTimeout` will disappear into outer space. –  Jun 08 '17 at 02:28

4 Answers4

1

MutationObserver

Here's a demonstration using MutationObserver – but it's pretty bare metal and awkward to interact with. If we want to use this, we'll want to make our own function to abstract away some of the complexity

var target =
  document.getElementById('main')

var observer =
  new MutationObserver(muts =>
    muts.forEach(m => console.log(m)))
 
observer.observe(target, {childList: true});
 
setTimeout(() => {
  var span1 = document.createElement('span')
  span1.textContent = 'cat'
  target.appendChild(span1)
}, 1000)

// { Mutation }
<div id="main"></div>

taming MutationObserver into a sane API

We can make our own nextMutation function which takes a document query selector and returns a Promise of the next added child

const nextMutation = sel =>
  new Promise((resolve,_) => {
    const obs = new MutationObserver(muts => {
      for (const {addedNodes} of muts)
        if (addedNodes.length > 0) {
          resolve(addedNodes[0])
          obs.disconnect() // stop observing
          return           // stop iterating
        }
    })
    obs.observe(document.querySelector(sel), {childList: true})
  })
   

// demo mutation: add some element to #main in 1 second
setTimeout(() => {
  var span1 = document.createElement('span')
  span1.textContent = 'cat'
  document.querySelector('#main').appendChild(span1)
}, 1000)

nextMutation('#main')
  .then(child => console.log('added child', child)) // added child: <span>cat</span>
  .catch(console.error)
<div id="main"></div>

timeout after waiting for too long

The above nextMutation will wait indefinitely until a child node is added to the target. What if we want to spit out an error if the child isn't added within X seconds? Of course we can augment our nextMutation function to do this

This code snippet also demonstrates that you can attach multiple observers to the same target. Below, an element is added to the target 1 second after domReady. The observer that waits up to 2 seconds will catch this mutation successfully – however, the observer that only waits .5 seconds will throw an error because it no mutation was observed

const nextMutation = sel =>
  new Promise((resolve, reject) => {
    const obs = new MutationObserver(muts => {
      for (const {addedNodes} of muts)
        if (addedNodes.length > 0) {
          resolve(addedNodes[0])
          obs.disconnect()
          return
        }
    })
    obs.observe(document.querySelector(sel), {childList: true})
  })
  
const awaitNextMutation = (sel, ms) =>
  Promise.race([
    nextMutation(sel),
    new Promise((_,reject) =>
      setTimeout(reject, ms, Error('awaitNextMutation timeout')))
  ])
   
// demo mutation: add some element to #main in 1 second
setTimeout(() => {
  var span1 = document.createElement('span')
  span1.textContent = 'cat'
  document.querySelector('#main').appendChild(span1)
}, 1000)

// wait for up to 2 seconds; since the child is added within
// 1 second, this promise will resolve just fine
awaitNextMutation('#main', 2000)
  .then(child => console.log('added child', child)) // added child: <span>cat</span>
  .catch(console.error)

// this one only waits 500 ms so an Error will throw
awaitNextMutation('#main', 500)
  .then(child => console.log('added child', child))
  .catch(console.error) // => Error awaitNextMutation timeout
<div id="main"></div>
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • I still find ES6's arrow functions exceedingly gross and hard to read. I'm sure they're useful given they behave differently but I don't like them much. – Clonkex Jun 08 '17 at 03:34
  • @Clonkex they're particularly useful for functional programs but I agree with you that when mixed with statements and other imperative constructs, code can get a little muddy – It's a good thing tho; it just means more (appropriate) abstractions need to be made to make the code more readable. – I've answered many questions that (imho) effectively use arrow functions in functional programs. I invite you to peruse them and let me know if it sways your opinion in one way or another. – Mulan Jun 08 '17 at 03:42
  • I have a vague idea what functional programming is, but despite repeated attempts I've never understand what it does or what it's for, and part of that is down to the confusing terminology. Even `imperative constructs` means nothing to me. Perhaps this is where self-teaching has its limits; maybe I need professional training to push through these advanced concepts. – Clonkex Jun 08 '17 at 04:10
  • 1
    @Clonkex *Imperative constructs* refers to *statements* (not *expressions*) like `if`, `for`, `return`. There's OOP constructs here too like `new` keyword before `MutationObserver` and `Promise` and also some imperative OOP stuff going on inside the `setTimeout` setting object properties etc – functional programming, on the other hand, relies on immutable data structures, pure functions, and functions composed of other functions. I am also self-taught (of course with the help of my peer group), but enough study will show you some distinct and surprising advantages of FP (and disadvantages too) – Mulan Jun 08 '17 at 04:30
  • @Clonkex if you have any further comments/questions, I'm happy to entertain you with discussion ^_^ – Mulan Jun 08 '17 at 04:47
  • Thanks for the explanation! Re the offer, I'll just leave it for now. I've just realised now that I think the reason I often have trouble with more advanced programming concepts is that I have such a solid grasp on English that when a new programming technique involves using a different English word, I have trouble disassociating it with the standard English meaning and applying the programming meaning to it. Also you have to remember a lot of new things at once to understand new concepts like this and I have trouble with memory sometimes. Maybe that's normal though, idk :P – Clonkex Jun 08 '17 at 06:43
  • Also hooray I've got tomorrow off and then it's a long weekend here in Australia! :D And it's only about 15 minutes 'til I get to go home! :P – Clonkex Jun 08 '17 at 06:44
0

I believe that MutationObserver is exactly wat you need here. Please read this: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

Kosh
  • 16,966
  • 2
  • 19
  • 34
  • Personally I prefer [arrive.js](https://github.com/uzairfarooq/arrive) for its ease of use, but it uses mutation observers internally so both options will work. – Clonkex Jun 08 '17 at 01:57
  • I don't think it's OK to include a third-party library or plugin just for one function. Also those libraries cause a lot of pain sometimes J. IMHO. – Kosh Jun 08 '17 at 02:02
  • Tiny libraries like this is what I love about JS. If they didn't exist I'd have to do everything by hand. – Clonkex Jun 08 '17 at 02:08
  • 1
    Yes, but it's a delicate balance. – Kosh Jun 08 '17 at 02:12
  • Depends on context. If you're making a website where performance/data usage matters, you definitely don't want to include every little library, but if you're making a small tool or admin interface it often doesn't matter how many libraries you use, so it's more important to quickly write reliable and maintainable code. – Clonkex Jun 08 '17 at 03:13
0

You can avoid callbacks, promises and recursion by executing this logic synchronously via sequential executor nsynjs:

function addFooClicked() {
    $("#myDiv").append("<div class='foo'>Foo</div>");
}

function synchronousCode() {
    var getUIDs = function () {
        var foo = $('.foo').get();
        while ( foo.length == 0 ) {
            console.log("waiting 5 seconds...");
            nsynWait(nsynjsCtx,5000);
            foo = $('.foo').get();
        };
        console.log("got elements:",foo.length);
        return foo;
    };
    
    var res = getUIDs();
    console.log("returning...");
    return res;
}

nsynjs.run(synchronousCode,{},function(r){
    console.log("synchronousCode finished, res=",r);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>
<script src="https://rawgit.com/amaksr/nsynjs/master/wrappers/nsynWait.js"></script>
<body>
  <button id="buttonStart" onclick="addFooClicked()">Click me few times</button>
  <div id="myDiv"></div>
</body>

Snippet Updated

amaksr
  • 7,555
  • 2
  • 16
  • 17
-2

If you're downvoting this, please explain why in the comments!

Are you wanting the code to stall until the data is present? The way I see it, the best way is to ensure the data is there first. It's not really a scripting issue so much as a logic issue.

In this case, I highly recommend arrive.js to add a callback for when the element has arrived in the DOM.


Alternative Bad Way That Can Be Useful in Limited Situations

Another way to do it would be to not return until the condition is met. This will block the browser for up to 30 seconds (so it's very bad for a user-facing website), but it might suit your case:

var getUIDs = function () {

    $list = $('.foo');
    var startTime = new Date().getTime();
    var currentTime = startTime;

    while ( !$foo.length) {

        currentTime = new Date().getTime();
        if ( (currentTime - startTime) > 30000 ) {
            return null; //or whatever you want to do to indicate an error
        }

    }

    return fooArray;

}
Clonkex
  • 3,373
  • 7
  • 38
  • 55
  • 1
    I'll definitely take a look at this. You're right that there's a logic issue and that's how I've been trying to solve it. I guess I was expecting that there is a better solution than a third party script for the problem of: function A in Module Y calls function B in Module X. Write functions A and B so that A can wait up to 30 seconds for a response. – brianrhea Jun 08 '17 at 01:50
  • @brianrhea That's very specific to the situation, so there's no built-in language constructs to aid with that. In this situation you want to wait for a DOM element to arrive, hence my suggestion of arrive.js. In essence you're wanting a function to wait for a specific condition to be met before it returns; you can do that in JS for sure, but it will block the browser until it does return and definitely isn't ideal. Answer edited to support this idea. – Clonkex Jun 08 '17 at 02:08
  • Damn I wish people would explain their downvotes. This answer is just as correct as any other. – Clonkex Jun 08 '17 at 03:05
  • 1
    Probably they downvote because your solution will halt the browser for up to 30000 ms within `while(!$foo.length)` loop. – Kosh Jun 08 '17 at 03:25
  • @KoshVery Well duh, that's the non-recommended way offered as an alternative, not my suggested answer. Do they not even read before downvoting?? – Clonkex Jun 08 '17 at 03:27