1. jQuery and jquery-state
In web development, jQuery has made a significant impact on how we develop dynamic web applications. Its vast array of functions, including event handling, HTML document traversal and manipulation, and AJAX communication, has made it a highly popular tool for developers worldwide.
Out of the box, jQuery does not come with built-in state management capabilities. To manage state within our modules, we must use the $.data()
method.
If a module becomes too complex to rely on $.data()
, it may be necessary to adopt a JavaScript MVC. However, whether or not this approach makes sense depends on the specific project requirements. In some cases, adopting a JavaScript MVC may be like using a shotgun to kill a fly – it may be overkill for the situation.
Let’s explore the functionality of the jquery-state
plugin, which offers a PubSub jQuery State Management solution, aiming is to strike a balance by providing a means to manage state without requiring significant modifications to the existing application architecture.
2. jquery-state fundamentals
2.1. Setting State
You can set state of an UI element by calling .state()
function. For example:
<button type="button" class="btn">Button 1</button>
<button type="button" class="btn">Button 2</button>
<button type="button" class="btn">Button 3</button>
$('.btn')
.state({ active: false }, 'buttonActivation')
.on('click', function (event) {
event.preventDefault();
$(event.currentTarget).state({ active: true }, 'buttonActivation');
});
We can see that each .btn
starts out in the { active: false }
state, which is subscribed to the buttonActivation
namespace.
On click of any button, we will apply the state of { active: true }
.
This operation is naturally paranoid, meaning that the state is accrued rather than replaced.
Each of these objects are also stored in a global state history array.
2.2. State Change Announcement
When an element’s state changes, it emits an event that can be detected by listening for it on the element. By doing so, we can enable other modules to respond to these state changes, something similar to the useEffect hook in ReactJS.
$('.btn').on('state:buttonActivation', function (event, fullState, currentState) {
if (currentState.active) {
$(event.currentTarget).addClass('btn--active');
}
});
Passed to the event handler are the following arguments:
-
- the jQuery Event object,
- the full, accumulated state object for the element,
- an object representing the state change that just occurred
2.3. Recalling an element’s state
To return the state of an element:
$('.btn:last').state();
If you pass a collection of 1+ elements to the state
method, it will return the first element’s state.
$('.btn').state();
2.4. Recalling a page’s state history
To retrieve all of the state applied to all of the elements on a page, use:
$.state()
2.5. Destroying a page’s state history
Though you probably wouldn’t want to, unless you knew exactly what you were doing, you are able to destroy the page’s global history by calling:
$.state('destroy');
Note: this destroys the global array, not state that has already been set on individual elements. This was mostly added for testing purposes.
2.6. Querying stateful elements by namespace
You can use:
$.state('elementsIn', 'buttonActivation');
to return a collection of all elements that have state subscribed to the given namespace.
3. Let’s build a simple app
3.1. Demo time
We built a simple app, which is our imaginative shopping website, you can take a look at it here: jquery-state-demo
Or you can just take a look at our Github code: DelightInCode jquery-state-demo
3.2. Let’s break it down
Instead of going from top to bottom, we will try to break the demo down into “components”, which means, the app will be divided into “sections”, (e.g. Cart, Cart detail, and Product), each with it’s own UI element (the HTML) and functionality (the jQuery / Javascript).
3.2.1. Cart
Check out the cart’s HTML:
<div class="card cart">
<h3 class="number-of-item">Cart: <span class="count">0</span> <span class="total-money">($0)</span></h3>
<button class="button show-cart-button" onclick="showCart()">
<span class="text">Show cart</span>
</button>
</div>
As you can see, our cart breaks down into 3 dynamic pieces:
-
count
: the number of the items added to the carttotal-money
: the amount of money in totalshowCart()
: a button after clicking it will show us a list of added products
Great start, now let checkout the Javascript, started by declaring a new variable to query the element, setting-up its default state. I will call the state cart
and its value is products: []
.
const cartEl = $(".cart");
cartEl.state({ products: [] }, "cart");
const cartState = cartEl.state().cart; // <- to easier access the state `cart` without re-typing all the thing
I also introduce a function to quickly retrieve the lasted cart
state value. This is because the jquery-state
default return the entire history of the state, instead of just the last one.
function getProductsInCart() {
// just get the last element in the state array
const { products } = cartState[cartState.length - 1 || 0];
return products;
}
Lastly, setting up something that will be triggered by the change of cart
state.
cartEl.on("state:cart", function (event, fullState, currentState) {
// this function is triggered on cart state change
cartEl.find(".number-of-item .count").get(0).innerText =
currentState.products.length;
cartEl
.find(".number-of-item .total-money")
.get(0).innerText = `($${currentState.products
.reduce((acc, curr) => acc + curr.price, 0)
.toFixed(2)})`;
if (
showCartButtonEl.css("display") === "none" &&
currentState.products.length
) {
showCartButtonEl.css("display", "block");
}
if (
showCartButtonEl.css("display") === "block" &&
currentState.products.length === 0
) {
showCartButtonEl.css("display", "none");
}
if (getCartDetailStatus) {
renderProductsInCart();
}
});
Basically, we update the text of count
, recalculate the total-money
, and show the showCartButtonEl
.
You can spot an interesting similarity with this code:
useEffect(() => {
// do things
}, [cart])
Notes: Notice something weird?
Where is the showCartButtonEl
coming from? What is getCartDetailStatus
? What is renderProductsInCart
()?
All will be cleared in just a few minutes.
3.2.2. Cart detail
The HTML:
<div class="row card cart-detail">
<div>
<h4 style="margin-bottom: 0">Cart</h4>
<button style="margin-bottom: 0" class="button" onclick="clearCart()">
Clear
</button>
</div>
<ol class="added-products"></ol>
</div>
Similarly to the Cart, we will need the showCartButtonEl
and its default state is cartDetailStatus
, which has value of { isActive: false }
. This means, the Cart Detail will be hidden.
const showCartButtonEl = $(".show-cart-button");
showCartButtonEl.state({ isActive: false }, "cartDetailStatus");
const cartDetailStatus = showCartButtonEl.state().cartDetailStatus;
const cartDetailEl = $(".cart-detail");
Next, add a function to update the state of cartDetailStatus
to show the Cart Detail.
function getCartDetailStatus() {
const { isActive } = cartDetailStatus[cartDetailStatus.length - 1 || 0] || {};
return isActive;
}
function showCart() {
showCartButtonEl.state(
{
isActive: !getCartDetailStatus(),
},
"cartDetailStatus"
);
}
We will dynamically render the products inside of the cart
state above into the cartDetailEl
.
function renderProductsInCart() {
cartDetailEl.find(".added-products").empty();
getProductsInCart().forEach((product) => {
cartDetailEl
.find(".added-products")
.append(`<li>${product.name} ($${product.price})</li>`);
});
}
We can add a function to empty out the cart, which basically, update the state of cart
back to its origin (aka { products: [] }
)
function clearCart() {
cartEl.state(
{
products: [],
},
"cart"
);
// Hide the Show Cart button since there is nothing in the Cart anyway
showCartButtonEl.state(
{
isActive: false,
},
"cartDetailStatus"
);
}
Finally, we need to do something when the cartDetailStatus
is changed
showCartButtonEl.on(
"state:cartDetailStatus",
function (event, fullState, currentState) {
if (currentState.isActive) {
cartDetaiEl.get(0).style.display = "block";
showCartButtonEl.find(".text").get(0).innerText = "Hide cart";
renderProductsInCart();
} else {
cartDetaiEl.get(0).style.display = "none";
showCartButtonEl.find(".text").get(0).innerText = "Show cart";
}
}
);
3.2.3. List of products
The HTML:
<div class="row" id="list-of-products"></div>
Last but not least, we need a list of products to choose from, so let fake an array of products and render it onto the DOM.
const products = [
{
id: 1,
name: "Product 1",
thumbnail:
"<https://images.unsplash.com/photo-1576618148400-f54bed99fcfd?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=360&q=80>",
price: 13.99,
},
{
id: 2,
name: "Product 2",
thumbnail:
"<https://images.unsplash.com/photo-1572635196237-14b3f281503f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=360&q=80>",
price: 399.99,
},
{
id: 3,
name: "Product 3",
thumbnail:
"<https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=360&q=80>",
price: 1350.39,
},
];
function renderProducts() {
products.forEach((product) => {
$("#list-of-products").append(
`<div class="four columns">
<div class="card">
<img class="thumbnail" src="${product.thumbnail}" onClick="addProduct( '${product.id}')">
<h3>${product.name}</h3>
<p>$${product.price}</p>
<button type="button" class="add-to-card button" onClick="addProduct( '${product.id}')">
Add to cart
</button>
</div>
</div>`
);
});
}
renderProducts();
Awesome, what is left is just actually add the product to the cart on button clicked. All we need to do is add a new item to the cart
’s products array.
function addProduct(id) {
cartEl.state(
{
products: [
...getProductsInCart(),
products.find((p) => p.id.toString() === id),
],
},
"cart"
);
}
With our app now functioning correctly, we can experiment with adjusting the code to gain a deeper understanding of jquery-state. Our hope is that this exploration will provide you with a better understanding of the jquery-state plugin. Thank you.
If you’re interested in building a website using either WordPress or ReactJS and are in need of guidance or assistance, don’t hesitate to contact us. Our experienced team can help you navigate the process of website development, whether you’re looking to create a simple website or a more complex application. We’re well-versed in both WordPress and ReactJS, and can provide you with the guidance and resources you need to ensure your project is successful. Reach out to us today to learn more about how we can assist you in building your ideal website.