mirror of
https://github.com/microsoft/frontend-bootcamp.git
synced 2026-01-26 14:56:42 +08:00
Merge branch 'master' into fix/readme-typo
This commit is contained in:
BIN
assets/flux.png
BIN
assets/flux.png
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 50 KiB |
BIN
assets/todo-components.png
Normal file
BIN
assets/todo-components.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
52
index.html
52
index.html
@@ -16,26 +16,27 @@
|
|||||||
<h2>Day 1</h2>
|
<h2>Day 1</h2>
|
||||||
Get set up and learn to build a todo app using HTML, CSS, JavaScript, and React.
|
Get set up and learn to build a todo app using HTML, CSS, JavaScript, and React.
|
||||||
</li>
|
</li>
|
||||||
<li class="Tile Tile--numbered">
|
|
||||||
<a target="_blank" href="./step1-01/" class="Tile-link">
|
|
||||||
HTML/CSS/JS
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="Tile Tile--numbered">
|
<li class="Tile Tile--numbered">
|
||||||
<div class="Tile-link">
|
<div class="Tile-link">
|
||||||
Todo HTML & CSS
|
Intro to HTML
|
||||||
<div class="Tile-links">
|
<div class="Tile-links">
|
||||||
<a target="_blank" href="./step1-02/demo/">demo</a> | <a target="_blank" href="./step1-02/exercise/">exercise</a> |
|
<a target="_blank" href="./step1-01/demo/">demo</a> | <a target="_blank" href="./step1-01/exercise/">exercise</a>
|
||||||
<a target="_blank" href="./step1-02/final/">final</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="Tile Tile--numbered">
|
<li class="Tile Tile--numbered">
|
||||||
<div class="Tile-link">
|
<div class="Tile-link">
|
||||||
Todo JS
|
Intro to CSS
|
||||||
<div class="Tile-links">
|
<div class="Tile-links">
|
||||||
<a target="_blank" href="./step1-03/demo/">demo</a> | <a target="_blank" href="./step1-03/exercise/">exercise</a> |
|
<a target="_blank" href="./step1-02/demo/">demo</a> | <a target="_blank" href="./step1-02/exercise/">exercise</a>
|
||||||
<a target="_blank" href="./step1-03/final/">final</a>
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="Tile Tile--numbered">
|
||||||
|
<div class="Tile-link">
|
||||||
|
Intro to JS
|
||||||
|
<div class="Tile-links">
|
||||||
|
<a target="_blank" href="./step1-03/demo/">demo</a> | <a target="_blank" href="./step1-03/exercise/">exercise</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -109,7 +110,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="Tile Tile--numbered">
|
<li class="Tile Tile--numbered">
|
||||||
<div class="Tile-link">
|
<div class="Tile-link">
|
||||||
Testing with jest
|
React Context
|
||||||
<div class="Tile-links">
|
<div class="Tile-links">
|
||||||
<a target="_blank" href="./step2-04/demo/">demo</a> | <a target="_blank" href="./step2-04/exercise/">exercise</a>
|
<a target="_blank" href="./step2-04/demo/">demo</a> | <a target="_blank" href="./step2-04/exercise/">exercise</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,7 +118,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="Tile Tile--numbered">
|
<li class="Tile Tile--numbered">
|
||||||
<div class="Tile-link">
|
<div class="Tile-link">
|
||||||
Redux: Reducers
|
Redux: The Store
|
||||||
<div class="Tile-links">
|
<div class="Tile-links">
|
||||||
<a target="_blank" href="./step2-05/demo/">demo</a> | <a target="_blank" href="./step2-05/exercise/">exercise</a>
|
<a target="_blank" href="./step2-05/demo/">demo</a> | <a target="_blank" href="./step2-05/exercise/">exercise</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,35 +126,16 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="Tile Tile--numbered">
|
<li class="Tile Tile--numbered">
|
||||||
<div class="Tile-link">
|
<div class="Tile-link">
|
||||||
Redux: Dispatch Actions
|
Redux: React Binding
|
||||||
<div class="Tile-links">
|
<div class="Tile-links">
|
||||||
<a target="_blank" href="./step2-06/demo/">demo</a> | <a target="_blank" href="./step2-06/exercise/">exercise</a>
|
<a target="_blank" href="./step2-06/demo/">demo</a> | <a target="_blank" href="./step2-06/exercise/">exercise</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="Tile Tile--numbered">
|
<li class="Tile Tile--numbered">
|
||||||
<div class="Tile-link">
|
<a target="_blank" href="./step2-07/demo/" class="Tile-link">
|
||||||
Redux: Connect to UI
|
|
||||||
<div class="Tile-links">
|
|
||||||
<a target="_blank" href="./step2-07/demo/">demo</a> | <a target="_blank" href="./step2-07/exercise/">exercise</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="Tile Tile--numbered">
|
|
||||||
<div class="Tile-link">
|
|
||||||
Redux: Reduce Boilerplate
|
|
||||||
<div class="Tile-links">
|
|
||||||
<a target="_blank" href="./step2-08/demo/">demo</a> | <a target="_blank" href="./step2-08/exercise/">exercise</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="Tile Tile--numbered">
|
|
||||||
<div class="Tile-link">
|
|
||||||
Redux: Service Calls
|
Redux: Service Calls
|
||||||
<div class="Tile-links">
|
</a>
|
||||||
<a target="_blank" href="./step2-09/demo/">demo</a> | <a target="_blank" href="./step2-09/exercise/">exercise</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import marked, { Renderer } from 'marked';
|
|
||||||
import hljs from 'highlight.js/lib/highlight';
|
|
||||||
import javascript from 'highlight.js/lib/languages/javascript';
|
|
||||||
import typescript from 'highlight.js/lib/languages/typescript';
|
|
||||||
|
|
||||||
hljs.registerLanguage('javascript', javascript);
|
|
||||||
hljs.registerLanguage('typescript', typescript);
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
const div = document.getElementById('markdownReadme');
|
|
||||||
|
|
||||||
// Create your custom renderer.
|
|
||||||
const renderer = new Renderer();
|
|
||||||
renderer.code = (code, language) => {
|
|
||||||
// Check whether the given language is valid for highlight.js.
|
|
||||||
const validLang = !!(language && hljs.getLanguage(language));
|
|
||||||
// Highlight only if the language is valid.
|
|
||||||
const highlighted = validLang ? hljs.highlight(language, code).value : code;
|
|
||||||
// Render the highlighted code with `hljs` class.
|
|
||||||
return `<pre><code class="hljs ${language}">${highlighted}</code></pre>`;
|
|
||||||
};
|
|
||||||
marked.setOptions({ renderer });
|
|
||||||
|
|
||||||
if (div) {
|
|
||||||
const response = await fetch(div.dataset['src'] || '../README.md');
|
|
||||||
const markdownText = await response.text();
|
|
||||||
div.innerHTML = marked(markdownText, { baseUrl: '../' });
|
|
||||||
restoreScroll(div);
|
|
||||||
|
|
||||||
div.addEventListener('scroll', evt => {
|
|
||||||
saveScroll(div);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('resize', evt => {
|
|
||||||
saveScroll(div);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollKey = `${window.location.pathname}_scrolltop`;
|
|
||||||
|
|
||||||
function saveScroll(div) {
|
|
||||||
window.localStorage.setItem(scrollKey, String(div.scrollTop));
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreScroll(div) {
|
|
||||||
const scrollTop = window.localStorage.getItem(scrollKey);
|
|
||||||
if (scrollTop) {
|
|
||||||
div.scrollTop = parseInt(scrollTop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
22
package-lock.json
generated
22
package-lock.json
generated
@@ -8363,25 +8363,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react": {
|
"react": {
|
||||||
"version": "16.7.0",
|
"version": "16.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-16.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-16.8.3.tgz",
|
||||||
"integrity": "sha512-StCz3QY8lxTb5cl2HJxjwLFOXPIFQp+p+hxQfc8WE0QiLfCtIlKj8/+5tjjKm8uSTlAW+fCPaavGFS06V9Ar3A==",
|
"integrity": "sha512-3UoSIsEq8yTJuSu0luO1QQWYbgGEILm+eJl2QN/VLDi7hL+EN18M3q3oVZwmVzzBJ3DkM7RMdRwBmZZ+b4IzSA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"scheduler": "^0.12.0"
|
"scheduler": "^0.13.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-dom": {
|
"react-dom": {
|
||||||
"version": "16.7.0",
|
"version": "16.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.3.tgz",
|
||||||
"integrity": "sha512-D0Ufv1ExCAmF38P2Uh1lwpminZFRXEINJe53zRAbm4KPwSyd6DY/uDoS0Blj9jvPpn1+wivKpZYc8aAAN/nAkg==",
|
"integrity": "sha512-ttMem9yJL4/lpItZAQ2NTFAbV7frotHk5DZEHXUOws2rMmrsvh1Na7ThGT0dTzUIl6pqTOi5tYREfL8AEna3lA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"scheduler": "^0.12.0"
|
"scheduler": "^0.13.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
@@ -8888,9 +8888,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"scheduler": {
|
"scheduler": {
|
||||||
"version": "0.12.0",
|
"version": "0.13.3",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.3.tgz",
|
||||||
"integrity": "sha512-t7MBR28Akcp4Jm+QoR63XgAi9YgCUmgvDHqf5otgAj4QvdoBE4ImCX0ffehefePPG+aitiYHp0g/mW6s4Tp+dw==",
|
"integrity": "sha512-UxN5QRYWtpR1egNWzJcVLk8jlegxAugswQc984lD3kU7NuobsO37/sRfbpTdBjtnD5TBNFA2Q2oLV5+UmPSmEQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"object-assign": "^4.1.1"
|
"object-assign": "^4.1.1"
|
||||||
|
|||||||
@@ -63,8 +63,8 @@
|
|||||||
"live-server": "^1.2.1",
|
"live-server": "^1.2.1",
|
||||||
"marked": "^0.6.1",
|
"marked": "^0.6.1",
|
||||||
"office-ui-fabric-react": "^6.144.0",
|
"office-ui-fabric-react": "^6.144.0",
|
||||||
"react": "^16.7.0",
|
"react": "^16.8.3",
|
||||||
"react-dom": "^16.7.0",
|
"react-dom": "^16.8.3",
|
||||||
"react-redux": "^6.0.0",
|
"react-redux": "^6.0.0",
|
||||||
"redux": "^4.0.1",
|
"redux": "^4.0.1",
|
||||||
"redux-devtools-extension": "^2.13.8",
|
"redux-devtools-extension": "^2.13.8",
|
||||||
|
|||||||
@@ -1,49 +1 @@
|
|||||||
# Step 1 Exercise
|
Readme is in index.html
|
||||||
|
|
||||||
The power of HTML is its ability to represent complex information in a way that conveys meaning. In this exercise you are going to be creating an HTML page for my favorite recipe.
|
|
||||||
|
|
||||||
## The Exercise
|
|
||||||
|
|
||||||
1. Create a recipe page to host our recipe (page title, headings, sections, paragraphs, lists etc)
|
|
||||||
2. Use title, header, main, footer, headings (h1/h2 etc), paragraphs, lists
|
|
||||||
3. Use ordered and unordered lists appropriately
|
|
||||||
4. Add the `baked_beans.jpg` iagine included in this folder
|
|
||||||
5. Add an anchor tag around 'Wisconsin Beer Brats'
|
|
||||||
|
|
||||||
## 4th of July Baked Beans
|
|
||||||
|
|
||||||
It's great how a single meal can take you back dozens of years. This is one of those recipes that never seems to fail to impress.
|
|
||||||
|
|
||||||
I learned this recipe for the cousin of one of my college friends back in Nashville Tenessee. We had an amazing 4th of July feast which included this recipe and some bratworst like these Wisconsin Beer Brats https://www.culinaryhill.com/wisconsin-beer-brats/
|
|
||||||
|
|
||||||
Prep Time: 10 minutes
|
|
||||||
Cook time: 3+ hours
|
|
||||||
Servings: 12
|
|
||||||
|
|
||||||
**Ingredients**
|
|
||||||
1LB Bacon chopped
|
|
||||||
3 Cans Bush's Origin Baked Beans
|
|
||||||
1 Walla Wall Onion chopped
|
|
||||||
2 ground garlic cloves
|
|
||||||
3 Tablespoons of mustard
|
|
||||||
2 Tablespoons of molasses
|
|
||||||
3 Tablespoons of brown sugar
|
|
||||||
|
|
||||||
**Directions**
|
|
||||||
Cook bacon until it is mostly cooked, then drain most of the grease and put aside
|
|
||||||
Cook onion in remaining bacon grease
|
|
||||||
Combine onions and bacon, then add garlic, cook for a few more minutes
|
|
||||||
Add beans and get up to simmer temperate
|
|
||||||
Add mustard until your beans are nice and yellow
|
|
||||||
Add molassas until color darkens again
|
|
||||||
Add brown sugar until properly sweet
|
|
||||||
Simmer for a long time, occassionally sturing
|
|
||||||
|
|
||||||
**Expert Tipes**
|
|
||||||
Burning off most of the liquid gives you nice, hearty, sticky beans.
|
|
||||||
If the beans get too try, you can always add beer!
|
|
||||||
|
|
||||||
**Nutritional Information**
|
|
||||||
Calories: lots
|
|
||||||
Fat: lots
|
|
||||||
Fun: lots
|
|
||||||
|
|||||||
@@ -1,4 +1,76 @@
|
|||||||
<html>
|
<html>
|
||||||
<head></head>
|
<body>
|
||||||
<body></body>
|
<div
|
||||||
|
class="codepen"
|
||||||
|
data-theme-id="36294"
|
||||||
|
data-prefill
|
||||||
|
data-editable="true"
|
||||||
|
data-height="100%"
|
||||||
|
data-theme-id="1"
|
||||||
|
data-default-tab="js,result"
|
||||||
|
>
|
||||||
|
<pre data-lang="js">
|
||||||
|
/*
|
||||||
|
Step 1 Exercise
|
||||||
|
|
||||||
|
The power of HTML is its ability to represent complex information in a way that conveys meaning. In this exercise you are going to be creating an HTML page for my favorite recipe.
|
||||||
|
|
||||||
|
## The Exercise
|
||||||
|
|
||||||
|
1. Create a recipe page to host our recipe
|
||||||
|
2. Use title, header, main, footer, headings (h1/h2 etc), paragraphs, lists
|
||||||
|
3. Use ordered and unordered lists appropriately
|
||||||
|
4. Add the `baked_beans.jpg` image included in this folder: `https://raw.githubusercontent.com/Microsoft/frontend-bootcamp/master/step1-01/exercise/baked_beans.jpg`
|
||||||
|
5. Add an anchor tag around 'Wisconsin Beer Brats'
|
||||||
|
|
||||||
|
## The Recipe
|
||||||
|
|
||||||
|
Title:
|
||||||
|
4th of July Baked Beans
|
||||||
|
|
||||||
|
Description:
|
||||||
|
It's great how a single meal can take you back dozens of years. This is one of those recipes that never seems to fail to impress.
|
||||||
|
|
||||||
|
I learned this recipe from the cousin of one of my college friends back in Nashville Tennessee. We had an amazing 4th of July feast which included this recipe and some bratwurst like these Wisconsin Beer Brats https://www.culinaryhill.com/wisconsin-beer-brats/
|
||||||
|
|
||||||
|
Prep Time: 10 minutes
|
||||||
|
Cook time: 3+ hours
|
||||||
|
Servings: 12
|
||||||
|
|
||||||
|
Ingredients:
|
||||||
|
1LB Bacon chopped
|
||||||
|
3 Cans Bush's Original Baked Beans
|
||||||
|
1 Walla Wall Onion chopped
|
||||||
|
2 ground garlic cloves
|
||||||
|
3 Tablespoons of mustard
|
||||||
|
2 Tablespoons of molasses
|
||||||
|
3 Tablespoons of brown sugar
|
||||||
|
|
||||||
|
Directions:
|
||||||
|
Cook bacon until it is mostly cooked, then drain most of the grease and put aside
|
||||||
|
Cook onion in remaining bacon grease
|
||||||
|
Combine onions and bacon, then add garlic, cook for a few more minutes
|
||||||
|
Add beans and get up to simmer temperature
|
||||||
|
Add mustard until your beans are nice and yellow
|
||||||
|
Add molasses until color darkens again
|
||||||
|
Add brown sugar until properly sweet
|
||||||
|
Simmer for a long time, occasionally stirring
|
||||||
|
|
||||||
|
Expert Tips:
|
||||||
|
Burning off most of the liquid gives you nice, hearty, sticky beans.
|
||||||
|
If the beans get too dry, you can always add beer!
|
||||||
|
|
||||||
|
Nutritional Information:
|
||||||
|
Calories: lots
|
||||||
|
Fat: lots
|
||||||
|
Fun: lots
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
<pre data-lang="html">Add Recipe Here</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -48,98 +48,6 @@ Selectors can be a single tag, class, ID, or attribute. It can also be a [combin
|
|||||||
Below is a series of selectors and property/value combinations that we'll apply to our CSS demo page.
|
Below is a series of selectors and property/value combinations that we'll apply to our CSS demo page.
|
||||||
|
|
||||||
```css
|
```css
|
||||||
/* Targeting the entire page */
|
|
||||||
body {
|
|
||||||
font: 1.2em sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Targeting an HTML tag */
|
|
||||||
h1 {
|
|
||||||
/* Color name */
|
|
||||||
color: black;
|
|
||||||
|
|
||||||
/* 6-digit hex */
|
|
||||||
background: #ababab;
|
|
||||||
|
|
||||||
/* Margin: specified separately for each side */
|
|
||||||
margin-bottom: 15px;
|
|
||||||
margin-top: 15px;
|
|
||||||
|
|
||||||
/* Shorthand: Padding applies to all sides */
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
/* Border shorthand and 3-digit hex */
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Overriding inherited styles */
|
|
||||||
span {
|
|
||||||
color: #004578;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sibling selector */
|
|
||||||
a ~ a {
|
|
||||||
/* Changing elements from inline to block */
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Targeting a class name */
|
|
||||||
.tiles {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Descendant selector */
|
|
||||||
.tiles img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Direct descendant selector */
|
|
||||||
.tiles > div {
|
|
||||||
/* rgb color */
|
|
||||||
background: rgb(10, 10, 10);
|
|
||||||
color: white;
|
|
||||||
flex-basis: 100%;
|
|
||||||
/* Padding/margin shorthand. Goes clockwise from top.
|
|
||||||
10px - all
|
|
||||||
10px 20px - top/bottom left/right
|
|
||||||
10px 20px 15px - top left/right bottom
|
|
||||||
*/
|
|
||||||
padding: 10px 20px 15px;
|
|
||||||
margin: 10px 20px 10px 0;
|
|
||||||
border: 1px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Qualified selector */
|
|
||||||
div.important-links {
|
|
||||||
background: #004578;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style inheritance only works for unstyled elements */
|
|
||||||
a {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover pseudo-selector */
|
|
||||||
a:hover {
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Positional pseudo-selector */
|
|
||||||
.tiles > div:last-child {
|
|
||||||
/* overrides margin-right but leaves other margins alone */
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ID selector */
|
|
||||||
#contact-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Attribute selector */
|
|
||||||
input[type='submit'] {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Next Step
|
## Next Step
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="./style.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<h1>This is my <span>Title</span></h1>
|
|
||||||
<div class="tiles">
|
|
||||||
<div class="important-links">
|
|
||||||
<h2>Important Links</h2>
|
|
||||||
<a href="#">We're Awesome</a>
|
|
||||||
<a href="#">Learn More</a>
|
|
||||||
<a href="#">Hire Us</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2>Our Logo</h2>
|
|
||||||
<img src="../../assets/fabric.jpg" width="100" alt="fabric logo" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2>Contact Us</h2>
|
|
||||||
<div id="contact-form">
|
|
||||||
<label>Email</label><input type="email" />
|
|
||||||
<input value="Submit" type="submit" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
132
step1-02/demo/index.html
Normal file
132
step1-02/demo/index.html
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<div
|
||||||
|
class="codepen"
|
||||||
|
data-theme-id="36294"
|
||||||
|
data-prefill
|
||||||
|
data-editable="true"
|
||||||
|
data-height="100%"
|
||||||
|
data-theme-id="1"
|
||||||
|
data-default-tab="html,result"
|
||||||
|
>
|
||||||
|
<pre data-lang="css">
|
||||||
|
/* Targeting the entire page */
|
||||||
|
//body {
|
||||||
|
font: 1.2em sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Targeting an HTML tag */
|
||||||
|
//h1 {
|
||||||
|
/* Color name */
|
||||||
|
color: black;
|
||||||
|
|
||||||
|
/* 6-digit hex */
|
||||||
|
background: #ababab;
|
||||||
|
|
||||||
|
/* Margin: specified separately for each side */
|
||||||
|
margin-bottom: 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
/* Shorthand: Padding applies to all sides */
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
/* Border shorthand and 3-digit hex */
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overriding inherited styles */
|
||||||
|
//span {
|
||||||
|
color: #004578;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sibling selector */
|
||||||
|
//a + a {
|
||||||
|
/* Changing elements from inline to block */
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Targeting a class name */
|
||||||
|
//.tiles {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Descendant selector */
|
||||||
|
//.tiles img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Direct descendant selector */
|
||||||
|
//.tiles > div {
|
||||||
|
/* rgb color */
|
||||||
|
background: rgb(10, 10, 10);
|
||||||
|
color: white;
|
||||||
|
flex-basis: 100%;
|
||||||
|
/* Padding/margin shorthand. Goes clockwise from top.
|
||||||
|
10px - all
|
||||||
|
10px 20px - top/bottom left/right
|
||||||
|
10px 20px 15px - top left/right bottom
|
||||||
|
*/
|
||||||
|
padding: 10px 20px 15px;
|
||||||
|
margin: 10px 20px 10px 0;
|
||||||
|
border: 1px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Qualified selector */
|
||||||
|
//div.important-links {
|
||||||
|
background: #004578;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style inheritance only works for unstyled elements */
|
||||||
|
//a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover pseudo-selector */
|
||||||
|
//a:hover {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Positional pseudo-selector */
|
||||||
|
//.tiles > div:last-child {
|
||||||
|
/* overrides margin-right but leaves other margins alone */
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ID selector */
|
||||||
|
//#contact-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attribute selector */
|
||||||
|
//input[type='submit'] {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
<pre data-lang="html">
|
||||||
|
<h1>This is my <span>Title</span></h1>
|
||||||
|
<div class="tiles">
|
||||||
|
<div class="important-links">
|
||||||
|
<h2>Important Links</h2>
|
||||||
|
<a href="#">We're Awesome</a>
|
||||||
|
<a href="#">Learn More</a>
|
||||||
|
<a href="#">Hire Us</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Our Logo</h2>
|
||||||
|
<img src="../../assets/fabric.jpg" width="100" alt="fabric logo" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Contact Us</h2>
|
||||||
|
<div id="contact-form">
|
||||||
|
<label>Email</label><input type="email" />
|
||||||
|
<input value="Submit" type="submit" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
/* body {
|
|
||||||
font: 1.2em sans-serif;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* h1 {
|
|
||||||
color: black;
|
|
||||||
background: #ababab;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
margin-top: 15px;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* span {
|
|
||||||
color: #004578;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* a ~ a {
|
|
||||||
display: block;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .tiles {
|
|
||||||
display: flex;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .tiles img {
|
|
||||||
width: 100%;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .tiles > div {
|
|
||||||
background: rgb(10, 10, 10);
|
|
||||||
color: white;
|
|
||||||
flex-basis: 100%;
|
|
||||||
padding: 10px 20px 15px;
|
|
||||||
margin: 10px 20px 10px 0;
|
|
||||||
border: 1px solid white;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* div.important-links {
|
|
||||||
background: #004578;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* a {
|
|
||||||
color: white;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* a:hover {
|
|
||||||
color: #ccc;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .tiles > div:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* #contact-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* input[type='submit'] {
|
|
||||||
margin-top: 10px;
|
|
||||||
} */
|
|
||||||
27
step1-02/exercise/answers.css
Normal file
27
step1-02/exercise/answers.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
h2 {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 + div {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.myList li {
|
||||||
|
border: 1px solid green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.myClass {
|
||||||
|
background: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
#myId {
|
||||||
|
background: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.myClass.otherClass {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
section > div:last-child {
|
||||||
|
border: 1px solid pink;
|
||||||
|
}
|
||||||
56
step1-02/exercise/index.html
Normal file
56
step1-02/exercise/index.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<div
|
||||||
|
class="codepen"
|
||||||
|
data-theme-id="36294"
|
||||||
|
data-prefill
|
||||||
|
data-editable="true"
|
||||||
|
data-height="100%"
|
||||||
|
data-theme-id="1"
|
||||||
|
data-default-tab="html,result"
|
||||||
|
>
|
||||||
|
<pre data-lang="css">
|
||||||
|
/* 1. */
|
||||||
|
|
||||||
|
/* 2. */
|
||||||
|
|
||||||
|
/* 3. */
|
||||||
|
|
||||||
|
/* 4. */
|
||||||
|
|
||||||
|
/* 5. */
|
||||||
|
|
||||||
|
/* 6. */
|
||||||
|
|
||||||
|
/* Bonus */
|
||||||
|
</pre>
|
||||||
|
<pre data-lang="html">
|
||||||
|
<section>
|
||||||
|
<h2>1. Text Color: Red</h2>
|
||||||
|
<div>2. Color Green (hint: Sibling Selector)</div>
|
||||||
|
<ul class="myList">
|
||||||
|
<li>
|
||||||
|
3. Border Green
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<main>
|
||||||
|
<div class="myClass">4. Background Green</div>
|
||||||
|
<div class="myClass otherClass">
|
||||||
|
5. Background Green & Color White
|
||||||
|
(Hint Qualified Selector)
|
||||||
|
</div>
|
||||||
|
<div id="myId" class="otherClass">6. Background Yellow</div>
|
||||||
|
</main>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Don't Style Me
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div>Bonus: Border Pink</div>
|
||||||
|
</section>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -52,11 +52,10 @@ let myFunction = function(myNumberParam) {
|
|||||||
|
|
||||||
### Adding Variables
|
### Adding Variables
|
||||||
|
|
||||||
Let's start off our demo by adding some variables to our [script tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script). The other examples on this page will reference these variables.
|
Let's start off our demo by adding a variable to our [script tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script). This variable will be global and constant.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const match = 'a';
|
const match = 'a';
|
||||||
let matches = 0;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Functions
|
## Functions
|
||||||
@@ -65,6 +64,8 @@ Functions are reusable pieces of functionality. Functions can take inputs (param
|
|||||||
|
|
||||||
In our example we'll create a function called `displayMatches` (camelCase is typical for functions) and we'll invoke this function every time that our submit button is clicked. For now we'll simply have our function call `alert("I'm Clicked")`, which is a function that creates an alert in your browser.
|
In our example we'll create a function called `displayMatches` (camelCase is typical for functions) and we'll invoke this function every time that our submit button is clicked. For now we'll simply have our function call `alert("I'm Clicked")`, which is a function that creates an alert in your browser.
|
||||||
|
|
||||||
|
> Note that we want to place `matches` inside of the function so that it resets to 0 each time the function is called
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function displayMatches() {
|
function displayMatches() {
|
||||||
alert("I'm Clicked");
|
alert("I'm Clicked");
|
||||||
@@ -118,7 +119,7 @@ button.onclick = displayMatches();
|
|||||||
You can also combine these together like this:
|
You can also combine these together like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
document.querySelector('.submit').onclick = displayMatches();
|
document.querySelector('.submit').onclick = displayMatches;
|
||||||
```
|
```
|
||||||
|
|
||||||
Wire this up and see you function in action!
|
Wire this up and see you function in action!
|
||||||
@@ -138,11 +139,12 @@ function displayMatches() {
|
|||||||
|
|
||||||
## Conditionals
|
## Conditionals
|
||||||
|
|
||||||
Next we want to compare each `letter` with our `match` value, and if they are the same, we will increment our `matches` variable. Remember that `letter = match` would set the `letter` variable to the value in `match`, so to do comparisons, we use the equality operator `==` or the strict equality operator `===`.
|
Next we want to compare each `letter` with our global `match` value, and if they are the same, we will increment a `matches` variable. Remember that `letter = match` would set the `letter` variable to the value in `match`, so to do comparisons, we use the equality operator `==` or the strict equality operator `===`.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function displayMatches() {
|
function displayMatches() {
|
||||||
const text = 'abcda';
|
const text = 'abcda';
|
||||||
|
let matches = 0;
|
||||||
for (let letter of text) {
|
for (let letter of text) {
|
||||||
if (letter === match) {
|
if (letter === match) {
|
||||||
matches++;
|
matches++;
|
||||||
|
|||||||
@@ -1,19 +1,58 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="../css-demo/css-demo-final.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
<body>
|
||||||
<div>
|
<div
|
||||||
<div class="tiles">
|
class="codepen"
|
||||||
<div>
|
data-theme-id="36294"
|
||||||
<h2>Contact Us</h2>
|
data-prefill
|
||||||
<div id="contact-form">
|
data-editable="true"
|
||||||
<label>Email</label><input id="email" type="email" />
|
data-height="100%"
|
||||||
<input class="submit" value="Submit" type="submit" />
|
data-theme-id="1"
|
||||||
</div>
|
data-default-tab="js,result"
|
||||||
</div>
|
>
|
||||||
</div>
|
<pre data-lang="css">
|
||||||
|
|
||||||
|
body {
|
||||||
|
font: 1.2em sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiles > div {
|
||||||
|
background: rgb(10, 10, 10);
|
||||||
|
color: white;
|
||||||
|
flex-basis: 100%;
|
||||||
|
padding: 10px 20px 15px;
|
||||||
|
margin: 10px 20px 10px 0;
|
||||||
|
border: 1px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#contact-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='submit'] {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
<pre data-lang="html">
|
||||||
|
<div class="tiles">
|
||||||
|
<div>
|
||||||
|
<h2>Contact Us</h2>
|
||||||
|
<div id="contact-form">
|
||||||
|
<label>Email</label><input id="email" type="email" />
|
||||||
|
<input class="submit" value="Submit" type="submit" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</pre>
|
||||||
|
<pre data-lang="js">
|
||||||
|
// Count the number of 'a' characters in the email form.
|
||||||
|
// Update button to reflect that number.
|
||||||
|
|
||||||
|
</pre
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
|
||||||
</body>
|
</body>
|
||||||
<script></script>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
1. Create a function named `getFavs` and set its contents to `alert('clicked')`
|
||||||
|
2. Create a variable `button` and set it to a reference to our button by using `document.querySelector('button')`
|
||||||
|
3. Add a click event listener to the button that calls `getFavs`. Click the button and make sure it calls our alert.
|
||||||
|
4. Replace the alert with a new `favList` variable set to an empty array: `[]`
|
||||||
|
5. Create a const variable `inputs` set to all of the inputs on the page. `querySelectorAll` will help here
|
||||||
|
6. Iterate over all of the inputs using `for (const input of inputs) {}`
|
||||||
|
7. For each iteration use an `if` statement to check if `input.checked` is equal to true
|
||||||
|
8. If the above tests passes, push the `input.parentNode.textContent` onto the `favList` array. Pass that text into `favList.push()` as the parameter to add it to the array
|
||||||
|
9. Outside of the for loop, use `document.querySelector('.favorites')` to target the div at the bottom of the page. Set the div's `textContent` to `favList.join(' ')`. This will join each of the foods together into a string separated by a space.
|
||||||
|
|||||||
14
step1-03/exercise/answer.js
Normal file
14
step1-03/exercise/answer.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
function getFavs() {
|
||||||
|
let favList = [];
|
||||||
|
const inputs = document.querySelectorAll('input');
|
||||||
|
for (const input of inputs) {
|
||||||
|
if (input.checked === true) {
|
||||||
|
favList.push(input.parentNode.textContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.querySelector('.favorites').textContent = favList.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
let button = document.querySelector('button');
|
||||||
|
|
||||||
|
button.addEventListener('click', getFavs);
|
||||||
@@ -1,37 +1,48 @@
|
|||||||
<html>
|
<html>
|
||||||
<style>
|
|
||||||
label,
|
|
||||||
button {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<body>
|
<body>
|
||||||
<label><input type="checkbox" />Ice cream</label>
|
<div
|
||||||
<label><input type="checkbox" />Pizza</label>
|
class="codepen"
|
||||||
<label><input type="checkbox" />Tacos</label>
|
data-theme-id="36294"
|
||||||
<label><input type="checkbox" />Meatloaf</label>
|
data-prefill
|
||||||
<label><input type="checkbox" />Brocolli</label>
|
data-editable="true"
|
||||||
|
data-height="100%"
|
||||||
|
data-theme-id="1"
|
||||||
|
data-default-tab="js,result"
|
||||||
|
>
|
||||||
|
<pre data-lang="css">
|
||||||
|
label,
|
||||||
|
button {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
<pre data-lang="html">
|
||||||
|
<label><input type="checkbox" />Ice cream</label>
|
||||||
|
<label><input type="checkbox" />Pizza</label>
|
||||||
|
<label><input type="checkbox" />Tacos</label>
|
||||||
|
<label><input type="checkbox" />Meatloaf</label>
|
||||||
|
<label><input type="checkbox" />Brocolli</label>
|
||||||
|
|
||||||
<button>Display Your Favorites</button>
|
<button>Display Your Favorites</button>
|
||||||
|
|
||||||
<div class="favorites"></div>
|
<div class="favorites"></div>
|
||||||
|
</pre>
|
||||||
|
<pre data-lang="js">
|
||||||
|
/*
|
||||||
|
1. Create a function named `getFavs` and set its contents to `alert('clicked')`
|
||||||
|
2. Create a variable `button` and set it to a reference to our button by using `document.querySelector('button')`
|
||||||
|
3. Add a click event listener to the button that calls `getFavs`. Click the button and make sure it calls our alert.
|
||||||
|
4. Replace the alert with a new `favList` variable set to an empty array: `[]`
|
||||||
|
5. Create a const variable `inputs` set to all of the inputs on the page. `querySelectorAll` will help here
|
||||||
|
6. Iterate over all of the inputs using `for (const input of inputs) {}`
|
||||||
|
7. For each iteration use an `if` statement to check if `input.checked` is equal to true
|
||||||
|
8. If the above tests passes, push the `input.parentNode.textContent` onto the `favList` array. Pass that text into `favList.push()` as the parameter to add it to the array
|
||||||
|
9. Outside of the for loop, use `document.querySelector('.favorites')` to target the div at the bottom of the page. Set the div's `textContent` to `favList.join(' ')`. This will join each of the foods together into a string separated by a space.
|
||||||
|
*/
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<script>
|
|
||||||
function getFavs() {
|
|
||||||
let favList = [];
|
|
||||||
const inputs = document.querySelectorAll('input');
|
|
||||||
for (const input of inputs) {
|
|
||||||
if (input.checked === true) {
|
|
||||||
favList.push(input.parentNode.textContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.querySelector('.favorites').textContent = favList.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
let button = document.querySelector('button');
|
|
||||||
|
|
||||||
button.addEventListener('click', getFavs);
|
|
||||||
</script>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,36 +1,10 @@
|
|||||||
# Step 4 - Introduction To React Demo
|
# Step 4 - Introduction To React Demo
|
||||||
|
|
||||||
In our last example we saw how we could take a static HTML page and turn it into an interactive page with some buttons and their `onclick` handlers.
|
|
||||||
|
|
||||||
In this example we'll see how React turns that paradigm completely around. With React, the entire DOM is generated and maintained by JavaScript, directly inside the browser. This makes it easier to assemble your application out of reusable pieces, maintain state within a component, and pass data between them.
|
|
||||||
|
|
||||||
In this demo we'll be creating a simple counter that will display a count and increment on click.
|
In this demo we'll be creating a simple counter that will display a count and increment on click.
|
||||||
|
|
||||||
## Building the App
|
## React Hello World
|
||||||
|
|
||||||
If you already have the app running from a previous step, stop it with `ctrl+C`. Start the app version used in this step by running `npm start` from the root of the `frontend-bootcamp` folder. Click the "demo" link under day 1 step 4 to see results.
|
```js
|
||||||
|
|
||||||
## index.html
|
|
||||||
|
|
||||||
The `index.html` file in a React project is going to be pretty mimimal. React is loading all of the application onto the page, so the only content you'll find in the page source is a div with an `id` of "app".
|
|
||||||
|
|
||||||
> Note that our bundling tool, webpack, is adding this script tag to the HTML file we provided
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script src="../../step1-04/final/step1-04/final.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
## index.tsx
|
|
||||||
|
|
||||||
This is the entry point to your application.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
ReactDOM.render(<p>Hello World</p>, document.getElementById('app'));
|
ReactDOM.render(<p>Hello World</p>, document.getElementById('app'));
|
||||||
@@ -90,9 +64,9 @@ export class App extends React.Component {
|
|||||||
|
|
||||||
## Counter Component
|
## Counter Component
|
||||||
|
|
||||||
In this example we'll start with an already scaffolded-out control. The goal of our counter is to track how many times the counter button is clicked. In the past JavaScript demo we might have accessed the counter element using `document.querySelector('.counter')` and manually incremented the number found there. While using the DOM as your data store works, it's REALLY hard to scale past the most basic demo.
|
In this example we'll start with an already scaffolded-out control. The goal of our counter is to track how many times the counter button is clicked.
|
||||||
|
|
||||||
React solves this by allowing each control to specify its own data store, called **state**. We can reference values in state when we render our UI, and we can also update state over the lifetime of our application.
|
React allows each control to specify its own data store, called **state**. We can reference values in state when we render our UI, and we can also update state over the lifetime of our application.
|
||||||
|
|
||||||
### Adding State
|
### Adding State
|
||||||
|
|
||||||
@@ -113,7 +87,7 @@ constructor(props) {
|
|||||||
|
|
||||||
#### Using [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring) for props and state
|
#### Using [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring) for props and state
|
||||||
|
|
||||||
Both `props` are `state` are JavaScript objects. They have a bunch of key/value pairs in them which you can access via `this.props.foo` or `this.state.bar`. Sometimes they have MANY values inside of them which you need access to. You could do this:
|
Both `props` and `state` are JavaScript objects. They have a bunch of key/value pairs in them which you can access via `this.props.foo` or `this.state.bar`. Sometimes they have MANY values inside of them which you need access to. You could do this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
let cat = this.props.cat;
|
let cat = this.props.cat;
|
||||||
@@ -205,11 +179,13 @@ This function will update our component's state, incrementing the counter value
|
|||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
_onButtonClick = () => {
|
_onButtonClick = () => {
|
||||||
this.setState(prevState => ({ counter: prevState.counter + 1 }));
|
this.setState({
|
||||||
|
counter: this.state.counter + 1
|
||||||
|
});
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
> This isn't exactly a method, but a class property that is set to an [arrow function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions). This mostly works the same as `onButtonClick() { }` but eliminates the need for [extra boilerplate](https://medium.freecodecamp.org/this-is-why-we-need-to-bind-event-handlers-in-class-components-in-react-f7ea1a6f93eb) used to avoid potential "gotchas" with [how `this` works in JavaScript](https://codeburst.io/javascript-the-keyword-this-for-beginners-fb5238d99f85).)
|
> Note that this could also be written as `this.setState(prevState => ({ counter: prevState.counter + 1 }));` to ensure that state is not updated until the previous state has been determined
|
||||||
|
|
||||||
Now that we have a function to increment our count, all that's left is to connect it to our button.
|
Now that we have a function to increment our count, all that's left is to connect it to our button.
|
||||||
|
|
||||||
@@ -217,11 +193,9 @@ Now that we have a function to increment our count, all that's left is to connec
|
|||||||
<button onClick={this._onButtonClick}>Click</button>
|
<button onClick={this._onButtonClick}>Click</button>
|
||||||
```
|
```
|
||||||
|
|
||||||
> Note the syntax is a bit different than in HTML: `onclick="funcName()"` in HTML vs `onClick={this.funcName}` in JSX.
|
|
||||||
|
|
||||||
> Also note that each `Counter` maintains its own state! You can modify the state inside of one counter without affecting the others.
|
> Also note that each `Counter` maintains its own state! You can modify the state inside of one counter without affecting the others.
|
||||||
|
|
||||||
## Bonus: Using a Button component
|
## Using a Button component
|
||||||
|
|
||||||
Buttons are among the most commonly written components. Custom buttons help abstract common styling, add icons or other decorations, and increase functionality (menu buttons etc). Let's take a quick look at a custom button component to see how it comes together.
|
Buttons are among the most commonly written components. Custom buttons help abstract common styling, add icons or other decorations, and increase functionality (menu buttons etc). Let's take a quick look at a custom button component to see how it comes together.
|
||||||
|
|
||||||
|
|||||||
36
step1-05/TodoApp.html
Normal file
36
step1-05/TodoApp.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>todos</h1>
|
||||||
|
<div class="addTodo">
|
||||||
|
<input class="textfield" placeholder="add todo" />
|
||||||
|
<button class="submit">Add</button>
|
||||||
|
</div>
|
||||||
|
<nav class="filter">
|
||||||
|
<button class="selected">all</button>
|
||||||
|
<button>active</button>
|
||||||
|
<button>completed</button>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<ul class="todos">
|
||||||
|
<li class="todo">
|
||||||
|
<label><input type="checkbox" /> Todo 1</label>
|
||||||
|
</li>
|
||||||
|
<li class="todo">
|
||||||
|
<label><input type="checkbox" /> Todo 2</label>
|
||||||
|
</li>
|
||||||
|
<li class="todo">
|
||||||
|
<label><input type="checkbox" /> Todo 3</label>
|
||||||
|
</li>
|
||||||
|
<li class="todo">
|
||||||
|
<label><input type="checkbox" /> Todo 4</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<footer>
|
||||||
|
<span>4 items left</span>
|
||||||
|
<button class="submit">Clear Completed</button>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
# Building a Static Page
|
# Building a Static Page
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
To start off our todo application we are going to follow the steps outlined in [Thinking in React](https://reactjs.org/docs/thinking-in-react.html). The first step of the process is to break our application into a component hierarchy. For this app, we're going to keep it simple and just use four parts.
|
To start off our todo application we are going to follow the steps outlined in [Thinking in React](https://reactjs.org/docs/thinking-in-react.html). The first step of the process is to break our application into a component hierarchy. For this app, we're going to keep it simple and just use four parts.
|
||||||
|
|
||||||
- TodoHeader
|
- TodoHeader
|
||||||
@@ -9,66 +7,57 @@ To start off our todo application we are going to follow the steps outlined in [
|
|||||||
- TodoListItem
|
- TodoListItem
|
||||||
- TodoFooter
|
- TodoFooter
|
||||||
|
|
||||||
We could go a lot deeper into creating buttons, inputs and checkboxes, but this is a great place to start. Often you'll want to start with a single large control and then break it up into smaller pieces.
|
You can find the HTML for our application in `step1-05/TodoApp.html`
|
||||||
|
|
||||||
### TodoApp
|
## TodoHeader
|
||||||
|
|
||||||
|
We are going to store all of our components inside of a `components` folder. Lets create that now. We'll then start with the `TodoHeader` inside of a file called `TodoHeader.tsx`. This file format tells our application that this file includes React code written in Typescript.
|
||||||
|
|
||||||
|
> We'll talk about Typescript soon, but for now know that all valid JavaScript is valid Typescript
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TodoFooter } from './components/TodoFooter';
|
|
||||||
import { TodoHeader } from './components/TodoHeader';
|
|
||||||
import { TodoList } from './components/TodoList';
|
|
||||||
|
|
||||||
export class TodoApp extends React.Component {
|
export class TodoHeader extends React.Component<any, any> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<header>
|
||||||
<TodoHeader />
|
<h1>todos</h1>
|
||||||
<TodoList />
|
<div className="addTodo">
|
||||||
<TodoFooter />
|
<input className="textfield" placeholder="add todo" />
|
||||||
</div>
|
<button className="submit">Add</button>
|
||||||
|
</div>
|
||||||
|
<nav className="filter">
|
||||||
|
<button className="completed">all</button>
|
||||||
|
<button>active</button>
|
||||||
|
<button>completed</button>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
We'll start off with all of the files scaffolded and imported into our App. This will let us dive right into each control and see updates quickly.
|
|
||||||
|
|
||||||
### TodoHeader
|
|
||||||
|
|
||||||
Our objective for now is to create a static version of our application, so we'll copy over the entire header tag from a previous step, minus any function calls we added.
|
|
||||||
|
|
||||||
> Note that since this is React we had to change `class` to `className`, but nothing else changes.
|
> Note that since this is React we had to change `class` to `className`, but nothing else changes.
|
||||||
|
|
||||||
```jsx
|
## TodoListItem
|
||||||
return (
|
|
||||||
<header>
|
|
||||||
<h1>todos</h1>
|
|
||||||
<div className="addTodo">
|
|
||||||
<input className="textfield" placeholder="add todo" />
|
|
||||||
<button className="submit">Add</button>
|
|
||||||
</div>
|
|
||||||
<nav className="filter">
|
|
||||||
<button className="completed">all</button>
|
|
||||||
<button>active</button>
|
|
||||||
<button>completed</button>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### TodoListItem
|
|
||||||
|
|
||||||
Any time you see repeated complex elements, that is usually a sign that you should create a new component. With a few props you can typically abstract all of those elements into a single component. This is certainly the case with todo items.
|
Any time you see repeated complex elements, that is usually a sign that you should create a new component. With a few props you can typically abstract all of those elements into a single component. This is certainly the case with todo items.
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
return (
|
import React from 'react';
|
||||||
<li className="todo">
|
|
||||||
<label>
|
export class TodolistItem extends React.Component<any, any> {
|
||||||
<input type="checkbox" /> Todo 1
|
render() {
|
||||||
</label>
|
return (
|
||||||
</li>
|
<li className="todo">
|
||||||
);
|
<label>
|
||||||
|
<input type="checkbox" /> Todo 1
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> Note that I've removed the title span as it was only needed to make targeting that text easier.
|
> Note that this control could also be created as a function instead of a class: `export const TodoListItem = (props) => {}`
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TodoFooter } from './components/TodoFooter';
|
|
||||||
import { TodoHeader } from './components/TodoHeader';
|
|
||||||
import { TodoList } from './components/TodoList';
|
|
||||||
|
|
||||||
export class TodoApp extends React.Component<any, any> {
|
export class TodoApp extends React.Component<any, any> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<TodoHeader />
|
<p>Hello World</p>
|
||||||
<TodoList />
|
|
||||||
<TodoFooter />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export const TodoFooter = (props: any) => {
|
|
||||||
return (
|
|
||||||
<footer>
|
|
||||||
<div>Footer</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export class TodoHeader extends React.Component<any, any> {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<header>
|
|
||||||
<div>Header</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export class TodoList extends React.Component<any, any> {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<span>List</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export class TodoListItem extends React.Component<any, any> {
|
|
||||||
render() {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
## Exercise
|
## Exercise
|
||||||
|
|
||||||
If you already have the app running from a previous step, stop it with `ctrl+C`. Start the app version used in this step by running `npm start` from the root of the `frontend-bootcamp` folder. Click the "exercise" link under day 1 step 5 to see results.
|
|
||||||
|
|
||||||
### TodoFooter
|
### TodoFooter
|
||||||
|
|
||||||
1. Update the TodoFooter component, copying over the `<footer>` tag and all of its children
|
1. Add a TodoFooter component, copying over the `<footer>` tag and all of its children from `TodoApp.html` in the `step1-05` folder.
|
||||||
2. Remove any `onclick` properties, and change `class` to `className`
|
2. Remove any `onclick` properties, and change `class` to `className`
|
||||||
|
|
||||||
### TodoList
|
### TodoList
|
||||||
|
|
||||||
1. Update the TodoList component like you did with the footer.
|
1. Add a TodoList component like you did with the footer.
|
||||||
2. Import TodoListItem and add 4 of them inside of the `<ul>`
|
2. Import TodoListItem and add 4 of them inside of the `<ul>`
|
||||||
3. Bonus points for using a [`for`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Loops_and_iteration) loop or using [`map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) to create 4 list items based on the array `[1,2,3,4]`
|
3. Bonus points for using a [`for`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Loops_and_iteration) loop or using [`map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) to create 4 list items based on the array `[1,2,3,4]`
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ constructor(props) {
|
|||||||
To avoid reaching into state over and over, we once again use destructuring to pull out the pieces we need.
|
To avoid reaching into state over and over, we once again use destructuring to pull out the pieces we need.
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
const { filter, todos = [] } = this.state;
|
const { filter, todos } = this.state;
|
||||||
```
|
```
|
||||||
|
|
||||||
> Note that I've set `todos` to default to an empty array so that the `todos` variable is never undefined
|
> Note that I've set `todos` to default to an empty array so that the `todos` variable is never undefined
|
||||||
@@ -69,7 +69,7 @@ I've already pulled out our props into `filter` and `todos` variables, and writt
|
|||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
{
|
{
|
||||||
filteredTodos.map(id => <TodoListItem key={id} id={id} {...todos[id]} />)
|
filteredTodos.map(id => <TodoListItem key={id} id={id} {...todos[id]} />);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export class TodoApp extends React.Component<any, Store> {
|
|||||||
filter: 'all'
|
filter: 'all'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { filter, todos } = this.state;
|
const { filter, todos } = this.state;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export class TodoHeader extends React.Component<TodoHeaderProps, TodoHeaderState
|
|||||||
return (
|
return (
|
||||||
<Stack gap={10}>
|
<Stack gap={10}>
|
||||||
<Stack horizontal horizontalAlign="center">
|
<Stack horizontal horizontalAlign="center">
|
||||||
<Text variant="xxLarge">todos - step2-03 demo</Text>
|
<Text variant="xxLarge">todos</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack horizontal gap={10}>
|
<Stack horizontal gap={10}>
|
||||||
@@ -40,9 +40,7 @@ export class TodoHeader extends React.Component<TodoHeaderProps, TodoHeaderState
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<PrimaryButton onClick={this.onAdd} styles={{ root: { backgroundColor: 'maroon' }, rootHovered: { background: 'green' } }}>
|
<PrimaryButton onClick={this.onAdd}>Add</PrimaryButton>
|
||||||
Add
|
|
||||||
</PrimaryButton>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Pivot onLinkClick={this.onFilter}>
|
<Pivot onLinkClick={this.onFilter}>
|
||||||
|
|||||||
@@ -1,128 +1,116 @@
|
|||||||
# Step 2.4: Testing TypeScript code with Jest (Demo)
|
# Step 2.4 - React Context (Demo)
|
||||||
|
|
||||||
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
|
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
|
||||||
|
|
||||||
[Jest](https://jestjs.io/) is a test framework made by Facebook and is very popular in the React and wider JS ecosystems.
|
In this step, we describe some problems we encounter when creating a more complex application.
|
||||||
|
|
||||||
In this exercise, we will work on implementing simple unit tests using Jest.
|
We will solve these problems with the React Context API. The Context API consists of Provider and Consumer components. Let's take a look at what is in this step:
|
||||||
|
|
||||||
## Jest Features
|
1. The problem of complex applications
|
||||||
|
2. React Context API
|
||||||
|
3. Consuming context from a Class Component
|
||||||
|
4. Consuming context from a Functional Component
|
||||||
|
|
||||||
- Multi-threaded and isolated test runner
|
---
|
||||||
- Provides a fake browser-like environment if needed (window, document, DOM, etc) using jsdom
|
|
||||||
- Snapshots: Jest can create text-based snapshots of rendered components. These snapshots can be checked in and show API or large object changes alongside code changes in pull requests.
|
|
||||||
- Code coverage is integrated (`--coverage`)
|
|
||||||
- Very clear error messages showing where a test failure occurred
|
|
||||||
|
|
||||||
## How to use Jest
|
## The problem of complex applications
|
||||||
|
|
||||||
- Using `create-react-app` or other project generators, Jest should already be pre-configured. Running `npm test` usually will trigger it!
|
React represents a single component like this:
|
||||||
- A `jest.config.js` file is used for configuration
|
|
||||||
- `jsdom` might not have enough API from real browsers, for those cases, polyfills are required. Place these inside `jest.setup.js` and hook up the setup file in `jest.config.js`
|
|
||||||
- in order to use `enzyme` library to test React Components, more config bits are needed inside `jest.setup.js`
|
|
||||||
|
|
||||||
## What does a test look like?
|
```
|
||||||
|
(props) => view;
|
||||||
|
```
|
||||||
|
|
||||||
|
In a real application, these functions are composed. It looks more like this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. Data needs to be passed down from component to component via props. Even when some components do not need to know about some data. This is a problem called **props drilling**
|
||||||
|
|
||||||
|
2. There is a lack of coordination of changes that can happen to the data
|
||||||
|
|
||||||
|
Even in our simple application, we saw this problem. For example, `<TodoList>` has this props interface:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// describe(), it() and expect() are globally exported, so they don't need to be imported when jest runs these tests
|
interface TodoListProps {
|
||||||
describe('Something to be tested', () => {
|
complete: (id: string) => void;
|
||||||
it('should describe the behavior', () => {
|
remove: (id: string) => void;
|
||||||
expect(true).toBe(true);
|
todos: Store['todos'];
|
||||||
});
|
filter: FilterTypes;
|
||||||
});
|
edit: (id: string, label: string) => void;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing React components using Enzyme
|
All of these props are not used, except to be passed down to a child Component, `TodoListItem`:
|
||||||
|
|
||||||
[Enzyme](https://airbnb.io/enzyme/) is made by Airbnb and provides utilities to help test React components.
|
```js
|
||||||
|
<TodoListItem todos="{todos}" complete="{complete}" remove="{remove}" edit="{edit}" />
|
||||||
In a real app using ReactDOM, the top-level component will be rendered on the page using `ReactDOM.render()`. Enzyme provides a lighter-weight `mount()` function which is usually adequate for testing purposes.
|
|
||||||
|
|
||||||
`mount()` returns a wrapper that can be inspected and provides functionality like `find()`, simulating clicks, etc.
|
|
||||||
|
|
||||||
The following code demonstrates how Enzyme can be used to help test React components.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import React from 'react';
|
|
||||||
import { mount } from 'enzyme';
|
|
||||||
import { TestMe } from './TestMe';
|
|
||||||
|
|
||||||
describe('TestMe Component', () => {
|
|
||||||
it('should have a non-clickable component when the original InnerMe is clicked', () => {
|
|
||||||
const wrapper = mount(<TestMe name="world" />);
|
|
||||||
wrapper.find('#innerMe').simulate('click');
|
|
||||||
expect(wrapper.find('#innerMe').text()).toBe('Clicked');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Foo Component Tests', () => {
|
|
||||||
it('allows us to set props', () => {
|
|
||||||
const wrapper = mount(<Foo bar="baz" />);
|
|
||||||
expect(wrapper.props().bar).toBe('baz');
|
|
||||||
wrapper.setProps({ bar: 'foo' });
|
|
||||||
expect(wrapper.props().bar).toBe('foo');
|
|
||||||
|
|
||||||
wrapper.find('button').simulate('click');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Advanced topics
|
## React Context API
|
||||||
|
|
||||||
### Mocking
|
Let's solve these problems with the React Context API. Context is React's way to share data from components to their descendant children components without explicitly passing down through props at every level of the tree. In simpler terms, it solves the props drilling issue mentioned above!
|
||||||
|
|
||||||
Mocking functions is a large part of what makes Jest a powerful testing library. Jest actually intercepts the module loading process in Node.js, allowing it to mock entire modules if needed.
|
React context is created by calling `createContext()` with some initial data. Use the `<TodoContext.Provider>` component to wrap a part of the component tree that should be handed the context.
|
||||||
|
|
||||||
There are many ways to mock, as you'd imagine in a language as flexible as JS. We only look at the simplest case, but there's a lot of depth here.
|
### Providing context with `<TodoContext.Provider>`
|
||||||
|
|
||||||
To mock a function:
|
```js
|
||||||
|
// To create a completed empty context
|
||||||
|
const TodoContext = React.createContext(undefined);
|
||||||
|
|
||||||
```ts
|
class TodoApp extends React.Component {
|
||||||
it('some test function', () => {
|
render() {
|
||||||
const mockCallback = jest.fn(x => 42 + x);
|
|
||||||
mockCallback(1);
|
// Pass in some state and function to the provider's value prop
|
||||||
mockCallback(2);
|
return (
|
||||||
expect(mockCallback).toHaveBeenCalledTimes(2);
|
<TodoContext.Provider
|
||||||
});
|
value={{
|
||||||
|
...this.state,
|
||||||
|
addTodo={this._addTodo},
|
||||||
|
setFilter={this._setFilter},
|
||||||
|
/* same goes for remove, complete, and clear */
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<TodoHeader />
|
||||||
|
<TodoList />
|
||||||
|
<TodoFooter />
|
||||||
|
</div>
|
||||||
|
</TodoContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Read more about jest mocking [here](https://jestjs.io/docs/en/mock-functions.html).
|
### Consume context from a Class Component
|
||||||
|
|
||||||
### Async Testing
|
Inside a class-based child component, such as the `<TodoHeader>` component, the value can be accessed from the component's `context` property like this:
|
||||||
|
|
||||||
For testing async scenarios, the test runner needs some way to know when the scenario is finished. Jest tests can handle async scenarios using callbacks, promises, or async/await.
|
```js
|
||||||
|
class TodoHeader extends React.Component {
|
||||||
|
render() {
|
||||||
|
// Step 1: use the context prop
|
||||||
|
return <div>Filter is {this.context.filter}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
```ts
|
// Step 2: be sure to set the contextType property of the component class
|
||||||
// Callback
|
TodoHeader.contextType = TodoContext;
|
||||||
it('tests callback functions', (done) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
done();
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Returning a promise
|
|
||||||
it('tests promise functions', () => {
|
|
||||||
return someFunctionThatReturnsPromise());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Async/await (recommended)
|
|
||||||
it('tests async functions', async () => {
|
|
||||||
expect(await someFunction()).toBe(5);
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Demo
|
### Consume context from a Functional Component
|
||||||
|
|
||||||
## Jest basics
|
If you're using the functional component syntax, you can access the context with the `useContext()` hook. Note that `useContext()` requires a recent release of React (16.8):
|
||||||
|
|
||||||
In this repo, we can start an inner loop development of tests by running `npm test` from the root of the `frontend-bootcamp` folder.
|
```js
|
||||||
|
const TodoFooter = props => {
|
||||||
|
const context = useContext(TodoContext);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={context.clear()}>Clear Completed</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
Take a look at code inside `demo/src`:
|
There is another legal syntax for accessing context with the `<TodoContext.Consumer>`, but we'll leave that out as an exercise for you!
|
||||||
|
|
||||||
1. `index.ts` exports a few functions for a counter as well as a function for squaring numbers. We'll use this last function to demonstrate how mocks work.
|
|
||||||
|
|
||||||
2. `multiply.ts` is a contrived example of a function that is exported
|
|
||||||
|
|
||||||
3. `index.spec.ts` is the test file
|
|
||||||
|
|
||||||
Note how tests are re-run when either test files or source files under `src` are saved.
|
|
||||||
|
|||||||
@@ -5,11 +5,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="ms-Fabric">
|
<body class="ms-Fabric">
|
||||||
<div id="markdownReadme" data-src="./README.md"></div>
|
<div id="markdownReadme" data-src="./README.md"></div>
|
||||||
<div id="app">
|
<div id="app"></div>
|
||||||
For this step, we look at unit testing. Run
|
|
||||||
<pre>npm test</pre>
|
|
||||||
in the command line.
|
|
||||||
</div>
|
|
||||||
<script src="../../assets/scripts.js"></script>
|
<script src="../../assets/scripts.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { mount } from 'enzyme';
|
|
||||||
import { TestMe } from './TestMe';
|
|
||||||
|
|
||||||
describe('TestMe Component', () => {
|
|
||||||
it('should have a non-clickable component when the original InnerMe is clicked', () => {
|
|
||||||
const wrapper = mount(<TestMe name="world" />);
|
|
||||||
wrapper.find('#innerMe').simulate('click');
|
|
||||||
expect(wrapper.find('#innerMe').text()).toBe('Clicked');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export interface TestMeProps {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestMeState {
|
|
||||||
clicked: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TestMe = (props: TestMeProps) => {
|
|
||||||
return (
|
|
||||||
<div id="testMe">
|
|
||||||
<InnerMe name={props.name} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export class InnerMe extends React.Component<TestMeProps, TestMeState> {
|
|
||||||
state = {
|
|
||||||
clicked: false
|
|
||||||
};
|
|
||||||
|
|
||||||
onClick = () => {
|
|
||||||
this.setState({ clicked: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return !this.state.clicked ? (
|
|
||||||
<div onClick={this.onClick} id="innerMe">
|
|
||||||
Hello {this.props.name}, Click Me
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div id="innerMe">Clicked</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
step2-04/demo/src/TodoContext.ts
Normal file
4
step2-04/demo/src/TodoContext.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// The typing forces us to put something inside createContext(); start with undefined
|
||||||
|
export const TodoContext = React.createContext(undefined);
|
||||||
99
step2-04/demo/src/components/TodoApp.tsx
Normal file
99
step2-04/demo/src/components/TodoApp.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack } from 'office-ui-fabric-react';
|
||||||
|
import { TodoFooter } from './TodoFooter';
|
||||||
|
import { TodoHeader } from './TodoHeader';
|
||||||
|
import { TodoList } from './TodoList';
|
||||||
|
import { Store } from '../store';
|
||||||
|
import { TodoContext } from '../TodoContext';
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
export class TodoApp extends React.Component<any, Store> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
todos: {},
|
||||||
|
filter: 'all'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<TodoContext.Provider
|
||||||
|
value={{
|
||||||
|
...this.state,
|
||||||
|
addTodo: this._addTodo,
|
||||||
|
remove: this._remove,
|
||||||
|
complete: this._complete,
|
||||||
|
clear: this._clear,
|
||||||
|
setFilter: this._setFilter,
|
||||||
|
edit: this._edit
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack horizontalAlign="center">
|
||||||
|
<Stack style={{ width: 400 }} gap={25}>
|
||||||
|
<TodoHeader />
|
||||||
|
<TodoList />
|
||||||
|
<TodoFooter />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</TodoContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addTodo = label => {
|
||||||
|
const { todos } = this.state;
|
||||||
|
const id = index++;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
todos: { ...todos, [id]: { label } }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private _remove = id => {
|
||||||
|
const newTodos = { ...this.state.todos };
|
||||||
|
delete newTodos[id];
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
todos: newTodos
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private _complete = id => {
|
||||||
|
const newTodos = { ...this.state.todos };
|
||||||
|
newTodos[id].completed = !newTodos[id].completed;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
todos: newTodos
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private _edit = (id, label) => {
|
||||||
|
const newTodos = { ...this.state.todos };
|
||||||
|
newTodos[id] = { ...newTodos[id], label };
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
todos: newTodos
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private _clear = () => {
|
||||||
|
const { todos } = this.state;
|
||||||
|
const newTodos = {};
|
||||||
|
|
||||||
|
Object.keys(this.state.todos).forEach(id => {
|
||||||
|
if (!todos[id].completed) {
|
||||||
|
newTodos[id] = todos[id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
todos: newTodos
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private _setFilter = filter => {
|
||||||
|
this.setState({
|
||||||
|
filter: filter
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
17
step2-04/demo/src/components/TodoFooter.tsx
Normal file
17
step2-04/demo/src/components/TodoFooter.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { DefaultButton, Stack, Text } from 'office-ui-fabric-react';
|
||||||
|
import { TodoContext } from '../TodoContext';
|
||||||
|
|
||||||
|
export const TodoFooter = () => {
|
||||||
|
const context = useContext(TodoContext);
|
||||||
|
const itemCount = Object.keys(context.todos).filter(id => !context.todos[id].completed).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack horizontal horizontalAlign="space-between">
|
||||||
|
<Text>
|
||||||
|
{itemCount} item{itemCount === 1 ? '' : 's'} left
|
||||||
|
</Text>
|
||||||
|
<DefaultButton onClick={() => context.clear()}>Clear Completed</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
64
step2-04/demo/src/components/TodoHeader.tsx
Normal file
64
step2-04/demo/src/components/TodoHeader.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack, Text, Pivot, PivotItem, TextField, PrimaryButton } from 'office-ui-fabric-react';
|
||||||
|
import { FilterTypes } from '../store';
|
||||||
|
import { TodoContext } from '../TodoContext';
|
||||||
|
|
||||||
|
interface TodoHeaderState {
|
||||||
|
labelInput: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TodoHeader extends React.Component<{}, TodoHeaderState> {
|
||||||
|
constructor(props: {}) {
|
||||||
|
super(props);
|
||||||
|
this.state = { labelInput: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Stack gap={10}>
|
||||||
|
<Stack horizontal horizontalAlign="center">
|
||||||
|
<Text variant="xxLarge">todos</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack horizontal gap={10}>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<TextField
|
||||||
|
placeholder="What needs to be done?"
|
||||||
|
value={this.state.labelInput}
|
||||||
|
onChange={this.onChange}
|
||||||
|
styles={props => ({
|
||||||
|
...(props.focused && {
|
||||||
|
field: {
|
||||||
|
backgroundColor: '#c7e0f4'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
<PrimaryButton onClick={this.onAdd}>Add</PrimaryButton>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Pivot onLinkClick={this.onFilter}>
|
||||||
|
<PivotItem headerText="all" />
|
||||||
|
<PivotItem headerText="active" />
|
||||||
|
<PivotItem headerText="completed" />
|
||||||
|
</Pivot>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAdd = () => {
|
||||||
|
this.context.addTodo(this.state.labelInput);
|
||||||
|
this.setState({ labelInput: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onChange = (evt: React.FormEvent<HTMLInputElement>, newValue: string) => {
|
||||||
|
this.setState({ labelInput: newValue });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onFilter = (item: PivotItem) => {
|
||||||
|
this.context.setFilter(item.props.headerText as FilterTypes);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoHeader.contextType = TodoContext;
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { Stack } from 'office-ui-fabric-react';
|
import { Stack } from 'office-ui-fabric-react';
|
||||||
import { TodoListItem } from './TodoListItem';
|
import { TodoListItem } from './TodoListItem';
|
||||||
import { Store, FilterTypes } from '../store';
|
import { Store, FilterTypes } from '../store';
|
||||||
import { connect } from 'react-redux';
|
import { TodoContext } from '../TodoContext';
|
||||||
|
|
||||||
interface TodoListProps {
|
export const TodoList = () => {
|
||||||
todos: Store['todos'];
|
const context = useContext(TodoContext);
|
||||||
filter: FilterTypes;
|
const { filter, todos } = context;
|
||||||
}
|
|
||||||
|
|
||||||
const TodoList = (props: TodoListProps) => {
|
|
||||||
const { filter, todos } = props;
|
|
||||||
const filteredTodos = Object.keys(todos).filter(id => {
|
const filteredTodos = Object.keys(todos).filter(id => {
|
||||||
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
|
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
|
||||||
});
|
});
|
||||||
@@ -23,18 +19,3 @@ const TodoList = (props: TodoListProps) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state: Store) {
|
|
||||||
return { ...state };
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: any) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const component = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(TodoList);
|
|
||||||
|
|
||||||
export { component as TodoList };
|
|
||||||
76
step2-04/demo/src/components/TodoListItem.tsx
Normal file
76
step2-04/demo/src/components/TodoListItem.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react';
|
||||||
|
import { TodoContext } from '../TodoContext';
|
||||||
|
|
||||||
|
interface TodoListItemProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TodoListItemState {
|
||||||
|
editing: boolean;
|
||||||
|
editLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TodoListItem extends React.Component<TodoListItemProps, TodoListItemState> {
|
||||||
|
constructor(props: TodoListItemProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { editing: false, editLabel: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { id } = this.props;
|
||||||
|
const { todos, complete, remove } = this.context;
|
||||||
|
|
||||||
|
const item = todos[id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack horizontal verticalAlign="center" horizontalAlign="space-between">
|
||||||
|
{!this.state.editing && (
|
||||||
|
<>
|
||||||
|
<Checkbox label={item.label} checked={item.completed} onChange={() => complete(id)} />
|
||||||
|
<div>
|
||||||
|
<IconButton iconProps={{ iconName: 'Edit' }} onClick={this.onEdit} />
|
||||||
|
<IconButton iconProps={{ iconName: 'Cancel' }} onClick={() => remove(id)} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{this.state.editing && (
|
||||||
|
<Stack.Item grow>
|
||||||
|
<Stack horizontal gap={10}>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<TextField value={this.state.editLabel} onChange={this.onChange} />
|
||||||
|
</Stack.Item>
|
||||||
|
<DefaultButton onClick={this.onDoneEdit}>Save</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack.Item>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEdit = () => {
|
||||||
|
const { id } = this.props;
|
||||||
|
const { todos } = this.context;
|
||||||
|
const { label } = todos[id];
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
editing: true,
|
||||||
|
editLabel: this.state.editLabel || label
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDoneEdit = () => {
|
||||||
|
this.context.edit(this.props.id, this.state.editLabel);
|
||||||
|
this.setState({
|
||||||
|
editing: false,
|
||||||
|
editLabel: undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onChange = (evt: React.FormEvent<HTMLInputElement>, newValue: string) => {
|
||||||
|
this.setState({ editLabel: newValue });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoListItem.contextType = TodoContext;
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { mount } from 'enzyme';
|
|
||||||
|
|
||||||
describe('index', () => {
|
|
||||||
it('placeholder', () => {
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { multiply } from './multiply';
|
|
||||||
|
|
||||||
let counter = 0;
|
|
||||||
|
|
||||||
export function getCount() {
|
|
||||||
return counter;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function increment() {
|
|
||||||
return ++counter;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decrement() {
|
|
||||||
return --counter;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function square(x: number) {
|
|
||||||
return multiply(x, x);
|
|
||||||
}
|
|
||||||
10
step2-04/demo/src/index.tsx
Normal file
10
step2-04/demo/src/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { TodoApp } from './components/TodoApp';
|
||||||
|
import { initializeIcons } from '@uifabric/icons';
|
||||||
|
|
||||||
|
// Initializes the UI Fabric icons that we can use
|
||||||
|
// Choose one from this list: https://developer.microsoft.com/en-us/fabric#/styles/icons
|
||||||
|
initializeIcons();
|
||||||
|
|
||||||
|
ReactDOM.render(<TodoApp />, document.getElementById('app'));
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export function multiply(x: number, y: number) {
|
|
||||||
return x * y;
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,27 @@
|
|||||||
# Step 2.4: Testing TypeScript code with Jest (Exercise)
|
# Step 2.4 - React Context (Exercise)
|
||||||
|
|
||||||
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
|
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
|
||||||
|
|
||||||
Start the test runner by running `npm test` in the root of the `frontend-bootcamp` folder.
|
If you don't already have the app running, start it by running `npm start` from the root of the `frontend-bootcamp` folder. Click the "exercise" link under day 2 step 4 to see results.
|
||||||
|
|
||||||
## Basic testing
|
## TodoContext.Provider Component
|
||||||
|
|
||||||
1. Look at `exercise/src/stack.ts` for a sample implementation of a stack
|
1. Open `exercise/src/components/TodoApp.tsx`
|
||||||
|
|
||||||
2. Follow the instructions inside `stack.spec.ts` file to complete the two tests
|
2. Uncomment the missing functions inside the value prop
|
||||||
|
|
||||||
## Enzyme Testing
|
## TodoFooter, Context inside Functional Component
|
||||||
|
|
||||||
1. Open up `exercise/src/TestMe.spec.tsx`
|
1. Open `exercise/src/components/TodoFooter.tsx`
|
||||||
|
|
||||||
2. Fill in the test using Enzyme concepts introduced in the demo
|
2. Replace the two constants by using useContext(TodoContext)
|
||||||
|
|
||||||
|
## TodoHeader, Context inside Class Component
|
||||||
|
|
||||||
|
1. Open `exercise/src/components/TodoHeader.tsx`
|
||||||
|
|
||||||
|
2. Replace the `onAdd` with a real implementation using the `this.context` object
|
||||||
|
|
||||||
|
3. Replace the `onFilter` with a real implementation using the `this.context` object
|
||||||
|
|
||||||
|
4. Be sure to set the `contextType` of the TodoHeader component
|
||||||
|
|||||||
@@ -5,11 +5,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="ms-Fabric">
|
<body class="ms-Fabric">
|
||||||
<div id="markdownReadme" class="exercise" data-src="./README.md"></div>
|
<div id="markdownReadme" class="exercise" data-src="./README.md"></div>
|
||||||
<div id="app">
|
<div id="app"></div>
|
||||||
For this step, we look at unit testing. Run
|
|
||||||
<pre>npm test</pre>
|
|
||||||
in the command line.
|
|
||||||
</div>
|
|
||||||
<script src="../../assets/scripts.js"></script>
|
<script src="../../assets/scripts.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { mount } from 'enzyme';
|
|
||||||
import { TestMe } from './TestMe';
|
|
||||||
|
|
||||||
describe('TestMe Component', () => {
|
|
||||||
it('should render correctly when hovered', () => {
|
|
||||||
// TODO:
|
|
||||||
// 1. mount a <TestMe> Component here
|
|
||||||
// 2. use enzyme wrapper's find() method to retrieve the #innerMe element
|
|
||||||
// 3. simulate a hover with "mouseover" event via the simulate() API
|
|
||||||
// 4. make assertions with expect on the text() of the #innerMe element
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export interface TestMeProps {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestMeState {
|
|
||||||
enabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TestMe = (props: TestMeProps) => {
|
|
||||||
return (
|
|
||||||
<div id="testMe">
|
|
||||||
<InnerMe name={props.name} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export class InnerMe extends React.Component<TestMeProps, TestMeState> {
|
|
||||||
state = {
|
|
||||||
enabled: false
|
|
||||||
};
|
|
||||||
|
|
||||||
onMouseOver = () => {
|
|
||||||
this.setState({ enabled: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return !this.state.enabled ? (
|
|
||||||
<div onMouseOver={this.onMouseOver} id="innerMe">
|
|
||||||
Hello {this.props.name}, Hover Over Me
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div id="innerMe">Enabled</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
step2-04/exercise/src/TodoContext.ts
Normal file
4
step2-04/exercise/src/TodoContext.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// The typing forces us to put something inside createContext(); start with undefined
|
||||||
|
export const TodoContext = React.createContext(undefined);
|
||||||
100
step2-04/exercise/src/components/TodoApp.tsx
Normal file
100
step2-04/exercise/src/components/TodoApp.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack } from 'office-ui-fabric-react';
|
||||||
|
import { TodoFooter } from './TodoFooter';
|
||||||
|
import { TodoHeader } from './TodoHeader';
|
||||||
|
import { TodoList } from './TodoList';
|
||||||
|
import { Store } from '../store';
|
||||||
|
import { TodoContext } from '../TodoContext';
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
export class TodoApp extends React.Component<any, Store> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
todos: {},
|
||||||
|
filter: 'all'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<TodoContext.Provider
|
||||||
|
value={{
|
||||||
|
...this.state
|
||||||
|
// TODO: put the missing functions into the context value
|
||||||
|
// addTodo: this._addTodo,
|
||||||
|
// remove: this._remove,
|
||||||
|
// complete: this._complete,
|
||||||
|
// clear: this._clear,
|
||||||
|
// setFilter: this._setFilter,
|
||||||
|
// edit: this._edit
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack horizontalAlign="center">
|
||||||
|
<Stack style={{ width: 400 }} gap={25}>
|
||||||
|
<TodoHeader />
|
||||||
|
<TodoList />
|
||||||
|
<TodoFooter />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</TodoContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addTodo = label => {
|
||||||
|
const { todos } = this.state;
|
||||||
|
const id = index++;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
todos: { ...todos, [id]: { label } }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private _remove = id => {
|
||||||
|
const newTodos = { ...this.state.todos };
|
||||||
|
delete newTodos[id];
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
todos: newTodos
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private _complete = id => {
|
||||||
|
const newTodos = { ...this.state.todos };
|
||||||
|
newTodos[id].completed = !newTodos[id].completed;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
todos: newTodos
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private _edit = (id, label) => {
|
||||||
|
const newTodos = { ...this.state.todos };
|
||||||
|
newTodos[id] = { ...newTodos[id], label };
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
todos: newTodos
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private _clear = () => {
|
||||||
|
const { todos } = this.state;
|
||||||
|
const newTodos = {};
|
||||||
|
|
||||||
|
Object.keys(this.state.todos).forEach(id => {
|
||||||
|
if (!todos[id].completed) {
|
||||||
|
newTodos[id] = todos[id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
todos: newTodos
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private _setFilter = filter => {
|
||||||
|
this.setState({
|
||||||
|
filter: filter
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
21
step2-04/exercise/src/components/TodoFooter.tsx
Normal file
21
step2-04/exercise/src/components/TodoFooter.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { DefaultButton, Stack, Text } from 'office-ui-fabric-react';
|
||||||
|
import { TodoContext } from '../TodoContext';
|
||||||
|
|
||||||
|
export const TodoFooter = () => {
|
||||||
|
// TODO: replace the following with a useContext(TodoContext) calls
|
||||||
|
const todos = {};
|
||||||
|
const clear = () => {};
|
||||||
|
// - end of exercise for this file -
|
||||||
|
|
||||||
|
const itemCount = Object.keys(todos).filter(id => !todos[id].completed).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack horizontal horizontalAlign="space-between">
|
||||||
|
<Text>
|
||||||
|
{itemCount} item{itemCount === 1 ? '' : 's'} left
|
||||||
|
</Text>
|
||||||
|
<DefaultButton onClick={() => clear()}>Clear Completed</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
66
step2-04/exercise/src/components/TodoHeader.tsx
Normal file
66
step2-04/exercise/src/components/TodoHeader.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack, Text, Pivot, PivotItem, TextField, PrimaryButton } from 'office-ui-fabric-react';
|
||||||
|
import { FilterTypes } from '../store';
|
||||||
|
import { TodoContext } from '../TodoContext';
|
||||||
|
|
||||||
|
interface TodoHeaderState {
|
||||||
|
labelInput: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TodoHeader extends React.Component<{}, TodoHeaderState> {
|
||||||
|
constructor(props: {}) {
|
||||||
|
super(props);
|
||||||
|
this.state = { labelInput: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Stack gap={10}>
|
||||||
|
<Stack horizontal horizontalAlign="center">
|
||||||
|
<Text variant="xxLarge">todos</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack horizontal gap={10}>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<TextField
|
||||||
|
placeholder="What needs to be done?"
|
||||||
|
value={this.state.labelInput}
|
||||||
|
onChange={this.onChange}
|
||||||
|
styles={props => ({
|
||||||
|
...(props.focused && {
|
||||||
|
field: {
|
||||||
|
backgroundColor: '#c7e0f4'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
<PrimaryButton onClick={this.onAdd}>Add</PrimaryButton>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Pivot onLinkClick={this.onFilter}>
|
||||||
|
<PivotItem headerText="all" />
|
||||||
|
<PivotItem headerText="active" />
|
||||||
|
<PivotItem headerText="completed" />
|
||||||
|
</Pivot>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAdd = () => {
|
||||||
|
// TODO: insert a this.context.addTodo call
|
||||||
|
// HINT: this.context.addTodo(this.state.labelInput);
|
||||||
|
this.setState({ labelInput: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onChange = (evt: React.FormEvent<HTMLInputElement>, newValue: string) => {
|
||||||
|
this.setState({ labelInput: newValue });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onFilter = (item: PivotItem) => {
|
||||||
|
// TODO: insert a this.context.setFilter call
|
||||||
|
// HINT: this.context.setFilter(item.props.headerText as FilterTypes);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: TodoHeader.contextType = TodoContext;
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { Stack } from 'office-ui-fabric-react';
|
import { Stack } from 'office-ui-fabric-react';
|
||||||
import { TodoListItem } from './TodoListItem';
|
import { TodoListItem } from './TodoListItem';
|
||||||
import { Store, FilterTypes } from '../store';
|
import { Store, FilterTypes } from '../store';
|
||||||
import { connect } from 'react-redux';
|
import { TodoContext } from '../TodoContext';
|
||||||
|
|
||||||
interface TodoListProps {
|
export const TodoList = () => {
|
||||||
todos: Store['todos'];
|
const context = useContext(TodoContext);
|
||||||
filter: FilterTypes;
|
const { filter, todos } = context;
|
||||||
}
|
|
||||||
|
|
||||||
const TodoList = (props: TodoListProps) => {
|
|
||||||
const { filter, todos } = props;
|
|
||||||
const filteredTodos = Object.keys(todos).filter(id => {
|
const filteredTodos = Object.keys(todos).filter(id => {
|
||||||
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
|
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
|
||||||
});
|
});
|
||||||
@@ -23,18 +19,3 @@ const TodoList = (props: TodoListProps) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state: Store) {
|
|
||||||
return { ...state };
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: any) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const component = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(TodoList);
|
|
||||||
|
|
||||||
export { component as TodoList };
|
|
||||||
76
step2-04/exercise/src/components/TodoListItem.tsx
Normal file
76
step2-04/exercise/src/components/TodoListItem.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react';
|
||||||
|
import { TodoContext } from '../TodoContext';
|
||||||
|
|
||||||
|
interface TodoListItemProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TodoListItemState {
|
||||||
|
editing: boolean;
|
||||||
|
editLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TodoListItem extends React.Component<TodoListItemProps, TodoListItemState> {
|
||||||
|
constructor(props: TodoListItemProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { editing: false, editLabel: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { id } = this.props;
|
||||||
|
const { todos, complete, remove } = this.context;
|
||||||
|
|
||||||
|
const item = todos[id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack horizontal verticalAlign="center" horizontalAlign="space-between">
|
||||||
|
{!this.state.editing && (
|
||||||
|
<>
|
||||||
|
<Checkbox label={item.label} checked={item.completed} onChange={() => complete(id)} />
|
||||||
|
<div>
|
||||||
|
<IconButton iconProps={{ iconName: 'Edit' }} onClick={this.onEdit} />
|
||||||
|
<IconButton iconProps={{ iconName: 'Cancel' }} onClick={() => remove(id)} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{this.state.editing && (
|
||||||
|
<Stack.Item grow>
|
||||||
|
<Stack horizontal gap={10}>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<TextField value={this.state.editLabel} onChange={this.onChange} />
|
||||||
|
</Stack.Item>
|
||||||
|
<DefaultButton onClick={this.onDoneEdit}>Save</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack.Item>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEdit = () => {
|
||||||
|
const { id } = this.props;
|
||||||
|
const { todos } = this.context;
|
||||||
|
const { label } = todos[id];
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
editing: true,
|
||||||
|
editLabel: this.state.editLabel || label
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDoneEdit = () => {
|
||||||
|
this.context.edit(this.props.id, this.state.editLabel);
|
||||||
|
this.setState({
|
||||||
|
editing: false,
|
||||||
|
editLabel: undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onChange = (evt: React.FormEvent<HTMLInputElement>, newValue: string) => {
|
||||||
|
this.setState({ editLabel: newValue });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoListItem.contextType = TodoContext;
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { Stack } from './stack';
|
|
||||||
export { TestMe } from './TestMe';
|
|
||||||
10
step2-04/exercise/src/index.tsx
Normal file
10
step2-04/exercise/src/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { TodoApp } from './components/TodoApp';
|
||||||
|
import { initializeIcons } from '@uifabric/icons';
|
||||||
|
|
||||||
|
// Initializes the UI Fabric icons that we can use
|
||||||
|
// Choose one from this list: https://developer.microsoft.com/en-us/fabric#/styles/icons
|
||||||
|
initializeIcons();
|
||||||
|
|
||||||
|
ReactDOM.render(<TodoApp />, document.getElementById('app'));
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
// TODO: Import the stack here
|
|
||||||
|
|
||||||
describe('Stack', () => {
|
|
||||||
it('should push item to the top of the stack', () => {
|
|
||||||
// TODO: implement test here:
|
|
||||||
// 1. Instantiate a new Stack - i.e. const stack = new Stack<number>();
|
|
||||||
// 2. Use stack push calls to add some items to the stack
|
|
||||||
// 3. Write assertions via the expect() API
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pop the item from the top of stack', () => {
|
|
||||||
// TODO: implement test here:
|
|
||||||
// 1. Instantiate a new Stack - i.e. const stack = new Stack<number>();
|
|
||||||
// 2. Use stack push calls to add some items to the stack
|
|
||||||
// 3. pop a few items off the stack
|
|
||||||
// 4. write assertions via the expect() API
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
export class Stack<T> {
|
|
||||||
private _items: T[] = [];
|
|
||||||
|
|
||||||
/** Add an item to the top of the stack. */
|
|
||||||
push(item: T) {
|
|
||||||
this._items.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Remove the top item from the stack and return it. */
|
|
||||||
pop(): T {
|
|
||||||
if (this._items.length > 0) {
|
|
||||||
return this._items.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return the top item from the stack without removing it. */
|
|
||||||
peek(): T {
|
|
||||||
if (this._items.length > 0) {
|
|
||||||
return this._items[this._items.length - 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the number of items in the stack/ */
|
|
||||||
get count(): number {
|
|
||||||
return this._items.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +1,124 @@
|
|||||||
# Step 2.5 - Redux: Reducers (Demo)
|
# Step 2.5 - Redux: The Store (Demo)
|
||||||
|
|
||||||
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
|
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
|
||||||
|
|
||||||
## Flux
|
In this step, we will look at solving the problems of complex application (as mentioned in Step 4) with a library called Redux.
|
||||||
|
|
||||||
Ideally React gives us a mental model of:
|
1. Introduction to Redux
|
||||||
|
2. Why Use Redux?
|
||||||
|
3. Creating the Redux store
|
||||||
|
4. Writing reducers
|
||||||
|
5. Dispatching actions
|
||||||
|
|
||||||
```
|
---
|
||||||
f(data) => view
|
|
||||||
```
|
|
||||||
|
|
||||||
We can pass shared state data down to components using props and update it using callbacks, but as an application grows larger, passing around callbacks becomes unwieldy and the state data flow becomes hard to understand. To keep the application maintainable, we need a well-defined pattern for sharing and updating application state.
|
## Introduction to Redux
|
||||||
|
|
||||||
Facebook invented the [Flux](https://facebook.github.io/flux/) architectural pattern to solve this shared state issue. [Redux](https://redux.js.org/) is an implementation of Flux.
|
As a reminder, the problem that we want to address are:
|
||||||
|
|
||||||
Redux is used inside many large, complex applications because of its clarity and predictability. It is easy to debug and easily extensible via its middleware architecture. In this lesson, we'll explore the heart of how Redux manages state.
|
1. Data needs to be passed down from component to component via props. Even when some components do not need to know about some data.
|
||||||
|
2. There is a lack of coordination of changes that can happen to the data
|
||||||
|
|
||||||
Redux expects the data (store) to be a singleton state tree. It listens for messages to manipulate the state and passes updates down to views.
|
Redux is an implementation of the Flux architectural pattern:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### View
|
### View
|
||||||
|
|
||||||
A view is a React component that consumes the store as its data. There is a special way Redux maps data from the state tree into the different React components. The components will know to re-render when these bits of state are changed.
|
A view is a React component that consumes the store as its data.
|
||||||
|
|
||||||
### Action
|
### Action
|
||||||
|
|
||||||
[Actions](https://redux.js.org/basics/actions) are messages that represent some event, such as a user's action or a network request. With the aid of *reducers*, they affect the overall state.
|
[Actions](https://redux.js.org/basics/actions) are serializable JSON messages that represent some event, such as a user's action or a network request. With the aid of _reducers_, they affect the overall state. At the minimum, it should contain a `type` key. Sometimes it contains additional data as a _payload_.
|
||||||
|
|
||||||
### Store
|
### Store
|
||||||
|
|
||||||
The [store](https://redux.js.org/basics/store) contains a singleton state tree. The state tree is immutable and needs to be re-created at every action. This helps connected views easily to know when to update by allowing them to do a simple reference comparison rather than a deep comparison. You can think of each state as a snapshot of the app at that point in time.
|
The [store](https://redux.js.org/basics/store) consists of a **state tree**, a **dispatcher**, and **reducers**.
|
||||||
|
|
||||||
### Dispatcher
|
1. The **state tree** is a _singleton_, _serializable_, _immutable_ json data. It is updated from one snapshot to another through `reducers`.
|
||||||
|
|
||||||
There is a single [dispatcher](https://redux.js.org/basics/data-flow), provided by the store. It simply informs the store of all the actions that need to be performed. Additional middleware (explained later) can be applied to the store, and the dispatcher's job is to dispatch the message through all the middleware layers.
|
2. The **dispatcher** accepts actions passing them to the reducers.
|
||||||
|
|
||||||
### Reducers
|
3. **Reducers** are functions that take in the current state tree and an action, producing the next snapshot of the state tree.
|
||||||
|
|
||||||
Redux uses [reducers](https://redux.js.org/basics/reducers) to manage state changes. This name comes from the "reducer" function passed to `Array.reduce()`.
|
## Why Use Redux?
|
||||||
|
|
||||||
A Redux reducer is a simple **pure function** (no side effects). Its only job is to transform the state from one immutable snapshot to another. It takes state + an action message as input, makes a modified copy of the state based on the action message type and payload, and returns the new state. (Reducers [should not modify](https://redux.js.org/introduction/three-principles#state-is-read-only) the previous state.)
|
There are lots of alternatives available, but here are some really good reasons to go with Redux:
|
||||||
|
|
||||||
**Mental Model**: Think of a reducer as part of the store. It should have no side effects and only define how data changes from one state to the next given action messages.
|
1. For more complex applications, Flux pattern forces code to be written in a way that is easy to reason about
|
||||||
|
2. There maybe a need to serialize the application state to be transmitted across the wire somehow
|
||||||
|
3. Dev tooling is really amazing
|
||||||
|
4. Popularity of the framework means the ecosystem is mature at this point
|
||||||
|
|
||||||
### Advanced: Middleware
|
# Creating the Redux store
|
||||||
|
|
||||||
From the [documentation site](https://redux.js.org/advanced/middleware):
|
|
||||||
|
|
||||||
> Redux middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.
|
|
||||||
|
|
||||||
We won't be covering middleware much in these lessons since it's a more advanced topic.
|
|
||||||
|
|
||||||
## Getting started with Redux
|
|
||||||
|
|
||||||
We begin the journey into Redux by looking at the store. The store consists of several parts:
|
|
||||||
|
|
||||||
1. **State/data** - We represent this both with an initial state and with a TypeScript interface.
|
|
||||||
2. **Reducers** - Responsible for reacting to action messages to change from one state to the next.
|
|
||||||
3. **Dispatcher** - There should be only one dispatcher, which is exported by the store. We'll look at this in Step 6!
|
|
||||||
|
|
||||||
### Create store
|
|
||||||
|
|
||||||
The [`createStore()`](https://redux.js.org/api/createstore) function is provided by Redux and can take in several arguments. The simplest form just takes in reducers.
|
The [`createStore()`](https://redux.js.org/api/createstore) function is provided by Redux and can take in several arguments. The simplest form just takes in reducers.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const store = createStore(reducer);
|
const store = createStore(reducer, initialState);
|
||||||
```
|
```
|
||||||
|
|
||||||
`createStore()` can also take in an initial state. There are two common sources for the initial state:
|
`createStore()` creates a store with a reducer, and some initial state.
|
||||||
|
|
||||||
1. Load initial data from a server
|
# Writing Reducers
|
||||||
2. Load data that is generated by a server-side rendering environment
|
|
||||||
|
We will write our reducers with the help of some utilities from `redux-starter-kit`. Here is how we will write our reducers:
|
||||||
|
|
||||||
|
## 1. Organize reducers according to the keys of the state tree object:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const store = createStore(reducer, {
|
import { createReducer } from 'redux-starter-kit';
|
||||||
/* the initial state */
|
|
||||||
|
const todosReducer = createReducer({}, {
|
||||||
|
// first argument is the initial state
|
||||||
|
// second argument is an object where the keys corresponds to the "action.type"
|
||||||
|
addTodo: (state, action) => ...
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filterReducer = createReducer('all', {
|
||||||
|
setFilter: (state, action) => ...
|
||||||
|
});
|
||||||
|
|
||||||
|
const reducer = combineReducer({
|
||||||
|
todos: todosReducer,
|
||||||
|
filter: filterReducer
|
||||||
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
`createStore()` can take a third argument that injects middleware, but we won't use this until later.
|
## 2. Write the reducers with mutables.
|
||||||
|
|
||||||
### Reducers
|
`createReducer()` will automatically translate all the mutations to the state into immutable snapshots (!!!!!):
|
||||||
|
|
||||||
Remember that the [reducers are **pure**](https://redux.js.org/introduction/three-principles#changes-are-made-with-pure-functions). Pure functions have no side effects. They always return the same output given the same input (idempotent). They are easily testable.
|
|
||||||
|
|
||||||
Reducers look at the action's message to decide what to do to the state. A convention established in the Flux community is that the action message (payload) should include a `type` key. Another convention is using switch statements against the `type` key to trigger further reducer functions.
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
function reducer(state: Store['todos'], payload: any): Store['todos'] {
|
const todosReducer = createReducer(
|
||||||
switch (payload.type) {
|
{},
|
||||||
case 'addTodo':
|
{
|
||||||
return addTodo(state, payload.id, payload.label);
|
// first argument is the initial state
|
||||||
|
// second argument is an object where the keys corresponds to the "action.type"
|
||||||
|
addTodo: (state, action) => {
|
||||||
|
state[action.id] = { label: action.label, completed: false };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
return state;
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In the demo and exercises for this step, I separated the pure and reducer functions into different files to make it cleaner. The tests inside `pureFunctions.spec.ts` should describe the behavior of the individual functions. They are easy to follow and easy to write.
|
# Dispatching Actions
|
||||||
|
|
||||||
|
Dispatching action will pass the action and the current state to the _reducers_. The root _reducer_ will produce a new snapshot for the entire state tree. We can inspect the affected snapshot with the help of `getState()`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const store = createStore(reducer, initialState);
|
||||||
|
store.dispatch({ type: 'addTodo', label: 'hello' });
|
||||||
|
store.dispatch({ type: 'addTodo', label: 'world' });
|
||||||
|
console.log(store.getState());
|
||||||
|
```
|
||||||
|
|
||||||
|
Creating these action messages by hand is tedious, so we use action creators to do that:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const actions = {
|
||||||
|
addTodo = (label: string) => ({ label, id: nextId(), completed: false })
|
||||||
|
};
|
||||||
|
|
||||||
|
store.dispatch(actions.addTodo('hello'));
|
||||||
|
```
|
||||||
|
|||||||
@@ -6,10 +6,18 @@
|
|||||||
<body class="ms-Fabric">
|
<body class="ms-Fabric">
|
||||||
<div id="markdownReadme" data-src="./README.md"></div>
|
<div id="markdownReadme" data-src="./README.md"></div>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
For this step, we look at unit testing. Run
|
<p>
|
||||||
<pre>npm test</pre>
|
Nothing to show here; just look at your console window for output. Hit F12 (<code>cmd+option+I</code> on Mac) to open console
|
||||||
in the command line.
|
window.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
To inspect Redux store, use the <a href="http://extension.remotedev.io/">Redux Dev Tool</a> extension for your browser:
|
||||||
|
<a href="https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd">Chrome</a>,
|
||||||
|
<a href="https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/">FireFox</a>. (Sorry, no Edge or IE support)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="../../assets/scripts.js"></script>
|
<script src="../../assets/scripts.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import uuid from 'uuid/v4';
|
import nextId from 'uuid/v4';
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
addTodo: (label: string) => ({ type: 'addTodo', id: uuid(), label }),
|
addTodo: (label: string) => ({ type: 'addTodo', id: nextId(), label }),
|
||||||
remove: (id: string) => ({ type: 'remove', id }),
|
remove: (id: string) => ({ type: 'remove', id }),
|
||||||
complete: (id: string) => ({ type: 'complete', id }),
|
complete: (id: string) => ({ type: 'complete', id }),
|
||||||
clear: () => ({ type: 'clear' }),
|
clear: () => ({ type: 'clear' }),
|
||||||
@@ -1,6 +1,15 @@
|
|||||||
import { reducer } from './reducers';
|
import { reducer } from './reducers';
|
||||||
import { createStore } from 'redux';
|
import { createStore } from 'redux';
|
||||||
|
import { actions } from './actions';
|
||||||
|
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||||
|
|
||||||
const store = createStore(reducer);
|
const store = createStore(reducer, {}, composeWithDevTools());
|
||||||
|
|
||||||
|
store.dispatch(actions.addTodo('hello'));
|
||||||
|
|
||||||
|
let action = actions.addTodo('world');
|
||||||
|
store.dispatch(action);
|
||||||
|
|
||||||
|
store.dispatch(actions.remove(action.id));
|
||||||
|
|
||||||
console.log(store.getState());
|
console.log(store.getState());
|
||||||
|
|||||||
@@ -1,20 +1,43 @@
|
|||||||
import { Store } from '../store';
|
import { Store } from '../store';
|
||||||
import { addTodo, remove, complete, clear } from './pureFunctions';
|
import { combineReducers } from 'redux';
|
||||||
|
import { createReducer } from 'redux-starter-kit';
|
||||||
|
|
||||||
export function reducer(state: Store['todos'], payload: any): Store['todos'] {
|
export const todosReducer = createReducer<Store['todos']>(
|
||||||
switch (payload.type) {
|
{},
|
||||||
case 'addTodo':
|
{
|
||||||
return addTodo(state, payload.id, payload.label);
|
addTodo(state, action) {
|
||||||
|
state[action.id] = { label: action.label, completed: false };
|
||||||
|
},
|
||||||
|
|
||||||
case 'remove':
|
remove(state, action) {
|
||||||
return remove(state, payload.id);
|
delete state[action.id];
|
||||||
|
},
|
||||||
|
|
||||||
case 'complete':
|
clear(state, action) {
|
||||||
return complete(state, payload.id);
|
Object.keys(state).forEach(key => {
|
||||||
|
if (state[key].completed) {
|
||||||
|
delete state[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
case 'clear':
|
complete(state, action) {
|
||||||
return clear(state);
|
state[action.id].completed = !state[action.id].completed;
|
||||||
|
},
|
||||||
|
|
||||||
|
edit(state, action) {
|
||||||
|
state[action.id].label = action.label;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return state;
|
export const filterReducer = createReducer<Store['filter']>('all', {
|
||||||
}
|
setFilter(state, action) {
|
||||||
|
return action.filter;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const reducer = combineReducers({
|
||||||
|
todos: todosReducer,
|
||||||
|
filter: filterReducer
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { addTodo } from './pureFunctions';
|
|
||||||
import { Store } from '../store';
|
|
||||||
|
|
||||||
describe('TodoApp reducers', () => {
|
|
||||||
it('can add an item', () => {
|
|
||||||
const state = <Store['todos']>{};
|
|
||||||
|
|
||||||
const newState = addTodo(state, '0', 'item1');
|
|
||||||
|
|
||||||
const keys = Object.keys(newState);
|
|
||||||
|
|
||||||
expect(newState).not.toBe(state);
|
|
||||||
expect(keys.length).toBe(1);
|
|
||||||
expect(newState[keys[0]].label).toBe('item1');
|
|
||||||
expect(newState[keys[0]].completed).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Store } from '../store';
|
|
||||||
|
|
||||||
export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] {
|
|
||||||
return { ...state, [id]: { label, completed: false } };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function remove(state: Store['todos'], id: string) {
|
|
||||||
// Clone the Todos
|
|
||||||
const newTodos = { ...state };
|
|
||||||
|
|
||||||
// Delete an item in the object by the key
|
|
||||||
delete newTodos[id];
|
|
||||||
|
|
||||||
return newTodos;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function complete(state: Store['todos'], id: string) {
|
|
||||||
// Clone the todo, overriding
|
|
||||||
const newTodo = { ...state[id], completed: !state[id].completed };
|
|
||||||
return { ...state, [id]: newTodo };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clear(state: Store['todos']) {
|
|
||||||
// Clone the todos
|
|
||||||
const newTodos = { ...state };
|
|
||||||
|
|
||||||
// Delete all todos based on the completed flag, looping over the keys of the todos
|
|
||||||
Object.keys(state).forEach(key => {
|
|
||||||
if (state[key].completed) {
|
|
||||||
delete newTodos[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return newTodos;
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,16 @@
|
|||||||
|
|
||||||
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
|
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
|
||||||
|
|
||||||
If you still have the app running from a previous step, stop it with `ctrl+c`. Start the tests instead by running `npm test` from the root of the `frontend-bootcamp` folder.
|
If you don't already have the app running, start it by running `npm start` from the root of the `frontend-bootcamp` folder. Click the "exercise" link under day 2 step 5 to see results.
|
||||||
|
|
||||||
1. First, take a look at the store interface in `exercise/src/store/index.ts`. Note that the `Store` interface has two keys: `todos` and `filter`. We'll concentrate on `todos`, which is an object where the keys are string IDs and the values are of a `TodoItem` type.
|
1. First, take a look at the store interface in `exercise/src/store/index.ts`. Note that the `Store` interface has two keys: `todos` and `filter`. We'll concentrate on `todos`, which is an object where the keys are string IDs and the values are of a `TodoItem` type.
|
||||||
|
|
||||||
2. Open `exercise/src/reducers/pureFunctions.ts` and fill in the missing function bodies.
|
2. Open `exercise/src/reducers/index.ts` and fill in the missing case statements for the switch on `action.type`.
|
||||||
|
|
||||||
3. Open `exercise/src/reducers/index.ts` and fill in the missing case statements for the switch on `action.type`.
|
3. Open `exercise/src/index.tsx` and write separate dispatch calls.
|
||||||
|
|
||||||
4. Open `exercise/src/reducers/pureFunctions.spec.ts` and implement tests for the functions you wrote for `remove`, `complete`, and `clear`.
|
4. Take a look what is written in the console (F12 on PC, cmd-option-I on Mac).
|
||||||
|
|
||||||
|
5. Install the [Chrome](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) or [Firefox](https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/) extensions
|
||||||
|
|
||||||
|
6. Observe the state changes, try doing "time travel"
|
||||||
|
|||||||
@@ -5,11 +5,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="ms-Fabric">
|
<body class="ms-Fabric">
|
||||||
<div id="markdownReadme" class="exercise" data-src="./README.md"></div>
|
<div id="markdownReadme" class="exercise" data-src="./README.md"></div>
|
||||||
<div id="app">
|
<div id="app"></div>
|
||||||
For this step, we look at unit testing. Run
|
|
||||||
<pre>npm test</pre>
|
|
||||||
in the command line.
|
|
||||||
</div>
|
|
||||||
<script src="../../assets/scripts.js"></script>
|
<script src="../../assets/scripts.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import uuid from 'uuid/v4';
|
import nextId from 'uuid/v4';
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
addTodo: (label: string) => ({ type: 'addTodo', id: uuid(), label }),
|
addTodo: (label: string) => ({ type: 'addTodo', id: nextId(), label }),
|
||||||
remove: (id: string) => ({ type: 'remove', id }),
|
remove: (id: string) => ({ type: 'remove', id }),
|
||||||
complete: (id: string) => ({ type: 'complete', id }),
|
complete: (id: string) => ({ type: 'complete', id }),
|
||||||
clear: () => ({ type: 'clear' }),
|
clear: () => ({ type: 'clear' }),
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import { reducer } from './reducers';
|
import { reducer } from './reducers';
|
||||||
import { createStore } from 'redux';
|
import { createStore } from 'redux';
|
||||||
|
import { actions } from './actions';
|
||||||
|
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||||
|
|
||||||
const store = createStore(reducer);
|
const store = createStore(reducer, {}, composeWithDevTools());
|
||||||
|
|
||||||
|
// TODO: try doing some store.dispatch() calls here
|
||||||
|
// HINT: remember to use the functions inside "actions" object
|
||||||
|
|
||||||
console.log(store.getState());
|
console.log(store.getState());
|
||||||
|
|||||||
@@ -1,16 +1,43 @@
|
|||||||
import { Store } from '../store';
|
import { Store } from '../store';
|
||||||
import { addTodo, remove, complete } from './pureFunctions';
|
import { combineReducers } from 'redux';
|
||||||
|
import { createReducer } from 'redux-starter-kit';
|
||||||
|
|
||||||
export function reducer(state: Store['todos'], payload: any): Store['todos'] {
|
export const todosReducer = createReducer<Store['todos']>(
|
||||||
switch (payload.type) {
|
{},
|
||||||
case 'addTodo':
|
{
|
||||||
return addTodo(state, payload.id, payload.label);
|
addTodo(state, action) {
|
||||||
|
// TODO: implement this reducer
|
||||||
|
},
|
||||||
|
|
||||||
// Fill in the blanks here for:
|
remove(state, action) {
|
||||||
// - remove
|
delete state[action.id];
|
||||||
// - complete
|
},
|
||||||
// - clear
|
|
||||||
|
clear(state, action) {
|
||||||
|
Object.keys(state).forEach(key => {
|
||||||
|
if (state[key].completed) {
|
||||||
|
delete state[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
complete(state, action) {
|
||||||
|
// TODO: implement this reducer
|
||||||
|
},
|
||||||
|
|
||||||
|
edit(state, action) {
|
||||||
|
// TODO: implement this reducer
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return state;
|
export const filterReducer = createReducer<Store['filter']>('all', {
|
||||||
}
|
setFilter(state, action) {
|
||||||
|
return action.filter;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const reducer = combineReducers({
|
||||||
|
todos: todosReducer,
|
||||||
|
filter: filterReducer
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { addTodo } from './pureFunctions';
|
|
||||||
import { Store } from '../store';
|
|
||||||
|
|
||||||
describe('TodoApp reducers', () => {
|
|
||||||
it('can add an item', () => {
|
|
||||||
const state = <Store['todos']>{};
|
|
||||||
|
|
||||||
const newState = addTodo(state, '0', 'item1');
|
|
||||||
|
|
||||||
const keys = Object.keys(newState);
|
|
||||||
|
|
||||||
// make sure that adding an item would not result in the same instance of state
|
|
||||||
// TODO: uncomment the below to get started
|
|
||||||
/*
|
|
||||||
|
|
||||||
expect(newState).not.toBe(state);
|
|
||||||
|
|
||||||
expect(keys.length).toBe(1);
|
|
||||||
expect(newState[keys[0]].label).toBe('item1');
|
|
||||||
expect(newState[keys[0]].completed).toBeFalsy();
|
|
||||||
*/
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: add a test for remove()
|
|
||||||
});
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { Store, FilterTypes } from '../store';
|
|
||||||
|
|
||||||
export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] {
|
|
||||||
// Write code to clone the state object while inserting a new TodoItem inside
|
|
||||||
// - the new object must be of the type TodoItem
|
|
||||||
// - the new state should be cloned using the spread syntax
|
|
||||||
// - return the new state
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function remove(state: Store['todos'], id: string) {
|
|
||||||
// Write code:
|
|
||||||
// - to clone the state object into new state object
|
|
||||||
// - remove and item from the new state by using the "delete" keyword
|
|
||||||
// - return the new state
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function complete(state: Store['todos'], id: string) {
|
|
||||||
// Clone the todo, overriding
|
|
||||||
const newTodo = { ...state[id], completed: !state[id].completed };
|
|
||||||
return { ...state, [id]: newTodo };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clear(state: Store['todos']) {
|
|
||||||
// Clone the todos
|
|
||||||
const newTodos = { ...state };
|
|
||||||
|
|
||||||
// Delete all todos based on the completed flag, looping over the keys of the todos
|
|
||||||
Object.keys(state).forEach(key => {
|
|
||||||
if (state[key].completed) {
|
|
||||||
delete newTodos[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return newTodos;
|
|
||||||
}
|
|
||||||
@@ -1,68 +1,69 @@
|
|||||||
# Step 2.6 - Redux: Dispatching actions and examining state (Demo)
|
# Step 2.6: Redux: React Binding (Demo)
|
||||||
|
|
||||||
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
|
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
|
||||||
|
|
||||||
In this step, we learn about the Redux methods `dispatch()` and `getState()`. Dispatching action messages to the store is the only means by which to instruct the reducers to modify the shared state tree.
|
Redux is currently the most popular Flux implementation, and the ecosystem of related libraries has grown as a result. This is one of the reasons why it is a very popular library within Microsoft products.
|
||||||
|
|
||||||
We also see how to compose reducers to make up the complete state shape.
|
Various GitHub users have collected "awesome lists" of tech and articles related to Redux. Here is [one such list](https://github.com/xgrommx/awesome-redux#react---a-javascript-library-for-building-user-interfaces), but it is literally impossible to list out all the related tech.
|
||||||
|
|
||||||
## Dispatch
|
In this step, we will continue with Redux. Step 2.5 code can be used with any other web frameworks. This step, we will hook up the Redux store with React components. The official way to do this is with the the [`react-redux`](https://react-redux.js.org/) library.
|
||||||
|
|
||||||
Given a store reference, you can [dispatch](https://redux.js.org/api/store#dispatch) an action to trigger the middleware and reducers. This changes the store and causes the view to re-render. (We'll look at how to pass the store and the dispatch function into the view later.)
|
We will demonstrate how to use `react-redux` to pass down the Redux store to the views:
|
||||||
|
|
||||||
```ts
|
1. Providing the Store to the Views
|
||||||
|
2. Mapping the Redux store to props
|
||||||
|
|
||||||
|
## Provide the Store Context
|
||||||
|
|
||||||
|
Class Components will access the Redux store via the `StoreContext` from `react-redux-hooks`. In Step 2.4, you saw how the context is hooked up. So, instead of providing our own context for Redux store, we just take one that is already been created. We need to first hook up the `<StoreContext.Provider>` component just like in Step 2.4.
|
||||||
|
|
||||||
|
```js
|
||||||
const store = createStore(reducers);
|
const store = createStore(reducers);
|
||||||
store.dispatch(actions.addTodo('id0', 'hello world'));
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<div>Hello World!</div>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
> Important note: Dispatches generally have a "fire and forget" approach. We expect React to re-render the UI correctly of its own accord. (Rendering isn't necessarily synchronous in React! Chaining async action creators is a topic for Step 9.)
|
## Mapping the Redux store to props
|
||||||
|
|
||||||
## Reducers scoped to a portion of the state tree
|
`react-redux` exports the [`connect()`](https://react-redux.js.org/api/connect) function that maps portions of the state tree and dispatch functions into props in the child React component. Let's look at how that is done.
|
||||||
|
|
||||||
In general, when an application grows, so does the complexity of the state tree. In a Redux application, it is best to have reducers that deal with only a sub-portion of the tree.
|
```js
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
In our example, we have two parts of our state: `todos` and `filter`. We will [split the reducer](https://redux.js.org/basics/reducers#splitting-reducers) to pass the todos to a `todosReducer()` function and just return `all` to the `filter` key for now. This organization helps in navigating and understanding the reducers because it matches the shape of the state one-to-one: there's a separate reducer for each key in state.
|
const MyComponent = props => {
|
||||||
|
return <div>
|
||||||
|
{props.prop1}
|
||||||
|
<button onClick={props.action1()}>Click Me</button>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
Compare this example which handles the whole state in one reducer...
|
const ConnectedComponent = connect(
|
||||||
|
state => {
|
||||||
```ts
|
prop1: state.key1,
|
||||||
// remember the shape of the store
|
prop2: state.key2
|
||||||
{
|
|
||||||
todos: {
|
|
||||||
id0: {...},
|
|
||||||
id1: {...},
|
|
||||||
},
|
},
|
||||||
|
dispatch => {
|
||||||
filter: 'all'
|
action1: (arg) => dispatch(actions.action1(arg)),
|
||||||
}
|
action2: (arg) => dispatch(actions.action2(arg)),
|
||||||
```
|
|
||||||
|
|
||||||
...to this one which splits it up.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
function reducers(state, action) {
|
|
||||||
return {
|
|
||||||
todos: function todoReducers(state['todos'], action) {
|
|
||||||
...
|
|
||||||
},
|
|
||||||
|
|
||||||
filter: function filterReducers(state['filter'], action) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
)(MyComponent);
|
||||||
```
|
```
|
||||||
|
|
||||||
With the second example, it is easy to understand which reducer changed a given part of the state.
|
So, that's a lot to digest. We'll go through these different parts:
|
||||||
|
|
||||||
## `getState()`
|
1. First, the `<MyComponent>` is simple component that expects to have props, without any knowledge of Redux. It is just a plain React Component.
|
||||||
|
|
||||||
To examine the state of the store, you can call `store.getState()` to get a snapshot of the current state.
|
2. The `connect()` function takes in several arguments.
|
||||||
|
|
||||||
In general, you should only include serializable things in the state so that you can easily save or transfer it. You can even save this state into a browser's local storage and restore for the next boot of your application!
|
- The first argument maps portions of the Redux _state tree_ into `<MyComponent>` _props_
|
||||||
|
- The second arguments maps dispatch functions into `<MyComponent>` _props_
|
||||||
|
|
||||||
## Visualizing the reducer and store change
|
3. Finally, `connect()` actually returns a function that **decorates** a `<MyComponent>` into `<ConnectedComponent>` - it is a strange syntax, so do study it more closely here.
|
||||||
|
|
||||||
If you want a really neat UI to show what the store looks when actions are dispatched to the store, use the [Redux DevTools extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd).
|
> Yes, `connect()` is a function that takes in functions as arguments that returns a function that takes in a component that return a component. Try to say this fast 10 times :)
|
||||||
|
|
||||||
This extension (available for Chrome and Firefox) is a work of genius! It lets you replay actions and step backwards to debug the current state of a Redux application. In a large enough application, this kind of debuggability is invaluable. It also helps developers who are not familiar with your application to quickly get a handle on how the state changes in response to some actions.
|
|
||||||
|
|||||||
@@ -5,11 +5,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="ms-Fabric">
|
<body class="ms-Fabric">
|
||||||
<div id="markdownReadme" data-src="./README.md"></div>
|
<div id="markdownReadme" data-src="./README.md"></div>
|
||||||
<div id="app">
|
<div id="app"></div>
|
||||||
For this step, we look at unit testing. Run
|
|
||||||
<pre>npm test</pre>
|
|
||||||
in the command line.
|
|
||||||
</div>
|
|
||||||
<script src="../../assets/scripts.js"></script>
|
<script src="../../assets/scripts.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,5 +4,7 @@ export const actions = {
|
|||||||
addTodo: (label: string) => ({ type: 'addTodo', id: uuid(), label }),
|
addTodo: (label: string) => ({ type: 'addTodo', id: uuid(), label }),
|
||||||
remove: (id: string) => ({ type: 'remove', id }),
|
remove: (id: string) => ({ type: 'remove', id }),
|
||||||
complete: (id: string) => ({ type: 'complete', id }),
|
complete: (id: string) => ({ type: 'complete', id }),
|
||||||
clear: () => ({ type: 'clear' })
|
clear: () => ({ type: 'clear' }),
|
||||||
|
setFilter: (filter: string) => ({ type: 'setFilter', filter }),
|
||||||
|
edit: (id: string, label: string) => ({ type: 'edit', id, label })
|
||||||
};
|
};
|
||||||
|
|||||||
17
step2-06/demo/src/components/TodoApp.tsx
Normal file
17
step2-06/demo/src/components/TodoApp.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack } from 'office-ui-fabric-react';
|
||||||
|
import { TodoFooter } from './TodoFooter';
|
||||||
|
import { TodoHeader } from './TodoHeader';
|
||||||
|
import { TodoList } from './TodoList';
|
||||||
|
|
||||||
|
export const TodoApp = () => {
|
||||||
|
return (
|
||||||
|
<Stack horizontalAlign="center">
|
||||||
|
<Stack style={{ width: 400 }} gap={25}>
|
||||||
|
<TodoHeader />
|
||||||
|
<TodoList />
|
||||||
|
<TodoFooter />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,40 +1,36 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Store } from '../store';
|
import { DefaultButton, Stack, Text } from 'office-ui-fabric-react';
|
||||||
import { DefaultButton, Text, Stack } from 'office-ui-fabric-react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { actions } from '../actions';
|
import { actions } from '../actions';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Store } from '../store';
|
||||||
|
|
||||||
interface TodoFooterProps {
|
interface TodoFooterProps {
|
||||||
clear: () => void;
|
|
||||||
todos: Store['todos'];
|
todos: Store['todos'];
|
||||||
|
clear: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TodoFooter = (props: TodoFooterProps) => {
|
const TodoFooter = (props: TodoFooterProps) => {
|
||||||
const itemCount = Object.keys(props.todos).filter(id => !props.todos[id].completed).length;
|
const { todos, clear } = props;
|
||||||
|
|
||||||
|
const itemCount = Object.keys(todos).filter(id => !todos[id].completed).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack horizontal horizontalAlign="space-between">
|
<Stack horizontal horizontalAlign="space-between">
|
||||||
<Text>
|
<Text>
|
||||||
{itemCount} item{itemCount === 1 ? '' : 's'} left
|
{itemCount} item{itemCount === 1 ? '' : 's'} left
|
||||||
</Text>
|
</Text>
|
||||||
<DefaultButton onClick={() => props.clear()}>Clear Completed</DefaultButton>
|
<DefaultButton onClick={() => clear()}>Clear Completed</DefaultButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state: Store) {
|
const ConnectedTodoFooter = connect(
|
||||||
return { ...state };
|
(state: Store) => ({
|
||||||
}
|
todos: state.todos
|
||||||
|
}),
|
||||||
function mapDispatchToProps(dispatch: any) {
|
dispatch => ({
|
||||||
return {
|
|
||||||
clear: () => dispatch(actions.clear())
|
clear: () => dispatch(actions.clear())
|
||||||
};
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const component = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(TodoFooter);
|
)(TodoFooter);
|
||||||
|
|
||||||
export { component as TodoFooter };
|
export { ConnectedTodoFooter as TodoFooter };
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Pivot, PivotItem, TextField, PrimaryButton, Text, Stack } from 'office-ui-fabric-react';
|
import { Stack, Text, Pivot, PivotItem, TextField, PrimaryButton } from 'office-ui-fabric-react';
|
||||||
import { FilterTypes, Store } from '../store';
|
import { FilterTypes } from '../store';
|
||||||
import { actions } from '../actions';
|
import { actions } from '../actions';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
interface TodoHeaderProps {
|
interface TodoHeaderProps {
|
||||||
addTodo: (label: string) => void;
|
addTodo: (label: string) => void;
|
||||||
setFilter: (filter: FilterTypes) => void;
|
setFilter: (filter: FilterTypes) => void;
|
||||||
filter: FilterTypes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TodoHeaderState {
|
interface TodoHeaderState {
|
||||||
@@ -24,12 +23,23 @@ class TodoHeader extends React.Component<TodoHeaderProps, TodoHeaderState> {
|
|||||||
return (
|
return (
|
||||||
<Stack gap={10}>
|
<Stack gap={10}>
|
||||||
<Stack horizontal horizontalAlign="center">
|
<Stack horizontal horizontalAlign="center">
|
||||||
<Text variant="xxLarge">todos - step2-08 demo</Text>
|
<Text variant="xxLarge">todos</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack horizontal gap={10}>
|
<Stack horizontal gap={10}>
|
||||||
<Stack.Item grow>
|
<Stack.Item grow>
|
||||||
<TextField placeholder="What needs to be done?" value={this.state.labelInput} onChange={this.onChange} />
|
<TextField
|
||||||
|
placeholder="What needs to be done?"
|
||||||
|
value={this.state.labelInput}
|
||||||
|
onChange={this.onChange}
|
||||||
|
styles={props => ({
|
||||||
|
...(props.focused && {
|
||||||
|
field: {
|
||||||
|
backgroundColor: '#c7e0f4'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<PrimaryButton onClick={this.onAdd}>Add</PrimaryButton>
|
<PrimaryButton onClick={this.onAdd}>Add</PrimaryButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -57,20 +67,12 @@ class TodoHeader extends React.Component<TodoHeaderProps, TodoHeaderState> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state: Store) {
|
const ConnectedTodoHeader = connect(
|
||||||
return { ...state };
|
state => {},
|
||||||
}
|
dispatch => ({
|
||||||
|
addTodo: label => dispatch(actions.addTodo(label)),
|
||||||
function mapDispatchToProps(dispatch: any) {
|
setFilter: filter => dispatch(actions.setFilter(filter))
|
||||||
return {
|
})
|
||||||
addTodo: (label: string) => dispatch(actions.addTodo(label)),
|
|
||||||
setFilter: (filter: FilterTypes) => dispatch(actions.setFilter(filter))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const component = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(TodoHeader);
|
)(TodoHeader);
|
||||||
|
|
||||||
export { component as TodoHeader };
|
export { ConnectedTodoHeader as TodoHeader };
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Stack } from 'office-ui-fabric-react';
|
import { Stack } from 'office-ui-fabric-react';
|
||||||
import { TodoListItem } from './TodoListItem';
|
import { TodoListItem } from './TodoListItem';
|
||||||
import { Store, FilterTypes } from '../store';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { Store } from '../store';
|
||||||
|
|
||||||
interface TodoListProps {
|
interface TodoListProps {
|
||||||
todos: Store['todos'];
|
todos: Store['todos'];
|
||||||
filter: FilterTypes;
|
filter: Store['filter'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const TodoList = (props: TodoListProps) => {
|
const TodoList = (props: TodoListProps) => {
|
||||||
@@ -24,17 +24,5 @@ const TodoList = (props: TodoListProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state: Store) {
|
const ConnectedTodoList = connect((state: Store) => ({ ...state }))(TodoList);
|
||||||
return { ...state };
|
export { ConnectedTodoList as TodoList };
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: any) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const component = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(TodoList);
|
|
||||||
|
|
||||||
export { component as TodoList };
|
|
||||||
92
step2-06/demo/src/components/TodoListItem.tsx
Normal file
92
step2-06/demo/src/components/TodoListItem.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react';
|
||||||
|
import { actions } from '../actions';
|
||||||
|
import { Store } from '../store';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
interface TodoListItemProps {
|
||||||
|
id: string;
|
||||||
|
todos: Store['todos'];
|
||||||
|
complete: (id: string) => void;
|
||||||
|
remove: (id: string) => void;
|
||||||
|
edit: (id: string, label: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TodoListItemState {
|
||||||
|
editing: boolean;
|
||||||
|
editLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TodoListItem extends React.Component<TodoListItemProps, TodoListItemState> {
|
||||||
|
constructor(props: TodoListItemProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { editing: false, editLabel: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { id } = this.props;
|
||||||
|
const { todos } = this.context.getState();
|
||||||
|
const dispatch = this.context.dispatch;
|
||||||
|
|
||||||
|
const item = todos[id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack horizontal verticalAlign="center" horizontalAlign="space-between">
|
||||||
|
{!this.state.editing && (
|
||||||
|
<>
|
||||||
|
<Checkbox label={item.label} checked={item.completed} onChange={() => dispatch(actions.complete(id))} />
|
||||||
|
<div>
|
||||||
|
<IconButton iconProps={{ iconName: 'Edit' }} onClick={this.onEdit} />
|
||||||
|
<IconButton iconProps={{ iconName: 'Cancel' }} onClick={() => dispatch(actions.remove(id))} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{this.state.editing && (
|
||||||
|
<Stack.Item grow>
|
||||||
|
<Stack horizontal gap={10}>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<TextField value={this.state.editLabel} onChange={this.onChange} />
|
||||||
|
</Stack.Item>
|
||||||
|
<DefaultButton onClick={this.onDoneEdit}>Save</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack.Item>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEdit = () => {
|
||||||
|
const { id } = this.props;
|
||||||
|
const { todos } = this.context.getState();
|
||||||
|
const { label } = todos[id];
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
editing: true,
|
||||||
|
editLabel: this.state.editLabel || label
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDoneEdit = () => {
|
||||||
|
this.props.edit(this.props.id, this.state.editLabel);
|
||||||
|
this.setState({
|
||||||
|
editing: false,
|
||||||
|
editLabel: undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onChange = (evt: React.FormEvent<HTMLInputElement>, newValue: string) => {
|
||||||
|
this.setState({ editLabel: newValue });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConnectedTodoListItem = connect(
|
||||||
|
(state: Store) => ({ todos: state.todos }),
|
||||||
|
dispatch => ({
|
||||||
|
complete: label => dispatch(actions.addTodo(label)),
|
||||||
|
remove: label => dispatch(actions.addTodo(label)),
|
||||||
|
edit: filter => dispatch(actions.setFilter(filter))
|
||||||
|
})
|
||||||
|
)(TodoListItem);
|
||||||
|
|
||||||
|
export { ConnectedTodoListItem as TodoListItem };
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
import { reducer } from './reducers';
|
import { reducer } from './reducers';
|
||||||
import { createStore } from 'redux';
|
import { createStore } from 'redux';
|
||||||
|
import { TodoApp } from './components/TodoApp';
|
||||||
|
import { initializeIcons } from '@uifabric/icons';
|
||||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||||
import { actions } from './actions';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
const store = createStore(reducer, {}, composeWithDevTools());
|
const store = createStore(reducer, {}, composeWithDevTools());
|
||||||
|
|
||||||
console.log(store.getState());
|
initializeIcons();
|
||||||
|
|
||||||
store.dispatch(actions.addTodo('hello'));
|
ReactDOM.render(
|
||||||
store.dispatch(actions.addTodo('world'));
|
<Provider store={store}>
|
||||||
|
<TodoApp />
|
||||||
console.log(store.getState());
|
</Provider>,
|
||||||
|
document.getElementById('app')
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,27 +1,43 @@
|
|||||||
import { Store } from '../store';
|
import { Store } from '../store';
|
||||||
import { addTodo, remove, complete, clear } from './pureFunctions';
|
import { combineReducers } from 'redux';
|
||||||
|
import { createReducer } from 'redux-starter-kit';
|
||||||
|
|
||||||
function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] {
|
export const todosReducer = createReducer<Store['todos']>(
|
||||||
switch (action.type) {
|
{},
|
||||||
case 'addTodo':
|
{
|
||||||
return addTodo(state, action.id, action.label);
|
addTodo(state, action) {
|
||||||
|
state[action.id] = { label: action.label, completed: false };
|
||||||
|
},
|
||||||
|
|
||||||
case 'remove':
|
remove(state, action) {
|
||||||
return remove(state, action.id);
|
delete state[action.id];
|
||||||
|
},
|
||||||
|
|
||||||
case 'clear':
|
clear(state, action) {
|
||||||
return clear(state);
|
Object.keys(state).forEach(key => {
|
||||||
|
if (state[key].completed) {
|
||||||
|
delete state[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
case 'complete':
|
complete(state, action) {
|
||||||
return complete(state, action.id);
|
state[action.id].completed = !state[action.id].completed;
|
||||||
|
},
|
||||||
|
|
||||||
|
edit(state, action) {
|
||||||
|
state[action.id].label = action.label;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return state;
|
export const filterReducer = createReducer<Store['filter']>('all', {
|
||||||
}
|
setFilter(state, action) {
|
||||||
|
return action.filter;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export function reducer(state: Store, action: any): Store {
|
export const reducer = combineReducers({
|
||||||
return {
|
todos: todosReducer,
|
||||||
todos: todoReducer(state.todos, action),
|
filter: filterReducer
|
||||||
filter: 'all'
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { addTodo, complete } from './pureFunctions';
|
|
||||||
import { Store } from '../store';
|
|
||||||
|
|
||||||
describe('TodoApp reducers', () => {
|
|
||||||
it('can add an item', () => {
|
|
||||||
const state = <Store['todos']>{};
|
|
||||||
|
|
||||||
const newState = addTodo(state, '0', 'item1');
|
|
||||||
|
|
||||||
const keys = Object.keys(newState);
|
|
||||||
|
|
||||||
expect(newState).not.toBe(state);
|
|
||||||
expect(keys.length).toBe(1);
|
|
||||||
expect(newState[keys[0]].label).toBe('item1');
|
|
||||||
expect(newState[keys[0]].completed).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can complete an item', () => {
|
|
||||||
const state = <Store['todos']>{};
|
|
||||||
|
|
||||||
let newState = addTodo(state, '0', 'item1');
|
|
||||||
|
|
||||||
const key = Object.keys(newState)[0];
|
|
||||||
|
|
||||||
newState = complete(newState, key);
|
|
||||||
|
|
||||||
expect(newState[key].completed).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Store, FilterTypes } from '../store';
|
|
||||||
|
|
||||||
export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] {
|
|
||||||
return { ...state, [id]: { label, completed: false } };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function remove(state: Store['todos'], id: string) {
|
|
||||||
const newTodos = { ...state };
|
|
||||||
|
|
||||||
delete newTodos[id];
|
|
||||||
|
|
||||||
return newTodos;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function complete(state: Store['todos'], id: string) {
|
|
||||||
// Clone the todo, overriding
|
|
||||||
const newTodo = { ...state[id], completed: !state[id].completed };
|
|
||||||
return { ...state, [id]: newTodo };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clear(state: Store['todos']) {
|
|
||||||
const newTodos = { ...state };
|
|
||||||
|
|
||||||
Object.keys(state).forEach(key => {
|
|
||||||
if (state[key].completed) {
|
|
||||||
delete newTodos[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return newTodos;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setFilter(state: Store['filter'], filter: FilterTypes) {
|
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,21 @@
|
|||||||
# Step 2.6 - Redux: Dispatching actions and examining state (Exercise)
|
# Step 2.6: Redux: React Binding (Exercise)
|
||||||
|
|
||||||
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
|
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
|
||||||
|
|
||||||
## Visualize state changes with Chrome extension
|
If you haven't arStart the app by running `npm start` from the root of the `frontend-bootcamp` folder. Click the "exercise" link under day 2 step 6 to see results.
|
||||||
|
|
||||||
If you still have `npm test` running from the previous step, stop it with `ctrl+C`. Start the app by running `npm start` from the root of the `frontend-bootcamp` folder. Click the "exercise" link under day 2 step 6.
|
At the beginning of this exercise, the "Add" and "Clear Completed" buttons do not work. We'll be fixing that in this step!
|
||||||
|
|
||||||
1. Install the [Redux DevTools extension](https://github.com/zalmoxisus/redux-devtools-extension)
|
1. Open `exercise/src/index.tsx` and wrap `<TodoApp>` with `<Provider>` as instructed in the comment
|
||||||
- [Chrome](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd)
|
|
||||||
- [Firefox](https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/)
|
|
||||||
|
|
||||||
2. Hit F12 (`cmd+option+I` on Mac) and open the inspector panel entitled **Redux**
|
2. Open `exercise/src/components/TodoFooter.tsx` and erase the "nullable" type modifier (i.e. the ?) in the interface definition of `TodoFooterProps`
|
||||||
|
|
||||||
3. Modify `exercise/src/index.tsx` to dispatch actions (you're not limited to adding todos; you can also remove and clear)
|
3. Uncomment the bottom bits of code and fill in `connect()` arguments - feel free to use `TodoListItem.tsx` as a guide
|
||||||
|
|
||||||
4. Explore the actions' effects using the extension
|
4. Repeat steps 2, 3 for the `TodoHeader.tsx` file
|
||||||
|
|
||||||
## Playing with dispatching actions inside tests
|
## Bonus exercise
|
||||||
|
|
||||||
Stop the app using `ctrl+C` and start the tests by running `npm test`.
|
For further reading, go here to learn more about the `mergeProps` and `options` parameters to `connect()`:
|
||||||
|
|
||||||
1. Open `exercise/src/reducers/reducer.spec.ts`
|
https://react-redux.js.org/api/connect
|
||||||
|
|
||||||
2. Follow the instructions to fill out the reducer tests
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import uuid from 'uuid/v4';
|
import uuid from 'uuid/v4';
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
addTodo: (label: string) => ({ type: 'addTodo', id: uuid(), label })
|
addTodo: (label: string) => ({ type: 'addTodo', id: uuid(), label }),
|
||||||
|
remove: (id: string) => ({ type: 'remove', id }),
|
||||||
|
complete: (id: string) => ({ type: 'complete', id }),
|
||||||
|
clear: () => ({ type: 'clear' }),
|
||||||
|
setFilter: (filter: string) => ({ type: 'setFilter', filter }),
|
||||||
|
edit: (id: string, label: string) => ({ type: 'edit', id, label })
|
||||||
};
|
};
|
||||||
|
|||||||
17
step2-06/exercise/src/components/TodoApp.tsx
Normal file
17
step2-06/exercise/src/components/TodoApp.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack } from 'office-ui-fabric-react';
|
||||||
|
import { TodoFooter } from './TodoFooter';
|
||||||
|
import { TodoHeader } from './TodoHeader';
|
||||||
|
import { TodoList } from './TodoList';
|
||||||
|
|
||||||
|
export const TodoApp = () => {
|
||||||
|
return (
|
||||||
|
<Stack horizontalAlign="center">
|
||||||
|
<Stack style={{ width: 400 }} gap={25}>
|
||||||
|
<TodoHeader />
|
||||||
|
<TodoList />
|
||||||
|
<TodoFooter />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
43
step2-06/exercise/src/components/TodoFooter.tsx
Normal file
43
step2-06/exercise/src/components/TodoFooter.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DefaultButton, Stack, Text } from 'office-ui-fabric-react';
|
||||||
|
import { actions } from '../actions';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Store } from '../store';
|
||||||
|
|
||||||
|
// TODO: these ?'s after the keys of an interface makes it optional
|
||||||
|
// and can be removed when you finished connecting this component
|
||||||
|
interface TodoFooterProps {
|
||||||
|
todos?: Store['todos'];
|
||||||
|
clear?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TodoFooter = (props: TodoFooterProps) => {
|
||||||
|
const { todos, clear } = props;
|
||||||
|
|
||||||
|
const itemCount = Object.keys(todos).filter(id => !todos[id].completed).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack horizontal horizontalAlign="space-between">
|
||||||
|
<Text>
|
||||||
|
{itemCount} item{itemCount === 1 ? '' : 's'} left
|
||||||
|
</Text>
|
||||||
|
<DefaultButton onClick={() => clear()}>Clear Completed</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: write out the mapping functions for state and dispatch functions
|
||||||
|
/*
|
||||||
|
HINT: you can get started by copy pasting below code as arguments to connect()
|
||||||
|
|
||||||
|
(state: Store) => ({
|
||||||
|
// TODO: mapping for state
|
||||||
|
// HINT: look at what the component needed from the props interface
|
||||||
|
}),
|
||||||
|
dispatch => ({
|
||||||
|
// TODO: mapping for dispatch actions
|
||||||
|
// HINT: look at what the component needed from the props interface
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
const ConnectedTodoFooter = connect()(TodoFooter);
|
||||||
|
export { ConnectedTodoFooter as TodoFooter };
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Pivot, PivotItem, TextField, PrimaryButton, Stack, Text } from 'office-ui-fabric-react';
|
import { Stack, Text, Pivot, PivotItem, TextField, PrimaryButton } from 'office-ui-fabric-react';
|
||||||
import { FilterTypes, Store } from '../store';
|
import { FilterTypes } from '../store';
|
||||||
import { actions } from '../actions';
|
import { actions } from '../actions';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
// TODO: these ?'s after the keys of an interface makes it optional
|
||||||
|
// and can be removed when you finished connecting this component
|
||||||
interface TodoHeaderProps {
|
interface TodoHeaderProps {
|
||||||
addTodo: (label: string) => void;
|
addTodo?: (label: string) => void;
|
||||||
setFilter: (filter: FilterTypes) => void;
|
setFilter?: (filter: FilterTypes) => void;
|
||||||
filter: FilterTypes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TodoHeaderState {
|
interface TodoHeaderState {
|
||||||
@@ -24,12 +25,23 @@ class TodoHeader extends React.Component<TodoHeaderProps, TodoHeaderState> {
|
|||||||
return (
|
return (
|
||||||
<Stack gap={10}>
|
<Stack gap={10}>
|
||||||
<Stack horizontal horizontalAlign="center">
|
<Stack horizontal horizontalAlign="center">
|
||||||
<Text variant="xxLarge">todos - step2-08 exercise</Text>
|
<Text variant="xxLarge">todos</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack horizontal gap={10}>
|
<Stack horizontal gap={10}>
|
||||||
<Stack.Item grow>
|
<Stack.Item grow>
|
||||||
<TextField placeholder="What needs to be done?" value={this.state.labelInput} onChange={this.onChange} />
|
<TextField
|
||||||
|
placeholder="What needs to be done?"
|
||||||
|
value={this.state.labelInput}
|
||||||
|
onChange={this.onChange}
|
||||||
|
styles={props => ({
|
||||||
|
...(props.focused && {
|
||||||
|
field: {
|
||||||
|
backgroundColor: '#c7e0f4'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<PrimaryButton onClick={this.onAdd}>Add</PrimaryButton>
|
<PrimaryButton onClick={this.onAdd}>Add</PrimaryButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -57,20 +69,18 @@ class TodoHeader extends React.Component<TodoHeaderProps, TodoHeaderState> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state: Store) {
|
// TODO: write out the mapping functions for state and dispatch functions
|
||||||
return { ...state };
|
/*
|
||||||
}
|
HINT: you can get started by copy pasting below code as arguments to connect()
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: any) {
|
(state: Store) => ({
|
||||||
return {
|
// TODO: mapping for state
|
||||||
addTodo: (label: string) => dispatch(actions.addTodo(label)),
|
// HINT: look at what the component needed from the props interface
|
||||||
setFilter: (filter: FilterTypes) => dispatch(actions.setFilter(filter))
|
}),
|
||||||
};
|
dispatch => ({
|
||||||
}
|
// TODO: mapping for dispatch actions
|
||||||
|
// HINT: look at what the component needed from the props interface
|
||||||
const component = connect(
|
})
|
||||||
mapStateToProps,
|
*/
|
||||||
mapDispatchToProps
|
const ConnectedTodoHeader = connect()(TodoHeader);
|
||||||
)(TodoHeader);
|
export { ConnectedTodoHeader as TodoHeader };
|
||||||
|
|
||||||
export { component as TodoHeader };
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Stack } from 'office-ui-fabric-react';
|
import { Stack } from 'office-ui-fabric-react';
|
||||||
import { TodoListItem } from './TodoListItem';
|
import { TodoListItem } from './TodoListItem';
|
||||||
import { Store, FilterTypes } from '../store';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { Store } from '../store';
|
||||||
|
|
||||||
interface TodoListProps {
|
interface TodoListProps {
|
||||||
todos: Store['todos'];
|
todos: Store['todos'];
|
||||||
filter: FilterTypes;
|
filter: Store['filter'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const TodoList = (props: TodoListProps) => {
|
const TodoList = (props: TodoListProps) => {
|
||||||
@@ -24,17 +24,5 @@ const TodoList = (props: TodoListProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state: Store) {
|
const ConnectedTodoList = connect((state: Store) => ({ ...state }))(TodoList);
|
||||||
return { ...state };
|
export { ConnectedTodoList as TodoList };
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: any) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const component = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(TodoList);
|
|
||||||
|
|
||||||
export { component as TodoList };
|
|
||||||
92
step2-06/exercise/src/components/TodoListItem.tsx
Normal file
92
step2-06/exercise/src/components/TodoListItem.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react';
|
||||||
|
import { actions } from '../actions';
|
||||||
|
import { Store } from '../store';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
interface TodoListItemProps {
|
||||||
|
id: string;
|
||||||
|
todos: Store['todos'];
|
||||||
|
complete: (id: string) => void;
|
||||||
|
remove: (id: string) => void;
|
||||||
|
edit: (id: string, label: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TodoListItemState {
|
||||||
|
editing: boolean;
|
||||||
|
editLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TodoListItem extends React.Component<TodoListItemProps, TodoListItemState> {
|
||||||
|
constructor(props: TodoListItemProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { editing: false, editLabel: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { id } = this.props;
|
||||||
|
const { todos } = this.context.getState();
|
||||||
|
const dispatch = this.context.dispatch;
|
||||||
|
|
||||||
|
const item = todos[id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack horizontal verticalAlign="center" horizontalAlign="space-between">
|
||||||
|
{!this.state.editing && (
|
||||||
|
<>
|
||||||
|
<Checkbox label={item.label} checked={item.completed} onChange={() => dispatch(actions.complete(id))} />
|
||||||
|
<div>
|
||||||
|
<IconButton iconProps={{ iconName: 'Edit' }} onClick={this.onEdit} />
|
||||||
|
<IconButton iconProps={{ iconName: 'Cancel' }} onClick={() => dispatch(actions.remove(id))} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{this.state.editing && (
|
||||||
|
<Stack.Item grow>
|
||||||
|
<Stack horizontal gap={10}>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<TextField value={this.state.editLabel} onChange={this.onChange} />
|
||||||
|
</Stack.Item>
|
||||||
|
<DefaultButton onClick={this.onDoneEdit}>Save</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack.Item>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEdit = () => {
|
||||||
|
const { id } = this.props;
|
||||||
|
const { todos } = this.context.getState();
|
||||||
|
const { label } = todos[id];
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
editing: true,
|
||||||
|
editLabel: this.state.editLabel || label
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDoneEdit = () => {
|
||||||
|
this.props.edit(this.props.id, this.state.editLabel);
|
||||||
|
this.setState({
|
||||||
|
editing: false,
|
||||||
|
editLabel: undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onChange = (evt: React.FormEvent<HTMLInputElement>, newValue: string) => {
|
||||||
|
this.setState({ editLabel: newValue });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConnectedTodoListItem = connect(
|
||||||
|
(state: Store) => ({ todos: state.todos }),
|
||||||
|
dispatch => ({
|
||||||
|
complete: label => dispatch(actions.addTodo(label)),
|
||||||
|
remove: label => dispatch(actions.addTodo(label)),
|
||||||
|
edit: filter => dispatch(actions.setFilter(filter))
|
||||||
|
})
|
||||||
|
)(TodoListItem);
|
||||||
|
|
||||||
|
export { ConnectedTodoListItem as TodoListItem };
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
import { reducer } from './reducers';
|
import { reducer } from './reducers';
|
||||||
import { createStore } from 'redux';
|
import { createStore } from 'redux';
|
||||||
|
import { TodoApp } from './components/TodoApp';
|
||||||
|
import { initializeIcons } from '@uifabric/icons';
|
||||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||||
import { actions } from './actions';
|
// TODO: import { Provider } from 'react-redux';
|
||||||
|
|
||||||
const store = createStore(reducer, {}, composeWithDevTools());
|
const store = createStore(reducer, {}, composeWithDevTools());
|
||||||
|
|
||||||
console.log(store.getState());
|
initializeIcons();
|
||||||
|
|
||||||
// TODO: dispatch several actions and see the effects on state inside the Redux devtools
|
// TODO: wrap the <TodoApp> component with a <Provider store={store}> component
|
||||||
|
ReactDOM.render(<TodoApp />, document.getElementById('app'));
|
||||||
// store.dispatch(actions.???);
|
|
||||||
// store.dispatch(actions.???);
|
|
||||||
// store.dispatch(actions.???);
|
|
||||||
|
|
||||||
console.log(store.getState());
|
|
||||||
|
|||||||
@@ -1,27 +1,43 @@
|
|||||||
import { Store } from '../store';
|
import { Store } from '../store';
|
||||||
import { addTodo, remove, complete, clear } from './pureFunctions';
|
import { combineReducers } from 'redux';
|
||||||
|
import { createReducer } from 'redux-starter-kit';
|
||||||
|
|
||||||
function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] {
|
export const todosReducer = createReducer<Store['todos']>(
|
||||||
switch (action.type) {
|
{},
|
||||||
case 'addTodo':
|
{
|
||||||
return addTodo(state, action.id, action.label);
|
addTodo(state, action) {
|
||||||
|
state[action.id] = { label: action.label, completed: false };
|
||||||
|
},
|
||||||
|
|
||||||
case 'remove':
|
remove(state, action) {
|
||||||
return remove(state, action.id);
|
delete state[action.id];
|
||||||
|
},
|
||||||
|
|
||||||
case 'clear':
|
clear(state, action) {
|
||||||
return clear(state);
|
Object.keys(state).forEach(key => {
|
||||||
|
if (state[key].completed) {
|
||||||
|
delete state[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
case 'complete':
|
complete(state, action) {
|
||||||
return complete(state, action.id);
|
state[action.id].completed = !state[action.id].completed;
|
||||||
|
},
|
||||||
|
|
||||||
|
edit(state, action) {
|
||||||
|
state[action.id].label = action.label;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return state;
|
export const filterReducer = createReducer<Store['filter']>('all', {
|
||||||
}
|
setFilter(state, action) {
|
||||||
|
return action.filter;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export function reducer(state: Store, action: any): Store {
|
export const reducer = combineReducers({
|
||||||
return {
|
todos: todosReducer,
|
||||||
todos: todoReducer(state.todos, action),
|
filter: filterReducer
|
||||||
filter: 'all'
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { addTodo, complete } from './pureFunctions';
|
|
||||||
import { Store } from '../store';
|
|
||||||
|
|
||||||
describe('TodoApp reducers', () => {
|
|
||||||
it('can add an item', () => {
|
|
||||||
const state = <Store['todos']>{};
|
|
||||||
|
|
||||||
const newState = addTodo(state, '0', 'item1');
|
|
||||||
|
|
||||||
const keys = Object.keys(newState);
|
|
||||||
|
|
||||||
expect(newState).not.toBe(state);
|
|
||||||
expect(keys.length).toBe(1);
|
|
||||||
expect(newState[keys[0]].label).toBe('item1');
|
|
||||||
expect(newState[keys[0]].completed).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can complete an item', () => {
|
|
||||||
const state = <Store['todos']>{};
|
|
||||||
|
|
||||||
let newState = addTodo(state, '0', 'item1');
|
|
||||||
|
|
||||||
const key = Object.keys(newState)[0];
|
|
||||||
|
|
||||||
newState = complete(newState, key);
|
|
||||||
|
|
||||||
expect(newState[key].completed).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Store, FilterTypes } from '../store';
|
|
||||||
|
|
||||||
export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] {
|
|
||||||
return { ...state, [id]: { label, completed: false } };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function remove(state: Store['todos'], id: string) {
|
|
||||||
const newTodos = { ...state };
|
|
||||||
|
|
||||||
delete newTodos[id];
|
|
||||||
|
|
||||||
return newTodos;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function complete(state: Store['todos'], id: string) {
|
|
||||||
// Clone the todo, overriding
|
|
||||||
const newTodo = { ...state[id], completed: !state[id].completed };
|
|
||||||
return { ...state, [id]: newTodo };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clear(state: Store['todos']) {
|
|
||||||
const newTodos = { ...state };
|
|
||||||
|
|
||||||
Object.keys(state).forEach(key => {
|
|
||||||
if (state[key].completed) {
|
|
||||||
delete newTodos[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return newTodos;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setFilter(state: Store['filter'], filter: FilterTypes) {
|
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { createStore } from 'redux';
|
|
||||||
import { reducer } from '.';
|
|
||||||
import { actions } from '../actions';
|
|
||||||
|
|
||||||
describe('reducers', () => {
|
|
||||||
it('should add items', () => {
|
|
||||||
// 1. Use Redux's createStore() to create a store. Pass in a reducer along with the initial state.
|
|
||||||
//
|
|
||||||
// 2. Call store.dispatch() with some action messages to indicate the kind of
|
|
||||||
// action to perform (in this case, addTodo)
|
|
||||||
//
|
|
||||||
// 3. Assert with expect() on the resultant store.getState().todos
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tests left for you to write:
|
|
||||||
// - remove
|
|
||||||
// - clear
|
|
||||||
// - complete
|
|
||||||
});
|
|
||||||
@@ -1,88 +1,48 @@
|
|||||||
# Step 2.7: Connect Redux store to view (Demo)
|
# Step 2.7: Service calls (Demo)
|
||||||
|
|
||||||
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
|
[Lessons](../)
|
||||||
|
|
||||||
Redux is currently the most popular Flux implementation, and the ecosystem of related libraries has grown as a result. This is one of the reasons why it is a very popular library within Microsoft products.
|
> Note: this step doesn't work with the live site on github.io. Clone the repo to try this step out.
|
||||||
|
|
||||||
Various GitHub users have collected "awesome lists" of tech and articles related to Redux. Here is [one such list](https://github.com/xgrommx/awesome-redux#react---a-javascript-library-for-building-user-interfaces), but it is literally impossible to list out all the related tech.
|
## `redux-thunk`: side effects inside action creators
|
||||||
|
|
||||||
In this step, we introduce but one useful library that works with Redux: [`react-redux`](https://react-redux.js.org/).
|
The [Redux Thunk](https://github.com/reduxjs/redux-thunk) middleware allows writing actions that make service calls.
|
||||||
|
|
||||||
## The official React Redux binding: `react-redux`
|
Remember those simple little action functions? They're called action creators. These little functions can be charged with superpowers to allow asynchronous side effects to happen while creating the messages. Asynchronous side effects include service calls against APIs.
|
||||||
|
|
||||||
That's right, Redux doesn't just work with React. It can also be used with Vue.js, Angular, and React Native, to name a few.
|
Action creators are a natural place to put service calls. Redux Thunk middleware passes `dispatch()` and `getState()` from the store into the action creators. This allows the action creator itself to dispatch different actions in between async side effects. Combined with the async / await syntax, coding service calls is a cinch!
|
||||||
|
|
||||||
### `<Provider>` component
|
Most of the time, in a single-page app, we apply **optimistic UI updates**. We can update the UI before the network call completes so the UI feels more responsive.
|
||||||
|
|
||||||
The store doesn't magically get passed to the views. It has to be supplied by a `react-redux` component called [`<Provider>`](https://react-redux.js.org/api/provider). A `<Provider>` can be placed anywhere, but it's best to just make it available at the root the app:
|
## Action creator with a thunk
|
||||||
|
|
||||||
```js
|
[What's a thunk?](https://daveceddia.com/what-is-a-thunk/) - it is a wrapper function that returns a function. What does it do? Let's find out!
|
||||||
const store = createStore(reducers);
|
|
||||||
|
|
||||||
const App = () => {
|
This action creator just returns an object:
|
||||||
return (
|
|
||||||
<Provider store={store}>
|
|
||||||
<div>Hello World!</div>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### `connect()` higher-order function
|
|
||||||
|
|
||||||
`react-redux` provides a [`connect()`](https://react-redux.js.org/api/connect) function that turns the Redux store and dispatch functions into props for React components. The state and action dispatchers are passed along with a `<Provider>` component.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const OldComponent = props => {
|
|
||||||
return <div>{props.foo}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const NewComponent = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
mergeProps,
|
|
||||||
options
|
|
||||||
)(OldComponent);
|
|
||||||
```
|
|
||||||
|
|
||||||
`connect()` takes in a few functions that map portions of the state tree and dispatcher functions into props. It is a **higher-order function**, meaning that the return value of `connect()` is a function that decorates `OldComponent` into a `NewComponent` with all the mapped props.
|
|
||||||
|
|
||||||
Next we'll learn about how to write `mapStateToProps` and `mapDispatchToProps`. For demonstration purposes, we'll assume the store and the component props look like this:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface Store {
|
function addTodo(label: string) {
|
||||||
foo: string;
|
return { type: 'addTodo', id: uuid(), label };
|
||||||
// and probably some other properties
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ComponentProps {
|
|
||||||
foo: string;
|
|
||||||
addTodo: (label: string) => void;
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `mapStateToProps`
|
In order for us to make service calls, we need to supercharge this with the power of `redux-thunk`
|
||||||
|
|
||||||
A [`mapStateToProps`](https://react-redux.js.org/api/connect#mapstatetoprops-state-ownprops-object) function uses the state tree as a parameter and selects portions of it that will be passed to the component as part of `props`. When values in the state tree change, the `mapStateToProps` function is called, and the new props are passed to the component (causing React to re-render it).
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
function mapStateToProps(state: Store): Partial<ComponentProps> {
|
function addTodo(label: string) {
|
||||||
return {
|
return async (dispatch: any, getState: () => Store) => {
|
||||||
foo: state.foo
|
const addAction = actions.addTodo(label);
|
||||||
|
const id = addAction.id;
|
||||||
|
dispatch(addAction);
|
||||||
|
await service.add(id, getState().todos[id]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `mapDispatchToProps`
|
Let's make some observations:
|
||||||
|
|
||||||
A [`mapDispatchToProps`](https://react-redux.js.org/api/connect#mapdispatchtoprops-object-dispatch-ownprops-object) function generates props which are used to dispatch Redux actions. This function generally returns props which the component will use as callbacks in response to user actions.
|
1. The outer function has the same function signature as the previous one
|
||||||
|
2. It returns a function that has `dispatch` and `getState` as parameters
|
||||||
```ts
|
3. The inner function is `async` enabled, and can await on "side effects" like asynchronous service calls
|
||||||
function mapDispatchToProps(dispatch: any): Partial<ComponentProps> {
|
4. This inner function has the ability to dispatch additional actions because it has been passed the `dispatch()` function from the store
|
||||||
return {
|
5. This inner function also has access to the state tree via `getState()`
|
||||||
// the dispatched message COULD be generated by an
|
|
||||||
// action creator instead (see later steps)
|
|
||||||
addTodo: (label: string) => dispatch({ type: 'addTodo', label })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user