bootstrap icon indicating copy to clipboard operation
bootstrap copied to clipboard

BS Jquery plugins become available only after document loaded.

Open rpokrovskij opened this issue 4 years ago • 8 comments
trafficstars

Scaenario: Bootstrap 5 loaded as UMD with jquery.

Problem: BS Jquery plugins become available only after document was loaded. This broke migration to BS 5.

Expected behavior: plugin become available as soon as the script was loaded SYNCHRONOUSLY to other jquery plugins (and therefore developer can maintain the plugin load order and dependencies).

Sample. This doesn't work

    <script src="https://code.jquery.com/jquery-3.6.0.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>
    <script>
         $('[data-bs-toggle="tooltip"]').tooltip()
    </script>

When this works normally:

    <script src="https://code.jquery.com/jquery-3.6.0.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>
    <script>
        document.addEventListener("DOMContentLoaded", function (event) {
            (function () {
                $('[data-bs-toggle="tooltip"]').tooltip()
            })();
        });
    </script>

As code investigation shows the bootstrap component intentionally postpone the plugin availability:

From https://github.com/twbs/bootstrap/blob/main/js/src/util/index.js#L214

const defineJQueryPlugin = plugin => {
    onDOMContentLoaded(() => {
        const $ = getjQuery()
        if ($) {
            const name = plugin.NAME
            const JQUERY_NO_CONFLICT = $.fn[name]
            $.fn[name] = plugin.jQueryInterface
            $.fn[name].Constructor = plugin
            $.fn[name].noConflict = () => {
                $.fn[name] = JQUERY_NO_CONFLICT
                return plugin.jQueryInterface
            }
        }
    })
}

The reason is unkonwn This information is absent in migration doc. I think this should be considered as a bug (compatibility with jquery and previous code: now there is a new headache how to mange plugin load order).

More information: https://stackoverflow.com/questions/67953465/bootstrap-5-jquery-is-it-possible-to-force-bootstrap-5-to-inject-its-jquery-p/67957165#67957165

rpokrovskij avatar Jun 13 '21 11:06 rpokrovskij

It was done intentionally in #32024 :slightly_smiling_face: There was a reason for the change, see the issue for more information.

/CC @twbs/js-review

rohit2sharma95 avatar Jun 14 '21 07:06 rohit2sharma95

I'm unsure how to solve this and not reintroduce the aforementioned issue. If anyone has any ideas please make a PR.

XhmikosR avatar Jun 14 '21 17:06 XhmikosR

@XhmikosR create UMD/jquery bundle and maintain compatability with BS4 for it. actually it is also the best in terms of require correctness. You see, now your UMD bundle do not require the jquery and when people create application bundle (from UMD - this is one of scenarious fo legacy app) the bundler can load bootstrap first to jquery (there are no declaration of dependency) with no warning or errors, and the it become hard to understand what is wrong with app bundle.

And of course BS team need to append the migration doc.

P.S. boostrap.Tooltip('#myId') become available exactly after script is loaded , it is absolutele not clear and counter-intuitive why $('#myId'),tooltip() should be postponed...

rpokrovskij avatar Jun 15 '21 09:06 rpokrovskij

Arguably there has been some paradigm shift away from jQuery with v5 IMO. While in v4 (and earlier) Bootstrap heavily depended on jQuery this isn't the case with v5 anymore. The only leftovers are these definitions for those jQuery plugins. Thus I'm not sure if creating a separate jquery bundle is really a good approach or supports this idea.

That's also the reason why we don't require or import jQuery because that would make jQuery a dependency again which shouldn't be the case if someone uses Bootstrap without jQuery (by making use of the API introduced in v5).

I'd like to argue that bundlers are a different topic in general. Some of them require additional steps to properly set things up and they may provide extra functionality that depends on the bundler that is used. In webpack for example you may use the ProvidePlugin to have Webpack take care of importing jQuery automatically even when there are no explicit imports. IMO a bundler environment shouldn't make use of an already bundled version, like the UMD bundle, because of tree shaking but thats a different story.

About possible sollutions.

One you figured out by yourself already and I'm not sure what's wrong with this approach. For example, if you was to load Bootstrap in the <head> section, most likely you would have to delay the execution anyway because the DOM isn't parsed yet. Event listeners are executed in the order they have been added, so as long you add the DOMContentLoaded listener after including the Bootstrap bundle, you should be fine.

On a side note, in your example there is no need for the IIFE, so this would be enough.

document.addEventListener("DOMContentLoaded", function (event) {
  $('[data-bs-toggle="tooltip"]').tooltip()
});

In general, I'm not sure about the intention behind this data-bs-no-jquery. I can't imagine this is widely used. My assumption about this, as a use case, is that someone uses jQuery but got other plugins loaded that would conflict with the naming of those from Bootstrap and calling noConflict on each conflicting one, is tedious or not possible or something. Or someone is really concerned about the resource usage of the JS code.

We could either remove that flag or move it on the html element instead. This wouldn't require us to postpone the checks for that flag, like with on the body. And as such there would be no need to defer our plugin definitions on to the DOMContentLoaded event. But that's some breaking change so no idea if that's feasible.

In theory we also could remove those jQuery plugin stuff completely from our bundle and add some generic snippet to the documentations. Breaking change again but at least provides some value in terms of bundle size. And the user can decide how and when and if to define our components as jQuery plugins or not.

alpadev avatar Jun 15 '21 16:06 alpadev

@alpadev I still do not understand why boostrap.Tooltip('#myId') is available immediately when $("").tooltip() not ... Till I could understand this I will be very sceptic about my abilities to understand this decision. What about working with UMD with webpack I am ok with current situation, my report there could be understand just as a suggestion to "add this to the documentation" (load the jquery manually if you need it). This type of error is very unobvious and you can save many working hours for our collegues (many of us just do not understand how require and webpack works - in any case it is not the topic we track and think everyday) .

rpokrovskij avatar Jun 15 '21 18:06 rpokrovskij

As far as I can tell, the reason for this is data-bs-no-jquery or to be exact, that we check for this data attribute to exist when registering our components with jQuery as this will prevent us from doing so.

See https://github.com/twbs/bootstrap/blob/9485172017868952047da5f188bc13a92ef0435d/js/src/util/index.js#L194-L202

https://github.com/twbs/bootstrap/blob/9485172017868952047da5f188bc13a92ef0435d/js/src/util/index.js#L214-L218

The PR that added this flag https://github.com/twbs/bootstrap/pull/29191

As with the issue Rohit mentioned, if Bootstrap is loaded in the <head> section, document.body isn't defined (yet) and thus hasAttribute will throw an error. This has been worked around by defering the registration of jQuery plugins until the DOM is loaded because in that case we can be sure that document.body exists.

This means e.g. $().tooltip() is registered in an asynchronous way because of this logic, while we don't have such thing for our API that's why you're able to call bootstrap.Tooltip() synchronously.

alpadev avatar Jun 15 '21 18:06 alpadev

Why somebody who need data-bs-no-jquery functionality can't load bootsrap ahead to the jquery ? Effect will be the same. P.S. normally we import js at the end of document. JS in <head> looks very specific, it should be explained why they do it this way. P.P.S. again jquery UMD bundles would solve this. Sometimes obvious is best.

rpokrovskij avatar Jun 15 '21 22:06 rpokrovskij

Could you read the attribute from document.documentElement instead? That’s always available.

The fact that body may not exist is the reason why jQuery switched to attaching elements used for support tests directly to documentElement instead of body in 2.1.0.

mgol avatar Aug 08 '22 23:08 mgol