I'll break this down into it's seperate steps:
1) The JSON file
If we carefully choose the format of our JSON file, we can provide all of the data and even more that we need to construct the entire page.
Hence I would put all the information about all cars we have inside this file, so we never have to update the HTML file after adding brands or models.
If we would only keep the availability of a car inside the JSON file, we would need to update both the JSON file AND also the HTML file to add a brand or type.
The availability is also better noted as an integer representing the amount of cars there are available instead of a string. If it was a string, we would need to parse that string to see if there's still cars available.
By seperating the id of the car from it's product code, we can keep the product code as a string so that it can contain more than only numbers and also still keep an easy way to sort our cars. Remember that strings sort differently than integers: "10" < "9" === true and 10 < 9 === false. Else this could lead to problems if we ever have a car with code "999".
An added advantage is that this nicely maps to table columns if we'd ever move this into a database.
[
{
"availability": 25,
"brand": "bmw",
"code": "1000",
"id": 1,
"model": "m1"
},
{
"availability": null,
"brand": "bmw",
"code": "1001",
"id": 2,
"model": "m3"
},
{
"availability": 10,
"brand": "bmw",
"code": "1002",
"id": 3,
"model": "m5"
},
{
"availability": 7,
"brand": "ford",
"code": "1003",
"id": 4,
"model": "fiesta"
},
{
"availability": 14,
"brand": "ford",
"code": "1004",
"id": 5,
"model": "mondeo"
},
{
"availability": null,
"brand": "ford",
"code": "1005",
"id": 6,
"model": "escort"
}
]
2) Fetching the file
We have two mechanisms here to do this. Either the old XMLHttpRequest() if we have to be compatible with old browsers. Or the fetch() API for new browsers.
This choice will determine if we have to use callbacks or promises. ( Unless we transform the XMLHttpRequest version into a promise as well. )
XMLHttpRequest:
// The path where we can find the JSON file.
const PATH_CARS = 'http://path/to/cars.json';
// A getJSON function that will create an ajax request to the provided URL.
const getJSON = ( url, callback ) => {
// Create a new XMLHttpRequest.
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
const request = new XMLHttpRequest();
// Open the request before setting other properties. ( IE11 )
request.open( 'GET', url );
// When the request gets the file, we want to run the callback.
// The responseText will be the JSON string inside our json file.
request.onload = function() {
callback( request.responseText );
};
request.send();
};
// Use the function to get the file.
// Parse and log the contents of the file once it arrives.
getJSON( PATH_CARS, function( response ) {
// cars will be a string here. We want the actual JS object represented by the JSON string
const cars = JSON.parse( response );
console.log( cars );
});
fetch:
// The path where we can find the JSON file.
const PATH_CARS = 'http://path/to/cars.json';
// Same thing, but using the fetch API for browsers that support it.
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
// The fetch API uses promises instead of callbacks to handle the results.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
fetch( PATH_CARS )
.then( response => response.json())
.then( cars => {
console.log( cars );
});
3) Creating the table
We'll switch the logic around a little bit. Instead of having a fixed HTML that we want to update with values coming from a file, we can just create the entire table from the JSON file so that all the updates are already in the table.
If we then need to update the table again, we can just rerender the entire table instead of trying to match the HTML nodes with the correct values inside the JSON. This won't work that fast for huge amount of cars, ( think 1000+ ) but still way faster than updating every car individually.
This falls into what we call the Model-View-Controller architecture. The JSON file gives us a model of the cars. The HTML table is a view of that model. The javascript code binds it all together as the controller. The controller fetches the model, and turns the model into a view representing that model. Every time the model changes, ( you add a car to the JSON file ), we can request the controller to fetch the updated model ( load the JSON file ) and the update the view. ( render the tables again )
// We need to create a table for each brand.
// We need to create a table row for each car model of that type.
// For big projects, one would use a templating language to create the HTML.
// For something as small as thing, we can resort to simple string manipulation.
const createTables = brands => {
// Loop over all the brands, creating a table for each brand.
// I'll use a reduction this time, to show the difference and similarities between reduce() and the forEach() we used in the previous step.
const tables = brands.reduce(( html, brand ) => {
// Copy the header, replacing the brand name.
const header = `<table><thead><tr><th colspan="3">${ brand.name }</th></tr><tr><th>Product Code:</th><th>Model:</th><th>In Stock:</th></tr></thead><tbody>`;
// Loop over the cars and create a row for each car.
// Since we create the same amount of rows as we have cars inside the array, we can use .map()
const rows = brand.cars.map( car => {
// Since we changed the availability to a number, we hve to recreate the string for it.
// This allows us to easily change the label without having to change the logic in multiple places
const availability_label = Number.isInteger( car.availability )
? `${ car.availability } in stock.`
: 'End of life.';
return `<tr><td>${ car.code }</td><td>${ car.model }</td><td>${ availability_label }</td></tr>`;
});
// Append the current header, car rows and the closing tags to the previous HTML, then return.
return html += `${ header }${ rows.join('') }</tbody></table>`;
}, '');
// Return the HTML string. We could also just return the reduction directly, wihtout using th tables variable in between.
return tables;
};
4) Putting it all together
Using all the techniques and functions we created in the examples, we now have everything to create our entire app. I've added another helper function that groups all the cars into their brands, so creating the tables is easier and more clear.
I have mocked up the fetching of the JSON file in the example below so we can actually run the code. IN your own code, you would use the real fetch() or XMLHttpRequest() code.
// FAKE FETCH, DO NOT USE IN THE REAL CODE
const fetch = url => Promise.resolve({json: () => JSON.parse('[{"availability":25,"brand":"bmw","code":"1000","id":1,"model":"m1"},{"availability":null,"brand":"bmw","code":"1001","id":2,"model":"m3"},{"availability":10,"brand":"bmw","code":"1002","id":3,"model":"m5"},{"availability":7,"brand":"ford","code":"1003","id":4,"model":"fiesta"},{"availability":14,"brand":"ford","code":"1004","id":5,"model":"mondeo"},{"availability":null,"brand":"ford","code":"1005","id":6,"model":"escort"}]')});
// The path where we can find the JSON file.
const PATH_CARS = 'http://path/to/cars.json';
// Same thing, but using the fetch API for browsers that support it.
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
// The fetch API uses promises instead of callbacks to handle the results.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
const getCars = url => fetch( url )
.then( response => response.json())
.catch( error => console.error( error ));
// We need to group all the different cars into their respective brands.
const groupBrands = cars => {
// Create a temporary object we'll use to store the different brands.
const brands = {};
// Loop over all the car models, grouping them into the correct brand.
cars.forEach( car => {
// Extract the brand name from the car item.
const brand = car.brand;
// If we haven't seen this brand yet, add it to the different brands as an array.
if ( !brands.hasOwnProperty( brand )) brands[ brand ] = [];
// Push the car model to the brand.
brands[ brand ].push( car );
});
// We now have an object containign all the cars grouped by brand.
// It would be easier however, if we had ana rray we can loop over easily.
// So transform the object back into an array.
// We loop over the entries array of the object to extarct the name and cars at the same time, then wrap them back into an object.
return Object.entries( brands ).map(([ name, cars ]) => ({ name, cars }));
// This entire step can be done in one expression by using array.reduce() instead of array.forEach()
// We could also just return the object and loop over the entries in the render function.
// My personal preference is to always use an array to represent multiples of something:
// A 'collection' of 'brand' objects with each brand containing a 'collection' of 'car' objects.
// We could also already do this grouping inside the JSON file itsself, but I preferred to keep the JSON file itsself simple for this example.
};
// We need to create a table for each brand.
// We need to create a table row for each car model of that type.
// For big projects, one would use a templating language to create the HTML.
// For something as small as thing, we can resort to simple string manipulation.
const createTables = brands => {
// Loop over all the brands, creating a table for each brand.
// I'll use a reduction this time, to show the difference and similarities between reduce() and the forEach() we used in the previous step.
const tables = brands.reduce(( html, brand ) => {
// Copy the header, replacing the brand name.
const header = `<table><thead><tr><th colspan="3">${ brand.name }</th></tr><tr><th>Product Code:</th><th>Model:</th><th>In Stock:</th></tr></thead><tbody>`;
// Loop over the cars and create a row for each car.
// Since we create the same amount of rows as we have cars inside the array, we can use .map()
const rows = brand.cars.map( car => {
// Since we changed the availability to a number, we hve to recreate the string for it.
// This allows us to easily change the label without having to change the logic in multiple places
const availability_label = Number.isInteger( car.availability )
? `${ car.availability } in stock.`
: 'End of life.';
return `<tr><td>${ car.code }</td><td>${ car.model }</td><td>${ availability_label }</td></tr>`;
});
// Append the current header, car rows and the closing tags to the previous HTML, then return.
return html += `${ header }${ rows.join('') }</tbody></table>`;
}, '');
// Return the HTML string. We could also just return the reduction directly, wihtout using th tables variable in between.
return tables;
};
// We have a JSON file, we can fetch that file, we can create tables from the contents, time to put it all together.
// Fetch the JSON file.
getCars( PATH_CARS )
// Group the cars into brands.
.then( groupBrands )
// Create a table for each group.
.then( createTables )
// Render the tables into the page.
.then( html => {
const tableHook = document.querySelector( '#cars' );
if ( tableHook ) tableHook.innerHTML = html;
// else throw new Error(); something went wrong.
})
// Catch any errors encountered.
.catch( error => console.error( error ));
<html>
<head>
<title>Car Stocks</title>
</head>
<body>
<div id="cars"></div>
</body>
</html>
5) Upgrades
Alot of the code above can be written way shorter, but I intentionally used the longer versions to keep the amount of new things to learn to a minimum. Same code can be written with callbacks, in the case that promises aren't supported. Internally, the functions will mostly stay the same.
I'll leave re-adding the CSS again up to you, since that was working well already.