├── .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 |
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 |
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 |
Submit answers
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 |
129 |
130 | ${' ' + answer.text}
131 |
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 |
165 |
166 | ${' ' + answer.text}
167 |
168 |
169 | `;
170 | }
171 |
172 | //========== Entry point
173 |
174 | ReactDOM.render(
175 | html`<${Quiz} entries=${addUiProperties(entries)} />`,
176 | document.getElementById('root'));
177 |
--------------------------------------------------------------------------------