jBob

Opinionated utilities
for jQuery and Bootstrap

npm install jbob

Press F12 to open the Inspector and see what happens!

Core Classes

Most of features in jBob are implicitly activated adding specific CSS classes directly into the HTML. The init() function provides to attach all the proper jQuery handlers, and manage initialization of other components fetched asynchronously.

.async-modal

When clicked, an .async-modal button will fetch a Bootstrap modal from a given URL and will display it. When closed, the modal is destroyed and removed from the DOM.

The URL can be both in the data-modal-url attribute or, if the trigger is an anchor, in the href attribute.

Anchor with attribute Anchor with href

Display Code
<button class="btn btn-primary async-modal" data-modal-url="/samples/modal_contents.html">Button with attribute</button>
<a class="btn btn-primary async-modal" href="#" data-modal-url="/samples/modal_contents.html">Anchor with attribute</a>
<a class="btn btn-primary async-modal" href="/samples/modal_contents.html">Anchor with href</a>

.async-tab

A Bootstrap tab having the .async-tab CSS class will fetch his contents from the URL in data-tab-url when activated.

When the tab is no longer active, his contents are removed again from the DOM and to be reloaded at the next activation. Unless the .keep-contents is also used, in which case the contents are loaded once.

This content is already in the page. Click the Second tab!



Display Code
<ul class="nav nav-tabs" role="tablist">
    <li class="nav-item" role="presentation">
        <a class="nav-link active" data-bs-toggle="tab" data-bs-target="#first-tab" type="button" role="tab">First</a>
    </li>
    <li class="nav-item" role="presentation">
        <a class="nav-link async-tab" data-tab-url="/samples/remote_content.html" data-bs-toggle="tab" data-bs-target="#second-tab" type="button" role="tab">Second</a>
    </li>
    <li class="nav-item" role="presentation">
        <a class="nav-link async-tab keep-contents" data-tab-url="/samples/remote_content.html" data-bs-toggle="tab" data-bs-target="#third-tab" type="button" role="tab">Third (loaded once)</a>
    </li>
</ul>

<div class="tab-content">
    <div class="tab-pane fade show active" id="first-tab" role="tabpanel">
        <p>
            This content is already in the page. Click the Second tab!
        </p>
    </div>
    <div class="tab-pane fade" id="second-tab" role="tabpanel">
        <!-- This is left empty: the contents of the tab are fetched every time the tab itself is activated -->
    </div>
    <div class="tab-pane fade" id="third-tab" role="tabpanel">
        <!-- This is left empty: the contents of the tab are fetched once, when the tab itself is activated -->
    </div>
</div>

.async-accordion

A Bootstrap accordion having the .async-accordion CSS class will fetch his contents from the URL in data-accordion-url when activated (every time it is activated, by default; only once, if the .keep-contents is also used).

The behavior is to be attached to the main .accordion-item: you can provide your static contents for .accordion-header, the fetched HTML is appended into .accordion-body



Display Code
<div class="accordion">
    <div class="accordion-item async-accordion" data-accordion-url="/samples/remote_content.html">
        <h2 class="accordion-header">
            <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#first-accordion">First</button>
        </h2>
        <div id="first-accordion" class="accordion-collapse collapse">
            <div class="accordion-body">
                <!-- This is left empty: the contents of the accordion are fetched when the item itself is activated -->
            </div>
        </div>
    </div>
    <div class="accordion-item async-accordion" data-accordion-url="/samples/remote_content.html">
        <h2 class="accordion-header">
            <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#second-accordion">Second</button>
        </h2>
        <div id="second-accordion" class="accordion-collapse collapse">
            <div class="accordion-body">
                <!-- Same as above -->
            </div>
        </div>
    </div>
    <div class="accordion-item async-accordion keep-contents" data-accordion-url="/samples/remote_content.html">
        <h2 class="accordion-header">
            <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#third-accordion">Third (loaded once)</button>
        </h2>
        <div id="third-accordion" class="accordion-collapse collapse">
            <div class="accordion-body">
                <!-- Same as above -->
            </div>
        </div>
    </div>
</div>

.async-popover

A .async-popover button has a popover which contents as dinamycally fetched from the given URL when activated. On the contrary of other components, an async popover keeps its contents even when closed (so are fetched just once).

Remember that Bootstrap popovers have to be explicitely inited in your own JS: it is not automatically managed to avoid conflicts with other popovers not handled with jBob.



Display Code
<button type="button" class="btn btn-light async-popover" data-contents-url="/samples/remote_content.html" data-bs-toggle="popover" data-bs-content="placeholder" data-bs-html="true" data-bs-trigger="hover">Hover Me!</button>

.modal-form

A form within a Bootstrap modal and having the .modal-form CSS class, when submitted is serialized (using the serializeForm() function), submitted to the given action URL using his own method, and - on success - the parent modal is closed.



Display Code
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modal-form-sample">Open modal with form</button>

<div class="modal fade" tabindex="-1" id="modal-form-sample">
    <div class="modal-dialog">
        <div class="modal-content">
            <form method="POST" class="modal-form">
                <input type="hidden" class="skip-on-submit" name="will_not_be_serialized" value="1">

                <div class="modal-header">
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <div class="mb-3">
                        <label for="example" class="form-label">Write something</label>
                        <input type="text" class="form-control" name="example" id="example">
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
                    <button type="submit" class="btn btn-primary">Save</button>
                </div>
            </form>
        </div>
    </div>
</div>

.dynamic-table

A dynamic table permits to add and remove rows dynamically. The click on the element with the CSS class .add-row triggers the append of a new row, the click on .remove-row removes the current row.

The contents of tfoot are used as template for newly added contents: remember to hide it, using a specific CSS rule or with a hidden attribute.

First column Second column Third column
First row: something First row: something else
Second row: something Second row: something else


Display Code
<table class="table dynamic-table">
    <thead>
        <tr>
            <th width="40%">First column</th>
            <th width="40%">Second column</th>
            <th width="20%">Third column</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>First row: something</td>
            <td>First row: something else</td>
            <td><button class="btn btn-danger remove-row">Remove</button></td>
        </tr>
        <tr>
            <td>Second row: something</td>
            <td>Second row: something else</td>
            <td><button class="btn btn-danger remove-row">Remove</button></td>
        </tr>
        <tr>
            <td colspan="3"><button class="btn btn-info add-row">Add</button></td>
        </tr>
    </tbody>

    <tfoot hidden>
        <tr>
            <td><input class="form-control" type="text" placeholder="A new row's something!"></td>
            <td><input class="form-control" type="text" placeholder="A new row's something else!"></td>
            <td><button class="btn btn-danger remove-row">Remove</button></td>
        </tr>
    </tfoot>
</table>

.infinite-scroll

Infinite scroll is based on Bootstrap's pagination: you can implement single pages linked each other, using classic pagination, while jBob uses the pagination widget as reference to dinamycally load and inject contents in the user's viewport.

The .infinite-scroll node's contents of each loaded page will be isolated and appended to the .infinite-scroll in the initial page; the .active class set in the pagination buttons will be used to determine the next page to load.

Example
Example
Example
Example


Display Code
<div class="row row-cols-1 row-cols-md-4 g-4 infinite-scroll">
    <div class="col">
        <div class="card">
            <img src="https://picsum.photos/200/300?random=1" class="card-img-top" alt="Example">
        </div>
    </div>
    <div class="col">
        <div class="card">
            <img src="https://picsum.photos/200/300?random=2" class="card-img-top" alt="Example">
        </div>
    </div>
    <div class="col">
        <div class="card">
            <img src="https://picsum.photos/200/300?random=3" class="card-img-top" alt="Example">
        </div>
    </div>
    <div class="col">
        <div class="card">
            <img src="https://picsum.photos/200/300?random=4" class="card-img-top" alt="Example">
        </div>
    </div>

    <nav>
        <ul class="pagination">
            <li class="page-item active"><a class="page-link" href="/samples/scroll1.html">1</a></li>
            <li class="page-item"><a class="page-link" href="/samples/scroll2.html">2</a></li>
            <li class="page-item"><a class="page-link" href="/samples/scroll3.html">3</a></li>
        </ul>
    </nav>
</div>

Other Classes

A few other CSS classes are managed, and provide addictional behaviors when found in the DOM.

.delete-on-close

When applied to a Bootstrap's modal, it is removed from DOM when closed. Natively used for all modals fetched with .async-modal



Display Code
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modal-delete-sample">Open modal</button>

<div class="modal fade delete-on-close" tabindex="-1" id="modal-delete-sample">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <p>
                    This will be removed from DOM when closed.
                </p>
                <p>
                    This means it cannot be opened again if you click again on the button!
                </p>
            </div>
        </div>
    </div>
</div>

JS Methods

jBob provides some JS function, mostly used internally and exposed for convenience. Just init() is required.

init(params)

Accepts an optional object for parameters:

  • initFunction: a callback to be called on all HTML fragments fetched asynchronously, to provide your own extra initialization on new HTML nodes injected into the page. A parameters is passed to the function, which is the new HTML node itself. This will be called for every new piece of HTML attached in the DOM by jBob: when fetching contents with async widgets, when adding a row to a .dynamic-table, when using fetchNode() and more. For your convenience, it is also invoked once on the entire $('body') when the library is inited
  • fixBootstrap: an array of Bootstrap's jQuery plugins for which enforce initialization. "Modal" and "Popover" are handled by default, you can add others. This may be required under certain situations, where Bootstrap fails to identify jQuery and do not inits his own plugins. In this case, you have to explicitely import Boostrap and make it globally accessible from the window object under the name bootstrap.
import * as bootstrap from 'bootstrap';
window.bootstrap = bootstrap;

import jBob from 'jbob';

$(document).ready(function() {
    let j = new jBob;

    j.init({
        initFunction: function(container) {
            $('#myselector', container).myfunction();
        },
        fixBootstrap: ['Tooltip', 'Toast'],
    });
});

initElements(container)

To be used when you append new elements in the DOM, to call the initialization function on the new fragment. This implies jBob own initialization and your own custom, as provided to init()

Can be invoked multiple times on the same element (for example: if you append a new .async-tab to an existing tabs group); jBob provides to init his own interactions once by tagging the already inited nodes with the .jb-in CSS class.

$(document).ready(function() {
    let j = new jBob;

    j.init({
        initFunction: function(container) {
            $('#myselector', container).myfunction();
        },
    });

    $.ajax({
        url: '/my/endpoint',
        method: 'GET',
        dataType: 'HTML',
        success: (data) => {
            data = $(data);

            /*
                Here, all jBob behaviors are inited and the function
                in initFunction() is called on $data
            */
            j.initElements(data);

            $('body').append(data);
        }
    });
});

assignIDs(selector, container)

For each node matching the jQuery selector found in container, generates a random ID attribute (if none is already found).

j.assignIDs('.my-button', $('body'));

fetchNode(url, node)

Fetches HTML from the given url and replaces the contents of node in DOM.

The function itself returns a Promise to track actual fetch activity.

j.fetchNode('/my/endpoint', $('.target-node'));

reloadNode(node)

Almost like fetchNode(), but can be directly applied to "async" nodes for auto refetch their contents.

Otherwise, node must have a data-reload-url attribute with the URL from where get the new HTML. node is replaced with the new HTML (not only his contents).

<div class="target-node" data-reload-url="/my/endpoint"></div>

...

j.reloadNode($('.target-node'));

submitButton(form)

Utility intended to easily retrieve the submit button of a form; it may be both within the form itself (button[type=submit]) or external (having a form attribute which value matches the id of the intended form).

<form id="this-is-a-form">
    <input type="text" name="test">
</form>

<button form="this-is-a-form" type="submit">Submit</button>

...

j.submitButton(form).prop('disabled', true);

serializeForm(form)

Like the jQuery's native serialize(), but skips elements having the .skip-on-submit CSS class.

<form>
    <input type="hidden" class="skip-on-submit" name="no_serialize" value="1">
    <input type="text" name="serialize">
</form>

...

j.serializeForm($('form'));

makeSpinner()

Just creates a div with a Bootstrap Spinner.

let spinner = j.makeSpinner();
$('.target-node').empty().append(spinner);

onScreen(node, offset = 0)

Tests if the given jQuery node is completely within the current viewport. Optionally, pass a second argument with an extra offset to be considered (e.g. if node is at 200px from the viewport).

$(window).on('scroll', () => {
    if (j.onScreen($('.target-node'), 200)) {
        console.log('yes');
    }
});

JS Events

A few jQuery events are triggered, to permit deeper integration and custom behaviors.

jb-before-async-fetch

Triggered by .async-modal, .async-tab, .async-accordion and .infinite-scroll before to start the actual fetch of contents.

$('.async-modal').on('jb-before-async-fetch', (e) => {
    console.log('Start Fetching');
});

jb-after-async-fetch

Triggered by .async-modal, .async-tab, .async-accordion and .infinite-scroll when the fetch is complete and the retrieved data have been appended in the DOM.

The callback receives an extra parameter which is true when everything goes well and false when an error occurred.

$('.async-modal').on('jb-after-async-fetch', (e, status) => {
    if (status == true) {
        console.log('ok');
    }
    else {
        console.log('ko');
    }
});

Use Cases

jBob has been implemented by an old-school developer who never embraced the paradigm proposed by libraries like React or Angular (tragically foundered on "modern" SSR techniques, that is: generating the HTML on the server, in the same way as as Tim Berners-Lee did in 1990, this time with far more complex tooling) but who did not want to give up building web applications with dynamic interactions.

Here are a few suggestions on how to leverage jBob in your applications.

Use .async-modal for the "delete" buttons in your interface, so to generate on-demand a notice modal with informations about the element to be deleted, ask confirmation, and provide the actual "delete" code.

Use .async-tab for more complex panels, so to posticipate the generation of all involved sub-panels and provide a faster experience.

The .dynamic-table can actually be used for any kind of multiple, grouped, dynamic values to be recorded, such as users' contacts (multiple emails, multiple phone numbers...) or filtering (multiple rules described by multiple parameters).

Handle all the names of inputs within the table as arrays to permit full serialization of the contents.


<tr>
    <td>
        <select name="type[]">
            <option value="email">email</option>
            <option value="phone">phone</option>
            <option value="mobile">mobile</option>
        </select>
    </td>
    <td>
        <input type="text" name="value[]">
    </td>
</tr>

The .skip-on-submit class can be used client-side to decorate a form with addictional informations, to be retrieved and managed before or after the actual submission. Implement your business logic once, and trigger it according to the fields in each form.

fetchNode() can be used to populate a portion of your page according to the selection of a select or a radio button, where each option provides (perhaps with a data attribute?) his own URL to be fetched.

The initial purpose of .async-popover was to provide contextual informations about elements, without having to retrieve them all the times the elements themselves are shown. Get them only when required to offload pages' generation.

.infinite-scroll provides a naive solution that plays well with the pagination features of most web frameworks: you just have to generate the different pages, easily discoverable by crawlers (for SEO purpose), and jBob provides to concatenate them to obtain an infinite scroll effect.