├── .gitignore
├── CaseStudy
├── EveryPlantSelectionApp
│ ├── client
│ │ ├── app
│ │ │ ├── components
│ │ │ │ └── PlantSelectionInput
│ │ │ │ │ ├── PlantSelectionInput.jsx
│ │ │ │ │ ├── PlantSelectionInput.test.jsx
│ │ │ │ │ ├── PlantSelectionInputSuggestion.jsx
│ │ │ │ │ ├── __snapshots__
│ │ │ │ │ └── PlantSelectionInput.test.jsx.snap
│ │ │ │ │ ├── package.json
│ │ │ │ │ └── usePlantsLike.js
│ │ │ └── index.jsx
│ │ ├── babel.config.js
│ │ ├── dist
│ │ │ └── index.html
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── readme.md
│ │ └── webpack.config.js
│ ├── readme.md
│ └── server
│ │ ├── babel.config.js
│ │ ├── index.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── plantData
│ │ ├── data.json
│ │ ├── package.json
│ │ ├── plantData.js
│ │ └── plantData.test.js
│ │ └── readme.md
└── readme.md
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | npm-debug.log*
3 | node_modules/
4 | jspm_packages/
5 | .npm
6 | .DS_Store
7 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/client/app/components/PlantSelectionInput/PlantSelectionInput.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState, useRef, useEffect} from 'react';
2 | import PlantSelectionInputSuggestion from './PlantSelectionInputSuggestion.jsx';
3 | import usePlantsLike from './usePlantsLike';
4 |
5 | const MIN_QUERY_LENGTH = 3;
6 |
7 | const KEYCODES = {
8 | ENTER: 13,
9 | ARROW_DOWN: 40,
10 | ARROW_UP: 38
11 | };
12 |
13 | export default ({ isInitiallityOpen, value, onSelect }) => {
14 |
15 | const inputRef = useRef();
16 |
17 | const [isOpen, setIsOpen] = useState(isInitiallityOpen || false);
18 | const [query, setQuery] = useState(value);
19 |
20 | const [selectedIndex, setSelectedIndex] = useState(1);
21 | const {loading, plants} = usePlantsLike(query);
22 |
23 | function onKeyUp(e) {
24 | switch (e.keyCode) {
25 | case KEYCODES.ENTER:
26 | if (selectedIndex > -1) {
27 | onSelect(plants[selectedIndex]);
28 | }
29 | break;
30 | case KEYCODES.ARROW_UP:
31 | setSelectedIndex(selectedIndex === 0 ? 0 : selectedIndex - 1);
32 | break;
33 | case KEYCODES.ARROW_DOWN:
34 | setSelectedIndex(
35 | selectedIndex === plants.length - 1 ? selectedIndex : selectedIndex + 1
36 | );
37 | break;
38 | }
39 | }
40 |
41 | useEffect(() => {
42 | setSelectedIndex(-1);
43 | }, [loading, plants]);
44 |
45 | return
46 |
{
48 | setQuery(inputRef.current.value);
49 | setIsOpen(true);
50 | }}
51 | onBlur={() => setIsOpen(false)}
52 | onChange={() => setQuery(inputRef.current.value)}
53 | ref={inputRef}
54 | autoComplete="off"
55 | aria-autocomplete="true"
56 | spellCheck="false"
57 | aria-expanded={String(isOpen)}
58 | role="combobox"
59 | onKeyUp={onKeyUp}
60 | defaultValue={value} />
61 | {
62 | isOpen &&
63 |
64 | {
65 | !plants.length && (!query || query.length < MIN_QUERY_LENGTH) &&
66 | -
67 | Please begin typing a plant name...
68 |
69 | }
70 | {
71 | !plants.length && query && query.length >= MIN_QUERY_LENGTH && loading &&
72 | -
73 | Loading...
74 |
75 | }
76 | {
77 | !plants.length && query && query.length >= MIN_QUERY_LENGTH && !loading &&
78 | -
79 | No plants with that name found...
80 |
81 | }
82 | {
83 | plants.map(
84 | (plant, index) =>
85 | setSelectedIndex(index)}
90 | onClick={() => {
91 | onSelect(plant);
92 | }}
93 | />
94 | )
95 | }
96 |
97 | }
98 |
;
99 | };
100 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/client/app/components/PlantSelectionInput/PlantSelectionInput.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import PlantSelectionInput from './';
4 |
5 | describe('PlantSelectionInput', () => {
6 |
7 | it('Should render deterministically to its snapshot', () => {
8 | expect(
9 | renderer
10 | .create(
11 | {}}
13 | />
14 | )
15 | .toJSON()
16 | ).toMatchSnapshot();
17 | });
18 |
19 | describe('With configured isInitiallyOpen & value properties', () => {
20 | it('Should render deterministically to its snapshot', () => {
21 | expect(
22 | renderer
23 | .create(
24 | {}}
28 | />
29 | )
30 | .toJSON()
31 | ).toMatchSnapshot();
32 | });
33 | });
34 |
35 | });
36 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/client/app/components/PlantSelectionInput/PlantSelectionInputSuggestion.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState, useRef, useEffect} from 'react';
2 |
3 | export default ({ plant, isSelected, onClick, onMouseEnter }) => {
4 |
5 | const ref = useRef();
6 | const [didSelectByMouseEnter, setDidSelectByMouseEnter] = useState(false);
7 |
8 | useEffect(() => {
9 | if (isSelected && !didSelectByMouseEnter) {
10 | ref.current.scrollIntoView(false);
11 | }
12 | if (!isSelected) {
13 | setDidSelectByMouseEnter(false);
14 | }
15 | }, [isSelected]);
16 |
17 | return (
18 | {
25 | setDidSelectByMouseEnter(true);
26 | onMouseEnter(e);
27 | }}
28 | >
29 |
30 | {plant.genus}
31 | {' '}
32 | {plant.species}
33 |
34 |
35 | {plant.family}
36 |
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/client/app/components/PlantSelectionInput/__snapshots__/PlantSelectionInput.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`PlantSelectionInput Should render deterministically to its snapshot 1`] = `
4 |
7 |
18 |
19 | `;
20 |
21 | exports[`PlantSelectionInput With configured isInitiallyOpen & value properties Should render deterministically to its snapshot 1`] = `
22 |
25 |
37 |
38 | `;
39 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/client/app/components/PlantSelectionInput/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "PlantSelectionInput.jsx"
3 | }
4 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/client/app/components/PlantSelectionInput/usePlantsLike.js:
--------------------------------------------------------------------------------
1 | import {useState, useEffect} from 'react';
2 | import debounce from 'debounce-promise';
3 | import fetch from 'node-fetch';
4 |
5 | const makeRequest = debounce(query => {
6 | return (
7 | fetch(`http://localhost:3000/plants/${query}`)
8 | .then(response => response.json())
9 | );
10 | }, 300, { leading: true });
11 |
12 | export default (query, generateDataURL) => {
13 |
14 | const [loading, setLoading] = useState(false);
15 | const [plants, setPlants] = useState([]);
16 |
17 | useEffect(() => {
18 |
19 | if (!query) {
20 | setPlants([]);
21 | return;
22 | }
23 |
24 | setLoading(true);
25 |
26 | makeRequest(query).then(data => {
27 | setLoading(false);
28 | setPlants(data);
29 | });
30 |
31 | }, [query]);
32 |
33 | return {
34 | loading,
35 | plants
36 | }
37 |
38 | };
39 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/client/app/index.jsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom';
2 | import React from 'react';
3 | import PlantSelectionInput from './components/PlantSelectionInput';
4 |
5 | ReactDOM.render(
6 | alert(`You selected ${plant.fullyQualifiedName}`)
9 | }
10 | />,
11 | document.getElementById('root')
12 | );
13 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/client/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env',
5 | {
6 | targets: {
7 | node: 'current',
8 | },
9 | }
10 | ],
11 | '@babel/preset-react'
12 | ]
13 | };
14 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/client/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | EveryPlant Selection App
5 |
6 |
85 |
86 |
87 |
88 | EveryPlant Selection App
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "PlantSelectionClient",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "app/index.jsx",
6 | "type": "module",
7 | "scripts": {
8 | "test": "npx jest",
9 | "start": "webpack-dev-server --open --mode development"
10 | },
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "@babel/preset-env": "^7.6.3",
15 | "babel-loader": "^8.0.6",
16 | "babel-preset-react": "^6.24.1",
17 | "debounce-promise": "^3.1.2",
18 | "enzyme": "^3.10.0",
19 | "node-fetch": "^2.6.0",
20 | "react": "^16.10.2",
21 | "react-dom": "^16.10.2",
22 | "to-string-loader": "^1.1.5"
23 | },
24 | "devDependencies": {
25 | "@babel/cli": "^7.6.4",
26 | "@babel/core": "^7.6.4",
27 | "@babel/preset-react": "^7.6.3",
28 | "babel-cli": "^6.26.0",
29 | "babel-jest": "^24.9.0",
30 | "enzyme-adapter-react-15": "^1.4.1",
31 | "jest": "^24.9.0",
32 | "react-test-renderer": "^16.11.0",
33 | "webpack": "^4.41.2",
34 | "webpack-cli": "^3.3.9",
35 | "webpack-dev-server": "^3.8.2"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/client/readme.md:
--------------------------------------------------------------------------------
1 | # AutoSuggestion Client
2 |
3 | ## Install
4 |
5 | ```bash
6 | $ npm install
7 | ```
8 |
9 | ## Run
10 |
11 | ```bash
12 | $ npm start
13 | ```
14 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/client/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: './app/index.jsx',
5 | devServer: {
6 | contentBase: path.join(__dirname, 'dist'),
7 | compress: true,
8 | port: 9000
9 | },
10 | output: {
11 | filename: 'main.js',
12 | path: path.resolve(__dirname, 'dist'),
13 | },
14 | module: {
15 | rules: [
16 | {
17 | test: /\.(js|jsx)$/,
18 | exclude: /node_modules/,
19 | use: {
20 | loader: 'babel-loader',
21 | options: {
22 | presets: ['@babel/react']
23 | }
24 | }
25 | }
26 | ]
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/readme.md:
--------------------------------------------------------------------------------
1 | # AutoSuggestion client & server
2 |
3 | This repository includes both the *EveryPlant* *AutoSuggestion* client (React) and server (Node.js).
4 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/server/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@babel/preset-env'],
3 | };
4 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/server/index.js:
--------------------------------------------------------------------------------
1 | import TrieSearch from 'trie-search';
2 | import express from 'express';
3 | import plantData from './plantData';
4 |
5 | const app = express();
6 | const port = process.env.PORT || 3000;
7 |
8 | app.get('/plants/:query', (req, res) => {
9 | const query = req.params.query;
10 | res.setHeader('Access-Control-Allow-Origin', '*');
11 | res.json( plantData.query(query) );
12 | });
13 |
14 | app.listen(port, () =>
15 | console.log(`App listening on port ${port}!`))
16 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "PlantSelectionServer",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "start": "npx babel-node index.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "csvtojson": "^2.0.10",
14 | "express": "^4.17.1",
15 | "trie-search": "^1.2.11"
16 | },
17 | "devDependencies": {
18 | "@babel/cli": "^7.6.4",
19 | "@babel/core": "^7.6.4",
20 | "@babel/node": "^7.6.3",
21 | "@babel/preset-env": "^7.6.3",
22 | "babel-jest": "^24.9.0",
23 | "jest": "^24.9.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/server/plantData/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "plantData.js"
3 | }
4 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/server/plantData/plantData.js:
--------------------------------------------------------------------------------
1 | import TrieSearch from 'trie-search';
2 | import plantData from './data.json';
3 |
4 | const MIN_QUERY_LENGTH = 3;
5 |
6 | const trie = new TrieSearch(['fullyQualifiedName'], {
7 | ignoreCase: true,
8 | // splitOnRegEx: true,
9 | // min: 3
10 | });
11 |
12 | trie.addAll(
13 | plantData.map(plant => {
14 | return {
15 | ...plant,
16 | fullyQualifiedName:
17 | `${plant.family} ${plant.genus} ${plant.species}`
18 | };
19 | })
20 | );
21 |
22 | export default {
23 | query(partialString) {
24 | if (partialString.length < MIN_QUERY_LENGTH) {
25 | return [];
26 | }
27 | return trie.get(partialString).slice(0, 30);
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/server/plantData/plantData.test.js:
--------------------------------------------------------------------------------
1 | import plantData from './plantData';
2 |
3 | describe('plantData', () => {
4 |
5 | describe('Query length < 3', () => {
6 | it('Returns emnpty array', () => {
7 | expect(plantData.query('')).toEqual([]);
8 | expect(plantData.query('a')).toEqual([]);
9 | expect(plantData.query('al')).toEqual([]);
10 | });
11 | });
12 |
13 | describe('Valid query length', () => {
14 | describe('Non-existing prefix', () => {
15 | it('Returns empty array', () => {
16 | expect(plantData.query('fooBarDoesNotExist')).toEqual([]);
17 | });
18 | });
19 |
20 | describe('Family name search (Acanthaceae)', () => {
21 | it('Returns plants with family name of "Acanthaceae"', () =>{
22 | const results = plantData.query('Acanthaceae');
23 | expect(
24 | results.filter(plant => plant.family === 'Acanthaceae')
25 | ).toHaveLength(results.length);
26 | });
27 | });
28 |
29 | describe('Family+Genus name search (Acanthaceae Thunbergia)', () => {
30 | it('Returns plants with family and genus of "Acanthaceae Thunbergia"', () =>{
31 | const results = plantData.query('Acanthaceae Thunbergia');
32 | expect(results.length).toBeGreaterThan(0);
33 | expect(
34 | results.filter(plant =>
35 | plant.family === 'Acanthaceae' &&
36 | plant.genus === 'Thunbergia'
37 | )
38 | ).toHaveLength(results.length);
39 | });
40 | });
41 |
42 | describe('Partial family+Genus name search (Acantu Thunbe)', () => {
43 | it('Returns plants with family and genus of "Acanthaceae Thunbergia"', () =>{
44 | const results = plantData.query('Acant Thun');
45 | expect(results.length).toBeGreaterThan(0);
46 | expect(
47 | results.filter(plant =>
48 | /\bAcant/i.test(plant.fullyQualifiedName) &&
49 | /\bThun/i.test(plant.fullyQualifiedName)
50 | )
51 | ).toHaveLength(results.length);
52 | });
53 | });
54 |
55 | describe('Family+Genus+Species name search (Acanthaceae Thunbergia acutibracteata)', () => {
56 |
57 | const EXPECTED_PLANT_DATA = {
58 | family: 'Acanthaceae',
59 | genus: 'Thunbergia',
60 | species: 'acutibracteata',
61 | fullyQualifiedName: 'Acanthaceae Thunbergia acutibracteata',
62 | id: 7779
63 | };
64 |
65 | it('Returns only the single plant', () => {
66 | expect(
67 | plantData.query('Acanthaceae Thunbergia acutibracteata')
68 | ).toEqual([
69 | EXPECTED_PLANT_DATA
70 | ]);
71 | });
72 | describe('Case variants', () => {
73 | it('Returns the same plant regardless of query case', () => {
74 | expect(
75 | plantData.query('acanthaceae thunbergia acutibracteata')
76 | ).toEqual([
77 | EXPECTED_PLANT_DATA
78 | ]);
79 | expect(
80 | plantData.query('Acanthaceae Thunbergia ACUTIbracteata')
81 | ).toEqual([
82 | EXPECTED_PLANT_DATA
83 | ]);
84 | expect(
85 | plantData.query('Acanthaceae THUNBERGIA acutibracteata')
86 | ).toEqual([
87 | EXPECTED_PLANT_DATA
88 | ]);
89 | });
90 | });
91 | });
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/CaseStudy/EveryPlantSelectionApp/server/readme.md:
--------------------------------------------------------------------------------
1 | # AutoSuggestion Server
2 |
3 | ## Install
4 |
5 | ```bash
6 | $ npm install
7 | ```
8 |
9 | ## Run
10 |
11 | ```bash
12 | $ npm start
13 | ```
14 |
15 | ## Data
16 |
17 | For the purposes of this demo, the data lives within `./plantData/data.json`.
18 |
--------------------------------------------------------------------------------
/CaseStudy/readme.md:
--------------------------------------------------------------------------------
1 | # Case Study (Chapter 19 material)
2 |
3 | This directory contains the code for the *Case Study* chapter.
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Packt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | # Clean Code in JavaScript
5 |
6 |
7 |
8 | This is the code repository for [Clean Code in JavaScript](https://www.packtpub.com/in/web-development/clean-code-in-javascript?utm_source=github&utm_medium=repository&utm_campaign=9781789957648), published by Packt.
9 |
10 | **Develop reliable, maintainable, and robust JavaScript**
11 |
12 | ## What is this book about?
13 | Building robust apps starts with creating clean code. In this book, you’ll explore techniques for doing this by learning everything from the basics of JavaScript through to the practices of clean code. You’ll write functional, intuitive, and maintainable code while also understanding how your code affects the end user and the wider community.
14 |
15 | This book covers the following exciting features:
16 | * Understand the true purpose of code and the problems it solves for your end-users and colleagues
17 | * Discover the tenets and enemies of clean code considering the effects of cultural and syntactic conventions
18 | * Use modern JavaScript syntax and design patterns to craft intuitive abstractions
19 | * Maintain code quality within your team via wise adoption of tooling and advocating best practices
20 | * Learn the modern ecosystem of JavaScript and its challenges like DOM reconciliation and state management
21 |
22 | If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1789957648) today!
23 |
24 |
26 |
27 |
28 | ## Instructions and Navigations
29 | All of the code is organized into folders. For example, Chapter19.
30 |
31 | The code will look like the following:
32 | ```
33 | function validatePostalCode(code) {
34 | return /^[0-9]{5}(?:-[0-9]{4})?$/.test(code);
35 | }
36 | ```
37 |
38 | **Following is what you need for this book:**
39 |
40 | Clean coding is an important skill in the portfolio of any developer willing to write reliable and intuitive code. This book presents principles, patterns, anti-patterns, and practices supported by use cases and directions for writing clean JavaScript code. It helps you refactor your legacy codebase in JavaScript and modernize your web apps.
41 |
42 | With the following software and hardware list you can run all code files present in the book (Code is for chapter 19 : CaseStudy).
43 |
44 | ### Software and Hardware List
45 |
46 | | Chapter | Software required | OS required |
47 | | -------- | ------------------------------------| -----------------------------------|
48 | | 19 | Node.js, | Windows |
49 |
50 |
51 |
52 |
53 | We also provide a PDF file that has color images of the screenshots/diagrams used in this book. [Click here to download it]( https://static.packt-cdn.com/downloads/9781789957648_ColorImages.pdf).
54 |
55 |
56 | ### Related products
57 | * Building Forms with Vue.js [[Packt]](https://www.packtpub.com/in/business-other/building-forms-with-vue-js?utm_source=github&utm_medium=repository&utm_campaign=9781839213335) [[Amazon]](https://www.amazon.com/dp/B07YY7MGDD)
58 |
59 | * Web Development with Angular and Bootstrap - Third Edition [[Packt]](https://www.packtpub.com/in/web-development/web-development-angular-and-bootstrap-third-edition?utm_source=github&utm_medium=repository&utm_campaign=9781788838108) [[Amazon]](https://www.amazon.com/dp/B07KJJ2ZCF)
60 |
61 | ## Get to Know the Author
62 | **James Padolsey** is a passionate JavaScript and UI engineer with over 12 years' experience. James began his journey into JavaScript as a teenager, teaching himself how to build websites for school and small freelance projects. In the early years, he was a prolific blogger, sharing his unique solutions to common problems in the domains of jQuery, JavaScript, and the DOM. He later contributed to the jQuery library itself and authored a chapter within the jQuery Cookbook published by O'Reilly Media. Over subsequent years, James has been exposed to many unique software projects in his employment at Stripe, Twitter, and Facebook, informing his philosophy on what clean coding truly means in the ever-changing ecosystem of JavaScript.
63 |
64 |
65 | ### Suggestions and Feedback
66 | [Click here](https://docs.google.com/forms/d/e/1FAIpQLSdy7dATC6QmEL81FIUuymZ0Wy9vH1jHkvpY57OiMeKGqib_Ow/viewform) if you have any feedback or suggestions.
67 | ### Download a free PDF
68 |
69 | If you have already purchased a print or Kindle version of this book, you can get a DRM-free PDF version at no cost.
Simply click on the link to claim your free PDF.
70 | https://packt.link/free-ebook/9781789957648
--------------------------------------------------------------------------------