Reading the JSON config file is easiest solution for what we want to do here.
The config file that we’re going to use is in config/categories.json
config/categories.json
[
{
"title": "Fruits",
"items": [
"Apples",
"Blackberries",
"Raspberries"
]
},
{
...
}
]
We’ll want to use a separate createcategories.js module, which can be made by using tests to help drive the direction of the code.
tests/main.js
require(
[
"../tests/filtercheckboxesbytext.spec",
...
"../tests/createcategories.spec"
],
The TestRunner tells us that the createCategories test doesn’t exist, so we should create that.
createcategories.spec.js
/*jslint browser */
/*global define, describe, it, expect */
define(["json!categories", "createcategories"], function (catData, createCategories) {
});
Reading the categories.json file is the next issue.
We can easily read and parse the JSON file using a require-json plugin that’s obtained from https://github.com/millermedeiros/requirejs-plugins
We just need lib/text.js and src/json.js from there, both of which we can place in our lib/require/plugins/ folder.
We can then tell the main.js file for the tests and for the code where to find them.
tests/main.js
paths: {
"text": ["../lib/require/plugins/text"],
"json": ["../lib/require/plugins/json"],
"categories": "../tests/categories.json",
...
},
src/main.js
paths: {
"text": ["../lib/require/plugins/text"],
"json": ["../lib/require/plugins/json"],
...
},
The tests need to use a separate categories.json file, as the one that the ordinary code uses will change and we still want to keep the tests working. Because of that, the config/categories.json file is copied to tests/categories.json, and we tell the code where to find the json file.
tests/main.js
paths: {
"categories": "../tests/categories.json",
...
},
tests/main.js
paths: {
"categories": "../../config/categories.json",
...
},
And with that tests return back to passing.
Testing with categories.json
We now want to create the createCategories.init function and test that the createCategories init code gets run:
createcategories.json.js
it("is initialized from the search results code", function () {
spyOn(createCategories, "init");
searchResults.init(catData);
expect(createCategories.init).toHaveBeenCalled();
});
We need to update the searchresults code, first to move the existing init code to a separate function:
searchresults.js
function initSearchResults() {
$(categories).filterCheckboxesByText($(searchField));
...
}
return {
init: function () {
initSearchResults();
}
};
This lets us easily call the createCategories function.
searchresults.js
define(
[
"createcategories",
"jquery",
...
],
function (createCategories, $, checkboxList, resultsController) {
...
return {
init: function (catData) {
createCategories.init(catData);
initSearchResults();
}
};
which needs us to create the createCategories init function:
createcategories.js
/*jslint browser */
/*global define */
define([], function () {
"use strict";
function init(catData) {
console.log(catData);
}
return {
init: init
};
});
The moment of truth
We can then require the categories file and use it.
src/main.js
require(["json!categories", "searchresults"], function (catData, searchResults) {
"use strict";
searchResults.init(catData);
});
Which shows the contents of the categories JSON object in the console browser.
Creating a place for the categories
When we have category data, we want to remove any existing categories below the search area, and create new ones from the category data.
Currently though, the search area is called mytextbox, which isn’t a good name. It would be much better to have a name such as categorysearch instead.
index.html
<!--<p><input type="text" id="mytextbox" placeholder="Search Foods" autocomplete="off"></p>-->
<p><input type="text" id="categorysearch" placeholder="Search Foods" autocomplete="off"></p>
searchresults.js
// var searchField = “#mytextbox”;
var searchField = “#categorysearch”;
createsandbox.js
```javascript
html: "<p><input type='text' id='categorysearch'></p>" +
Adding a filter test
While making this change I decided to find out what happens if I misspell categorysearch, and the tests all still pass. That is not good.
searchresults.js
var searchField = "#badname";
We need to add a test to catch such an obvious issue.
searchresults.spec.js
it("uses a search filter", function () {
categorysearch = $("#categorysearch");
categorysearch.val("fru");
expect($("#categories p:visible").length).toBe(2);
$(categorysearch).trigger("keyup");
expect($("#categories p:visible").length).toBe(1);
});
That test fails, as expected, and when we give the searchfield a name that matches the category search input field, the test passes.
searchresults.js
var searchField = "#categorysearch";
Remove old categories before adding new ones
Next up we can remove any category sections before adding them in using the category data.
createcategories.spec.js
it("removes the categories section", function () {
document.body.innerHTML += "<div id='categories'>";
categories = document.querySelector("#categories");
searchResults.init(catData);
categories = document.querySelector("#categories");
expect(categories).toBeNull();
});
We can make that pass just by removing the element:
createcategories.js
function init() {
categories = document.querySelector("#categories");
categories.remove();
}
and it does pass when that’s the only test that runs, but other tests running before it have side-effects that result in the test failing because more than one categories element exists.
Fixing side-effects
Removing the other tests from the tests/main.js file, all of the createcategories.spec.js tests work, so it’s time to remove the side-effect from other tests.
Removing the sandbox after all of filtercheckboxes tests results in success here.
filtercheckboxes.spec.js
afterAll(function () {
sandbox.init({html: ""});
});
The same afterAll function is also added to the scrollmanager.spec.js and checkboxlist.spec.js tests.
After that, we’re left with a few other tests that remain to be fixed. The Search results
tests are the ones that fails, which we can make pass by returning from the createcategories init function if there is no catData.
createcategories.js
function init(catData) {
var categories = document.querySelector("#categories");
if (!catData) {
return;
}
categories.remove();
}
But if there are no categories, that code’s going to error, so we need to account for when there are no categories as well.
createcategories.spec.js
it("doesn't error when categories doesn't exist", function () {
categories = document.querySelector("#categories");
expect(categories).toBeNull();
searchResults.init(catData);
categories = document.querySelector("#categories");
});
createcategories.js
function init(catData) {
...
if (categories) {
categories.remove();
}
categories.remove();
}
And all of the tests are passing once more.
Future tests to do later
Working through the above situations helps me to understand that there are three situations in which I want the code to work:
- categories in HTML and no category data file
- no categories and category data file
- categories in HTML replaced with category data file
Instead of creating tests for all of those situations, I’ll put a pin in them until the createcategories code is done, then revisit them and add any needed tests to cover missing features.
createcategories.spec.js
xit("works in a variety of common situations", function () {
// TODO: there are three situations in which I want the code to work:
// * categories in HTML and no category data file
// * no categories and category data file
// * categories in HTML replaced with category data file
});
Adding categories to the page
Where do we go from here? Do we loop through each category item and add it to the page? Do we add a single item? Do we ignore the items and do something else?
We can use our overall goal to help drive the code. We need to create the following HTML code for the categories:
<div id="categories">
<p>
<input type="checkbox" id="chkFruits" value="Fruits">
<label for="chkFruits">Fruits</label>
</p>
<p>
<input type="checkbox" id="chkVegetables" value="Vegetables">
<label for="chkVegetables">Vegetables</label>
</p>
<p>
<input type="checkbox" id="chkNuts" value="Nuts">
<label for="chkNuts">Nuts</label>
</p>
<p>
<input type="checkbox" id="chkMeats" value="Meats">
<label for="chkMeats">Meats</label>
</p>
</div>
With that HTML structure in mind, we can start from the outside and work our way in. So first, the categories element needs to be added below the categorysearch input field.
The createcategories code doesn’t need to worry about adding the categorysearch field. That’s outside of the scope of the createcategories code. Instead, passing the categorysearch field into the init function makes a lot of sense, so that we can deal with that issue later on before passing it in.
createcategories.js
function init(catData, search) {
...
}
Which means updating the searchresults code to give it the search field.
init: function (catData) {
var search = document.querySelector(searchField);
createCategories.init(catData, search);
initSearchResults();
}
To test that the paragraph elements exist in the categories section, we need to start with a categorysearch element, and being the good tidy people that we are we clean up after ourselves after each test.
createcategories.spec.js
describe("Create categories", function () {
var search;
beforeEach(function () {
document.body.innerHTML += "<input id='categorysearch'>";
search = document.querySelector("#categorysearch");
});
afterEach(function () {
search = document.querySelector("#categorysearch");
search.remove();
});
...
it("adds paragraphs for each category", function () {
// yet to be implemented
});
We can now test that the categories section is added below the search field.
createcategories.spec.js
it("adds the categories section to the search field", function () {
searchResults.init(catData);
expect(search.nextSibling.id).toBe("categories");
});
createcategories.js
function init(catData, search) {
...
categories = document.createElement("div");
categories.id = "categories";
search.parentNode.appendChild(categories, search.nextSibling);
}
That causes some other tests to fail though. We need to ensure that we don’t guard against situations when the search element doesn’t exist.
createcategories.js
// if (!catData) {
if (!catData || !search) {
return;
}
And the test that checks that a categories element is removed, needs to be updated. Instead of checking that it’s removed, we can check that the categories element no longer has a parent node.
createcategories.spec.js
it("removes pre-existing categories sections", function () {
document.body.innerHTML += "<div id='categories'>";
categories = document.querySelector("#categories");
expect(categories.parentNode).not.toBeNull();
searchResults.init(catData);
// expect(categories).toBeNull();
expect(categories.parentNode).toBeNull();
});
To fix the other failing tests, the categories need to be cleaned up at the end of each test too.
createcategories.spec.js
describe("Create categories", function () {
var search;
var categories;
...
afterEach(function () {
search = document.querySelector("#categorysearch");
search.remove();
categories = document.querySelector("#categories");
if (categories) {
categories.remove();
}
});
And all of the tests return to passing, which is a relief.
Refactor to improve the code
Now that we have passing tests we can refactor the code. The process of removing an old categories section and adding a new one, we should move out to a separate function. I don’t know what to call it yet though, so I’ll just call it replaceOldWithNew for now.
function replaceOldWithNew(categories, search) {
if (categories) {
categories.remove();
}
categories = document.createElement("div");
categories.id = "categories";
search.parentNode.appendChild(categories, search.nextSibling);
}
function init(catData, search) {
var categories = document.querySelector("#categories");
if (!catData || !search) {
return;
}
// if (categories) {
// categories.remove();
// }
// categories = document.createElement("div");
// categories.id = "categories";
// search.parentNode.appendChild(categories, search.nextSibling);
replaceOldWithNew(categories, search);
}
The replaceOldWithNew function name isn’t a good name, so we can now focus on what else to call it.
Terms like refresh or recreate aren’t suitable. How about replenish? No. When you have old sugar and replace it with new, you reprovision? We’re closing in on the appropriate term. It’s one of the hardest programming jobs to come up with the correct names.
Renew! That’s our word.
createcategories.js
// function replaceOldWithNew(categories, search) {
function renewCategories(categories, search) {
...
}
...
// replaceOldWithNew(categories);
renewCategories(categories);
Now that we have a new categories section being added, this is a good time to take a break.
I’ll follow up with the rest of the process of using the JSON data to create categories, followed by the results section.