├── .nvmrc
├── 9781484249666.jpg
├── Chapter01
├── 1.1 - Framework's Way
│ ├── next.config.js
│ ├── style
│ │ └── index.css
│ ├── pages
│ │ ├── index.js
│ │ ├── pose.js
│ │ └── wa.js
│ └── package.json
├── README.md
└── 1.2 - History of JavaScript Frameworks
│ ├── AngularJS
│ ├── package.json
│ └── index.html
│ └── React
│ ├── index.html
│ ├── dist
│ └── index.html
│ ├── package.json
│ ├── .cache
│ └── 92
│ │ └── e54c60050c02716a394c0e4f88e8c1.json
│ └── index.js
├── Chapter02
├── favicon.ico
├── now.json
├── .babelrc
├── README.md
├── 01
│ ├── index.js
│ ├── getTodos.js
│ └── view.js
├── 02
│ ├── index.js
│ ├── view
│ │ ├── filters.js
│ │ ├── counter.js
│ │ ├── app.js
│ │ ├── todos.js
│ │ ├── filters.test.js
│ │ └── counter.test.js
│ └── getTodos.js
├── 03
│ ├── view
│ │ ├── filters.js
│ │ ├── counter.js
│ │ ├── todos.js
│ │ ├── filters.test.js
│ │ └── counter.test.js
│ ├── getTodos.js
│ ├── index.js
│ └── registry.js
├── 04
│ ├── view
│ │ ├── filters.js
│ │ ├── counter.js
│ │ ├── todos.js
│ │ ├── filters.test.js
│ │ └── counter.test.js
│ ├── getTodos.js
│ ├── index.js
│ └── registry.js
├── 05
│ ├── view
│ │ ├── filters.js
│ │ ├── counter.js
│ │ ├── todos.js
│ │ ├── filters.test.js
│ │ └── counter.test.js
│ ├── getTodos.js
│ ├── index.js
│ ├── registry.js
│ └── applyDiff.js
├── package.json
├── index.html
└── stats.js
├── Chapter03
├── favicon.ico
├── now.json
├── .babelrc
├── README.md
├── 00.3
│ ├── index.js
│ └── index.html
├── 00.1
│ ├── index.js
│ └── index.html
├── 00.2
│ ├── index.js
│ └── index.html
├── 01
│ ├── view
│ │ ├── filters.js
│ │ ├── counter.js
│ │ ├── filters.test.js
│ │ ├── todos.js
│ │ └── counter.test.js
│ ├── getTodos.js
│ ├── index.js
│ ├── registry.js
│ └── applyDiff.js
├── 01.1
│ ├── view
│ │ ├── filters.js
│ │ ├── app.js
│ │ ├── counter.js
│ │ ├── filters.test.js
│ │ ├── todos.js
│ │ └── counter.test.js
│ ├── getTodos.js
│ ├── index.js
│ ├── registry.js
│ └── applyDiff.js
├── 01.2
│ ├── view
│ │ ├── filters.js
│ │ ├── counter.js
│ │ ├── app.js
│ │ ├── filters.test.js
│ │ ├── counter.test.js
│ │ └── todos.js
│ ├── registry.js
│ ├── index.js
│ └── applyDiff.js
├── 01.4
│ ├── view
│ │ ├── filters.js
│ │ ├── counter.js
│ │ ├── app.js
│ │ ├── filters.test.js
│ │ ├── counter.test.js
│ │ └── todos.js
│ ├── registry.js
│ ├── index.js
│ └── applyDiff.js
├── 01.3
│ ├── view
│ │ ├── counter.js
│ │ ├── filters.js
│ │ ├── filters.test.js
│ │ └── counter.test.js
│ ├── registry.js
│ └── index.js
├── 00.4
│ ├── index.html
│ └── index.js
├── package.json
├── 00
│ ├── index.html
│ └── index.js
└── index.html
├── Chapter04
├── favicon.ico
├── now.json
├── 00.1
│ ├── index.js
│ ├── index.html
│ └── components
│ │ └── HelloWorld.js
├── 00.4
│ ├── index.js
│ ├── index.html
│ └── components
│ │ └── GitHubAvatar.js
├── 00
│ ├── index.js
│ ├── components
│ │ └── HelloWorld.js
│ └── index.html
├── .babelrc
├── README.md
├── 01
│ ├── index.js
│ └── components
│ │ ├── Footer.js
│ │ └── Application.js
├── 00.3
│ ├── index.js
│ ├── index.html
│ └── components
│ │ ├── HelloWorld.js
│ │ └── applyDiff.js
├── 00.2
│ ├── index.js
│ ├── index.html
│ └── components
│ │ └── HelloWorld.js
├── package.json
├── 00.5
│ ├── index.js
│ └── index.html
└── index.html
├── Chapter06
├── favicon.ico
├── README.md
├── package.json
├── 00
│ ├── index.js
│ ├── pages.js
│ ├── index.html
│ └── router.js
├── 00.1
│ ├── pages.js
│ ├── index.js
│ ├── index.html
│ └── router.js
├── 01.1
│ ├── index.js
│ ├── index.html
│ └── pages.js
├── 02
│ ├── index.js
│ ├── router.js
│ ├── pages.js
│ └── index.html
├── 01
│ ├── index.js
│ ├── pages.js
│ └── index.html
├── 00.2
│ ├── index.js
│ ├── index.html
│ └── pages.js
└── index.html
├── Chapter07
├── favicon.ico
├── .babelrc
├── README.md
├── 05
│ ├── index.js
│ └── model
│ │ └── observable.js
├── 03.1
│ ├── model
│ │ ├── filter.js
│ │ ├── model.js
│ │ ├── eventBus.js
│ │ └── eventCreators.js
│ ├── view
│ │ ├── counter.js
│ │ └── filters.js
│ ├── index.js
│ └── registry.js
├── 02.1
│ ├── index.js
│ └── index.html
├── 00
│ ├── view
│ │ ├── counter.js
│ │ ├── filters.js
│ │ └── app.js
│ ├── registry.js
│ ├── index.js
│ └── model
│ │ └── model.test.js
├── 01.1
│ ├── view
│ │ ├── counter.js
│ │ └── filters.js
│ ├── registry.js
│ └── index.js
├── 01
│ ├── view
│ │ ├── counter.js
│ │ ├── filters.js
│ │ └── app.js
│ ├── index.js
│ ├── registry.js
│ └── model
│ │ └── model.test.js
├── 02.2
│ ├── view
│ │ ├── counter.js
│ │ └── filters.js
│ ├── model
│ │ ├── observable.js
│ │ └── observable.test.js
│ ├── index.js
│ └── registry.js
├── 02
│ ├── view
│ │ ├── counter.js
│ │ ├── filters.js
│ │ └── app.js
│ ├── index.js
│ ├── registry.js
│ └── model
│ │ ├── observable.js
│ │ └── observable.test.js
├── 03
│ ├── view
│ │ ├── counter.js
│ │ ├── filters.js
│ │ └── app.js
│ ├── model
│ │ ├── eventBus.js
│ │ ├── eventCreators.js
│ │ └── eventBus.test.js
│ ├── index.js
│ └── registry.js
├── 04
│ ├── view
│ │ ├── counter.js
│ │ └── filters.js
│ ├── registry.js
│ ├── index.js
│ └── model
│ │ └── actionCreators.js
├── package.json
├── 05.1
│ ├── model
│ │ └── observable.js
│ ├── index.js
│ └── components
│ │ └── Application.js
└── index.html
├── Chapter05
├── public
│ ├── favicon.ico
│ ├── 00
│ │ ├── index.html
│ │ ├── todos.js
│ │ └── index.js
│ ├── 01
│ │ ├── index.html
│ │ ├── todos.js
│ │ └── index.js
│ ├── index.html
│ └── 02
│ │ ├── index.html
│ │ ├── todos.js
│ │ ├── http.js
│ │ └── index.js
├── README.md
├── package.json
└── server.js
├── errata.md
├── Contributing.md
├── LICENSE
├── .gitignore
├── Chapter08
└── ADR-001.MD
└── LICENSE.txt
/.nvmrc:
--------------------------------------------------------------------------------
1 | v10.12.0
2 |
--------------------------------------------------------------------------------
/9781484249666.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Apress/frameworkless-front-end-development/HEAD/9781484249666.jpg
--------------------------------------------------------------------------------
/Chapter01/1.1 - Framework's Way/next.config.js:
--------------------------------------------------------------------------------
1 | const withCSS = require('@zeit/next-css')
2 | module.exports = withCSS()
3 |
--------------------------------------------------------------------------------
/Chapter02/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Apress/frameworkless-front-end-development/HEAD/Chapter02/favicon.ico
--------------------------------------------------------------------------------
/Chapter03/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Apress/frameworkless-front-end-development/HEAD/Chapter03/favicon.ico
--------------------------------------------------------------------------------
/Chapter04/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Apress/frameworkless-front-end-development/HEAD/Chapter04/favicon.ico
--------------------------------------------------------------------------------
/Chapter06/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Apress/frameworkless-front-end-development/HEAD/Chapter06/favicon.ico
--------------------------------------------------------------------------------
/Chapter07/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Apress/frameworkless-front-end-development/HEAD/Chapter07/favicon.ico
--------------------------------------------------------------------------------
/Chapter03/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "name": "frameworkless-events",
4 | "alias": "frameworkless-events"
5 | }
--------------------------------------------------------------------------------
/Chapter04/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "name": "frameworkless-events",
4 | "alias": "frameworkless-events"
5 | }
--------------------------------------------------------------------------------
/Chapter02/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "name": "frameworkless-rendering",
4 | "alias": "frameworkless-rendering"
5 | }
--------------------------------------------------------------------------------
/Chapter05/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Apress/frameworkless-front-end-development/HEAD/Chapter05/public/favicon.ico
--------------------------------------------------------------------------------
/Chapter04/00.1/index.js:
--------------------------------------------------------------------------------
1 | import HelloWorld from './components/HelloWorld.js'
2 |
3 | window.customElements.define('hello-world', HelloWorld)
4 |
--------------------------------------------------------------------------------
/Chapter04/00.4/index.js:
--------------------------------------------------------------------------------
1 | import GitHubAvatar from './components/GitHubAvatar.js'
2 |
3 | window.customElements.define('github-avatar', GitHubAvatar)
4 |
--------------------------------------------------------------------------------
/Chapter04/00/index.js:
--------------------------------------------------------------------------------
1 | import HelloWorld from './components/HelloWorld.js'
2 |
3 | window
4 | .customElements
5 | .define('hello-world', HelloWorld)
6 |
--------------------------------------------------------------------------------
/Chapter01/README.md:
--------------------------------------------------------------------------------
1 | # Chapter 1 - Let's Talk About Frameworks
2 |
3 | [](https://github.com/frameworkless-movement/manifesto)
--------------------------------------------------------------------------------
/Chapter02/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "test": {
4 | "plugins": [
5 | "transform-es2015-modules-commonjs"
6 | ]
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/Chapter03/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "test": {
4 | "plugins": [
5 | "transform-es2015-modules-commonjs"
6 | ]
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/Chapter04/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "test": {
4 | "plugins": [
5 | "transform-es2015-modules-commonjs"
6 | ]
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/Chapter07/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "test": {
4 | "plugins": [
5 | "transform-es2015-modules-commonjs"
6 | ]
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/Chapter01/1.2 - History of JavaScript Frameworks/AngularJS/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "start": "http-server"
4 | },
5 | "devDependencies": {
6 | "http-server": "^0.11.1"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Chapter06/README.md:
--------------------------------------------------------------------------------
1 | # Chapter 6 - Routing
2 |
3 | [](https://github.com/frameworkless-movement/manifesto)
4 |
5 | To start the examples just run:
6 |
7 | npm start
--------------------------------------------------------------------------------
/Chapter02/README.md:
--------------------------------------------------------------------------------
1 | # Chapter 2 - Rendering
2 |
3 | [](https://github.com/frameworkless-movement/manifesto)
4 |
5 | To start the examples just run:
6 |
7 | npm start
--------------------------------------------------------------------------------
/Chapter05/README.md:
--------------------------------------------------------------------------------
1 | # Chapter 5 - HTTP Requests
2 |
3 | [](https://github.com/frameworkless-movement/manifesto)
4 |
5 | To start the examples just run:
6 |
7 | npm start
--------------------------------------------------------------------------------
/Chapter04/README.md:
--------------------------------------------------------------------------------
1 | # Chapter 4 - Web Components
2 |
3 | [](https://github.com/frameworkless-movement/manifesto)
4 |
5 | To start the examples just run:
6 |
7 | npm start
--------------------------------------------------------------------------------
/Chapter07/README.md:
--------------------------------------------------------------------------------
1 | # Chapter 7 - State Management
2 |
3 | [](https://github.com/frameworkless-movement/manifesto)
4 |
5 | To start the examples just run:
6 |
7 | npm start
--------------------------------------------------------------------------------
/Chapter03/README.md:
--------------------------------------------------------------------------------
1 | # Chapter 3 - Managing DOM Events
2 |
3 | [](https://github.com/frameworkless-movement/manifesto)
4 |
5 | To start the examples just run:
6 |
7 | npm start
--------------------------------------------------------------------------------
/Chapter04/00/components/HelloWorld.js:
--------------------------------------------------------------------------------
1 | export default class HelloWorld extends HTMLElement {
2 | connectedCallback () {
3 | window.requestAnimationFrame(() => {
4 | this.innerHTML = '
43 |
{ this.div = div }} className='box' />
44 |
45 |
46 | )
47 | }
48 | }
49 |
50 | export default PosedExample
51 |
--------------------------------------------------------------------------------
/Chapter05/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 | const uuidv4 = require('uuid/v4')
4 | const findIndex = require('lodash.findindex')
5 |
6 | const PORT = 8080
7 |
8 | const app = express()
9 | let todos = []
10 |
11 | app.use(express.static('public'))
12 | app.use(bodyParser.json())
13 |
14 | app.get('/api/todos', (req, res) => {
15 | res.send(todos)
16 | })
17 |
18 | app.post('/api/todos', (req, res) => {
19 | const newTodo = {
20 | completed: false,
21 | ...req.body,
22 | id: uuidv4()
23 | }
24 |
25 | todos.push(newTodo)
26 |
27 | res.status(201)
28 | res.send(newTodo)
29 | })
30 |
31 | app.patch('/api/todos/:id', (req, res) => {
32 | const updateIndex = findIndex(
33 | todos,
34 | t => t.id === req.params.id
35 | )
36 | const oldTodo = todos[updateIndex]
37 |
38 | const newTodo = {
39 | ...oldTodo,
40 | ...req.body
41 | }
42 |
43 | todos[updateIndex] = newTodo
44 |
45 | res.send(newTodo)
46 | })
47 |
48 | app.delete('/api/todos/:id', (req, res) => {
49 | todos = todos.filter(
50 | t => t.id !== req.params.id
51 | )
52 |
53 | res.status(204)
54 | res.send()
55 | })
56 |
57 | app.listen(PORT)
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 | .cache
63 | dist
64 | Chapter03/old
65 |
--------------------------------------------------------------------------------
/Chapter03/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Frameworkless Frontend Development: DOM Events
6 |
7 |
8 |
9 |
Frameworkless Frontend Development: DOM Events
10 |
11 | - DOM Events API
12 |
19 | - TodoMVC Application with Events
20 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Chapter08/ADR-001.MD:
--------------------------------------------------------------------------------
1 | # ADR-001: Redux State Structure
2 |
3 | ## Context
4 | We have a React + Redux + Redux-Saga Cordova Application. Our product owner wants to test if the same application can run inside an Electron container. Right now our state contains both domain data and data relative to our particular UI. This is a roadblock for a real portable application. The "ui" part can use data, sagas and so on from the "core" part, but the contrary it's not permitted.
5 |
6 | ## Decision
7 | We will divide the redux state in two main "branches". The first one "core" will contain all the data that is relative to the domain of our application. The other branch is "ui", in this part of the state we keep the data relative to this specific UI (mobile application). For example the values in the forms or the open / close of menu or navigation data. In the future the "core" part could become a separate package that the Electron application could use.
8 |
9 | ## Status
10 | accepted
11 |
12 | ## Consequences
13 | Now it's easier to understand where to put variables in the Redux state. This is really helpful when try to solve a bug, because we usually know upfront where to look. Another consequence is that sometime we need some wrappers around sagas becase we can't just import ui part in the core.
14 |
--------------------------------------------------------------------------------
/Chapter03/01.2/view/todos.js:
--------------------------------------------------------------------------------
1 | let template
2 |
3 | const createNewTodoNode = () => {
4 | if (!template) {
5 | template = document.getElementById('todo-item')
6 | }
7 |
8 | return template
9 | .content
10 | .firstElementChild
11 | .cloneNode(true)
12 | }
13 |
14 | const getTodoElement = (todo, index, events) => {
15 | const {
16 | text,
17 | completed
18 | } = todo
19 |
20 | const element = createNewTodoNode()
21 |
22 | element.querySelector('input.edit').value = text
23 | element.querySelector('label').textContent = text
24 |
25 | if (completed) {
26 | element.classList.add('completed')
27 | element
28 | .querySelector('input.toggle')
29 | .checked = true
30 | }
31 |
32 | const handler = e => events.deleteItem(index)
33 |
34 | element
35 | .querySelector('button.destroy')
36 | .addEventListener('click', handler)
37 |
38 | return element
39 | }
40 |
41 | export default (targetElement, { todos }, events) => {
42 | const newTodoList = targetElement.cloneNode(true)
43 |
44 | newTodoList.innerHTML = ''
45 |
46 | todos
47 | .map((todo, index) => getTodoElement(todo, index, events))
48 | .forEach(element => {
49 | newTodoList.appendChild(element)
50 | })
51 |
52 | return newTodoList
53 | }
54 |
--------------------------------------------------------------------------------
/Chapter03/00/index.js:
--------------------------------------------------------------------------------
1 | let button = document.querySelector('#property')
2 | button.onclick = () => {
3 | console.log('Click managed using onclick property')
4 | }
5 |
6 | button = document.querySelector('#eventListener1')
7 | button.addEventListener('click', () => {
8 | console.log('Click managed using addEventListener')
9 | })
10 |
11 | button = document.querySelector('#eventListener2')
12 | button.addEventListener('click', () => {
13 | console.log('First handler')
14 | })
15 | button.addEventListener('click', () => {
16 | console.log('Second handler')
17 | })
18 |
19 | button = document.querySelector('#eventListener3')
20 | const firstHandler = () => {
21 | console.log('First handler')
22 | }
23 |
24 | const secondHandler = () => {
25 | console.log('Second handler')
26 | }
27 |
28 | button.addEventListener('click', firstHandler)
29 | button.addEventListener('click', secondHandler)
30 |
31 | window.setTimeout(() => {
32 | const element = document.querySelector('#eventListener3')
33 | element.removeEventListener('click', firstHandler)
34 | element.removeEventListener('click', secondHandler)
35 | console.log('Removed Event Handlers')
36 | }, 1000)
37 |
38 | button = document.querySelector('#event')
39 | button.addEventListener('click', e => {
40 | console.log('event', e)
41 | })
42 |
43 |
--------------------------------------------------------------------------------
/Chapter07/02.2/model/observable.test.js:
--------------------------------------------------------------------------------
1 | import observableFactory from './observable.js'
2 |
3 | let observable
4 |
5 | describe('observable factory with proxy', () => {
6 | beforeEach(() => {
7 | observable = observableFactory({
8 | property: 'value'
9 | })
10 | })
11 |
12 | test('listeners should be invoked immediatly', () => {
13 | let counter = 0
14 | observable.addChangeListener(data => {
15 | counter++
16 | })
17 | expect(counter).toBe(1)
18 | })
19 |
20 | test('listeners should be invoked when changing data', () => {
21 | let counter = 0
22 | observable.addChangeListener(data => {
23 | counter++
24 | })
25 | observable.property = 'another value'
26 | expect(counter).toBe(2)
27 | })
28 |
29 | test('listeners should be removed when unsubscribing', () => {
30 | let counter = 0
31 | const unsubscribe = observable.addChangeListener(data => {
32 | counter++
33 | })
34 | unsubscribe()
35 | observable.property = 'another value'
36 | expect(counter).toBe(1)
37 | })
38 |
39 | test('in listeners state should be immutable', () => {
40 | observable.addChangeListener(data => {
41 | expect(() => {
42 | data.property = 'another value'
43 | }).toThrow()
44 | })
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/Chapter04/00.4/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Frameworkless Frontend Development: Web Components
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Chapter04/00.5/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Frameworkless Frontend Development: Web Components
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Chapter05/public/02/http.js:
--------------------------------------------------------------------------------
1 | const request = async params => {
2 | const {
3 | method = 'GET',
4 | url,
5 | headers = {},
6 | body
7 | } = params
8 |
9 | const config = {
10 | url,
11 | method,
12 | headers,
13 | data: body
14 | }
15 |
16 | return axios(config)
17 | }
18 |
19 | const get = async (url, headers) => {
20 | const response = await request({
21 | url,
22 | headers,
23 | method: 'GET'
24 | })
25 |
26 | return response.data
27 | }
28 |
29 | const post = async (url, body, headers) => {
30 | const response = await request({
31 | url,
32 | headers,
33 | method: 'POST',
34 | body
35 | })
36 | return response.data
37 | }
38 |
39 | const put = async (url, body, headers) => {
40 | const response = await request({
41 | url,
42 | headers,
43 | method: 'PUT',
44 | body
45 | })
46 | return response.data
47 | }
48 |
49 | const patch = async (url, body, headers) => {
50 | const response = await request({
51 | url,
52 | headers,
53 | method: 'PATCH',
54 | body
55 | })
56 | return response.data
57 | }
58 |
59 | const deleteRequest = async (url, headers) => {
60 | const response = await request({
61 | url,
62 | headers,
63 | method: 'DELETE'
64 | })
65 | return response.data
66 | }
67 |
68 | export default {
69 | get,
70 | post,
71 | put,
72 | patch,
73 | delete: deleteRequest
74 | }
75 |
--------------------------------------------------------------------------------
/Chapter04/00.4/components/GitHubAvatar.js:
--------------------------------------------------------------------------------
1 | const ERROR_IMAGE = 'https://files-82ee7vgzc.now.sh'
2 | const LOADING_IMAGE = 'https://files-8bga2nnt0.now.sh'
3 |
4 | const getGitHubAvatarUrl = async user => {
5 | if (!user) {
6 | return
7 | }
8 |
9 | const url = `https://api.github.com/users/${user}`
10 |
11 | const response = await fetch(url)
12 | if (!response.ok) {
13 | throw new Error(response.statusText)
14 | }
15 | const data = await response.json()
16 | return data.avatar_url
17 | }
18 |
19 | export default class GitHubAvatar extends HTMLElement {
20 | constructor () {
21 | super()
22 | this.url = LOADING_IMAGE
23 | }
24 |
25 | get user () {
26 | return this.getAttribute('user')
27 | }
28 |
29 | set user (value) {
30 | this.setAttribute('user', value)
31 | }
32 |
33 | render () {
34 | window.requestAnimationFrame(() => {
35 | this.innerHTML = ''
36 | const img = document.createElement('img')
37 | img.src = this.url
38 | this.appendChild(img)
39 | })
40 | }
41 |
42 | async loadNewAvatar () {
43 | const { user } = this
44 | if (!user) {
45 | return
46 | }
47 | try {
48 | this.url = await getGitHubAvatarUrl(user)
49 | } catch (e) {
50 | this.url = ERROR_IMAGE
51 | }
52 |
53 | this.render()
54 | }
55 |
56 | connectedCallback () {
57 | this.render()
58 | this.loadNewAvatar()
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Chapter03/01.4/view/todos.js:
--------------------------------------------------------------------------------
1 | let template
2 |
3 | const createNewTodoNode = () => {
4 | if (!template) {
5 | template = document.getElementById('todo-item')
6 | }
7 |
8 | return template
9 | .content
10 | .firstElementChild
11 | .cloneNode(true)
12 | }
13 |
14 | const getTodoElement = (todo, index) => {
15 | const {
16 | text,
17 | completed
18 | } = todo
19 |
20 | const element = createNewTodoNode()
21 |
22 | element.querySelector('input.edit').value = text
23 | element.querySelector('label').textContent = text
24 |
25 | if (completed) {
26 | element.classList.add('completed')
27 | element
28 | .querySelector('input.toggle')
29 | .checked = true
30 | }
31 |
32 | element
33 | .querySelector('button.destroy')
34 | .dataset
35 | .index = index
36 |
37 | return element
38 | }
39 |
40 | export default (targetElement, state, events) => {
41 | const { todos } = state
42 | const { deleteItem } = events
43 | const newTodoList = targetElement.cloneNode(true)
44 |
45 | newTodoList.innerHTML = ''
46 |
47 | todos
48 | .map((todo, index) => getTodoElement(todo, index))
49 | .forEach(element => {
50 | newTodoList.appendChild(element)
51 | })
52 |
53 | newTodoList.addEventListener('click', e => {
54 | if (e.target.matches('button.destroy')) {
55 | deleteItem(e.target.dataset.index)
56 | }
57 | })
58 |
59 | return newTodoList
60 | }
61 |
--------------------------------------------------------------------------------
/Chapter07/02/model/observable.test.js:
--------------------------------------------------------------------------------
1 | import observableFactory from './observable.js'
2 |
3 | let observable
4 | let state
5 | const actions = {
6 | aDummySetter: data => {
7 | state = data
8 | }
9 | }
10 |
11 | describe('observable factory', () => {
12 | beforeEach(() => {
13 | state = {}
14 | observable = observableFactory(actions, () => state)
15 | })
16 |
17 | test('listeners should be invoked immediatly', () => {
18 | let counter = 0
19 | observable.addChangeListener(data => {
20 | counter++
21 | })
22 | expect(counter).toBe(1)
23 | })
24 |
25 | test('listeners should be invoked when changing data', () => {
26 | let counter = 0
27 | observable.addChangeListener(data => {
28 | counter++
29 | })
30 | observable.aDummySetter('Value')
31 | expect(counter).toBe(2)
32 | })
33 |
34 | test('listeners should be removed when unsubscribing', () => {
35 | let counter = 0
36 | const unsubscribe = observable.addChangeListener(data => {
37 | counter++
38 | })
39 | unsubscribe()
40 | observable.aDummySetter('Value')
41 | expect(counter).toBe(1)
42 | })
43 |
44 | test('in listeners state should be immutable', () => {
45 | observable.aDummySetter({
46 | name: 'Value'
47 | })
48 | observable.addChangeListener(data => {
49 | expect(() => {
50 | data.name = 'Another Value'
51 | }).toThrow()
52 | })
53 | })
54 | })
55 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Freeware License, some rights reserved
2 |
3 | Copyright (c) 2019 Francesco Strazzullo
4 |
5 | Permission is hereby granted, free of charge, to anyone obtaining a copy
6 | of this software and associated documentation files (the "Software"),
7 | to work with the Software within the limits of freeware distribution and fair use.
8 | This includes the rights to use, copy, and modify the Software for personal use.
9 | Users are also allowed and encouraged to submit corrections and modifications
10 | to the Software for the benefit of other users.
11 |
12 | It is not allowed to reuse, modify, or redistribute the Software for
13 | commercial use in any way, or for a user’s educational materials such as books
14 | or blog articles without prior permission from the copyright holder.
15 |
16 | The above copyright notice and this permission notice need to be included
17 | in all copies or substantial portions of the software.
18 |
19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | AUTHORS OR COPYRIGHT HOLDERS OR APRESS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | SOFTWARE.
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Chapter07/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Frameworkless Frontend Development: State Management
7 |
8 |
9 |
10 |
11 |
Frameworkless Frontend Development: State Management
12 |
13 | - External state
14 | -
15 | Observable State
16 |
20 |
21 | -
22 | Reactive Programming
23 |
28 |
29 | -
30 | Event Bus
31 |
35 |
36 | - Redux
37 | -
38 | State Management with Web Components
39 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/Chapter02/01/view.js:
--------------------------------------------------------------------------------
1 | const getTodoElement = todo => {
2 | const {
3 | text,
4 | completed
5 | } = todo
6 |
7 | return `
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 | `
19 | }
20 |
21 | const getTodoCount = todos => {
22 | const notCompleted = todos
23 | .filter(todo => !todo.completed)
24 |
25 | const { length } = notCompleted
26 | if (length === 1) {
27 | return '1 Item left'
28 | }
29 |
30 | return `${length} Items left`
31 | }
32 |
33 | export default (targetElement, state) => {
34 | const {
35 | currentFilter,
36 | todos
37 | } = state
38 |
39 | const element = targetElement.cloneNode(true)
40 |
41 | const list = element.querySelector('.todo-list')
42 | const counter = element.querySelector('.todo-count')
43 | const filters = element.querySelector('.filters')
44 |
45 | list.innerHTML = todos.map(getTodoElement).join('')
46 | counter.textContent = getTodoCount(todos)
47 |
48 | Array
49 | .from(filters.querySelectorAll('li a'))
50 | .forEach(a => {
51 | if (a.textContent === currentFilter) {
52 | a.classList.add('selected')
53 | } else {
54 | a.classList.remove('selected')
55 | }
56 | })
57 |
58 | return element
59 | }
60 |
--------------------------------------------------------------------------------
/Chapter07/01.1/index.js:
--------------------------------------------------------------------------------
1 | import todosView from './view/todos.js'
2 | import counterView from './view/counter.js'
3 | import filtersView from './view/filters.js'
4 | import appView from './view/app.js'
5 | import applyDiff from './applyDiff.js'
6 |
7 | import registry from './registry.js'
8 |
9 | import stateFactory from './model/state.js'
10 |
11 | registry.add('app', appView)
12 | registry.add('todos', todosView)
13 | registry.add('counter', counterView)
14 | registry.add('filters', filtersView)
15 |
16 | const loadState = () => {
17 | const serializedState = window
18 | .localStorage
19 | .getItem('state')
20 |
21 | if (!serializedState) {
22 | return
23 | }
24 |
25 | return JSON.parse(serializedState)
26 | }
27 |
28 | const state = stateFactory(loadState())
29 |
30 | const {
31 | addChangeListener,
32 | ...events
33 | } = state
34 |
35 | const render = (state) => {
36 | window.requestAnimationFrame(() => {
37 | const main = document.querySelector('#root')
38 |
39 | const newMain = registry.renderRoot(
40 | main,
41 | state,
42 | events)
43 |
44 | applyDiff(document.body, main, newMain)
45 | })
46 | }
47 |
48 | addChangeListener(render)
49 |
50 | addChangeListener(state => {
51 | Promise.resolve().then(() => {
52 | window
53 | .localStorage
54 | .setItem('state', JSON.stringify(state))
55 | })
56 | })
57 |
58 | addChangeListener(state => {
59 | console.log(
60 | `Current State (${(new Date()).getTime()})`,
61 | state
62 | )
63 | })
64 |
--------------------------------------------------------------------------------
/Chapter07/03/model/eventBus.test.js:
--------------------------------------------------------------------------------
1 | import eventBusFactory from './eventBus'
2 | let eventBus
3 |
4 | const counterModel = (state, event) => {
5 | if (!event) {
6 | return {
7 | counter: 0
8 | }
9 | }
10 |
11 | if (event.type !== 'COUNTER') {
12 | return state
13 | }
14 |
15 | return {
16 | counter: state.counter++
17 | }
18 | }
19 |
20 | describe('eventBus', () => {
21 | beforeEach(() => {
22 | eventBus = eventBusFactory(counterModel)
23 | })
24 |
25 | test('subscribers should be invoked when the model catch the event', () => {
26 | let counter = 0
27 |
28 | eventBus.subscribe(() => counter++)
29 |
30 | eventBus.dispatch({ type: 'COUNTER' })
31 |
32 | expect(counter).toBe(1)
33 | })
34 |
35 | test('subscribers should not be invoked when the model does not catch the event', () => {
36 | let counter = 0
37 |
38 | eventBus.subscribe(() => counter++)
39 |
40 | eventBus.dispatch({ type: 'NOT_COUNTER' })
41 |
42 | expect(counter).toBe(0)
43 | })
44 |
45 | test('subscribers should receive an immutable state', () => {
46 | eventBus.dispatch({ type: 'COUNTER' })
47 | eventBus.subscribe((state) => {
48 | expect(() => {
49 | state.counter = 0
50 | }).toThrow()
51 | })
52 | })
53 |
54 | test('should throw error in the model does not return a state', () => {
55 | const eventBus = eventBusFactory(() => {
56 | return undefined
57 | })
58 |
59 | expect(() => {
60 | eventBus.dispatch({ type: 'EVENT' })
61 | }).toThrow()
62 | })
63 | })
64 |
--------------------------------------------------------------------------------
/Chapter05/public/00/index.js:
--------------------------------------------------------------------------------
1 | import todos from './todos.js'
2 |
3 | const printResult = (action, result) => {
4 | const time = (new Date()).toTimeString()
5 | const node = document.createElement('p')
6 | node.textContent = `${action.toUpperCase()}: ${JSON.stringify(result)} (${time})`
7 |
8 | document
9 | .querySelector('div')
10 | .appendChild(node)
11 | }
12 |
13 | const onListClick = async () => {
14 | const result = await todos.list()
15 | printResult('list todos', result)
16 | }
17 |
18 | const onAddClick = async () => {
19 | const result = await todos.create('A simple todo Element')
20 | printResult('add todo', result)
21 | }
22 |
23 | const onUpdateClick = async () => {
24 | const list = await todos.list()
25 |
26 | const { id } = list[0]
27 | const newTodo = {
28 | id,
29 | completed: true
30 | }
31 |
32 | const result = await todos.update(newTodo)
33 | printResult('update todo', result)
34 | }
35 |
36 | const onDeleteClick = async () => {
37 | const list = await todos.list()
38 | const { id } = list[0]
39 |
40 | const result = await todos.delete(id)
41 | printResult('delete todo', result)
42 | }
43 |
44 | document
45 | .querySelector('button[data-list]')
46 | .addEventListener('click', onListClick)
47 |
48 | document
49 | .querySelector('button[data-add]')
50 | .addEventListener('click', onAddClick)
51 |
52 | document
53 | .querySelector('button[data-update]')
54 | .addEventListener('click', onUpdateClick)
55 |
56 | document
57 | .querySelector('button[data-delete]')
58 | .addEventListener('click', onDeleteClick)
59 |
--------------------------------------------------------------------------------
/Chapter05/public/02/index.js:
--------------------------------------------------------------------------------
1 | import todos from './todos.js'
2 |
3 | const printResult = (action, result) => {
4 | const time = (new Date()).toTimeString()
5 | const node = document.createElement('p')
6 | node.textContent = `${action.toUpperCase()}: ${JSON.stringify(result)} (${time})`
7 |
8 | document
9 | .querySelector('div')
10 | .appendChild(node)
11 | }
12 |
13 | const onListClick = async () => {
14 | const result = await todos.list()
15 | printResult('list todos', result)
16 | }
17 |
18 | const onAddClick = async () => {
19 | const result = await todos.create('A simple todo Element')
20 | printResult('add todo', result)
21 | }
22 |
23 | const onUpdateClick = async () => {
24 | const list = await todos.list()
25 |
26 | const { id } = list[0]
27 | const newTodo = {
28 | id,
29 | completed: true
30 | }
31 |
32 | const result = await todos.update(newTodo)
33 | printResult('update todo', result)
34 | }
35 |
36 | const onDeleteClick = async () => {
37 | const list = await todos.list()
38 | const { id } = list[0]
39 |
40 | const result = await todos.delete(id)
41 | printResult('delete todo', result)
42 | }
43 |
44 | document
45 | .querySelector('button[data-list]')
46 | .addEventListener('click', onListClick)
47 |
48 | document
49 | .querySelector('button[data-add]')
50 | .addEventListener('click', onAddClick)
51 |
52 | document
53 | .querySelector('button[data-update]')
54 | .addEventListener('click', onUpdateClick)
55 |
56 | document
57 | .querySelector('button[data-delete]')
58 | .addEventListener('click', onDeleteClick)
59 |
--------------------------------------------------------------------------------
/Chapter04/01/components/Footer.js:
--------------------------------------------------------------------------------
1 | const getTodoCount = todos => {
2 | const notCompleted = todos
3 | .filter(todo => !todo.completed)
4 |
5 | const { length } = notCompleted
6 | if (length === 1) {
7 | return '1 Item left'
8 | }
9 |
10 | return `${length} Items left`
11 | }
12 |
13 | export default class Footer extends HTMLElement {
14 | static get observedAttributes () {
15 | return [
16 | 'filter',
17 | 'todos'
18 | ]
19 | }
20 |
21 | get todos () {
22 | if (!this.hasAttribute('todos')) {
23 | return []
24 | }
25 |
26 | return JSON.parse(this.getAttribute('todos'))
27 | }
28 |
29 | set todos (value) {
30 | this.setAttribute('todos', JSON.stringify(value))
31 | }
32 |
33 | get filter () {
34 | return this.getAttribute('filter')
35 | }
36 |
37 | set filter (value) {
38 | this.setAttribute('filter', value)
39 | }
40 |
41 | connectedCallback () {
42 | const template = document.getElementById('footer')
43 | const content = template
44 | .content
45 | .firstElementChild
46 | .cloneNode(true)
47 |
48 | this.appendChild(content)
49 |
50 | const {
51 | filter,
52 | todos
53 | } = this
54 |
55 | this
56 | .querySelectorAll('li a')
57 | .forEach(a => {
58 | if (a.textContent === filter) {
59 | a.classList.add('selected')
60 | } else {
61 | a.classList.remove('selected')
62 | }
63 | })
64 |
65 | const label = getTodoCount(todos)
66 |
67 | this
68 | .querySelector('span.todo-count')
69 | .textContent = label
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Chapter05/public/01/index.js:
--------------------------------------------------------------------------------
1 | import todos from './todos.js'
2 |
3 | const NEW_TODO_TEXT = 'A simple todo Element'
4 |
5 | const printResult = (action, result) => {
6 | const time = (new Date()).toTimeString()
7 | const node = document.createElement('p')
8 | node.textContent = `${action.toUpperCase()}: ${JSON.stringify(result)} (${time})`
9 |
10 | document
11 | .querySelector('div')
12 | .appendChild(node)
13 | }
14 |
15 | const onListClick = async () => {
16 | const result = await todos.list()
17 | printResult('list todos', result)
18 | }
19 |
20 | const onAddClick = async () => {
21 | const result = await todos.create(NEW_TODO_TEXT)
22 | printResult('add todo', result)
23 | }
24 |
25 | const onUpdateClick = async () => {
26 | const list = await todos.list()
27 |
28 | const { id } = list[0]
29 | const newTodo = {
30 | id,
31 | completed: true
32 | }
33 |
34 | const result = await todos.update(newTodo)
35 | printResult('update todo', result)
36 | }
37 |
38 | const onDeleteClick = async () => {
39 | const list = await todos.list()
40 | const { id } = list[0]
41 |
42 | const result = await todos.delete(id)
43 | printResult('delete todo', result)
44 | }
45 |
46 | document
47 | .querySelector('button[data-list]')
48 | .addEventListener('click', onListClick)
49 |
50 | document
51 | .querySelector('button[data-add]')
52 | .addEventListener('click', onAddClick)
53 |
54 | document
55 | .querySelector('button[data-update]')
56 | .addEventListener('click', onUpdateClick)
57 |
58 | document
59 | .querySelector('button[data-delete]')
60 | .addEventListener('click', onDeleteClick)
61 |
--------------------------------------------------------------------------------
/Chapter04/01/components/Application.js:
--------------------------------------------------------------------------------
1 | import { EVENTS } from './List.js'
2 |
3 | export default class App extends HTMLElement {
4 | constructor () {
5 | super()
6 | this.state = {
7 | todos: [],
8 | filter: 'All'
9 | }
10 |
11 | this.template = document
12 | .getElementById('todo-app')
13 | }
14 |
15 | deleteItem (index) {
16 | this.state.todos.splice(index, 1)
17 | this.syncAttributes()
18 | }
19 |
20 | addItem (text) {
21 | this.state.todos.push({
22 | text,
23 | completed: false
24 | })
25 | this.syncAttributes()
26 | }
27 |
28 | syncAttributes () {
29 | this.list.todos = this.state.todos
30 | this.footer.todos = this.state.todos
31 | this.footer.filter = this.state.filter
32 | }
33 |
34 | connectedCallback () {
35 | window.requestAnimationFrame(() => {
36 | const content = this.template
37 | .content
38 | .firstElementChild
39 | .cloneNode(true)
40 |
41 | this.appendChild(content)
42 |
43 | this
44 | .querySelector('.new-todo')
45 | .addEventListener('keypress', e => {
46 | if (e.key === 'Enter') {
47 | this.addItem(e.target.value)
48 | e.target.value = ''
49 | }
50 | })
51 |
52 | this.footer = this
53 | .querySelector('todomvc-footer')
54 |
55 | this.list = this.querySelector('todomvc-list')
56 | this.list.addEventListener(
57 | EVENTS.DELETE_ITEM,
58 | e => {
59 | this.deleteItem(e.detail.index)
60 | }
61 | )
62 |
63 | this.syncAttributes()
64 | })
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Chapter07/00/index.js:
--------------------------------------------------------------------------------
1 | import todosView from './view/todos.js'
2 | import counterView from './view/counter.js'
3 | import filtersView from './view/filters.js'
4 | import appView from './view/app.js'
5 | import applyDiff from './applyDiff.js'
6 |
7 | import registry from './registry.js'
8 |
9 | import modelFactory from './model/model.js'
10 |
11 | registry.add('app', appView)
12 | registry.add('todos', todosView)
13 | registry.add('counter', counterView)
14 | registry.add('filters', filtersView)
15 |
16 | const model = modelFactory()
17 |
18 | const events = {
19 | addItem: text => {
20 | model.addItem(text)
21 | render(model.getState())
22 | },
23 | updateItem: (index, text) => {
24 | model.updateItem(index, text)
25 | render(model.getState())
26 | },
27 | deleteItem: (index) => {
28 | model.deleteItem(index)
29 | render(model.getState())
30 | },
31 | toggleItemCompleted: (index) => {
32 | model.toggleItemCompleted(index)
33 | render(model.getState())
34 | },
35 | completeAll: () => {
36 | model.completeAll()
37 | render(model.getState())
38 | },
39 | clearCompleted: () => {
40 | model.clearCompleted()
41 | render(model.getState())
42 | },
43 | changeFilter: filter => {
44 | model.changeFilter(filter)
45 | render(model.getState())
46 | }
47 | }
48 |
49 | const render = (state) => {
50 | window.requestAnimationFrame(() => {
51 | const main = document.querySelector('#root')
52 |
53 | const newMain = registry.renderRoot(
54 | main,
55 | state,
56 | events)
57 |
58 | applyDiff(document.body, main, newMain)
59 | })
60 | }
61 |
62 | render(model.getState())
63 |
--------------------------------------------------------------------------------
/Chapter02/05/applyDiff.js:
--------------------------------------------------------------------------------
1 | const isNodeChanged = (node1, node2) => {
2 | const n1Attributes = node1.attributes
3 | const n2Attributes = node2.attributes
4 | if (n1Attributes.length !== n2Attributes.length) {
5 | return true
6 | }
7 |
8 | const differentAttribute = Array
9 | .from(n1Attributes)
10 | .find(attribute => {
11 | const { name } = attribute
12 | const attribute1 = node1
13 | .getAttribute(name)
14 | const attribute2 = node2
15 | .getAttribute(name)
16 |
17 | return attribute1 !== attribute2
18 | })
19 |
20 | if (differentAttribute) {
21 | return true
22 | }
23 |
24 | if (node1.children.length === 0 &&
25 | node2.children.length === 0 &&
26 | node1.textContent !== node2.textContent) {
27 | return true
28 | }
29 |
30 | return false
31 | }
32 |
33 | const applyDiff = (
34 | parentNode,
35 | realNode,
36 | virtualNode) => {
37 | if (realNode && !virtualNode) {
38 | realNode.remove()
39 | return
40 | }
41 |
42 | if (!realNode && virtualNode) {
43 | parentNode.appendChild(virtualNode)
44 | return
45 | }
46 |
47 | if (isNodeChanged(virtualNode, realNode)) {
48 | realNode.replaceWith(virtualNode)
49 | return
50 | }
51 |
52 | const realChildren = Array.from(realNode.children)
53 | const virtualChildren = Array.from(virtualNode.children)
54 |
55 | const max = Math.max(
56 | realChildren.length,
57 | virtualChildren.length
58 | )
59 | for (let i = 0; i < max; i++) {
60 | applyDiff(
61 | realNode,
62 | realChildren[i],
63 | virtualChildren[i]
64 | )
65 | }
66 | }
67 |
68 | export default applyDiff
69 |
--------------------------------------------------------------------------------
/Chapter03/01.1/applyDiff.js:
--------------------------------------------------------------------------------
1 | const isNodeChanged = (node1, node2) => {
2 | const n1Attributes = node1.attributes
3 | const n2Attributes = node2.attributes
4 | if (n1Attributes.length !== n2Attributes.length) {
5 | return true
6 | }
7 |
8 | const differentAttribute = Array
9 | .from(n1Attributes)
10 | .find(attribute => {
11 | const { name } = attribute
12 | const attribute1 = node1
13 | .getAttribute(name)
14 | const attribute2 = node2
15 | .getAttribute(name)
16 |
17 | return attribute1 !== attribute2
18 | })
19 |
20 | if (differentAttribute) {
21 | return true
22 | }
23 |
24 | if (node1.children.length === 0 &&
25 | node2.children.length === 0 &&
26 | node1.textContent !== node2.textContent) {
27 | return true
28 | }
29 |
30 | return false
31 | }
32 |
33 | const applyDiff = (
34 | parentNode,
35 | realNode,
36 | virtualNode) => {
37 | if (realNode && !virtualNode) {
38 | realNode.remove()
39 | return
40 | }
41 |
42 | if (!realNode && virtualNode) {
43 | parentNode.appendChild(virtualNode)
44 | return
45 | }
46 |
47 | if (isNodeChanged(virtualNode, realNode)) {
48 | realNode.replaceWith(virtualNode)
49 | return
50 | }
51 |
52 | const realChildren = Array.from(realNode.children)
53 | const virtualChildren = Array.from(virtualNode.children)
54 |
55 | const max = Math.max(
56 | realChildren.length,
57 | virtualChildren.length
58 | )
59 | for (let i = 0; i < max; i++) {
60 | applyDiff(
61 | realNode,
62 | realChildren[i],
63 | virtualChildren[i]
64 | )
65 | }
66 | }
67 |
68 | export default applyDiff
69 |
--------------------------------------------------------------------------------
/Chapter03/01.2/applyDiff.js:
--------------------------------------------------------------------------------
1 | const isNodeChanged = (node1, node2) => {
2 | const n1Attributes = node1.attributes
3 | const n2Attributes = node2.attributes
4 | if (n1Attributes.length !== n2Attributes.length) {
5 | return true
6 | }
7 |
8 | const differentAttribute = Array
9 | .from(n1Attributes)
10 | .find(attribute => {
11 | const { name } = attribute
12 | const attribute1 = node1
13 | .getAttribute(name)
14 | const attribute2 = node2
15 | .getAttribute(name)
16 |
17 | return attribute1 !== attribute2
18 | })
19 |
20 | if (differentAttribute) {
21 | return true
22 | }
23 |
24 | if (node1.children.length === 0 &&
25 | node2.children.length === 0 &&
26 | node1.textContent !== node2.textContent) {
27 | return true
28 | }
29 |
30 | return false
31 | }
32 |
33 | const applyDiff = (
34 | parentNode,
35 | realNode,
36 | virtualNode) => {
37 | if (realNode && !virtualNode) {
38 | realNode.remove()
39 | return
40 | }
41 |
42 | if (!realNode && virtualNode) {
43 | parentNode.appendChild(virtualNode)
44 | return
45 | }
46 |
47 | if (isNodeChanged(virtualNode, realNode)) {
48 | realNode.replaceWith(virtualNode)
49 | return
50 | }
51 |
52 | const realChildren = Array.from(realNode.children)
53 | const virtualChildren = Array.from(virtualNode.children)
54 |
55 | const max = Math.max(
56 | realChildren.length,
57 | virtualChildren.length
58 | )
59 | for (let i = 0; i < max; i++) {
60 | applyDiff(
61 | realNode,
62 | realChildren[i],
63 | virtualChildren[i]
64 | )
65 | }
66 | }
67 |
68 | export default applyDiff
69 |
--------------------------------------------------------------------------------
/Chapter03/01.4/applyDiff.js:
--------------------------------------------------------------------------------
1 | const isNodeChanged = (node1, node2) => {
2 | const n1Attributes = node1.attributes
3 | const n2Attributes = node2.attributes
4 | if (n1Attributes.length !== n2Attributes.length) {
5 | return true
6 | }
7 |
8 | const differentAttribute = Array
9 | .from(n1Attributes)
10 | .find(attribute => {
11 | const { name } = attribute
12 | const attribute1 = node1
13 | .getAttribute(name)
14 | const attribute2 = node2
15 | .getAttribute(name)
16 |
17 | return attribute1 !== attribute2
18 | })
19 |
20 | if (differentAttribute) {
21 | return true
22 | }
23 |
24 | if (node1.children.length === 0 &&
25 | node2.children.length === 0 &&
26 | node1.textContent !== node2.textContent) {
27 | return true
28 | }
29 |
30 | return false
31 | }
32 |
33 | const applyDiff = (
34 | parentNode,
35 | realNode,
36 | virtualNode) => {
37 | if (realNode && !virtualNode) {
38 | realNode.remove()
39 | return
40 | }
41 |
42 | if (!realNode && virtualNode) {
43 | parentNode.appendChild(virtualNode)
44 | return
45 | }
46 |
47 | if (isNodeChanged(virtualNode, realNode)) {
48 | realNode.replaceWith(virtualNode)
49 | return
50 | }
51 |
52 | const realChildren = Array.from(realNode.children)
53 | const virtualChildren = Array.from(virtualNode.children)
54 |
55 | const max = Math.max(
56 | realChildren.length,
57 | virtualChildren.length
58 | )
59 | for (let i = 0; i < max; i++) {
60 | applyDiff(
61 | realNode,
62 | realChildren[i],
63 | virtualChildren[i]
64 | )
65 | }
66 | }
67 |
68 | export default applyDiff
69 |
--------------------------------------------------------------------------------
/Chapter03/01/applyDiff.js:
--------------------------------------------------------------------------------
1 | const isNodeChanged = (node1, node2) => {
2 | const n1Attributes = node1.attributes
3 | const n2Attributes = node2.attributes
4 | if (n1Attributes.length !== n2Attributes.length) {
5 | return true
6 | }
7 |
8 | const differentAttribute = Array
9 | .from(n1Attributes)
10 | .find(attribute => {
11 | const { name } = attribute
12 | const attribute1 = node1
13 | .getAttribute(name)
14 | const attribute2 = node2
15 | .getAttribute(name)
16 |
17 | return attribute1 !== attribute2
18 | })
19 |
20 | if (differentAttribute) {
21 | return true
22 | }
23 |
24 | if (node1.children.length === 0 &&
25 | node2.children.length === 0 &&
26 | node1.textContent !== node2.textContent) {
27 | return true
28 | }
29 |
30 | return false
31 | }
32 |
33 | const applyDiff = (
34 | parentNode,
35 | realNode,
36 | virtualNode) => {
37 | if (realNode && !virtualNode) {
38 | realNode.remove()
39 | return
40 | }
41 |
42 | if (!realNode && virtualNode) {
43 | parentNode.appendChild(virtualNode)
44 | return
45 | }
46 |
47 | if (isNodeChanged(virtualNode, realNode)) {
48 | realNode.replaceWith(virtualNode)
49 | return
50 | }
51 |
52 | const realChildren = Array.from(realNode.children)
53 | const virtualChildren = Array.from(virtualNode.children)
54 |
55 | const max = Math.max(
56 | realChildren.length,
57 | virtualChildren.length
58 | )
59 | for (let i = 0; i < max; i++) {
60 | applyDiff(
61 | realNode,
62 | realChildren[i],
63 | virtualChildren[i]
64 | )
65 | }
66 | }
67 |
68 | export default applyDiff
69 |
--------------------------------------------------------------------------------
/Chapter04/00.3/components/applyDiff.js:
--------------------------------------------------------------------------------
1 | const isNodeChanged = (node1, node2) => {
2 | const n1Attributes = node1.attributes
3 | const n2Attributes = node2.attributes
4 | if (n1Attributes.length !== n2Attributes.length) {
5 | return true
6 | }
7 |
8 | const differentAttribute = Array
9 | .from(n1Attributes)
10 | .find(attribute => {
11 | const { name } = attribute
12 | const attribute1 = node1
13 | .getAttribute(name)
14 | const attribute2 = node2
15 | .getAttribute(name)
16 |
17 | return attribute1 !== attribute2
18 | })
19 |
20 | if (differentAttribute) {
21 | return true
22 | }
23 |
24 | if (node1.children.length === 0 &&
25 | node2.children.length === 0 &&
26 | node1.textContent !== node2.textContent) {
27 | return true
28 | }
29 |
30 | return false
31 | }
32 |
33 | const applyDiff = (
34 | parentNode,
35 | realNode,
36 | virtualNode) => {
37 | if (realNode && !virtualNode) {
38 | realNode.remove()
39 | return
40 | }
41 |
42 | if (!realNode && virtualNode) {
43 | parentNode.appendChild(virtualNode)
44 | return
45 | }
46 |
47 | if (isNodeChanged(virtualNode, realNode)) {
48 | realNode.replaceWith(virtualNode)
49 | return
50 | }
51 |
52 | const realChildren = Array.from(realNode.children)
53 | const virtualChildren = Array.from(virtualNode.children)
54 |
55 | const max = Math.max(
56 | realChildren.length,
57 | virtualChildren.length
58 | )
59 | for (let i = 0; i < max; i++) {
60 | applyDiff(
61 | realNode,
62 | realChildren[i],
63 | virtualChildren[i]
64 | )
65 | }
66 | }
67 |
68 | export default applyDiff
69 |
--------------------------------------------------------------------------------
/Chapter07/00/model/model.test.js:
--------------------------------------------------------------------------------
1 | import modelFactory from './model.js'
2 |
3 | describe('TodoMVC Model', () => {
4 | test('data should be immutable', () => {
5 | const model = modelFactory()
6 |
7 | expect(() => {
8 | model.getState().currentFilter = 'WRONG'
9 | }).toThrow()
10 | })
11 |
12 | test('should add an item', () => {
13 | const model = modelFactory()
14 |
15 | model.addItem('dummy')
16 |
17 | const { todos } = model.getState()
18 |
19 | expect(todos.length).toBe(1)
20 | expect(todos[0]).toEqual({
21 | text: 'dummy',
22 | completed: false
23 | })
24 | })
25 |
26 | test('should not add an item when a falsy text is provided', () => {
27 | const model = modelFactory()
28 |
29 | model.addItem('')
30 | model.addItem(undefined)
31 | model.addItem(0)
32 | model.addItem()
33 | model.addItem(false)
34 |
35 | const { todos } = model.getState()
36 |
37 | expect(todos.length).toBe(0)
38 | })
39 |
40 | test('should update an item', () => {
41 | const model = modelFactory({
42 | todos: [{
43 | text: 'dummy',
44 | completed: false
45 | }]
46 | })
47 |
48 | model.updateItem(0, 'new-dummy')
49 |
50 | const { todos } = model.getState()
51 |
52 | expect(todos[0].text).toBe('new-dummy')
53 | })
54 |
55 | test('should not update an item when an invalid index is provided', () => {
56 | const model = modelFactory({
57 | todos: [{
58 | text: 'dummy',
59 | completed: false
60 | }]
61 | })
62 |
63 | model.updateItem(1, 'new-dummy')
64 |
65 | const { todos } = model.getState()
66 |
67 | expect(todos[0].text).toBe('dummy')
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/Chapter03/01.3/index.js:
--------------------------------------------------------------------------------
1 | import todosView from './view/todos.js'
2 | import counterView from './view/counter.js'
3 | import filtersView from './view/filters.js'
4 | import appView from './view/app.js'
5 | import applyDiff from './applyDiff.js'
6 |
7 | import registry from './registry.js'
8 |
9 | registry.add('app', appView)
10 | registry.add('todos', todosView)
11 | registry.add('counter', counterView)
12 | registry.add('filters', filtersView)
13 |
14 | const state = {
15 | todos: [],
16 | currentFilter: 'All'
17 | }
18 |
19 | const events = {
20 | addItem: text => {
21 | state.todos.push({
22 | text,
23 | completed: false
24 | })
25 | render()
26 | },
27 | updateItem: (index, text) => {
28 | state.todos[index].text = text
29 | render()
30 | },
31 | deleteItem: (index) => {
32 | state.todos.splice(index, 1)
33 | render()
34 | },
35 | toggleItemCompleted: (index) => {
36 | const {
37 | completed
38 | } = state.todos[index]
39 | state.todos[index].completed = !completed
40 | render()
41 | },
42 | completeAll: () => {
43 | state.todos.forEach(t => {
44 | t.completed = true
45 | })
46 | render()
47 | },
48 | clearCompleted: () => {
49 | state.todos = state.todos.filter(
50 | t => !t.completed
51 | )
52 | render()
53 | },
54 | changeFilter: filter => {
55 | state.currentFilter = filter
56 | render()
57 | }
58 | }
59 |
60 | const render = () => {
61 | window.requestAnimationFrame(() => {
62 | const main = document.querySelector('#root')
63 |
64 | const newMain = registry.renderRoot(
65 | main,
66 | state,
67 | events)
68 |
69 | applyDiff(document.body, main, newMain)
70 | })
71 | }
72 |
73 | render()
74 |
--------------------------------------------------------------------------------
/Chapter07/00/view/app.js:
--------------------------------------------------------------------------------
1 | let template
2 |
3 | const allTodosCompleted = todos => {
4 | if (todos.length === 0) {
5 | return false
6 | }
7 | return !todos.find(t => !t.completed)
8 | }
9 |
10 | const noCompletedItemIsPresent = todos => !todos.find(t => t.completed)
11 |
12 | const getTemplate = () => {
13 | if (!template) {
14 | template = document.getElementById('todo-app')
15 | }
16 |
17 | return template
18 | .content
19 | .firstElementChild
20 | .cloneNode(true)
21 | }
22 |
23 | const addEvents = (targetElement, events) => {
24 | const { clearCompleted, completeAll, addItem } = events
25 |
26 | targetElement
27 | .querySelector('.new-todo')
28 | .addEventListener('keypress', e => {
29 | if (e.key === 'Enter') {
30 | addItem(e.target.value)
31 | e.target.value = ''
32 | }
33 | })
34 |
35 | targetElement
36 | .querySelector('input.toggle-all')
37 | .addEventListener('click', completeAll)
38 |
39 | targetElement
40 | .querySelector('.clear-completed')
41 | .addEventListener('click', clearCompleted)
42 | }
43 |
44 | export default (targetElement, state, events) => {
45 | const newApp = targetElement.cloneNode(true)
46 |
47 | newApp.innerHTML = ''
48 | newApp.appendChild(getTemplate())
49 |
50 | if (noCompletedItemIsPresent(state.todos)) {
51 | newApp
52 | .querySelector('.clear-completed')
53 | .classList
54 | .add('hidden')
55 | } else {
56 | newApp
57 | .querySelector('.clear-completed')
58 | .classList
59 | .remove('hidden')
60 | }
61 |
62 | newApp
63 | .querySelector('input.toggle-all')
64 | .checked = allTodosCompleted(state.todos)
65 |
66 | addEvents(newApp, events)
67 |
68 | return newApp
69 | }
70 |
--------------------------------------------------------------------------------
/Chapter07/01/view/app.js:
--------------------------------------------------------------------------------
1 | let template
2 |
3 | const allTodosCompleted = todos => {
4 | if (todos.length === 0) {
5 | return false
6 | }
7 | return !todos.find(t => !t.completed)
8 | }
9 |
10 | const noCompletedItemIsPresent = todos => !todos.find(t => t.completed)
11 |
12 | const getTemplate = () => {
13 | if (!template) {
14 | template = document.getElementById('todo-app')
15 | }
16 |
17 | return template
18 | .content
19 | .firstElementChild
20 | .cloneNode(true)
21 | }
22 |
23 | const addEvents = (targetElement, events) => {
24 | const { clearCompleted, completeAll, addItem } = events
25 |
26 | targetElement
27 | .querySelector('.new-todo')
28 | .addEventListener('keypress', e => {
29 | if (e.key === 'Enter') {
30 | addItem(e.target.value)
31 | e.target.value = ''
32 | }
33 | })
34 |
35 | targetElement
36 | .querySelector('input.toggle-all')
37 | .addEventListener('click', completeAll)
38 |
39 | targetElement
40 | .querySelector('.clear-completed')
41 | .addEventListener('click', clearCompleted)
42 | }
43 |
44 | export default (targetElement, state, events) => {
45 | const newApp = targetElement.cloneNode(true)
46 |
47 | newApp.innerHTML = ''
48 | newApp.appendChild(getTemplate())
49 |
50 | if (noCompletedItemIsPresent(state.todos)) {
51 | newApp
52 | .querySelector('.clear-completed')
53 | .classList
54 | .add('hidden')
55 | } else {
56 | newApp
57 | .querySelector('.clear-completed')
58 | .classList
59 | .remove('hidden')
60 | }
61 |
62 | newApp
63 | .querySelector('input.toggle-all')
64 | .checked = allTodosCompleted(state.todos)
65 |
66 | addEvents(newApp, events)
67 |
68 | return newApp
69 | }
70 |
--------------------------------------------------------------------------------
/Chapter07/02/view/app.js:
--------------------------------------------------------------------------------
1 | let template
2 |
3 | const allTodosCompleted = todos => {
4 | if (todos.length === 0) {
5 | return false
6 | }
7 | return !todos.find(t => !t.completed)
8 | }
9 |
10 | const noCompletedItemIsPresent = todos => !todos.find(t => t.completed)
11 |
12 | const getTemplate = () => {
13 | if (!template) {
14 | template = document.getElementById('todo-app')
15 | }
16 |
17 | return template
18 | .content
19 | .firstElementChild
20 | .cloneNode(true)
21 | }
22 |
23 | const addEvents = (targetElement, events) => {
24 | const { clearCompleted, completeAll, addItem } = events
25 |
26 | targetElement
27 | .querySelector('.new-todo')
28 | .addEventListener('keypress', e => {
29 | if (e.key === 'Enter') {
30 | addItem(e.target.value)
31 | e.target.value = ''
32 | }
33 | })
34 |
35 | targetElement
36 | .querySelector('input.toggle-all')
37 | .addEventListener('click', completeAll)
38 |
39 | targetElement
40 | .querySelector('.clear-completed')
41 | .addEventListener('click', clearCompleted)
42 | }
43 |
44 | export default (targetElement, state, events) => {
45 | const newApp = targetElement.cloneNode(true)
46 |
47 | newApp.innerHTML = ''
48 | newApp.appendChild(getTemplate())
49 |
50 | if (noCompletedItemIsPresent(state.todos)) {
51 | newApp
52 | .querySelector('.clear-completed')
53 | .classList
54 | .add('hidden')
55 | } else {
56 | newApp
57 | .querySelector('.clear-completed')
58 | .classList
59 | .remove('hidden')
60 | }
61 |
62 | newApp
63 | .querySelector('input.toggle-all')
64 | .checked = allTodosCompleted(state.todos)
65 |
66 | addEvents(newApp, events)
67 |
68 | return newApp
69 | }
70 |
--------------------------------------------------------------------------------