├── .gitignore ├── html ├── quiz.html ├── counting-clicks.html ├── expandable-sections.html └── index.html ├── snowpack.config.json ├── package.json ├── LICENSE ├── js ├── counting-clicks.js ├── expandable-sections.js └── quiz.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .DS_Store -------------------------------------------------------------------------------- /html/quiz.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Quiz 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /snowpack.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mount": { 3 | "html": "/", 4 | "js": "/js" 5 | }, 6 | "devOptions": { 7 | "open": "none" 8 | }, 9 | "// devOptions": [ 10 | "Switch off automatic opening of browser tabs" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /html/counting-clicks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Counting clicks 6 | 7 | 8 |

Counting clicks

9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /html/expandable-sections.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Expandable sections 6 | 7 | 8 |

Expandable sections

9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimal-react", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "snowpack dev", 6 | "build": "snowpack build" 7 | }, 8 | "devDependencies": { 9 | "@snowpack/plugin-react-refresh": "^2.1.0", 10 | "snowpack": "^2.9.0" 11 | }, 12 | "dependencies": { 13 | "htm": "^3.0.4", 14 | "immer": "^7.0.7", 15 | "react": "^16.13.1", 16 | "react-dom": "^16.13.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Examples in this project 6 | 7 | 8 |

Examples in this project

9 | 10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Axel Rauschmayer 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 | -------------------------------------------------------------------------------- /js/counting-clicks.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import {html} from 'htm/react'; 3 | import {useState} from 'react'; 4 | 5 | //========== Model 6 | 7 | const rootModel = { 8 | numberOfClicks: 0, 9 | }; 10 | 11 | //========== Component 12 | 13 | function CountingClicks({rootModel: initialRootModel}) { 14 | const [rootModel, setRootModel] = useState(initialRootModel); 15 | return html` 16 |
17 | Number of clicks: ${rootModel.numberOfClicks} 18 |

19 | 20 |

21 | `; 22 | 23 | function handleIncrement(event) { 24 | event.preventDefault(); 25 | const nextRootModel = { 26 | numberOfClicks: rootModel.numberOfClicks + 1, 27 | }; 28 | setRootModel(nextRootModel); 29 | } 30 | function handleReset(event) { 31 | const nextRootModel = { 32 | numberOfClicks: 0, 33 | }; 34 | setRootModel(nextRootModel); 35 | } 36 | } 37 | 38 | //========== Entry point 39 | 40 | ReactDOM.render( 41 | html`<${CountingClicks} rootModel=${rootModel} />`, 42 | document.getElementById('root')); 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minimal React project 2 | 3 | Trying out the examples online: https://rauschma.github.io/minimal-react/build/ 4 | 5 | ## Installing the examples on your computer 6 | 7 | * Download and install [Node.js](https://nodejs.org/en/) (this also installs the npm package manager). 8 | * Install the npm packages that this repository depends on: 9 | ``` 10 | cd minimal-react 11 | npm install 12 | ``` 13 | 14 | ## Running the examples locally 15 | 16 | * Start the development server: 17 | ``` 18 | cd minimal-react 19 | npm start 20 | ``` 21 | * The dev server prints root URLs to the console, e.g.: [`http://localhost:8080/`](http://localhost:8080/) 22 | * Open one of them in a web browser. 23 | * The browser tab is refreshed automatically when you change either HTML or JavaScript code. 24 | 25 | ## Building the examples 26 | 27 | You can also create a stand-alone version of this web app that doesn’t need the development server to run: 28 | 29 | ```js 30 | npm run build 31 | ``` 32 | 33 | Afterwards, the complete web app is in directory `minimal-react/build`, ready to be deployed. 34 | 35 | ## Technologies used in this project 36 | 37 | This is an exhaustive list of dependencies: 38 | 39 | * [Snowpack](https://www.snowpack.dev): `snowpack`, `@snowpack/plugin-react-refresh` 40 | * [React](https://reactjs.org): `react`, `react-dom` 41 | * [HTM](https://github.com/developit/htm): `htm` 42 | * [Immer](https://immerjs.github.io/immer/docs/introduction): `immer` -------------------------------------------------------------------------------- /js/expandable-sections.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import {html} from 'htm/react'; 3 | import {useState} from 'react'; 4 | 5 | //========== Model 6 | 7 | const sections = [ 8 | { 9 | title: 'Introduction', 10 | body: 'In this section, we are taking a first look at the ideas are covered by this document.', 11 | }, 12 | { 13 | title: 'The details', 14 | body: 'In this section, we examine the ideas in more details.', 15 | }, 16 | { 17 | title: 'Conclusion', 18 | body: 'In this section, we’ll look at what we have learned and next steps you can take.', 19 | }, 20 | ]; 21 | 22 | function addUiProperties(sections) { 23 | return sections.map((section) => ({ 24 | ...section, 25 | expanded: false, 26 | })); 27 | } 28 | 29 | function expandExactlyOneSection(sections, onlyExpandedIndex) { 30 | return sections.map((section, index) => ({ 31 | ...section, 32 | expanded: (index === onlyExpandedIndex), 33 | })); 34 | } 35 | 36 | //========== Components 37 | 38 | function Sections({sections: initialSections}) { 39 | const [sections, setSections] = useState(initialSections); 40 | return sections.map((section, index) => html` 41 | <${Section} key=${index} sections=${sections} setSections=${setSections} section=${section} sectionIndex=${index} /> 42 | `); 43 | } 44 | 45 | function Section({sections, setSections, section, sectionIndex}) { 46 | return html` 47 |
48 |

49 | 50 | ${section.expanded ? '▼ ' : '▶︎ '} 51 | ${section.title} 52 | 53 |

54 | ${ 55 | !section.expanded 56 | ? null 57 | : html` 58 |
59 | ${section.body} 60 |
61 | ` 62 | } 63 |
64 | `; 65 | 66 | function handleClick(sectionIndex, event) { 67 | event.preventDefault(); 68 | setSections(expandExactlyOneSection(sections, sectionIndex)); 69 | } 70 | } 71 | 72 | //========== Entry point 73 | 74 | ReactDOM.render( 75 | html`<${Sections} sections=${addUiProperties(sections)} />`, 76 | document.getElementById('root')); 77 | -------------------------------------------------------------------------------- /js/quiz.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {html} from 'htm/react'; 4 | import produce from 'immer'; 5 | 6 | //========== Model 7 | 8 | const entries = [ 9 | { 10 | question: 'When was JavaScript created?', 11 | answers: [ 12 | {text: '1984', correct: false}, 13 | {text: '1995', correct: true}, 14 | {text: '2001', correct: false}, 15 | ], 16 | }, 17 | { 18 | question: 'What does “Ecma” mean?', 19 | answers: [ 20 | {text: 'European Computer Manufacturers Association', correct: false}, 21 | {text: 'Enterprise Content Management Association', correct: false}, 22 | {text: 'Electronic Component Manufacturers Association', correct: false}, 23 | {text: 'It’s a proper name', correct: true}, 24 | ], 25 | }, 26 | { 27 | question: 'What does “TC39” mean?', 28 | answers: [ 29 | {text: 'Ecma Technical Committee 39', correct: true}, 30 | {text: 'Ecma Transactions on Computers 39', correct: false}, 31 | {text: 'Ecma Technical Communications 39', correct: false}, 32 | ], 33 | }, 34 | ]; 35 | 36 | function addUiProperties(entries) { 37 | return produce(entries, (draftEntries) => { 38 | for (const entry of draftEntries) { 39 | entry.open = true; 40 | for (const answer of entry.answers) { 41 | answer.checked = false; 42 | } 43 | } 44 | }); 45 | } 46 | 47 | function areAnswersCorrect(entry) { 48 | return entry.answers.every((answer) => answer.checked == answer.correct); 49 | } 50 | 51 | class RootController { 52 | constructor(entries, setEntries) { 53 | this.entries = entries; 54 | this.setEntries = setEntries; 55 | } 56 | setAnswerChecked(entryIndex, answerIndex, checked) { 57 | const newEntries = produce(this.entries, (draftEntries) => { 58 | draftEntries[entryIndex].answers[answerIndex].checked = checked; 59 | }); 60 | this.setEntries(newEntries); // refresh UI 61 | } 62 | closeEntry(entryIndex) { 63 | const newEntries = produce(this.entries, (draftEntries) => { 64 | draftEntries[entryIndex].open = false; 65 | }); 66 | this.setEntries(newEntries); // refresh UI 67 | } 68 | } 69 | 70 | //========== Components 71 | 72 | function Quiz({entries: initialEntries}) { 73 | const [entries, setEntries] = useState(initialEntries); 74 | const root = new RootController(entries, setEntries); 75 | return html` 76 | <${React.Fragment}> 77 |

Quiz

78 | <${AllEntries} root=${root} entries=${entries} /> 79 |
80 | <${Summary} entries=${entries} /> 81 | 82 | `; 83 | } 84 | 85 | function Summary({entries}) { 86 | const numberOfClosedEntries = entries.reduce( 87 | (acc, entry) => acc + (entry.open ? 0 : 1), 0); 88 | const numberOfCorrectEntries = entries.reduce( 89 | (acc, entry) => acc + (!entry.open && areAnswersCorrect(entry) ? 1 : 0), 0); 90 | return html` 91 | Correct: ${numberOfCorrectEntries} of ${numberOfClosedEntries} 92 | ${numberOfClosedEntries === 1 ? ' entry' : ' entries'} 93 | `; 94 | } 95 | 96 | function AllEntries({root, entries}) { 97 | return entries.map((entry, index) => { 98 | const entryKind = entry.open ? OpenEntry : ClosedEntry; 99 | return html` 100 | <${entryKind} key=${index} root=${root} entryIndex=${index} entry=${entry} />` 101 | }); 102 | } 103 | 104 | //----- OpenEntry 105 | 106 | function OpenEntry({root, entryIndex, entry}) { 107 | return html` 108 |
109 |

${entry.question}

110 | ${ 111 | entry.answers.map((answer, index) => html` 112 | <${OpenAnswer} key=${index} root=${root} 113 | entryIndex=${entryIndex} answerIndex=${index} answer=${answer} /> 114 | `) 115 | } 116 |

117 |
`; 118 | 119 | function handleClick(event) { 120 | event.preventDefault(); 121 | root.closeEntry(entryIndex); 122 | } 123 | } 124 | 125 | function OpenAnswer({root, entryIndex, answerIndex, answer}) { 126 | return html` 127 |
128 | 132 |
133 | `; 134 | 135 | function handleChange(_event) { 136 | // Toggle the checkbox 137 | root.setAnswerChecked(entryIndex, answerIndex, !answer.checked); 138 | } 139 | } 140 | 141 | //----- ClosedEntry 142 | 143 | function ClosedEntry({root, entryIndex, entry}) { 144 | return html` 145 |
146 |

${entry.question}

147 | ${ 148 | entry.answers.map((answer, index) => html` 149 | <${ClosedAnswer} key=${index} root=${root} entryIndex=${entryIndex} answer=${answer} answerIndex=${index} /> 150 | `) 151 | } 152 | ${ 153 | areAnswersCorrect(entry) 154 | ? html`

Correct!

` 155 | : html`

Wrong!

` 156 | } 157 |
`; 158 | } 159 | 160 | function ClosedAnswer({root, entryIndex, answerIndex, answer}) { 161 | const style = answer.correct ? {backgroundColor: 'lightgreen'} : {}; 162 | return html` 163 |
164 | 168 |
169 | `; 170 | } 171 | 172 | //========== Entry point 173 | 174 | ReactDOM.render( 175 | html`<${Quiz} entries=${addUiProperties(entries)} />`, 176 | document.getElementById('root')); 177 | --------------------------------------------------------------------------------