Not quite a long time ago, but roughly two years ago I wrote a blog post on updating and supporting, URL parameters with Vue.js.
The idea was this: Given an application that lets you perform various tweaks, it would be nice if the URL was updated to reflect the current state of the application. This would let you bookmark, or share, the URL with others and they would get the same view as you.
In that post, I built a very basic "data filtering" application and then updated it to support updates to the URL. I thought I'd revisit that post and demonstrate building it in vanilla JavaScript. As always, I'd love to hear your thoughts on this, especially if you've done something similar.
I'm going to cheat a bit and steal some of the text/images from the older post.
Here's our application in its default state:
There's a list of items that consist of people, cats, and a dog. Each item has a name and type. On top, there are filters for the name and type. If you enter any text, the items that match the name (ignoring case) will be shown. If you select one or more of the types, only those matching will be shown.
Let's take a look at the code. First, the HTML:
<div id="app">
<h2>Items</h2>
<p>
<input type="search" placeholder="Filter by name" id="nameFilter">
<input type="checkbox" value="person" id="personType" name="typeFilter">
<label for="personType">Only People</label>
<input type="checkbox" value="cat" id="catType" name="typeFilter">
<label for="catType">Only Cats</label>
<input type="checkbox" value="dog" id="dogType" name="typeFilter">
<label for="dogType">Only Dogs</label>
</p>
<ul id="results">
</ul>
</div>
This isn't too different from the earlier Vue version, but I've removed v-model
and other Vue declarations. Now, the JavaScript. First, I've got my data hard-coded on top.
Here's how it looks:
const ITEMS = [
{ name: "Ray", type: "person" },
{ name: "Lindy", type: "person" },
{ name: "Jacob", type: "person" },
{ name: "Lynn", type: "person" },
{ name: "Noah", type: "person" },
{ name: "Jane", type: "person" },
{ name: "Maisie", type: "person" },
{ name: "Carol", type: "person" },
{ name: "Ashton", type: "person" },
{ name: "Weston", type: "person" },
{ name: "Sammy", type: "cat" },
{ name: "Aleese", type: "cat" },
{ name: "Luna", type: "cat" },
{ name: "Pig", type: "cat" },
{ name: "Cayenne", type: "dog" }
]
Normally this would be loaded in via a network call or some such. Next, I define different variables and the "start-up" code:
let filteredItems = ITEMS;
let $results, $nameFilter, $typeFilter;
document.addEventListener('DOMContentLoaded', init, false);
function init() {
$results = document.querySelector('#results');
$nameFilter = document.querySelector('#nameFilter');
$typeFilter = document.querySelectorAll('input[name="typeFilter"]');
$nameFilter.addEventListener('input', updateFilter, false);
$typeFilter.forEach(f => f.addEventListener('change', updateFilter, false));
renderItems();
}
The only really interesting part is here that I listen for any change or input event on my fields on top, all of them going to the same particular function to handle those changes.
renderItems
just handles generated my HTML list:
function renderItems() {
let res = '';
filteredItems.forEach(i => res +=`<li>${i.name}</li>`);
$results.innerHTML = res;
}
But updateFilter
is a bit more complex. I need to potentially filter by text input as well as multiple different "type" filters:
function updateFilter() {
let selectedTypes = Array.from($typeFilter).reduce((res, cur) => {
if(cur.checked) res.push(cur.value);
return res;
}, []);
filteredItems = ITEMS.filter(i => {
if($nameFilter.value !== '' && i.name.toLowerCase().indexOf($nameFilter.value.toLowerCase()) === -1) return false;
if(selectedTypes.length && !selectedTypes.includes(i.type)) return false;
return true;
});
renderItems();
}
I think the only really odd thing above is Array.from
, because querySelectorAll
returns a NodeList
, not a real array.
All in all, I've got a bit more code than the Vue.js version, but I'm also not loading Vue, so a net win for this simple application.
You can test this yourself below.
Ok, for our new version, we need to do two things:
Let's start with the latter. In my updateFilter
method, in the end, I added a call to a new function, updateURL
:
function updateURL() {
let qp = new URLSearchParams();
if($nameFilter.value !== '') qp.set('filter', $nameFilter.value);
let selectedTypes = Array.from($typeFilter).reduce((res, cur) => {
if(cur.checked) res.push(cur.value);
return res;
}, []);
if(selectedTypes.length) qp.set('typeFilter', selectedTypes);
history.replaceState(null, null, "?"+qp.toString());
}
This uses the URLSearchParams API to generate a new query string. I begin by checking the input field for a value and if it exists, set the filter
param to it.
For the selected types, I check them all and simply append the value if they are checked. This creates an array I can then set to typeFilter
by relying on an automatic toString
conversion.
Finally, I use the replaceState method of the History API to update the URL. The third argument doesn't need to be a full URL as I'm just changing the values in the query string.
That part's rather easy, but to support recognizing the parameters on load, I've modified my init function:
let filteredItems = Array.from(ITEMS);
let $results, $nameFilter, $typeFilter, $peopleFilter, $catFilter, $dogFilter;
document.addEventListener('DOMContentLoaded', init, false);
function init() {
$results = document.querySelector('#results');
$nameFilter = document.querySelector('#nameFilter');
$typeFilter = document.querySelectorAll('input[name="typeFilter"]');
$peopleFilter = document.querySelector('#personType');
$catFilter = document.querySelector('#catType');
$dogFilter = document.querySelector('#dogType');
$nameFilter.addEventListener('input', updateFilter, false);
$typeFilter.forEach(f => f.addEventListener('change', updateFilter, false));
let qp = new URLSearchParams(window.location.search);
if(qp.get('filter')) $nameFilter.value = qp.get('filter');
let tf = qp.get('typeFilter');
if(tf) {
tf.split(',').forEach(t => {
if(t === 'people') $peopleFilter.checked = true;
if(t === 'cat') $catFilter.checked = true;
if(t === 'dog') $dogFilter.checked = true;
});
}
updateFilter();
renderItems();
}
I've added a few more variables to make it easier to check my individual type filters. I get my current query string and then begin checking for my two main values, filter
and typeFilter
. Working with filter
is easy, but for the typeFilter
, I need to check each possible value and check the appropriate box. Also, notice I've added a call to update the filter since it's possible we have filtering going on.
And that's it. Now, I'd like to show you on CodePen, but unfortunately it won't work correctly there. You can grab the code there if you want (https://codepen.io/cfjedimaster/pen/dygWQwj?editors=1011), but don't bother trying to use it there.
Instead, I put it in one of my repos and you can browse it here: https://cfjedimaster.github.io/webdemos/history/
Or, test an example with stuff already filtered: https://cfjedimaster.github.io/webdemos/history/?filter=y&typeFilter=person
Also published here.