npm install jbob
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.
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.
<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>
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!
<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>
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
<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>
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.
<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>
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.
<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>
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 | |
<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 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.
<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>
A few other CSS classes are managed, and provide addictional behaviors when found in the DOM.
When applied to a Bootstrap's modal, it is removed from DOM when closed. Natively used for all modals fetched with .async-modal
<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>
jBob provides some JS function, mostly used internally and exposed for convenience. Just init()
is required.
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 initedfixBootstrap
: 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'],
});
});
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);
}
});
});
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'));
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'));
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'));
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);
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'));
Just creates a div
with a Bootstrap Spinner.
let spinner = j.makeSpinner();
$('.target-node').empty().append(spinner);
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');
}
});
A few jQuery events are triggered, to permit deeper integration and custom behaviors.
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');
});
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');
}
});
Triggered by .dynamic-table
when a new row is added to the table.
The callback receives the row itself as parameter.
$('.dynamic-table').on('jb-table-row-added', (e, row) => {
console.log('row added');
});
Triggered by .dynamic-table
when a row is removed from the table.
The callback receives the row itself as parameter.
$('.dynamic-table').on('jb-table-row-removed', (e, row) => {
console.log('row removed');
});
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.