├── .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 |
  1. 67 | Please begin typing a plant name... 68 |
  2. 69 | } 70 | { 71 | !plants.length && query && query.length >= MIN_QUERY_LENGTH && loading && 72 |
  3. 73 | Loading... 74 |
  4. 75 | } 76 | { 77 | !plants.length && query && query.length >= MIN_QUERY_LENGTH && !loading && 78 |
  5. 79 | No plants with that name found... 80 |
  6. 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 | Clean Code in JavaScript 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 | https://www.packtpub.com/ 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

    --------------------------------------------------------------------------------