Getting started with jquery-state

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:

    1. the jQuery Event object,
    2. the full, accumulated state object for the element,
    3. 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:

    1. count: the number of the items added to the cart
    2. total-money: the amount of money in total
    3. showCart(): 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.

Leave A Reply