This section is included to assist the students to perform the activities in the book. It includes detailed steps that are to be performed by the students to achieve the objectives of the activities.
Solution:
var csv = 'name,price,unit ';
var elements = Array.from(document.getElementsByClassName('item'));
elements.forEach((el) => {});
var priceAndUnitElement = el.getElementsByTagName('span')[0];
var priceAndUnit = priceAndUnitElement.textContent.split("/");
var price = priceAndUnit[0].trim();
var unit = priceAndUnit[1].trim();
var name = el.getElementsByTagName('a')[0].textContent;
csv += `${name},${price},${unit} `;
console.log(csv);
After pressing Enter to execute the code, you should see the CSV printed in the console, as shown here:
Solution:
class TagsHolder extends HTMLElement {
}
customElements.define('tags-holder', TagsHolder);
render() {
this.shadowRoot.innerHTML = `
<link rel="stylesheet" type="text/css" href="../css/semantic.min.css" />
<div>
Filtered by tags:
<span class="tags"></span>
</div>`;
}
renderTagList() {
const tagsHolderElement = this.shadowRoot.querySelector('.tags');
tagsHolderElement.innerHTML = '';
const tags = this._selectedTags;
if (tags.length == 0) {
tagsHolderElement.innerHTML = 'No filters';
return;
}
tags.forEach(tag => {
const tagEl = document.createElement('span');
tagEl.className = "ui label orange";
tagEl.addEventListener('click', () => this.triggerTagClicked(tag));
tagEl.innerHTML = tag;
tagsHolderElement.appendChild(tagEl);
});
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._selectedTags = [];
this.render();
this.renderTagList();
}
get selectedTags() {
return this._selectedTags.slice(0);
}
triggerChanged(tag) {
const event = new CustomEvent('changed', { bubbles: true });
this.dispatchEvent(event);
}
triggerTagClicked(tag) {
const event = new CustomEvent('tag-clicked', {
bubbles: true,
detail: { tag },
});
this.dispatchEvent(event);
}
addTag(tag) {
if (!this._selectedTags.includes(tag)) {
this._selectedTags.push(tag);
this._selectedTags.sort();
this.renderTagList();
this.triggerChanged();
}
}
removeTag(tag) {
const index = this._selectedTags.indexOf(tag);
if (index >= 0) {
this._selectedTags.splice(index, 1);
this.renderTagList();
this.triggerChanged();
}
}
<div class="item">
Filtered by tags: <span class="tags"></span>
</div>
And add:
<tags-holder class="item"></tags-holder>
Also add:
<script src="tags_holder.js"></script>
You can see the final HTML on GitHub at https://github.com/TrainingByPackt/Professional-JavaScript/blob/master/Lesson01/Activity02/dynamic_storefront.html.
At the top, create a reference to the tags-holder component:
const filterByTagElement = document.querySelector('tags-holder');
Add event listeners to handle the changed and tag-clicked events:
filterByTagElement.addEventListener('tag-clicked', (e) => filterByTagElement.removeTag(e.detail.tag));
filterByTagElement.addEventListener('changed', () => applyFilters());
Remove the following functions and all references to them: createTagFilterLabel and updateTagFilterList.
In the filterByTags function, replace tagsToFilterBy with filterByTagElement.selectedTags.
In the addTagFilter method, replace the references to tagsToFilterBy with filterByTagElement.addTag.
Solution:
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See 'npm help json' for definitive documentation on these fields and exactly what they do.
Use 'npm install <pkg>' afterwards to install a package and save it as a dependency in the package.json file.
Press ^C at any time to quit.
package name: (Activity03)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to .../Lesson02/Activity03/package.json:
{
"name": "Activity03",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"author": "",
"license": "ISCs"
}
Is this OK? (yes)
$ npm install cheerio
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN [email protected] No description
npm WARN [email protected] No repository field.
+ [email protected] 19 packages from 45 contributors and audited 34 packages in 6.334s
found 0 vulnerabilities
const cheerio = require('cheerio');
const html = `
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<p>This is a paragraph.</p>
<div>
<p>This is a paragraph inside a div.</p>
</div>
<button>Click me!</button>
</body>
</html>
`;
const $ = cheerio.load(html);
$('div').append('<p>This is another paragraph.</p>');
We can also query the HTML, similar to what we did in Chapter 1, JavaScript, HTML, and the DOM, using CSS selectors. Let's query for all the paragraphs and print their content to the console. Notice that cheerio elements do not behave exactly like DOM elements, but they are very similar.
$('p').each((index, p) => {
console.log(`${index} - ${p.firstChild.data}`);
});
console.log($.html());
Now, you can run your application by calling it from Node.js:
Solution
$ node Lesson03/Activity04/
Static resources from /path/to/repo/Lesson03/Activity04/static
Loaded 21 products...
Go to: http://localhost:3000
$ npm init
...
$ npm install jsdom
added 97 packages from 126 contributors and audited 140 packages in 12.278s
found 0 vulnerabilities
const fs = require('fs');
const http = require('http');
const JSDOM = require('jsdom').JSDOM;
const page = 'http://localhost:3000';
console.log(`Downloading ${page}...`);
const request = http.get(page, (response) => {
if (response.statusCode != 200) {
console.error(`Error while fetching page ${page}: ${response.statusCode}`);
console.error(`Status message: ${response.statusMessage}`);
return;
}
let content = '';
response.on('data', (chunk) => content += chunk.toString());
response.on('close', () => {
console.log('Download finished.');
const document = new JSDOM(content).window.document;
writeCSV(extractProducts(document));
});
The preceding callback calls two functions: extractProducts and writeCSV. These functions are described in the upcoming steps.
function extractProducts(document) {
const products = [];
console.log('Parsing product data...');
Array.from(document.getElementsByClassName('item'))
.forEach((el) => {
process.stdout.write('.');
const priceAndUnitElement = el.getElementsByTagName('span')[0];
const priceAndUnit = priceAndUnitElement.textContent.split("/");
const price = priceAndUnit[0].trim().substr(1);
const unit = priceAndUnit[1].trim();
const name = el.getElementsByTagName('a')[0].textContent;
products.push({ name, price: parseFloat(price), unit });
});
console.log();
console.log(`Found ${products.length} products.`);
return products;
}
function writeCSV(products) {
const fileName = 'products.csv';
console.log(`Writing data to ${fileName}...`);
fs.open(fileName, 'w', (error, fileDescriptor) => {
if (error != null) {
console.error(`Can not write to file: ${fileName}`, error);
return;
}
// Write header
fs.writeSync(fileDescriptor, 'name,price,unit ');
// Write content
products.forEach((product) => {
const line = `${product.name},${product.price},${product.unit} `;
fs.writeSync(fileDescriptor, line);
});
console.log('Done.');
});
}
$ node .
Downloading http://localhost:3000...
Download finished.
Parsing product data...
.....................
Found 21 products.
Writing data to products.csv...
Solution
mkdir passcode
cd passcode
npm init -y
npm install --save express express-validator jwt-simple
mkdir routes
let config = {};
// random value below generated with command: openssl rand -base64 32
config.secret = "cSmdV7Nh4e3gIFTO0ljJlH1f/F0ROKZR/hZfRYTSO0A=";
module.exports = config;
const express = require('express');
const jwt = require('jwt-simple');
const { check, validationResult } = require('express-validator/check');
const router = express.Router();
// import our config file and get the secret value
const config = require('../config');
const express = require('express');
const app = express();
const { check, validationResult } = require('express-validator/check');
const router = express.Router();
// Import path and file system libraries for importing our route files
const path = require('path');
const fs = require('fs');
// Import library for handling HTTP errors
const createError = require('http-errors');
// Import library for working with JWT tokens
const jwt = require('jwt-simple');
// import our config file and get the secret value
const config = require('./../config');
const secret = config.secret;
// Create an array to keep track of valid passcodes
let passCodes = [];
router.get(['/code'], [
check('name').isString().isAlphanumeric().exists()
],
(req, res) => {
let codeObj = {};
codeObj.guest = req.body.name;
// Check that authorization header was sent
if (req.headers.authorization) {
let token = req.headers.authorization.split(" ")[1];
try {
req._guest = jwt.decode(token, secret);
} catch {
res.status(403).json({ error: 'Token is not valid.' });
}
// If the decoded object guest name property
if (req._guest.name) {
codeObj.creator = req._guest.name;
router.post(['/open'], [
check('code').isLength({ min: 4, max: 4 })
],
(req, res) => {
let code = passCodes.findIndex(obj => {
return obj.code === req.body.code;
});
if(code !== -1) {
passCodes.splice(code, 1);
res.json({ message: 'Pass code is valid, door opened.' });
} else {
res.status(403).json({ error: 'Pass code is not valid.' });
}
});
// Export route so it is available to import
module.exports = router;
const express = require('express');
const app = express();
// Import path and file system libraries for importing our route files
const path = require('path');
const fs = require('fs');
// Import library for handling HTTP errors
const createError = require('http-errors');
// Tell express to enable url encoding
app.use(express.urlencoded({extended: true}));
app.use(express.json());
// Import our index route
let lock = require('./routes/lock');
let checkIn = require('./routes/check-in');
app.use('/check-in', checkIn);
app.use('/lock', lock);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
npm start
TOKEN=$(curl -sd "name=john" -X POST http://localhost:3000/check-in
| jq -r ".token")
echo $TOKEN
You should get back a long string of letters and numbers like the following:
curl -sd "name=sarah" -X GET
-H "Authorization: Bearer ${TOKEN}"
http://localhost:3000/lock/code
| jq
You should get back an object containing a message and a four-digit code like in the following:
# IMPORTANT: Make sure to replace 4594, with your specific passcode!
curl -sd "code=4594" -X POST
http://localhost:3000/lock/open
| jq
Running the preceding command twice should return something like the following:
If your result is the same as shown in the preceding figure, then you have successfully completed the activity.
Solution:
npm install --save-dev webpack webpack-cli @babel/core @babel/cli @babel/preset-env
{
"presets": ["@babel/preset-env"]
}
const path = require("path");
module.exports = {
mode: 'development',
entry: "./build/js/viewer.js",
output: {
path: path.resolve(__dirname, "build"),
filename: "bundle.js"
}
};
import Light from './light.js';
let privateVars = new WeakMap();
class FlashingLight extends Light {
constructor(state=false, brightness=100, flashMode=true) {
super(state, brightness);
let info = {"flashMode": flashMode};
privateVars.set(this, info);
if(flashMode===true) {
this.startFlashing();
}
}
setFlashMode(flashMode) {
let info = privateVars.get(this);
info.flashMode = checkStateFormat(flashMode);
privateVars.set(this, info);
if(flashMode===true) {
this.startFlashing();
} else {
this.stopFlashing();
}
}
getFlashMode() {
let info = privateVars.get(this);
return info.flashMode;
}
startFlashing() {
let info = privateVars.get(this);
info.flashing = setInterval(this.toggle.bind(this),5000);
}
stopFlashing() {
let info = privateVars.get(this);
clearInterval(info.flashing);
}
}
export default FlashingLight;
button.onclick = function () {
new FlashingLight(true, slider.value, true);
}
npm run build
<script src="bundle.js" type="module"></script>
Solution
npm install --save-dev eslint prettier eslint-config-airbnb-base eslint-config-prettier eslint-plugin-jest eslint-plugin-import
{
"extends": ["airbnb-base", "prettier"],
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"env": {
"browser": true,
"node": true,
"es6": true,
"mocha": true,
"jest": true
},
"plugins": [],
"rules": {
"no-unused-vars": [
"error",
{
"vars": "local",
"args": "none"
}
],
"no-plusplus": "off",
}
}
node_modules
build
dist
"scripts": {
"start": "http-server",
"lint": "prettier --write js/*.js && eslint js/*.js"
},
npm install --save-dev puppeteer jest jest-puppeteer
"jest": {
"preset": "jest-puppeteer"
},
"scripts": {
"start": "http-server",
"lint": "prettier --write js/*.js && eslint js/*.js",
"test": "jest"
},
module.exports = {
server: {
command: 'npm start',
port: 8080,
},
}
describe('Calculator', () => {
beforeAll(async () => {
await page.goto('http://localhost:8080');
})
it('Check that 777 times 777 is 603729', async () => {
const seven = await page.$("#seven");
const multiply = await page.$("#multiply");
const equals = await page.$("#equals");
const clear = await page.$("#clear");
await seven.click();
await seven.click();
await seven.click();
await multiply.click();
await seven.click();
await seven.click();
await seven.click();
await equals.click();
const result = await page.$eval('#screen', e => e.innerText);
expect(result).toMatch('603729');
await clear.click();
})
it('Check that 3.14 divided by 2 is 1.57', async () => {
const one = await page.$("#one");
const two = await page.$("#two");
const three = await page.$("#three");
const four = await page.$("#four");
const divide = await page.$("#divide");
const decimal = await page.$("#decimal");
const equals = await page.$("#equals");
await three.click();
await decimal.click();
await one.click();
await four.click();
await divide.click();
await two.click();
await equals.click();
const result = await page.$eval('#screen', e => e.innerText);
expect(result).toMatch('1.57');
})
})
{
"hooks": {
"pre-commit": "npm run lint && npm test"
}
}
npm test
This should return positive results for two tests, as shown in the following figure:
Ensure the Git hook and linting is working by making a test commit.
Solution
function logUser(userList, user) {
if(!userList.includes(user)) {
userList.push(user);
}
}
Here, we used an includes method to check whether the user already exists. If they don't, they will be added to our list.
function userLeft(userList, user) {
const userIndex = userList.indexOf(user);
if (userIndex >= 0) {
userList.splice(userIndex, 1);
}
}
Here, we are using indexOf to get the current index of the user we want to remove. If the item doesn't exist, indexOf will return –1, so we are only using splice to remove the item if it exists.
function numUsers(userList) {
return userLeft.length;
}
function runSite() {
// Your user list for your website
const users = [];
// Simulate user viewing your site
logUser(users, 'user1');
logUser(users, 'user2');
logUser(users, 'user3');
// User left your website
userLeft(users, 'user2');
// More user goes to your website
logUser(users, 'user4');
logUser(users, 'user4');
logUser(users, 'user5');
logUser(users, 'user6');
// More user left your website
userLeft(users, 'user1');
userLeft(users, 'user4');
userLeft(users, 'user2');
console.log('Current user: ', users.join(', '));
}
runSite();
After defining the functions, running the preceding code will return the following output:
Solution
class School {
constructor() {
this.students = [];
}
}
In the School constructor, we simply initialize a list of students. Later, we will add new students to this list.
class Student {
constructor(name, age, gradeLevel) {
this.name = name;
this.age = age;
this.gradeLevel = gradeLevel;
this.courses = [];
}
}
In the student constructor, we are storing a list of courses, as well as the student's age, name, and gradeLevel.
class Course {
constructor(name, grade) {
this.name = name;
this.grade = grade;
}
}
The course constructor simply stores the name of the course and grade in the object.
addStudent(student) {
this.students.push(student);
}
findByGrade(gradeLevel) {
return this.students.filter((s) => s.gradeLevel === gradeLevel);
}
findByAge(age) {
return this.students.filter((s) => s.age === age);
}
findByName(name) {
return this.students.filter((s) => s.name === name);
}
calculateAverageGrade() {
const totalGrades = this.courses.reduce((prev, curr) => prev + curr.grade, 0);
return (totalGrades / this.courses.length).toFixed(2);
}
In the calculateAverageGrade method, we use array reduce to get the total grades of all the classes for our student. Then, we divide this by the number of courses in our courses list.
assignGrade(name, grade) {
this.courses.push(new Course(name, grade))
}
You should do your work in the student_manager.js file and modify the provided method template. You should see the TEST PASSED message if you implemented everything correctly:
Solution
function itemExist(array, item) {
return array.includes(item);
}
In pushUnique we will use array push to add new item to the bottom
function pushUnique(array, item) {
if (!itemExist(array, item)) {
array.push(item);
}
}
function createFilledArray(size, init) {
const newArray = new Array(size).fill(init);
return newArray;
}
In removeFirst we will use array.shift to remove the first item
function removeFirst(array) {
return array.shift();
}
function removeLast(array) {
return array.pop();
}
In cloneArray we will use spread operation to make clone for our array
function cloneArray(array) {
return […array];
}
class Food {
constructor(type, calories) {
this.type = type;
this.calories = calories;
}
getCalories() {
return this.calories;
}
}
After you have finished the refactor and run the existing code, you should see the same output:
Solution:
function calculate(id, callback) {
}
function calculate(id, callback) {
clientApi.getUsers((error, result) => {
if (error) { return callback(error); }
const currentUser = result.users.find((user) => user.id === id);
if (!currentUser) { return callback(new Error('user not found')); }
});
}
Here, we get all of the users, then we apply the find method to the user to find the user we want from the list. If that user does not exist, we call the callback function with the User not found error.
clientApi.getUsage(id, (error, usage) => {
if (error) { return callback(error); }
});
Then, we need to put the call to getUsage inside the callback of getUsers so it will run after we have finished calling getUsers. Here, the callback will be called with a list of numbers, which will be the usage. We will also call the callback with the error object if we receive an error from getUsage.
clientApi.getRate(id, (error, rate) => {
if (error) { return callback(error); }
let totalUsage = 0;
for (let i = 0; i < usage.length; i++) {
totalUsage += usage[i];
}
callback(null, {
id,
address: currentUser.address,
due: rate * totalUsage
});
});
We will put this call inside the callback for getUsage. This creates a nested chain request for all the information we need. Lastly, we will call the callback with the information we are calculating. For the final due amount, we use array reduce to calculate the total usage for that user, and then multiply that by the rate to get the final amount due.
calculate('DDW2AU', (error, result) => {
console.log(error, result);
});
You should see output like this:
calculate('XXX', (error, result) => {
console.log(error, result);
});
You should see the following output with the error returned:
Solution
async function calculate(id) {
}
const users = await clientApi.getUsers();
const currentUser = users.users.find((user) => user.id === id);
When we are using the await keyword, we must use async functions. The await keyword will break the control of our program and will only return and continue execution once the promise it is waiting for is resolved.
const usage = await clientApi.getUsage(currentUser.id);
const rate = await clientApi.getRate(currentUser.id);
return {
id,
address: currentUser.address,
due: (rate * usage.reduce((prev, curr) => curr + prev)).toFixed(2)
};
async function calculateAll() {
}
const result = await clientApi.getUsers();
return await Promise.all(result.users.map((user) => calculate(user.id)));
Because await will work on any promise and will wait until the value is resolved, it will also wait for our Promise.all. After it is resolved, the final array will be returned.
calculate('DDW2AU').then(console.log)
The output should be as follows:
calculateAll().then(console.log)
The output should be as follows:
As you can see, when we call async functions, we can treat them as functions that return a promise.
Solution:
Perform the following steps to complete this activity:
const EventEmitter = require('events');
class SmokeDetector extends EventEmitter {
constructor() {
super();
this.batteryLevel = 10;
}
}
In our constructor, because we are extending the EventEmitter class and we are assigning a custom property, batteryLevel, we will need to call super inside the constructor and set batteryLevel to 10.
test() {
if (this.batteryLevel > 0) {
this.batteryLevel -= 0.1;
if (this.batteryLevel < 0.5) {
this.emit('low battery');
}
return true;
}
return false;
}
Our test() method will check the battery level and emit a low battery event when the battery has less than 0.5 units. We will also reduce the battery level every time we run the test method.
class House {
constructor(numBedroom, numBathroom, numKitchen) {
this.numBathroom = numBathroom;
this.numBedroom = numBedroom;
this.numKitchen = numKitchen;
this.alarmListener = () => {
console.log('alarm is raised');
}
this.lowBatteryListener = () => {
console.log('alarm battery is low');
}
}
}
In the House class, we are storing some information about the house. We are also storing both of the event listener functions as properties of this object. This way, we can use the function reference to call removeListener when we want to detach a listener.
addDetector(detector) {
detector.on('alarm', this.alarmListener);
detector.on('low battery', this.lowBatteryListener);
}
Here, we are expecting the detector that's passed in to be an EventEmitter. We are attaching two event listeners to our detector argument. When these events are emitted, it will invoke our event emitter inside the object.
removeDetector(detector) {
detector.removeListener('alarm', this.alarmListener);
detector.removeListener('low battery', this.lowBatteryListener);
}
Here, we are using the function reference and the alarm argument to remove the listener attached to our listener. Once this is called, the events should not invoke our listener again.
const myHouse = new House(2, 2, 1);
const detector = new SmokeDetector();
myHouse.addDetector(detector);
for (let i = 0; i < 96; i++) {
detector.test();
}
Because the testing function will reduce the battery level, we will expect a low battery alarm to be emitted if we call it 96 times. This will produce the following output:
detector.emit('alarm');
The following is the output of the preceding code:
myHouse.removeDetector(detector);
detector.test();
detector.emit('alarm');
Because we just removed detector from our house, we should see no output from this:
Solution:
const fs = require('fs').promises;
const EventEmitter = require('events');
We need to create a FileWatcher class that extends EventEmitter. It will take the filename and delay as parameters in the constructor. In the constructor, we will also need to set the last modified time and the timer variable. We will keep them as undefined for now:
class FileWatcher extends EventEmitter {
constructor(file, delay) {
super();
this.timeModified = undefined;
this.file = file;
this.delay = delay;
this.watchTimer = undefined;
}
}
This is the most basic way to see whether a file has been changed.
startWatch() {
if (!this.watchTimer) {
this.watchTimer = setInterval(() => {
fs.stat(this.file).then((stat) => {
if (this.timeModified !== stat.mtime.toString()) {
console.log('modified');
this.timeModified = stat.mtime.toString();
}
}).catch((error) => {
console.error(error);
});
}, this.delay);
}
}
Here, we are using fs.stat to get the file's information and comparing the modified time with the last modified time. If they are not equal, we will output modified in the console.
stopWatch() {
if (this.watchTimer) {
clearInterval(this.watchTimer);
this.watchTimer = undefined;
}
}
The stopWatch method is very simple: we will check if we have a timer in this object. If we do, then we run clearInterval on that timer to clear that timer.
const watcher = new FileWatcher('test.txt', 1000);
watcher.startWatch();
We modified the file twice, which means we are seeing three modified messages. This is happening because when we start the watch, we class this as the file being modified.
startWatch() {
if (!this.watchTimer) {
this.watchTimer = setInterval(() => {
fs.stat(this.file).then((stat) => {
if (this.timeModified !== stat.mtime.toString()) {
fs.readFile(this.file, 'utf-8').then((content) => {
console.log('new content is: ', content);
}).catch((error) => {
console.error(error);
});
this.timeModified = stat.mtime.toString();
}
}).catch((error) => {
console.error(error);
});
}, this.delay);
}
}
When we modify test.txt and save it, our code should detect it and output the new content:
startWatch() {
if (!this.watchTimer) {
this.watchTimer = setInterval(() => {
fs.stat(this.file).then((stat) => {
if (this.timeModified !== stat.mtime.toString()) {
fs.readFile(this.file, 'utf-8').then((content) => {
this.emit('change', content);
}).catch((error) => {
this.emit('error', error);
});
this.timeModified = stat.mtime.toString();
}
}).catch((error) => {
this.emit('error', error);
});
}, this.delay);
}
}
Instead of outputting the content, we will emit an event with the new content. This makes our code much more flexible.
watcher.on('error', console.error);
watcher.on('change', (change) => {
console.log('new change:', change);
});
Solution
render() {
return (
<div>
<p>You have {this.state.items.length} items in your basket</p>
<button onClick={() => this.props.onCheckout(this.state.items)}>
Proceed to checkout
</button>
</div>
);
}
This follows on from the following investigation:
Finding the button in the Basket component's render method whose text is Proceed to checkout.
Noticing its onClick handler is currently a function that does nothing when called, () => {}.
Replacing the onClick handler with the correct call to this.props.onCheckout.
Solution
function test() {
assert.deepStrictEqual(
selectBasketItems(),
[],
'should be [] when selecting with no state'
);
assert.deepStrictEqual(
selectBasketItems({}),
[],
'should be [] when selecting with {} state'
);
}
function test() {
// other assertions
assert.deepStrictEqual(
selectBasketItems({basket: {}}),
[],
'should be [] when selecting with {} state.basket'
);
}
function test() {
// other assertions
assert.deepStrictEqual(
selectBasketItems({basket: {items: []}}),
[],
'should be [] when items is []'
);
}
function test() {
// other assertions
assert.deepStrictEqual(
selectBasketItems({
basket: {items: [{name: 'product-name'}]}
}),
[{name: 'product-name'}],
'should be items when items is set'
);
}
The full test function content after following the previous solution steps:
function test() {
assert.deepStrictEqual(
selectBasketItems(),
[],
'should be [] when selecting with no state'
);
assert.deepStrictEqual(
selectBasketItems({}),
[],
'should be [] when selecting with {} state'
);
assert.deepStrictEqual(
selectBasketItems({basket: {}}),
[],
'should be [] when selecting with {} state.basket'
);
assert.deepStrictEqual(
selectBasketItems({basket: {items: []}}),
[],
'should be [] when items is []'
);
assert.deepStrictEqual(
selectBasketItems({
basket: {items: [{name: 'product-name'}]}
}),
[{name: 'product-name'}],
'should be items when items is set'
);
}
Solution
{
basket {
items {
id
name
price
quantity
}
}
}
The following is the output of the preceding code:
function requestBasket() {
return dispatch => {
fetchFromBff(`{
basket {
items {
id
name
price
quantity
}
}
}`).then(data => {
dispatch({
type: REQUEST_BASKET_SUCCESS,
basket: data.basket
});
});
};
}
const appReducer = (state = defaultState, action) => {
switch (action.type) {
// other cases
case REQUEST_BASKET_SUCCESS:
return {
...state,
basket: action.basket
};
// other cases
}
};
const mapDispatchToProps = dispatch => {
return {
// other mapped functions
requestBasket() {
dispatch(requestBasket());
}
};
};
class App extends React.Component {
componentDidMount() {
this.props.requestBasket();
}
// render method
}
When loading up the application with all the preceding steps completed, it flashes with the "You have 0 items in your basket" message before changing to the following screenshot. When the fetch from the BFF completes, it is reduced into the store and causes a re-render. This will display the basket once again, as follows:
3.145.12.242