Page and all elements completely loaded

One thing about Bubble that has been bothering me pretty much from the start, and that I haven’t found a way around is that when Bubble reports that the page is fully loaded, both elements and repeating groups are not necessarily complete.

Plugins aside for now, there are two things I’d love to be able to do:

  • Know when the entire page and all elements are loaded and rendered
  • Know when Bubble is completing a task where it would make sense to show some UX element (for example a spinning icon while creating a new thing, but avoiding to manually having to add it to every single workflow in your app)

Anyone have any tips?

11 Likes

Well, here’s the thing: The Bubble “page is loaded (entire)” event is the equivalent of something like:

<script>
      window.addEventListener('DOMContentLoaded', function(){
        console.log('The page is loaded... whatever THAT means.');
     });
</script>

That seems to be what’s evaluated (if might be a jQuery ready function, but same thing) when Bubble reports the following in the console:

But as you’ve noticed, that doesn’t mean that all elements that your page requires are really visible to the user.

I found this interesting article that, from my testing, seems to be correct (it’s worth a read!!!):

https://swizec.com/blog/how-to-properly-wait-for-dom-elements-to-show-up-in-modern-browsers/swizec/6663

So, let’s take a real world example of this. I have a page with a custom calendar on it. A repeating group is used to display the calendar – this is the general technique pioneered by @codurly in another really awesome post – anyway, that RG has 42 cells in it.

If I’ve got expose ID attributes turned on in my project, I can do something like give all of the elements in that group a unique id attribute like so:

So now all of those elements in my page are going to have id’s like this. Here’s a view of inspecting the last of those elements in the chrome console – here’s “clickDay42”:

I could use the technique described in the article above to put some code in the header of my page to check if “clickDay42” is ready to go. The code is like this:

<script>
// check for clickDay visibility:
function myFunk() { 
if (!$("#clickDay42").size()) {
      console.log('Still Waiting for clickDay42...');
      window.requestAnimationFrame(myFunk);
    }else {
       console.log('clickDay42 is visible now...');
       bubble_fn_clickDay(true);
     }
};
myFunk();
</script>

And I add it to my page like this:

And loading the page with the console open will show me a bunch of waiting and then, finally, that element is visible. (The log statement, in fact, does get printed RIGHT BEFORE/at the time that my browser shows the full view – I didn’t do a video so you just have to trust me on that):

You might be wondering, “What the heck is that ‘bubble_fn_clickDay’ function you’re calling?” That’s just a JavaScript to Bubble element from Toolbox plugin, configured like this:

So, I’d have a way of getting that state back up into Bubble and using it somehow (like, turn a loading indicator on and off). I could do that by having an icon that is hidden when element “JS ClickDay42 is Visible”'s value is true, and shown when JS ClickDay42 is Visible’s value is false.

NOW, this is not what you’re looking for, really. There’s no easier way unfortunately to know when some specific thing is rendered and ready. And, of course, each page is going to be different.

There is another condition we can check (rather than ‘DOMContentLoaded’ which I mentioned way up at the top of this post). THAT condition will only (supposedly) when all additional resources like images have also loaded. It would be like this:

My page I’ve been discussing in the examples does not load any images, though, so this function executes at the same time as the Bubble “page is loaded (entire)” condition and is not helpful. To show you, I add a script like this:

Here’s what happens in the browser. As you can see, we are not that far along at all vis-a-vis my repeating group!:

And in fact, Bubble hasn’t even reported its “page is loaded (entire)” state yet.

If we continue execution, we ultimately get here and you can see from the console log that it took a bit of time…:

So THAT’S not what you’re looking for either.

Anyway, I’ve gone round and round on this and there’s no easy generic way AFAICT to know when the entire page and all elements are loaded and rendered and ready for action.

I’ve been messing around with ways that one might generalize this and have not yet come up with a good solution.

12 Likes

Even individual elements don’t know when they’ve completely loaded, because they get initialised and updated, then re-updated with every change to their dynamic expressions.

One approach you can take is, decide which particular element and piece of data you want to wait for, and put a conditional which includes a dynamic expression to the specific dependency.

1 Like

But wait… there’s more! Another lil’ update: I found this very interesting approach to doing an element-based “ready” type function:

Using the technique described there, you can do some pretty interesting things. First, here’s the script of the mutation observer function described in the article (with a lil’ change to make it work the way we need it to for our purposes in Bubble):

<script>
// mutation observer ready thing:
(function(win) {
    'use strict';
    
    var listeners = [], 
    doc = win.document, 
    MutationObserver = win.MutationObserver || win.WebKitMutationObserver,
    observer;
    
    function ready(selector, fn) {
        // Store the selector and callback to be monitored
        listeners.push({
            selector: selector,
            fn: fn
        });
        if (!observer) {
            // Watch for changes in the document
            observer = new MutationObserver(check);
            observer.observe(doc.documentElement, {
                childList: true,
                subtree: true
            });
        }
        // Check if the element is currently in the DOM
        check();
    }
        
    function check() {
        // Check the DOM for elements matching a stored selector
        for (var i = 0, len = listeners.length, listener, elements; i < len; i++) {
            listener = listeners[i];
            // Query for elements matching the specified selector
            elements = doc.querySelectorAll(listener.selector);
            for (var j = 0, jLen = elements.length, element; j < jLen; j++) {
                element = elements[j];
                // Make sure the callback isn't invoked with the 
                // same element more than once
                if (!element.ready) {
                    element.ready = true;
                    // Invoke the callback with the element
                    listener.fn.call(element, element);
                }
            }
        }
    }

    // Expose `ready`
    win.ready = ready;
            
})(this);

// REGISTER whatever elements we want using CSS selectors:

ready("**_some_css_selector_**", function(element) {
    console.log(element.id, ' is ready!!!!');

});
</script>

That part at the bottom is how we tell the script what page elements (DOM elements) we want to keep an eye on.

Let’s start by registering EVERY darn element on the page. If we make the some_css_selector “*” we’ll get all elements. Or if we set it to “[id]” we’ll register everything that has an id set. So let’s do that:

… here’s the bottom part of that:

The output of the console will now show us the order that things (with explicitly set ids) become ready on the page:

Scrolling to the bottom of the console, we can see that in fact it is “clickDay 42” is pretty much the thing that becomes ready last. So, if we’re interested in anything, that’s probably it:

OK! So now we could alter things and just register that one thing to keep an eye on:

    ready("#clickDay-42", function(element) {
        console.log(element.id, ' is ready!!!!');

[Quick aside: I changed the way I did the identifier because whitespace in CSS ids is problematic. So it’s not a typo, I just changed from a space to a dash. Don’t get spun.]

The console output now looks like so:

So we could do other things now, like push that value back up into Bubble using a JavaScript to Bubble element:

… and change our function to:

Now Bubble knows when that element is ready, too! We could take action on the trigger produced by the JS Ready (JavaScript to Bubble) element. Maybe hide a loading indicator or something, right?

While loading…

Once loaded…

12 Likes

This is great! Do you happen to have a live view / editor on this?

Sorry, no ‘cuz I was doing this on a real page in one of my sites. So I tried to be very detailed with the steps involved! (Obviously, you’ll need Toolbox plug-in for the JavaScript to Bubble actions.)

Thanks a million for the awesomely detailed replies guys, I can see this has taken some time both to research and write down/illustrate. I really appreciate it.

It’s a bit over my head from a tech perspective, so I’ll need some time to digest it all :slight_smile:

Great contribution @keith and @mishav!

(UPDATING this thread with slight variation that works for repeating groups of unknown length):

Use case: I need to know when a repeating group is updated, ie in a chat message app when a new message is added to the repeating group. I’m dealing with a full-list, so other repeating group types may behave differently.

Problem: @keith’s scenario is different because his repeating group always has 42 entries so he can just tag entry #42 and is notified when that particular entry is ready. I don’t know how many entries are in my repeating group at any given time.

Solution: (what @mishav suggested…i think)

So I added a conditional to my repeating group cell/content that tags the last cell of the repeating group with the ID Attribute “lastcell”:

Capture1

Then following along with @keith’s implementation, listening for lastcell to know when the repeating group is finished loading. You can see that as @keith pointed out, Bubble’s “page is loaded” event occurs before the repeating group is populated. “lastcell” is loaded about 1.5secs later. Here is what that looks like on page load:

Now here’s the great thing about @mishav’s suggestion. When the repeating group is updated, lastcell is re-assigned to the new last cell and we get another notification that we can act on! The result when a new message is added to the group:

Other thoughts: this would probably work for @keith’s use case too. It doesn’t require adding ID Attributes to anything except the one conditional above in the cell of the repeating group. So effectively if you continue on with @keith’s implementation of JS to Bubble, what we’ve built here is the ability to create an action “Do when repeating group is loaded (entire)”. Thank you guys so much!

4 Likes

Wait…there’s more…and its better!

tldr: There is an astoundingly simple bubble-only (no Javascript) solution to the problem of knowing when a repeating group is fully-loaded that seems more accurate/reliable than EventListeners and MutationObservers. Also, learn how to use your browser’s console AND USE IT.

Note: my case is a full-list repeating group with unknown number 0-200+ items that is main content of the page (think imessage). It presents a challenge because its fast when it has few items and slow when it has many. Other types of repeating groups may behave differently on load so refer to the detailed explanation to figure out your use case.

Here’s the solution that literally takes less than 1 minute to build, read below for the explanation:

  1. Create a custom state on your repeating group, type: Number, mine is named “LoadCount”.

  2. Create a new workflow “Do when codition is true”, every time, only when: [your repeating group’s name] is loading is “no”. Here is mine:


3. Add 1 action to that workflow that will add +1 to the custom state you created in Step 1.

pic2
4. Add another workflow “Do when condition is true”, every time, only when: [Your custom state’s name] = 2.
5. Create 1 action in this workflow that resets your custom state to 0.
6. Then add to Step 5’s workflow the action want to trigger when your repeating group is fully loaded. THAT IS IT!


Here’s why/how this works:

First on the browser console topic, it deserves a separate thread so I defer, just know its invaluable for solving problems like this.

The real problem with knowing when a repeating group is finished loading so you can then trigger an action, like turning off a “loading” screen in Keith’s example, is twofold:

The time it takes bubble to get the data to your browser (solved above) AND the time it takes the browser to render (which depends on formatting, cpu, etc).

Note that EventListeners, MutationObservers, and my solution are all aimed at the first part of the problem. I couldn’t find an attractive solution to the second part. Here’s the results first:

pic5
What is this? Same mutation observer from my post above, plus a couple console logs generated from bubble’s “When repeating group is loading” workflows. BTW this is my message repeating group loading 10 simple messages.

Note the two “slugs” of repeating group loads: YES I’m loading, NO I’m done, YES I’m loading, NO I’m done. The first apparently “informs” the page of how much content is coming, hence, bubble says “Page is loaded” because technically the layout of the page can be finalized.

But the second slug is when the actual data is loaded into the page. Therefore, the simple solution I listed above just counts how many slugs are loaded, ie, the repeating group is really finished loading the 2nd time "[myRepeatingGroup] is loading = no. Looks to me that it is a better, simpler, more reliable solution than configuring and maintaining a plugin (I feel you plugin developers!)

It’s not just conjecture. Here’s the same load with some timestamps added:

pic6

Yes, console timestamps/events are not 100% reliable, but this is pretty darn good result given there is NO JS, NO HEADACHE, A COUPLE SIMPLE WORKFLOWS. Here my mutation observer is labeled TESTID1… 400ms difference. No joke, my mutation observer previously had to have a 400ms setTimeout in order to get the result I wanted. The timeout was removed for this test.

Better still, lets throw 75 messages at it and see what happens:

pic7
Woah. The bubble method is 3.5secs slower…or is it? In fact, the bubble method almost exactly matches the screen as the messages fill the group. It is the mutation observer that is firing way too early, which is the reason I spent a whole additional week trying to solve this problem! I had to keep adding more delays to the mutation observer to get it to fire when the group was actually finished loading. No more. The simple bubble approach works every time, spot on, with no guessing how long to wait!

Just for fun, here’s 150 messages:

pic8

13 secs to load 150 messages? That is highly variable due the second part of the problem, the “network” dependency. I’m only loading 76KB of data here. I was getting significantly better load times earlier in the day. And I was using a somewhat-slow VPN throughout.

Is the mutation observer wrong? No, it’s just not measuring what we want. It fired at the same millisecond that the second slug of data began, ie, the first byte of data received is (correctly) identified as the mutation, not the last byte, which is what we want. This is more obvious with more data as shown above. And recall, my mutation observer is only monitoring the last cell of the repeating group. You may have the chops to rewrite the mutation observer to get that but “y tho”, the bubble method yields the accurate “is really done loading” event that we want.

So there you have it, a simple solution to a complicated problem. I admit that I accidentally discovered the two repeating group loading slugs because, out of frustration, I was console logging every single thing I was doing–shoutout to @keith for continually stressing the value of that approach.

The action “repeating group is loading” has been there as long as I can remember, maybe tried it once and it didn’t seem to work. Only by logging the results did I discover that there appears to be two slugs, always for my full list, and I verified with lots of messages.

Note to bubble: can we tame down the 150 console warnings I get about my full list repeating group?! It is only 68 KB of data. Maybe you can switch that to “only once” instead of “every time”?!

24 Likes

Hey, thanks for building on this @meyerhd2. Nice work. I’ve not dug super-deep into what you did, but looks really cool and quite smart. (Note that I didn’t really have a “use case” here as I never did anything to really take advantage of this knowledge. I just thought this was a REALLY interesting question from @petter, that I had also pondered and figured I’d dig in. I guess I mighta been bored at the moment!)

Rereading all of this, I’m really impressed by the level of effort that was put into answering this question and continues to expended here.

Rather than re-editing my prior post, I’m correcting/elaborating here:

The bubble-method I described above tells you when a repeating group is loaded AND rendered on the screen.<<part of @petter’s wish list.

Turns out that Bubble notifies its server when the repeating group is rendered, and that IS captured by the second “is loading” event. (I initially thought the rendering was not captured but extensive testing proved otherwise, again in the context of my full list type repeating group).

The repeating group is a vital feature for many Bubble apps and also a primary source of frustration the moment you have enough data in it to experience “slow loading”.

Hopefully this technique allows Bubble developers to configure their UX in a way that is fluid rather than frustrating, whether it is just notifying users with a loading icon or having users do other stuff until the repeating group is rendered.

5 Likes

Hey guys, sorry, I haven’t been spending enough time on the forum lately, but this thread grown into an extremely interesting read! Thanks a lot for all the awesome contributions.

I’ve tested this in a complex set of nested Repeating Groups now, and it works, albeit not 100% consistently (I wasn’t able to determine whether that was because of the nesting or not).

Using in a nested RG
Using this in a nested RG was a bit trickier, as you can’t access the IS LOADING state of the nested groups. I solved that with the If-then plugin from BDK, which can trigger a workflow inside a RG cell. I then set up a “master counter”, which was a number state that increased it’s number by 1 for each of the RG’s (I nested several levels). It’s basically the same as @meyerhd2 brilliant solution, but an added counter that gathers the result of all loading groups, not just the first. You need to be careful to include an additional constraint in the If-then workflow to avoid it triggering on every cell. I solved this by adding a “Current cell’s index is 1”.

Where the strangeness kicks in
The number the “final counter” should reach when all levels of the nested RG’s were finished loading, is 5, as I had 1 RG containing 4 more RG’s in different levels. And it did. Except… not every time. I tested it about 20 times, to see if I could find any pattern in what was causing the discrepancy in the loading states, but haven’t been able to find one yet. I’m open to any theories. It seems to land on 3 or 5, never 4, for some reason. It was close enough to work pretty well without a delay (showing the group when the counter reached 3), but I’m curious about what’s causing it.

Anyway, it helped me massively improve the UX of the page, and can’t wait to implemet it on other pages. Big thanks to @meyerhd2, @keith and @mishav for the awesome contributions to this thread!

2 Likes

I didn’t try nested groups, but did see similar things when experimenting with hidden repeating groups. Obviously Bubble has to load the data, but the variance looked to me like it came from the “is loading” indicator/function itself. Sometimes only the “is loading is true” indicator would fire but its corresponding “is loading is false” would not show when it was finished or vice versa. So there may be a reliable solution by taking either indication into account some way?

I’m still playing with this from time-to-time and will share if I come up with anything new.

1 Like

Hello @meyerhd2,

I found this interesting, so I thought I’d give it a try. My counter never gets off 0. I’ve tried it on a repeating group with only 8 rows, and and on another with 102 rows.

Do you, or anyone else, see what I did wrong?

Here’s my setup.

image

Thanks,
George

Is the repeating group in fact loaded (didn’t know what hidden meant)?

How do you know the counter never gets off 0? some sort of element display?

You can also experiment with [RG is loading is “yes”]. Technically should be equivalent, can’t remember exactly why I settled on “no”

Hey there @meyerhd2 . I am trying to fight the same problem and I was very delighted to find your solution. Unfortunately, it didn’t work for me :frowning:

In my case, I am trying to update a repeating group. I did a slow motion recording of the screen, and what I discovered was that Bubble loads the RG table really fast, but it takes its time to populate the data. And the caveat here is that data fields needs to be actually displayed on screen to be populated. As long as they are hidden, no data is being populated. In my implementation of your solution, data appears only AFTER I hide the “loader” covering the fields.

Do you have any comments to the above?

By the way, how do you actually make sure that the RG is rendered?

By the way, I just discovered another strange thing that breaks the entire solution (if it ever works for me)… When you try to load the same RG with the same data again. It appears that Bubble somehow holds the data in cache and it only takes 1 count of “if rg is loading NO”.

Here is the video showing both that RG content is being populated AFTER the loaded has been closed, and that when I load the same RG for the second time, the loader stays open and counter is 1.

If I understand correctly, you are hiding your repeating group and then try to show it at the end. If I am right, it won’t work, because for Bubble to do anything with an object, it needs to be visible on screen. In other words, hidden RG won’t be populated with data, and the counter won’t count - it will stay empty.

Otherwise, if you had it solved, I’d very much like to hear how.

My memory may be a bit foggy but I will try to share what I can:

I think that’s the first “Repeating Group is loaded” trigger. I think Bubble essentially tells the browser to render this group with 3 items (empty), then comes back later with the data that goes into those items (the second is loaded trigger). I also believe that the “is loaded” trigger is set when Bubble sends the data, not necessarily when it gets displayed on the screen, although at that point, the difference should only be microseconds, depending on the amount/type of content, of course.

I relied extensively on the browser’s developer’s console to try to figure this out. In my workflows, I sent console logs at every step with timestamps to see what was happening. I also used the dev console to read the data coming from Bubble. At the time, it think it was msearch and mget messages to/from Bubble that carried the repeating group content, but if you poke around you will see them.

Another thing I did in a different circumstance was to use a popup for the “source” repeating group. Even though the popup was never displayed, Bubble populated that repeating group as if it was. I then made the “hidden”/popup’s repeating group the source for the on-screen repeating group. That was a way of “pre-populating” my on-screen group. And as I recall, the “is loading” triggers worked the same for the “hidden”/popup’s repeating group.

At the end of the day, this is not a limitation of Bubble but of the browser. Rendering occurs in the browser. Every browser is different. Older mobile browsers are very slow when rendering. Bubble (or any server) can only tell you that it sent the data to the browser. The idea behind mutation observers is that it is a way for the browser to report back to Bubble that “the screen has changed”. Keith got it to work quite simply, but it is a constant drain on the server and the browser to keep checking the status every few milliseconds, plus every browser handles mutation observers differently too.

Food for thought: The method I described was “acceptable enough” in “most” situations, not all. Browser-based Gmail has the same limitations. Even though Google’s servers are highly optimized and fast, it takes browsers a while to render content. Google doesn’t use lots of “loading indicators”, it’s design just conveys that “something goes here but it’s not here yet”, then the content shows up in a couple seconds. It occurred to me that that is a smoother user experience rather than sometimes “flashing” a loading indicator that was only on the screen for a split second. I use a loading indicator if I know I have a complex search that rarely returned in less than 5 seconds. Anything less, rely on “obvious” design (like Gmail).

1 Like