Rewrite of Day 1 to use modern React (#294)
* update to hooks * more class to function * cleanup * finish ts final * update html lesson * add lessons page * clean up * move getters into context * adding type * fix bug * step 5 cleanup * init final pass * text tweak * fix ternaries * readme cleanup * fixed root readme
This commit is contained in:
Родитель
4998c158d2
Коммит
7cea32428e
12
README.md
12
README.md
|
@ -1,7 +1,5 @@
|
|||
# Frontend Bootcamp / Days in the Web
|
||||
|
||||
> 🚨🚨 This project is a work in progress! Issues and pull requests are encouraged. 🚨🚨
|
||||
|
||||
## Welcome
|
||||
|
||||
In this two-day workshop you'll learn the basics of frontend development while building a working web app.
|
||||
|
@ -73,7 +71,7 @@ Day one covers the basics of HTML, CSS and JavaScript, as well as an introductio
|
|||
### Day two
|
||||
|
||||
1. [TypeScript basics](step2-01)
|
||||
2. [UI Fabric component library](step2-02)
|
||||
2. [Fluent UI component library](step2-02)
|
||||
3. [Theming and styling](step2-03)
|
||||
4. [React Context](step2-04)
|
||||
5. [Redux: Store](step2-05)
|
||||
|
@ -97,12 +95,6 @@ If you are interested in JavaScript, TypeScript, React, Redux, or Design Systems
|
|||
- [@kenneth_chau](https://twitter.com/kenneth_chau)
|
||||
- [@micahgodbolt](https://twitter.com/micahgodbolt)
|
||||
|
||||
## Other projects from the UI Fabric team at Microsoft
|
||||
|
||||
- [UI Fabric](https://developer.microsoft.com/en-us/fabric) - [github repo](https://github.com/officedev/office-ui-fabric-react)
|
||||
- [Just](https://microsoft.github.io/just): The task library that just works - [github repo](https://github.com/Microsoft/just)
|
||||
- [Rush](https://rushjs.io): A scalable monorepo manager for the web - [github repo](https://github.com/Microsoft/web-build-tools/)
|
||||
|
||||
## Contributing
|
||||
|
||||
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
||||
|
@ -132,4 +124,4 @@ Microsoft's general trademark guidelines can be found at http://go.microsoft.com
|
|||
Privacy information can be found at https://privacy.microsoft.com/en-us/
|
||||
|
||||
Microsoft and any contributors reserve all other rights, whether under their respective copyrights, patents,
|
||||
or trademarks, whether by implication, estoppel or otherwise.
|
||||
or trademarks, whether by implication, estoppel or otherwise.
|
|
@ -1,11 +1,11 @@
|
|||
// prettier-ignore
|
||||
var appInsights=window.appInsights||function(a){
|
||||
function b(a){c[a]=function(){var b=arguments;c.queue.push(function(){c[a].apply(c,b)})}}var c={config:a},d=document,e=window;setTimeout(function(){var b=d.createElement("script");b.src=a.url||"https://az416426.vo.msecnd.net/scripts/a/ai.0.js",d.getElementsByTagName("script")[0].parentNode.appendChild(b)});try{c.cookie=d.cookie}catch(a){}c.queue=[];for(var f=["Event","Exception","Metric","PageView","Trace","Dependency"];f.length;)b("track"+f.pop());if(b("setAuthenticatedUserContext"),b("clearAuthenticatedUserContext"),b("startTrackEvent"),b("stopTrackEvent"),b("startTrackPage"),b("stopTrackPage"),b("flush"),!a.disableExceptionTracking){f="onerror",b("_"+f);var g=e[f];e[f]=function(a,b,d,e,h){var i=g&&g(a,b,d,e,h);return!0!==i&&c["_"+f](a,b,d,e,h),i}}return c
|
||||
var appInsights = window.appInsights || function (a) {
|
||||
function b(a) { c[a] = function () { var b = arguments; c.queue.push(function () { c[a].apply(c, b) }) } } var c = { config: a }, d = document, e = window; setTimeout(function () { var b = d.createElement("script"); b.src = a.url || "https://az416426.vo.msecnd.net/scripts/a/ai.0.js", d.getElementsByTagName("script")[0].parentNode.appendChild(b) }); try { c.cookie = d.cookie } catch (a) { } c.queue = []; for (var f = ["Event", "Exception", "Metric", "PageView", "Trace", "Dependency"]; f.length;)b("track" + f.pop()); if (b("setAuthenticatedUserContext"), b("clearAuthenticatedUserContext"), b("startTrackEvent"), b("stopTrackEvent"), b("startTrackPage"), b("stopTrackPage"), b("flush"), !a.disableExceptionTracking) { f = "onerror", b("_" + f); var g = e[f]; e[f] = function (a, b, d, e, h) { var i = g && g(a, b, d, e, h); return !0 !== i && c["_" + f](a, b, d, e, h), i } } return c
|
||||
}({
|
||||
instrumentationKey: "6ad37ae0-c4ab-4739-925c-1e2773c31f17"
|
||||
});
|
||||
|
||||
// prettier-ignore
|
||||
if (window.location.hostname !== 'localhost') {
|
||||
window.appInsights=appInsights,appInsights.queue&&0===appInsights.queue.length&&appInsights.trackPageView(null, null, {urlReferrer: document.referrer});
|
||||
window.appInsights = appInsights, appInsights.queue && 0 === appInsights.queue.length && appInsights.trackPageView(null, null, { urlReferrer: document.referrer });
|
||||
}
|
||||
|
|
|
@ -7,8 +7,21 @@ body {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
background: white;
|
||||
padding: 0px 5px;
|
||||
}
|
||||
|
||||
body.ms-Fabric {
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
|
|
18
index.html
18
index.html
|
@ -21,7 +21,8 @@
|
|||
<div class="Tile-link">
|
||||
Intro to HTML
|
||||
<div class="Tile-links">
|
||||
<a target="_blank" href="./step1-01/demo/">demo</a> | <a target="_blank" href="./step1-01/exercise/">exercise</a>
|
||||
<a target="_blank" href="./step1-01/lesson/">lesson</a> | <a target="_blank" href="./step1-01/demo/">demo</a> |
|
||||
<a target="_blank" href="./step1-01/exercise/">exercise</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -29,7 +30,8 @@
|
|||
<div class="Tile-link">
|
||||
Intro to CSS
|
||||
<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-02/lesson/">lesson</a> | <a target="_blank" href="./step1-02/demo/">demo</a> |
|
||||
<a target="_blank" href="./step1-02/exercise/">exercise</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -37,7 +39,8 @@
|
|||
<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>
|
||||
<a target="_blank" href="./step1-03/lesson/">lesson</a> | <a target="_blank" href="./step1-03/demo/">demo</a> |
|
||||
<a target="_blank" href="./step1-03/exercise/">exercise</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -45,7 +48,7 @@
|
|||
<div class="Tile-link">
|
||||
React Intro
|
||||
<div class="Tile-links">
|
||||
<a target="_blank" href="./step1-04/demo/">demo</a> |
|
||||
<a target="_blank" href="./step1-04/lesson/">lesson</a> | <a target="_blank" href="./step1-04/demo/">demo</a> |
|
||||
<a target="_blank" href="./step1-04/final/">final</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -87,7 +90,8 @@
|
|||
<div class="Tile-link">
|
||||
TypeScript Basics
|
||||
<div class="Tile-links">
|
||||
<a target="_blank" href="./step2-01/demo/">demo</a> | <a target="_blank" href="./step2-01/exercise/">exercise</a> | <a target="_blank" href="./step2-01/final/">final</a>
|
||||
<a target="_blank" href="./step2-01/demo/">demo</a> | <a target="_blank" href="./step2-01/exercise/">exercise</a> |
|
||||
<a target="_blank" href="./step2-01/final/">final</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -140,9 +144,7 @@
|
|||
Some Extra Bonus Lessons!! Yay! Bonus!!
|
||||
</li>
|
||||
<li class="Tile Tile--numbered">
|
||||
<a target="_blank" href="./bonus-servicecalls/demo/" class="Tile-link">
|
||||
Redux: Service Calls
|
||||
</a>
|
||||
<a target="_blank" href="./bonus-servicecalls/demo/" class="Tile-link"> Redux: Service Calls </a>
|
||||
</li>
|
||||
<li class="Tile Tile--numbered">
|
||||
<div class="Tile-link">
|
||||
|
|
|
@ -2,9 +2,13 @@ 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';
|
||||
import html from 'highlight.js/lib/languages/xml';
|
||||
import css from 'highlight.js/lib/languages/css';
|
||||
|
||||
hljs.registerLanguage('javascript', javascript);
|
||||
hljs.registerLanguage('typescript', typescript);
|
||||
hljs.registerLanguage('html', html);
|
||||
hljs.registerLanguage('css', css);
|
||||
|
||||
async function run() {
|
||||
const div = document.getElementById('markdownReadme');
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
<link rel="stylesheet" href="./style.css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
hr {
|
||||
margin: 40px;
|
||||
}
|
||||
|
@ -12,6 +15,48 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<section>
|
||||
<h2><a href="https://developer.mozilla.org/en-US/docs/Glossary/semantics" target="_blank" >Why Semantic HTML</a></h2>
|
||||
<div>
|
||||
<h3>Semantic</h3>
|
||||
<p>This is a paragraph about why semantic HTML is important.</p>
|
||||
<h4>A unordered list</h4>
|
||||
<ul>
|
||||
<li>Unordered item 1</li>
|
||||
<li>Another unordered item</li>
|
||||
<li>and another unordered item</li>
|
||||
</ul>
|
||||
<h4>An ordered list</h4>
|
||||
<ol>
|
||||
<li>Ordered item 1</li>
|
||||
<li>Another ordered item</li>
|
||||
<li>and another ordered item</li>
|
||||
</ol>
|
||||
<h4>Next up</h4>
|
||||
<a href="https://fluidframework.com/">Fluid Framework</a>
|
||||
</div>
|
||||
<br/><br/>
|
||||
<div>
|
||||
<div>Non Semantic</div>
|
||||
<div>This is a paragraph about why semantic HTML is important.</div>
|
||||
<div>A unordered list</div>
|
||||
<div>
|
||||
<div>Unordered item 1</div>
|
||||
<div>Another unordered item</div>
|
||||
<div>and another unordered item</div>
|
||||
</div>
|
||||
<div>An ordered list</div>
|
||||
<div>
|
||||
<div>Ordered item 1</div>
|
||||
<div>Another ordered item</div>
|
||||
<div>and another ordered item</div>
|
||||
</div>
|
||||
<div>Next up</div>
|
||||
<div onClick="location.href='https://fluidframework.com/'">Fluid Framework</a>
|
||||
</div>
|
||||
<br/>
|
||||
</section>
|
||||
<hr>
|
||||
<section>
|
||||
<h2><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Document_metadata">Document Metadata</a></h2>
|
||||
<pre>
|
||||
|
@ -173,6 +218,10 @@
|
|||
<label for="name">Enter your name: </label>
|
||||
<input type="text" name="name" id="name" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="name">Enter your password: </label>
|
||||
<input type="password" name="password" id="password" required />
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<input type="checkbox" id="option1" name="option1" checked />
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
# Step 1.1 - Introduction to HTML (Demo)
|
||||
# Step 1.1 - Introduction to HTML
|
||||
|
||||
[Demo](../demo/) | [Exercise](../exercise/)
|
||||
|
||||
## How the web works
|
||||
|
||||
|
@ -59,13 +61,9 @@ The [HTML demo page](https://microsoft.github.io/frontend-bootcamp/step1-01/demo
|
|||
</header>
|
||||
<main>
|
||||
<h2>About This Workshop</h2>
|
||||
<p>
|
||||
The first day provides an introduction to the fundamentals of the web: HTML, CSS and JavaScript.
|
||||
</p>
|
||||
<p>The first day provides an introduction to the fundamentals of the web: HTML, CSS and JavaScript.</p>
|
||||
<img src="../../assets/todo_screenshot.jpg" alt="Picture of the Todo App we will build" />
|
||||
<p>
|
||||
On the second day we'll dive into more advanced topics like TypeScript, testing, and state management.
|
||||
</p>
|
||||
<p>On the second day we'll dive into more advanced topics like TypeScript, testing, and state management.</p>
|
||||
</main>
|
||||
<footer>
|
||||
<h2>Get More Information</h2>
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../assets/step.css" />
|
||||
</head>
|
||||
<body class="ms-Fabric">
|
||||
<div id="markdownReadme" data-src="./README.md"></div>
|
||||
|
||||
<script src="../../assets/scripts.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -114,7 +114,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<h2>Our Logo</h2>
|
||||
<img src="../../assets/fabric.jpg" width="100" alt="fabric logo" />
|
||||
<img src="https://github.com/microsoft/frontend-bootcamp/blob/master/assets/fabric.jpg?raw=true" width="100" alt="fabric logo" />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Contact Us</h2>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# Step 1.2 - Introduction to CSS (Demo)
|
||||
|
||||
[Demo](../demo/) | [Exercise](../exercise/)
|
||||
|
||||
## CSS properties
|
||||
|
||||
Now that we've gone over adding HTML tags to the page, let's cover adding styles to those tags. We can do quite a lot with styles! We can change:
|
||||
|
@ -44,3 +46,5 @@ Here's a more detailed view from [Chris Eppstein](https://twitter.com/chriseppst
|
|||
<img src="https://raw.githubusercontent.com/Microsoft/frontend-bootcamp/master/assets/css-syntax.png"/>
|
||||
|
||||
A selector can be a single tag, class, ID, or attribute. It can also be a [combination](https://developer.mozilla.org/en-US/docs/Learn/CSS/Introduction_to_CSS/Combinators_and_multiple_selectors) of those elements.
|
||||
|
||||
Bonus: Check out the [Vocabs project](http://apps.workflower.fi/vocabs/css/en)
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../assets/step.css" />
|
||||
</head>
|
||||
<body class="ms-Fabric">
|
||||
<div id="markdownReadme" data-src="./README.md"></div>
|
||||
|
||||
<script src="../../assets/scripts.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -6,7 +6,7 @@ In this demo we are going to cover a few core basics of the language that will h
|
|||
|
||||
```html
|
||||
<div id="contact-form">
|
||||
<label for="email">Email</label><input id="email" type="email"/>
|
||||
<label for="email">Email</label><input id="email" type="email" />
|
||||
<input class="submit" value="Submit" type="submit" />
|
||||
</div>
|
||||
```
|
||||
|
@ -29,7 +29,7 @@ We can create a new variable with the keywords `var`, `let`, `const` and use the
|
|||
- **string**: `'single quotes'`, `"double quotes"`, or `` `backticks` ``
|
||||
- **array**: `[ 1, 2, 3, 'hello', 'world']`
|
||||
- **object**: `{ foo: 3, bar: 'hello' }`
|
||||
- **function**: `function(foo) { return foo + 1 }`
|
||||
- **function**: `function(foo) { return foo + 1 }` or `(foo) => { return foo + 1}`
|
||||
- **null**
|
||||
- **undefined**
|
||||
|
||||
|
@ -42,8 +42,8 @@ const myBoolean = true;
|
|||
const myNumber = 5;
|
||||
const myString = `Using backticks I can reuse other variables ${myNumber}`;
|
||||
const myArray = [1, 'cat', false, myString];
|
||||
const myObject = { key1: 'value1', anotherKey: myArray };
|
||||
const myFunction = function(myNumberParam) {
|
||||
const myObject = { key1: 'value1', anotherKey: myArray, lastKey: aFunction };
|
||||
const myFunction = (myNumberParam) => {
|
||||
console.log(myNumber + myNumberParam);
|
||||
};
|
||||
```
|
||||
|
@ -62,11 +62,11 @@ const match = 'a';
|
|||
|
||||
Functions are reusable pieces of functionality. Functions can take inputs (parameters) and return values (or neither). Functions can be called from within your program, from within other functions, or invoked from within the DOM itself.
|
||||
|
||||
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 displays an alert message box 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 `console.log("Clicked")`, which is a function that displays an alert message box in your browser.
|
||||
|
||||
```js
|
||||
function displayMatches() {
|
||||
alert("I'm Clicked");
|
||||
console.log('Clicked');
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -81,11 +81,11 @@ To execute a function we need to attach it to an event. There are a number of po
|
|||
To attach a function to an event, we use an [`addEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventListener) like this:
|
||||
|
||||
```js
|
||||
window.addEventListener('load', function() {
|
||||
window.addEventListener('load', function () {
|
||||
console.log('loaded');
|
||||
});
|
||||
|
||||
window.addEventListener('click', function() {
|
||||
window.addEventListener('click', function () {
|
||||
console.log('click');
|
||||
});
|
||||
```
|
||||
|
@ -97,10 +97,10 @@ window.addEventListener('click', function() {
|
|||
If you think this feels a little verbose, you're not alone. Many of the [most common event types](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers) are available as element properties. This way we can set properties like `onload` or `onclick` like this:
|
||||
|
||||
```js
|
||||
window.onload = function() {
|
||||
window.onload = function () {
|
||||
console.log('loaded!');
|
||||
};
|
||||
window.onclick = function() {
|
||||
window.onclick = function () {
|
||||
console.log('clicked!');
|
||||
};
|
||||
```
|
||||
|
@ -175,7 +175,6 @@ function displayMatches() {
|
|||
const email = document.getElementById('email');
|
||||
const text = email.value;
|
||||
console.log(text);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../assets/step.css" />
|
||||
</head>
|
||||
<body class="ms-Fabric">
|
||||
<div id="markdownReadme" data-src="./README.md"></div>
|
||||
|
||||
<script src="../../assets/scripts.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -21,7 +21,7 @@
|
|||
background: green;
|
||||
}
|
||||
</pre>
|
||||
<pre data-lang="javascript">
|
||||
<pre data-lang="typescript">
|
||||
ReactDOM.render(
|
||||
<div>Hello World</div>,
|
||||
document.getElementById('app')
|
||||
|
|
|
@ -1,27 +1,14 @@
|
|||
import React from 'react';
|
||||
import { Button } from './Button';
|
||||
|
||||
export class Counter extends React.Component<any, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
clicks: 0
|
||||
};
|
||||
}
|
||||
render() {
|
||||
const { text } = this.props;
|
||||
const { clicks } = this.state;
|
||||
return (
|
||||
<div>
|
||||
{text}: {clicks}
|
||||
<Button onClick={this._onButtonClick}>Click</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_onButtonClick = () => {
|
||||
this.setState({
|
||||
clicks: this.state.clicks + 1
|
||||
});
|
||||
};
|
||||
export const Counter = props => {
|
||||
const [clicks, setClicks] = React.useState(0);
|
||||
const handleClick = () => setClicks(clicks + 1);
|
||||
const { text } = props;
|
||||
return (
|
||||
<div>
|
||||
{text}: {clicks}
|
||||
<Button onClick={handleClick}>Click</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,24 +21,10 @@ The first parameter to `render()` looks a lot like HTML, but actually, it's [JSX
|
|||
|
||||
## Writing a React component
|
||||
|
||||
A React component is a piece of code that returns a portion of your application. This can include HTML markup, CSS styles, and JavaScript driven functionality.
|
||||
|
||||
Components can be created in two ways. The first is method is to use a [JavaScript class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes), which extends (inherits from) the `React.Component` class.
|
||||
|
||||
Classes in JavaScript provide a way to collect methods (functions) and properties (values) in an extensible container. We extend `React.Component` because it provides us with several built-in methods, including `render`.
|
||||
A React component is a function that returns a portion of your application. This can include HTML markup, CSS styles, and JavaScript driven functionality.
|
||||
|
||||
```jsx
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return <p>Hello World</p>;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We could also write this component as a function:
|
||||
|
||||
```jsx
|
||||
const App = props => {
|
||||
const App = (props) => {
|
||||
return <p>Hello World</p>;
|
||||
};
|
||||
```
|
||||
|
@ -49,20 +35,18 @@ Moving our "Hello World" markup into our App's `render` function, we can now upd
|
|||
ReactDOM.render(<App />, document.getElementById('app'));
|
||||
```
|
||||
|
||||
> Note that React components can be reused by writing them in the same way you would an HTML tag.
|
||||
|
||||
### Props
|
||||
|
||||
Whether you write the component as a class or a function, it can take in additional props using the same syntax as HTML attributes like `id` or `href`.
|
||||
A component can take in additional props using the same syntax as HTML attributes like `id` or `href`.
|
||||
|
||||
```jsx
|
||||
<App text="Hello World" />
|
||||
```
|
||||
|
||||
The `text` prop can be accessed inside your component via `props.text` in a function component or `this.props.text` in a class component.
|
||||
The `text` prop can be accessed inside your component via `props.text` in a component.
|
||||
|
||||
```jsx
|
||||
const App = props => {
|
||||
const App = (props) => {
|
||||
return <p>{props.text}</p>;
|
||||
};
|
||||
```
|
||||
|
@ -82,17 +66,17 @@ ReactDOM.render(
|
|||
> Note that a render function can only return a single element, so our two `App` components need to be wrapped in a `div`.
|
||||
|
||||
```jsx
|
||||
const App = props => {
|
||||
const App = (props) => {
|
||||
return <p>{props.text ? props.text : 'oops!'}</p>;
|
||||
};
|
||||
```
|
||||
|
||||
### Destructuring props
|
||||
|
||||
Writing `props.text` over and over in a function (or `this.props.text` in a class) can be quite tedious. Since this is all JavaScript, you could create a new variable for this text using variable assignment.
|
||||
Writing `props.text` over and over in a function can be quite tedious. Since this is all JavaScript, you could create a new variable for this text using variable assignment.
|
||||
|
||||
```jsx
|
||||
const App = props => {
|
||||
const App = (props) => {
|
||||
const text = props.text;
|
||||
return <p>{text ? text : 'you missed something'}</p>;
|
||||
};
|
||||
|
@ -101,15 +85,15 @@ const App = props => {
|
|||
This works fine for a single prop, but as your component starts to become more complex:
|
||||
|
||||
```jsx
|
||||
<MyComponent
|
||||
<App
|
||||
open={false}
|
||||
count={5}
|
||||
text="Hi there"
|
||||
text="Hello World"
|
||||
items={['cat', 'dog', 'bird']}
|
||||
config={{
|
||||
start: 1,
|
||||
end: 10,
|
||||
autoStart: true
|
||||
autoStart: true,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
@ -123,8 +107,6 @@ const open = props.open;
|
|||
const text = props.text;
|
||||
const count = props.count;
|
||||
const items = props.items;
|
||||
const start = props.config.start;
|
||||
const end = props.config.end;
|
||||
```
|
||||
|
||||
A common approach to simplify this process is to use a syntax called [destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring).
|
||||
|
@ -132,13 +114,7 @@ A common approach to simplify this process is to use a syntax called [destructur
|
|||
Destructuring allows you to pull individual pieces of information out of an object in a single statement.
|
||||
|
||||
```jsx
|
||||
const {
|
||||
open,
|
||||
text,
|
||||
count,
|
||||
items,
|
||||
config: { start, end }
|
||||
} = props;
|
||||
const { open, text, count, items } = props;
|
||||
```
|
||||
|
||||
So while this might be overkill right now, it makes it easier to add props down the road.
|
||||
|
@ -150,7 +126,7 @@ Before we move on, we'll modify our `ReactDOM.render` call to just include our A
|
|||
Next we'll be creating a `Counter` component. We'll add that to our App now, and then start to write the control.
|
||||
|
||||
```jsx
|
||||
const App = props => {
|
||||
const App = (props) => {
|
||||
return <Counter text="chickens" />;
|
||||
};
|
||||
|
||||
|
@ -161,70 +137,51 @@ ReactDOM.render(<App />, document.getElementById('app'));
|
|||
|
||||
## Writing a stateful Counter component
|
||||
|
||||
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.
|
||||
|
||||
> Most stateful components you'll see today will be `class` based. It is just recently possible to add state to function components through the use of [`hooks`](https://reactjs.org/docs/hooks-intro.html)
|
||||
The power of React, past being a good templating language, is that it provides us a way to maintain and modify state over the componet's lifecycle.
|
||||
|
||||
### Adding state
|
||||
|
||||
JavaScript classes use a `constructor` method to instantiate each copy of a class, along with any applicable state. Let's create a new component called `Counter` and give it a state containing a `clicks` property with a default value of `0`;
|
||||
State is added to a component by using the `useState` hook. [Hooks](https://reactjs.org/docs/hooks-intro.html) are special React methods that can only be called within a React component, and provide ways to maintain state and perform other lifecycle methods.
|
||||
|
||||
```js
|
||||
class Counter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
clicks: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
const Counter = (props) => {
|
||||
const [clicks, setClicks] = React.useState(0);
|
||||
};
|
||||
```
|
||||
|
||||
- The constructor takes in the component's `props`.
|
||||
- The [`super()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super) function calls the constructor of the parent class (in this case `React.Component`).
|
||||
- Our `clicks` state value can now be accessed via `this.state.clicks`. Later, we can update state by calling `this.setState({ clicks: 1 })`.
|
||||
- The component takes in some`props`.
|
||||
- `clicks` is a stateful value that will be updated each time `setClicks` is called with a new value
|
||||
|
||||
### Rendering our Counter
|
||||
|
||||
For our `Counter` component, the goal is to be able to track how many times the counter's button is clicked. We'll use the following markup.
|
||||
|
||||
```jsx
|
||||
render() {
|
||||
const {text} = this.props;
|
||||
const {clicks} = this.state;
|
||||
return (
|
||||
<div>
|
||||
{text}: {clicks}
|
||||
<button>Click</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const { text } = props;
|
||||
return (
|
||||
<div>
|
||||
{text}: {clicks}
|
||||
<button>Click</button>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
### Writing our button click handler
|
||||
|
||||
Our next step is to wire up the button to increment the `clicks` in our component state.
|
||||
|
||||
> By convention we place other methods below `render()`, and private methods (those for internal use only) are prefixed with an underscore.
|
||||
|
||||
This function will update our component's state, incrementing the clicks value by 1. (Note that `setState` only modifies the values of keys listed in the object passed as its parameter.)
|
||||
This function will increment the clicks value by 1.
|
||||
|
||||
```jsx
|
||||
_onButtonClick = () => {
|
||||
this.setState({
|
||||
clicks: this.state.clicks + 1
|
||||
});
|
||||
const handleClick = () => {
|
||||
setClicks(clicks + 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 the `setState` call could also be written as `this.setState(prevState => ({ clicks: prevState.clicks + 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.
|
||||
|
||||
```jsx
|
||||
<button onClick={this._onButtonClick}>Click</button>
|
||||
<button onClick={handleClick}>Click</button>
|
||||
```
|
||||
|
||||
> Also note that each `Counter` maintains its own state! You can modify the state inside of one counter without affecting the others.
|
||||
|
@ -242,9 +199,9 @@ To scale our application, we'll need to break up the file into smaller, reusable
|
|||
Open up `step1-04/final/components/Counter.tsx` and look at the `Counter` component.
|
||||
|
||||
```tsx
|
||||
export class Counter extends React.Component {
|
||||
export const Counter = (props) => {
|
||||
// ...
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
This file exports the Counter component as a **named export**. This means when we import it we do the following:
|
||||
|
@ -260,7 +217,7 @@ import { Counter } from './components/Counter';
|
|||
We typically use named exports, but it's also possible export a default value like this:
|
||||
|
||||
```tsx
|
||||
export default class Counter extends React.Component {
|
||||
export default const Counter = (props) =>{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
@ -279,7 +236,7 @@ Buttons are among the most commonly written components. Custom buttons help abst
|
|||
import React from 'react';
|
||||
import './Button.css';
|
||||
|
||||
export const Button = props => {
|
||||
export const Button = (props) => {
|
||||
return (
|
||||
<button className="Button" onClick={props.onClick}>
|
||||
{props.children}
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../assets/step.css" />
|
||||
</head>
|
||||
<body class="ms-Fabric">
|
||||
<div id="markdownReadme" data-src="./README.md"></div>
|
||||
|
||||
<script src="../../assets/scripts.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -11,31 +11,29 @@ You can find the HTML for our application in `step1-05/TodoApp.html`.
|
|||
|
||||
## TodoHeader
|
||||
|
||||
We'll store all of our components inside a `components` folder under `src`. Let's create that now. We'll then start writing the `TodoHeader` in `src/components/TodoHeader.tsx`. The `tsx` file extension tells our editor that this file includes React code written in TypeScript.
|
||||
We'll store all of our components inside a `components` folder under `step1-05/demo/src`. Let's create that now. We'll then start writing the `TodoHeader` in `src/components/TodoHeader.tsx`. The `tsx` file extension tells our editor 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
|
||||
import React from 'react';
|
||||
|
||||
export class TodoHeader extends React.Component<any, any> {
|
||||
render() {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
export const TodoHeader = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
> Note that since this is React, we had to change `class` to `className`, but nothing else changes.
|
||||
|
@ -47,28 +45,13 @@ Any time you see repeated complex elements, that is usually a sign that you shou
|
|||
```jsx
|
||||
import React from 'react';
|
||||
|
||||
export class TodolistItem extends React.Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" /> Todo 1
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
export const TodolistItem = () => {
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" /> Todo 1
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
> Note that this control could also be created as a function instead of a class:
|
||||
> ```jsx
|
||||
> export const TodoListItem = (props) => {
|
||||
> return (
|
||||
> <li className="todo">
|
||||
> <label>
|
||||
> <input type="checkbox" /> Todo 1
|
||||
> </label>
|
||||
> </li>
|
||||
> );
|
||||
> }
|
||||
> ```
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
export class TodoApp extends React.Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p>Hello World</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const TodoApp = () => {
|
||||
return (
|
||||
<div>
|
||||
<p>Hello World</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,13 +6,13 @@ If you don't already have the app running, start it by running `npm start` from
|
|||
|
||||
## TodoFooter
|
||||
|
||||
1. Add a TodoFooter component in the `components` folder, copying over the `<footer>` tag and all of its children from `TodoApp.html` in the `step1-05` folder. This component could be a function or class.
|
||||
2. Remove any `onclick` properties, and change `class` to `className`
|
||||
1. Add a TodoFooter component in the `step1-05/exercise/src/components` folder.
|
||||
2. Create a react component that returns the footer markup from `step1-05/TodoApp.html`. Make sure to import React, export the component, and change `class` to `className`.
|
||||
|
||||
## TodoList
|
||||
|
||||
1. Add a TodoList component like you did with the footer. This could also be function or class.
|
||||
2. Import TodoListItem and add four of them inside of the `<ul>`
|
||||
1. Add a TodoList component like you did with the footer.
|
||||
2. Import TodoListItem and add four of them inside of the `<ul>` (we'll be using live data later)
|
||||
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]`
|
||||
|
||||
## App
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import React from 'react';
|
||||
import { TodoHeader } from './components/TodoHeader';
|
||||
|
||||
export class TodoApp extends React.Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const TodoApp = () => {
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
import React from 'react';
|
||||
|
||||
export class TodoHeader extends React.Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<header>
|
||||
<h1>todos <small>(1.5 exercise)</small></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>
|
||||
);
|
||||
}
|
||||
export const TodoHeader = () => {
|
||||
return (
|
||||
<header>
|
||||
<h1>todos <small>(1.5 exercise)</small></h1>
|
||||
<div className="addTodo">
|
||||
<input className="textfield" placeholder="add todo" />
|
||||
<button className="submit">Add</button>
|
||||
</div>
|
||||
<nav className="filter">
|
||||
<button className="selected">all</button>
|
||||
<button>active</button>
|
||||
<button>completed</button>
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import React from "react";
|
||||
|
||||
export class TodoListItem extends React.Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" /> Todo 1
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
export const TodoListItem = () => {
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" /> Todo 1
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,71 +2,72 @@
|
|||
|
||||
In React, the data travels in one direction: top-down in the form of state propagating down the component hierarchy. Only the component containing the state can change the state itself. When a UI interaction occurs, a stateful component must pass down an event handler to the UI component triggering the event in order to signal a state change.
|
||||
|
||||
[Step #3 of "Thinking in React"](https://reactjs.org/docs/thinking-in-react.html) suggests finding the "minimal set of mutable state" that your application requires. So in this demo we are going to add that "minimal state" to our application and drive our UI off of that data. With that done, the next step will be to create ways to modify that state, which will in turn cascade down through our UI. This [reconciliation](https://reactjs.org/docs/reconciliation.html) process, figuring out what in your UI needs to change based on changing state, is what React excels at.
|
||||
[Step #3 of "Thinking in React"](https://reactjs.org/docs/thinking-in-react.html#step-3-identify-the-minimal-but-complete-representation-of-ui-state) suggests finding the "minimal set of mutable state" that your application requires. What pieces of state can we identify?
|
||||
|
||||
[Step #4 of "Thinking in React"](https://reactjs.org/docs/thinking-in-react.html#step-4-identify-where-your-state-should-live) asks us to think about where our state should live.
|
||||
|
||||
- Is the state local to a single component?
|
||||
- Is the state derived from another state?
|
||||
- Is the state primarily in one component but shared with others?
|
||||
- Is the state global?
|
||||
|
||||
## Adding state to TodoApp
|
||||
|
||||
Inside our `TodoApp` class, we will add the minimal state for our application, which includes just two keys: `todos` and `filter`. We don't need to worry about a `remaining` count because it can be calculated by counting the number of todos where the `completed` field is set to `false`.
|
||||
|
||||
So here is our full constructor:
|
||||
Inside of our `TodoApp` component we only need to track two pieces of state, our `todos` and the current `filter`. We don't need to worry about a `remaining` count because it can be calculated by counting the number of todos where `status` is set to `active`.
|
||||
|
||||
```jsx
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
todos: {
|
||||
'04': {
|
||||
label: 'Todo 4',
|
||||
completed: true
|
||||
},
|
||||
'03': {
|
||||
label: 'Todo 3',
|
||||
completed: false
|
||||
},
|
||||
'02': {
|
||||
label: 'Todo 2',
|
||||
completed: false
|
||||
},
|
||||
'01': {
|
||||
label: 'Todo 1',
|
||||
completed: false
|
||||
}
|
||||
export const TodoApp = () => {
|
||||
const [filter, setFilter] = React.useState<FilterTypes>('all');
|
||||
const [todos, setTodos] = React.useState<Todos>([
|
||||
{
|
||||
id: '04',
|
||||
label: 'Todo 4',
|
||||
status: 'completed',
|
||||
},
|
||||
filter: 'active'
|
||||
};
|
||||
}
|
||||
```
|
||||
{
|
||||
id: '03',
|
||||
label: 'Todo 3',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '02',
|
||||
label: 'Todo 2',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '01',
|
||||
label: 'Todo 1',
|
||||
status: 'active',
|
||||
},
|
||||
]);
|
||||
|
||||
> You could also use an array to represent your todos. Array manipulation can be easier in some cases, but this object approach simplifies other functionality and will ultimately be more performant.
|
||||
```
|
||||
|
||||
## Passing state through to UI
|
||||
|
||||
Now we can pass `filter` and `todos` into our components.
|
||||
|
||||
```jsx
|
||||
render() {
|
||||
const { filter, todos } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader filter={filter} />
|
||||
<TodoList todos={todos} filter={filter} />
|
||||
<TodoFooter todos={todos} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader filter={filter} />
|
||||
<TodoList todos={todos} filter={filter} />
|
||||
<TodoFooter todos={todos} />
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
## State-driven TodoList
|
||||
|
||||
I've already pulled out our props into `filter` and `todos` variables, and written a bit of JS that will return an array of filtered todo `id`s. We'll be using that filtered array to render our todo items.
|
||||
I've already pulled out our props into `filter` and `todos` variables, and written a bit of JS that will return an array of filtered todos. We'll be using that filtered array to render our todo items.
|
||||
|
||||
> `todos[id]` returns the todo matching the `id` passed in, and the spread operator (...) is the same as saying `label={todos[id].label} completed={todos[id].completed}`
|
||||
React requires any dynamic length list to have unique `key` properties, for which we can use the `todo.id`. This key helps React to only re-render the parts of the list that changes.
|
||||
|
||||
```jsx
|
||||
return (
|
||||
<ul className="todos">
|
||||
{filteredTodos.map(id => (
|
||||
<TodoListItem key={id} id={id} {...todos[id]} />
|
||||
{filteredTodos.map((todo) => (
|
||||
<TodoListItem key={todo.id} {...todo} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
@ -74,7 +75,7 @@ return (
|
|||
|
||||
## State-driven and stateful TodoHeader
|
||||
|
||||
Within the header, we've got a situation where we not only want to pass `filter` state down to it, but we also want to maintain state within the control. Fortunately, this is no problem at all for React. First off let's deal with the incoming state.
|
||||
In `TodoHeader.tsx` we are going to both display the selected filter state, and track the text for a new todo.
|
||||
|
||||
### Conditional class names
|
||||
|
||||
|
@ -82,21 +83,21 @@ In CSS-based styling, visual states are applied by adding and removing classes.
|
|||
|
||||
```jsx
|
||||
<nav className="filter">
|
||||
<button className={filter === 'all' ? 'selected' : ''}>all</button>
|
||||
<button className={filter === 'active' ? 'selected' : ''}>active</button>
|
||||
<button className={filter === 'completed' ? 'selected' : ''}>completed</button>
|
||||
<button className={filter === 'all' ? 'selected' : ''}> all</button>
|
||||
<button className={filter === 'active' ? 'selected' : ''}> active</button>
|
||||
<button className={filter === 'completed' ? 'selected' : ''}> completed</button>
|
||||
</nav>
|
||||
```
|
||||
|
||||
> The [ternary operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator) `condition ? expressionIfTrue : expressionIfFalse` is widely used in React code, as each expression could be a string for a className or even a JSX element.
|
||||
> The [ternary operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator) `condition ? ifTrue : ifFalse` is often used to conditionally render a string or JSX element. In the case that the condition is false the `className` is simply ommited.
|
||||
|
||||
### Adding a controlled input
|
||||
|
||||
In React, form elements such as `<input>`, `<textarea>`, and `<select>` can be used as either **uncontrolled** or **controlled**.
|
||||
|
||||
An **uncontrolled input** maintains its current value internally and updates it based on user interactions (entering text, choosing options, etc). The code only pulls the value from the input when it's needed, such as on submit. This is similar to how inputs in a plain HTML form work.
|
||||
An **uncontrolled input** maintains its current value internally and updates that value based on user interactions (entering text, choosing options, etc). Our code only polls the value from the input when it's needed, such as on submit. This is similar to how inputs in a plain HTML form work.
|
||||
|
||||
A **controlled input** takes its current value from a prop and use a callback to notify the parent component of changes made by the user. The input's value doesn't change until the parent component updates the input's props in response to the callback.
|
||||
A **controlled input** takes its current value from a prop or state and uses a callback to modify that value when a change is made by the user. This is usually the prefered method when writing React.
|
||||
|
||||
> The distinction between controlled and uncontrolled is important to understand when writing or using form components, and misunderstandings of this concept are a very common source of bugs. See [this article](https://goshakkk.name/controlled-vs-uncontrolled-inputs-react/) for a more detailed explanation.
|
||||
|
||||
|
@ -105,21 +106,21 @@ Let's try changing the text field in our `TodoHeader` component to a controlled
|
|||
1. A state variable to hold the input's value:
|
||||
|
||||
```jsx
|
||||
this.state = { labelInput: '' };
|
||||
const [inputText, setInputText] = React.useState('');
|
||||
```
|
||||
|
||||
2. A callback function to update that value:
|
||||
|
||||
```jsx
|
||||
_onChange = evt => {
|
||||
this.setState({ labelInput: evt.target.value });
|
||||
const onInput = (e) => {
|
||||
setInputText(e.target.value);
|
||||
};
|
||||
```
|
||||
|
||||
With those two pieces in place, we can update our uncontrolled input to being controlled.
|
||||
|
||||
```jsx
|
||||
<input value={this.state.labelInput} onChange={this._onChange} className="textfield" placeholder="add todo" />
|
||||
<input value={inputText} onChange={onInput} className="textfield" placeholder="add todo" />
|
||||
```
|
||||
|
||||
> If you have React Dev Tools installed, open them up and take a look at `labelInput` as we type in the input.
|
||||
|
|
|
@ -3,14 +3,12 @@ import { TodoFooter } from './components/TodoFooter';
|
|||
import { TodoHeader } from './components/TodoHeader';
|
||||
import { TodoList } from './components/TodoList';
|
||||
|
||||
export class TodoApp extends React.Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader />
|
||||
<TodoList />
|
||||
<TodoFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const TodoApp = () => {
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader />
|
||||
<TodoList />
|
||||
<TodoFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,30 +1,25 @@
|
|||
import React from 'react';
|
||||
|
||||
export class TodoHeader extends React.Component<any, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { labelInput: '' };
|
||||
}
|
||||
export const TodoHeader = (props) => {
|
||||
|
||||
render() {
|
||||
const { filter } = this.props;
|
||||
return (
|
||||
<header>
|
||||
<h1>todos <small>(1.6 demo)</small></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>
|
||||
);
|
||||
}
|
||||
const [inputText, setInputText] = React.useState('')
|
||||
const { filter } = props;
|
||||
|
||||
_onChange = evt => {
|
||||
this.setState({ labelInput: evt.target.value });
|
||||
const handleChange = e => {
|
||||
setInputText(e.target.value);
|
||||
};
|
||||
return (
|
||||
<header>
|
||||
<h1>todos <small>(1.6 demo)</small></h1>
|
||||
<div className="addTodo">
|
||||
<input className="textfield" placeholder="add todo" />
|
||||
<button className="submit">Add</button>
|
||||
</div>
|
||||
<nav className="filter">
|
||||
<button className="selected">all</button>
|
||||
<button>active</button>
|
||||
<button>completed</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
import React from 'react';
|
||||
import { TodoListItem } from './TodoListItem';
|
||||
|
||||
export class TodoList extends React.Component<any, any> {
|
||||
render() {
|
||||
const { filter, todos = {} } = this.props;
|
||||
export const TodoList = (props) => {
|
||||
const { filter, todos = [] } = props;
|
||||
|
||||
// filteredTodos returns an array of filtered todo keys [01,02,03]
|
||||
const filteredTodos = Object.keys(todos).filter(id => {
|
||||
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
|
||||
});
|
||||
const filteredTodos = todos.filter(todo => {
|
||||
return filter === 'all'
|
||||
|| (filter === 'completed' && todo.status === 'completed')
|
||||
|| (filter === 'active' && todo.status === 'active');
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="todos">
|
||||
{['01', '02', '03', '04'].map((id) => <TodoListItem />)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ul className="todos">
|
||||
{['01', '02', '03', '04'].map((todo) => <TodoListItem />)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import React from "react";
|
||||
|
||||
export class TodoListItem extends React.Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" /> Todo 1
|
||||
export const TodoListItem = () => {
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" /> Todo 1
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,6 @@ If you don't already have the app running, start it by running `npm start` from
|
|||
|
||||
### TodoListItem
|
||||
|
||||
1. Pull in `label` and `completed` from props using [destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring)
|
||||
2. Set the todo's text to `label` and the `checked` prop to `completed`
|
||||
1. Pull in `label` and `status` from props using [destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring)
|
||||
2. Set the `input` text to `label` and the `checked` prop to `true` if `status === 'completed`
|
||||
> Note that this is only half the work we need to do to make these controlled inputs work. What is the other half?
|
||||
|
|
|
@ -3,39 +3,37 @@ import { TodoFooter } from './components/TodoFooter';
|
|||
import { TodoHeader } from './components/TodoHeader';
|
||||
import { TodoList } from './components/TodoList';
|
||||
|
||||
export class TodoApp extends React.Component<any, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
todos: {
|
||||
'04': {
|
||||
label: 'Todo 4',
|
||||
completed: true
|
||||
},
|
||||
'03': {
|
||||
label: 'Todo 3',
|
||||
completed: false
|
||||
},
|
||||
'02': {
|
||||
label: 'Todo 2',
|
||||
completed: false
|
||||
},
|
||||
'01': {
|
||||
label: 'Todo 1',
|
||||
completed: false
|
||||
}
|
||||
},
|
||||
filter: 'all'
|
||||
};
|
||||
}
|
||||
render() {
|
||||
const { filter, todos = [] } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader filter={filter} />
|
||||
<TodoList todos={todos} filter={filter} />
|
||||
<TodoFooter todos={todos} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const defaultTodos = [
|
||||
{
|
||||
id: '04',
|
||||
label: 'Todo 4',
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
id: '03',
|
||||
label: 'Todo 3',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '02',
|
||||
label: 'Todo 2',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '01',
|
||||
label: 'Todo 1',
|
||||
status: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
export const TodoApp = () => {
|
||||
const [filter, setFilter] = React.useState('all');
|
||||
const [todos, setTodos] = React.useState(defaultTodos);
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader filter={filter} />
|
||||
<TodoList todos={todos} filter={filter} />
|
||||
<TodoFooter todos={todos} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
export const TodoFooter = (props: any) => {
|
||||
const itemCount = Object.keys(props.todos).filter(id => !props.todos[id].completed).length;
|
||||
export const TodoFooter = (props) => {
|
||||
const itemCount = props.todos.filter((todo) => todo.status === 'active').length;
|
||||
return (
|
||||
<footer>
|
||||
<span>4 items left</span>
|
||||
|
|
|
@ -1,30 +1,25 @@
|
|||
import React from 'react';
|
||||
|
||||
export class TodoHeader extends React.Component<any, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { labelInput: '' };
|
||||
}
|
||||
export const TodoHeader = (props) => {
|
||||
const [inputText, setInputText] = React.useState<string>('');
|
||||
const { filter } = props;
|
||||
|
||||
render() {
|
||||
const { filter } = this.props;
|
||||
|
||||
return (
|
||||
<header>
|
||||
<h1>todos <small>(1.6 exercise)</small></h1>
|
||||
<div className="addTodo">
|
||||
<input value={this.state.labelInput} onChange={this._onChange} className="textfield" placeholder="add todo" />
|
||||
<button className="submit">Add</button>
|
||||
</div>
|
||||
<nav className="filter">
|
||||
<button className={filter === 'all' ? 'selected' : ''}>all</button>
|
||||
<button className={filter === 'active' ? 'selected' : ''}>active</button>
|
||||
<button className={filter === 'completed' ? 'selected' : ''}>completed</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
_onChange = evt => {
|
||||
this.setState({ labelInput: evt.target.value });
|
||||
const onInput = e => {
|
||||
setInputText(e.target.value);
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<header>
|
||||
<h1>todos <small>(1.6 exercise)</small></h1>
|
||||
<div className="addTodo">
|
||||
<input value={inputText} onChange={onInput} className="textfield" placeholder="add todo" />
|
||||
<button className="submit">Add</button>
|
||||
</div>
|
||||
<nav className="filter">
|
||||
<button className={filter === 'all' ? 'selected' : ''}> all</button>
|
||||
<button className={filter === 'active' ? 'selected' : ''}>active</button>
|
||||
<button className={filter === 'completed' ? 'selected' : ''}>completed</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
|
@ -1,20 +1,21 @@
|
|||
import React from 'react';
|
||||
import { TodoListItem } from './TodoListItem';
|
||||
|
||||
export class TodoList extends React.Component<any, any> {
|
||||
render() {
|
||||
const { filter, todos } = this.props;
|
||||
export const TodoList = (props) => {
|
||||
const { filter, todos } = props;
|
||||
|
||||
// filteredTodos returns an array of filtered todo keys [01,02,03]
|
||||
const filteredTodos = Object.keys(todos).filter(id => {
|
||||
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
|
||||
});
|
||||
return (
|
||||
<ul className="todos">
|
||||
{filteredTodos.map(id => (
|
||||
<TodoListItem key={id} id={id} {...todos[id]} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
const filteredTodos = todos.filter((todo) => {
|
||||
if (todo.status === 'cleared') return false;
|
||||
return filter === 'all' ||
|
||||
(filter === 'completed' && todo.status === 'completed') ||
|
||||
(filter === 'active' && todo.status === 'active');
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="todos">
|
||||
{filteredTodos.map((todo) => (
|
||||
<TodoListItem key={todo.id} {...todo} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
export class TodoListItem extends React.Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" /> Todo 1
|
||||
export const TodoListItem = (props) => {
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" /> Todo 1
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
# Step 1.7 - Types and creating a UI-driven state (Demo)
|
||||
|
||||
Now that we have a UI that is purely driven by the state of our app, we need to add functionality to allow the UI to drive the state. This is often done by creating functions that call `setState` like we saw in the `TodoHeader`. Values from the state are then passed down child components as props.
|
||||
Now that we have a UI that is purely driven by the state of our app, we need to add functionality to allow the UI to modify the state. This is our core "business logic" and handles our basic "CRUD" operations: Create, Read, Update, Delete.
|
||||
|
||||
> We'll be learning in part 2 of this workshop how we can expose these functions without explicitly passing them down via props.
|
||||
|
||||
This is our core "business logic" and handles our basic "CRUD" operations: Create, Read, Update, Delete. We don't have time to walk through writing all of those functions, but you can see that they are already provided in the demo's `TodoApp` and passed into our components.
|
||||
This step in "Thinking in React" is called [Step 5: Add Inverse Data Flow](https://reactjs.org/docs/thinking-in-react.html#step-5-add-inverse-data-flow). Lets start by looking at the `TodoApp.tsx` and seeing how our components are going to be able to interact with app state.
|
||||
|
||||
## Intro to TypeScript
|
||||
|
||||
Taking a look at our components in `TodoApp`, you can see that our list of props is getting not just longer, but much more complex! We're passing through functions with various signatures, complex `todos` objects, and filter strings which are always one of three values.
|
||||
|
||||
As applications grow, it becomes difficult to remember what each function does or what each todo contains. Also, as JavaScript is a dynamically typed language, if I wanted to change the value of `todos` to an array inside my `TodoList`, JavaScript wouldn't care. But if `TodoListItems` was expecting an object, our application would break.
|
||||
As applications grow, it becomes difficult to remember what each function does or what each todo contains. Also, as JavaScript is a dynamically typed language, if I wanted to change the value of `filter` to a boolean, JavaScript wouldn't care. But if `TodoHeader` was expecting a string, our application would break.
|
||||
|
||||
For these two reasons, the industry is shifting to writing applications that are strongly typed, and many are using TypeScript to accomplish that.
|
||||
|
||||
|
@ -24,30 +22,28 @@ Let's dive in and see how TypeScript can help clarify our component props and gu
|
|||
|
||||
# Demo
|
||||
|
||||
Let's start off in the TodoList, as that has the most data flow up and down. There isn't any interactive UI in this component, as we're simply passing `completed` down to each `TodoListItem`, but we can write a props interface to make sure that everything gets passed down properly.
|
||||
Let's start off in the TodoList, as that has the most data flow up and down. There isn't any interactive UI in this component, as we're simply passing our `todo` down to each `TodoListItem`, but we can write a props interface to make sure that everything gets passed down properly.
|
||||
|
||||
## Writing TodoListProps
|
||||
|
||||
Looking at our `TodoApp` we know that `TodoList` has three props: `filter`, `todos`, and `complete`. We'll start by creating an interface called `TodoListProps` that represents this component's props.
|
||||
Looking at our `TodoApp` we know that `TodoList` has three props: `filter`, `todos`, and `toggleCompleted`. We'll start by creating an interface called `TodoListProps` that represents this component's props.
|
||||
|
||||
```ts
|
||||
interface TodoListProps {
|
||||
filter: any;
|
||||
toggleCompleted: any;
|
||||
todos: any;
|
||||
complete: any;
|
||||
}
|
||||
```
|
||||
|
||||
> Note that we're using the `any` keyword for now. This won't give us any type safety, but it does let us specify valid prop names we can pass to this component.
|
||||
|
||||
With that interface written, we'll add it to our component class.
|
||||
With that interface written, we'll add it to our component.
|
||||
|
||||
```ts
|
||||
export class TodoList extends React.Component<TodoListProps, any>
|
||||
export const TodoList = (props: TodoListProps) => {
|
||||
```
|
||||
|
||||
> Note that the first value in `<>` is for a props interface, and the second is for state.
|
||||
|
||||
Now that we have a typed component, let's go back to our `TodoApp` and see what happens if we try to change the name of a prop.
|
||||
|
||||
## Adding type safety
|
||||
|
@ -61,8 +57,8 @@ We know that `filter` shouldn't be an object, array or function, so we can speci
|
|||
```ts
|
||||
interface TodoListProps {
|
||||
filter: string;
|
||||
toggleCompleted: any;
|
||||
todos: any;
|
||||
complete: any;
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -71,8 +67,8 @@ But since we know that the filter can be only one of three values, we can make t
|
|||
```ts
|
||||
interface TodoListProps {
|
||||
filter: 'all' | 'active' | 'completed';
|
||||
toggleCompleted: any;
|
||||
todos: any;
|
||||
complete: any;
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -80,39 +76,38 @@ Now try going back to `TodoApp` and changing the `filter` attribute in `TodoList
|
|||
|
||||
### Complete Type
|
||||
|
||||
The `complete` prop isn't data, but a function. Fortunately, TypeScript can handle function types just as well as data.
|
||||
The `toggleComplete` prop isn't data, but a function.
|
||||
|
||||
```ts
|
||||
interface TodoListProps {
|
||||
filter: 'all' | 'active' | 'completed';
|
||||
toggleCompleted: (id: string) => void;
|
||||
todos: any;
|
||||
complete: (id: string) => void;
|
||||
}
|
||||
```
|
||||
|
||||
For functions we are only concerned with the parameters passed in and the return value. You can see in the example above that the function takes in an `id` of type string and returns `void`, which means it has no returned value.
|
||||
For functions we are concerned with the parameters passed in as well as returned. You can see in the example above that the function takes in an `id` of type string and returns `void`, which means it has no returned value.
|
||||
|
||||
> Technically, all functions in JavaScript return `undefined` if no other return value is specified, but declaring a return type of `void` causes TypeScript to error if you try to return a value from the function (or use its default returned value of `undefined`).
|
||||
|
||||
## Todos Type
|
||||
|
||||
The `todos` prop is interesting in that `todos` is an object with a bunch of unknown keys. So here's what that interface would look like.
|
||||
The `todos` prop is an array of objects where each of those objects represent a `todo`. For now we'll write that `todo` interface right into the list props;
|
||||
|
||||
```ts
|
||||
interface TodoListProps {
|
||||
filter: 'all' | 'active' | 'completed';
|
||||
todos: {
|
||||
[id: string]: {
|
||||
toggleCompleted: (id: string) => void;
|
||||
todos: [
|
||||
{
|
||||
id: string;
|
||||
label: string;
|
||||
completed: boolean;
|
||||
};
|
||||
};
|
||||
complete: (id: string) => void;
|
||||
status: string;
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
> Note that `[id: string]` does not indicate an array; it is an object [index signature](https://www.typescriptlang.org/docs/handbook/interfaces.html#indexable-types).
|
||||
|
||||
Now that our interface is complete, try changing the word "all" in `filter === all` and see that VS Code will tell you this condition will always be false. Compare this to plain JavaScript: if you had a typo in that line, you wouldn't understand why your filter wasn't working.
|
||||
|
||||
## Sharing types
|
||||
|
@ -120,44 +115,49 @@ Now that our interface is complete, try changing the word "all" in `filter === a
|
|||
Most of our components will need to specify types for `todos` and `filter`, so it's a good thing that TypeScript allows us to share types between files. I've already written up and exported those shared types in the file `TodoApp.types.ts`, so we just need to import them and use them in our interface.
|
||||
|
||||
```ts
|
||||
import { FilterTypes, Todos, CompleteTodo } from '../TodoApp.types';
|
||||
import { FilterTypes, Todos, ToggleCompleted } from '../TodoApp.types';
|
||||
|
||||
interface TodoListProps {
|
||||
complete: CompleteTodo;
|
||||
todos: Todos;
|
||||
filter: FilterTypes;
|
||||
toggleCompleted: ToggleCompleted;
|
||||
todos: Todos;
|
||||
}
|
||||
```
|
||||
|
||||
## Writing TodoListItemProps
|
||||
|
||||
Jumping down to the TodoListItem, as we start to write the `TodoListItemProps` we realize that two of the props, `label` and `completed`, have already been defined in the `TodoItem` interface. So we can make `TodoListItemProps` reuse the `TodoItem` interface by extending it.
|
||||
Jumping down to the TodoListItem, as we start to write the `TodoListItemProps` we realize that three of the props, `label`, `status`, `id`, have already been defined in the `TodoItem` interface. So we can make `TodoListItemProps` reuse the `TodoItem` interface by extending it.
|
||||
|
||||
```ts
|
||||
import { CompleteTodo } from '../TodoApp.types';
|
||||
import { ToggleCompleted } from '../TodoApp.types';
|
||||
|
||||
interface TodoListItemProps extends TodoItem {
|
||||
id: string;
|
||||
complete: CompleteTodo;
|
||||
toggleCompleted: ToggleCompleted;
|
||||
}
|
||||
```
|
||||
|
||||
The end result of this is an interface with all four properties: `id`, `complete`, `completed` and `label`.
|
||||
The end result of this is an interface with all four properties: `id`, `toggleCompleted`, `status` and `label`.
|
||||
|
||||
Next we can pull in the remaining props in the render function:
|
||||
|
||||
```jsx
|
||||
const { label, completed, complete, id } = this.props;
|
||||
const { label, status, id, toggleCompleted } = props;
|
||||
```
|
||||
|
||||
And then use the input's `onChange` event to fire our `complete` callback. We can see in the signature that `complete` expects an `id` of type string, so we'll pass our `id` prop in.
|
||||
And then use the input's `onChange` event to call a function that toggles the todo's completed state. We can see in the signature that `toggleCompleted` expects an `id` of type string, so we'll pass our `id` prop in.
|
||||
|
||||
> A [callback function](https://developer.mozilla.org/en-US/docs/Glossary/Callback_function) is a function passed into a component as a prop.
|
||||
|
||||
```jsx
|
||||
<input type="checkbox" checked={completed} onChange={() => complete(id)} />
|
||||
const handleCheck = () => toggleCompleted(id);
|
||||
...
|
||||
<input type="checkbox" checked={status === 'completed'} onChange={handleCheck} />
|
||||
```
|
||||
|
||||
> Note that the function param and prop name just happen to be the same. This isn't required.
|
||||
## Passing props down
|
||||
|
||||
Now that our todos are firing the `onChange` callback, give them a click and take look at how the app responds. Since our footer text is based on the number of unchecked todos, the footer will automatically update to reflect the new state.
|
||||
Now that we have added `toggleCompleted` to our `TodoListItemProps` we'll see that the `TodoListItem` in our `TodoList` is complaining about a missing prop. We successfully passed the function into our `TodoList`, but we aren't passing it down into `TodoListItem`. This process is often called `prop drilling` and can be a signal for refactoring (which you'll see in the final example).
|
||||
|
||||
```jsx
|
||||
<TodoListItem key={todo.id} {...todo} toggleCompleted={toggleCompleted} />
|
||||
```
|
||||
|
|
|
@ -5,3 +5,5 @@
|
|||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
|
|
@ -2,89 +2,75 @@ import React from 'react';
|
|||
import { TodoFooter } from './components/TodoFooter';
|
||||
import { TodoHeader } from './components/TodoHeader';
|
||||
import { TodoList } from './components/TodoList';
|
||||
import { Todos, FilterTypes } from './TodoApp.types';
|
||||
import { Todo, Todos, FilterTypes } from './TodoApp.types';
|
||||
|
||||
let index = 0;
|
||||
const defaultTodos: Todos = [
|
||||
{
|
||||
id: '04',
|
||||
label: 'Todo 4',
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
id: '03',
|
||||
label: 'Todo 3',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '02',
|
||||
label: 'Todo 2',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '01',
|
||||
label: 'Todo 1',
|
||||
status: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
interface TodoAppState {
|
||||
todos: Todos;
|
||||
filter: FilterTypes;
|
||||
}
|
||||
export const TodoApp = () => {
|
||||
const [filter, setFilter] = React.useState<FilterTypes>('all');
|
||||
const [todos, setTodos] = React.useState<Todos>(defaultTodos);
|
||||
|
||||
export class TodoApp extends React.Component<any, TodoAppState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
todos: {
|
||||
'04': {
|
||||
label: 'Todo 4',
|
||||
completed: true
|
||||
},
|
||||
'03': {
|
||||
label: 'Todo 3',
|
||||
completed: false
|
||||
},
|
||||
'02': {
|
||||
label: 'Todo 2',
|
||||
completed: false
|
||||
},
|
||||
'01': {
|
||||
label: 'Todo 1',
|
||||
completed: false
|
||||
}
|
||||
},
|
||||
filter: 'all'
|
||||
const addTodo = (label: string): void => {
|
||||
const getId = () => Date.now().toString();
|
||||
const newTodo: Todo = {
|
||||
id: getId(),
|
||||
label: label,
|
||||
status: 'active',
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filter, todos } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader addTodo={this._addTodo} setFilter={this._setFilter} filter={filter} />
|
||||
<TodoList complete={this._complete} todos={todos} filter={filter} />
|
||||
<TodoFooter clear={this._clear} todos={todos} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _addTodo = label => {
|
||||
const { todos } = this.state;
|
||||
const id = index++;
|
||||
|
||||
this.setState({
|
||||
todos: { ...todos, [id]: { label, completed: false } }
|
||||
});
|
||||
setTodos([...todos, newTodo]);
|
||||
};
|
||||
|
||||
private _complete = id => {
|
||||
const { todos } = this.state;
|
||||
const todo = todos[id];
|
||||
const newTodos = { ...todos, [id]: { ...todo, completed: !todo.completed } };
|
||||
|
||||
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];
|
||||
const toggleCompleted = (id: string) => {
|
||||
const newTodos = todos.map((todo): Todo => {
|
||||
if (todo.id === id) {
|
||||
return { ...todo, status: todo.status === 'active' ? 'completed' : 'active' };
|
||||
} else {
|
||||
return todo;
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
todos: newTodos
|
||||
});
|
||||
setTodos(newTodos);
|
||||
};
|
||||
|
||||
private _setFilter = filter => {
|
||||
this.setState({
|
||||
filter: filter
|
||||
const clearCompleted = () => {
|
||||
const updatedTodos = todos.map((todo): Todo => {
|
||||
if (todo.status === 'completed') {
|
||||
return { ...todo, status: 'cleared' };
|
||||
} else {
|
||||
return todo;
|
||||
}
|
||||
});
|
||||
setTodos(updatedTodos);
|
||||
};
|
||||
|
||||
const changeFilter = (filter: FilterTypes) => {
|
||||
setFilter(filter);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader filter={filter} changeFilter={changeFilter} addTodo={addTodo} />
|
||||
<TodoList todos={todos} filter={filter} toggleCompleted={toggleCompleted} />
|
||||
<TodoFooter todos={todos} clearCompleted={clearCompleted} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
export type FilterTypes = 'all' | 'active' | 'completed';
|
||||
export type TodoType = 'active' | 'completed' | 'cleared';
|
||||
|
||||
export type CompleteTodo = (id) => void;
|
||||
|
||||
export interface TodoItem {
|
||||
export interface Todo {
|
||||
id: string;
|
||||
label: string;
|
||||
completed: boolean;
|
||||
status: TodoType;
|
||||
}
|
||||
|
||||
export interface Todos {
|
||||
[id: string]: TodoItem;
|
||||
}
|
||||
export type Todos = Todo[];
|
||||
|
||||
export type AddTodo = (label: string) => void;
|
||||
export type ToggleCompleted = (id: string) => void;
|
||||
export type ClearCompleted = () => void;
|
||||
export type ChangeFilter = (filter: FilterTypes) => void;
|
||||
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import React from 'react';
|
||||
import { Todos } from '../TodoApp.types';
|
||||
|
||||
export const TodoFooter = (props: any) => {
|
||||
const itemCount = Object.keys(props.todos).filter(id => !props.todos[id].completed).length;
|
||||
export const TodoFooter = (props) => {
|
||||
const { clearCompleted, todos } = props;
|
||||
|
||||
const itemCount = todos.filter((todo) => todo.status === 'active').length;
|
||||
|
||||
|
||||
return (
|
||||
<footer>
|
||||
<span>
|
||||
{itemCount} item{itemCount === 1 ? '' : 's'} left
|
||||
</span>
|
||||
<button className="submit">Clear Completed</button>
|
||||
<button className="submit">
|
||||
Clear Completed
|
||||
</button>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,31 +1,34 @@
|
|||
import React from 'react';
|
||||
import { FilterTypes } from '../TodoApp.types';
|
||||
|
||||
export class TodoHeader extends React.Component<any, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { labelInput: '' };
|
||||
}
|
||||
export const TodoHeader = (props) => {
|
||||
const [inputText, setInputText] = React.useState<string>('');
|
||||
const { filter, addTodo, changeFilter } = props;
|
||||
|
||||
render() {
|
||||
const { filter } = this.props;
|
||||
return (
|
||||
<header>
|
||||
<h1>todos <small>(1.7 demo)</small></h1>
|
||||
<div className="addTodo">
|
||||
<input value={this.state.labelInput} onChange={this._onChange} className="textfield" placeholder="add todo" />
|
||||
<button className="submit">Add</button>
|
||||
</div>
|
||||
<nav className="filter">
|
||||
<button className={filter === 'all' ? 'selected' : ''}>all</button>
|
||||
<button className={filter === 'active' ? 'selected' : ''}>active</button>
|
||||
<button className={filter === 'completed' ? 'selected' : ''}>completed</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
_onChange = evt => {
|
||||
this.setState({ labelInput: evt.target.value });
|
||||
const onInput = (e) => {
|
||||
setInputText(e.target.value);
|
||||
};
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
if (inputText.length > 0) addTodo(inputText);
|
||||
setInputText('');
|
||||
};
|
||||
|
||||
const onFilter = (e) => {
|
||||
changeFilter(e.currentTarget.textContent)
|
||||
};
|
||||
|
||||
return (
|
||||
<header>
|
||||
<h1>todos <small>(1.6 exercise)</small></h1>
|
||||
<div className="addTodo">
|
||||
<input value={inputText} onChange={onInput} className="textfield" placeholder="add todo" />
|
||||
<button className="submit">Add</button>
|
||||
</div>
|
||||
<nav className="filter">
|
||||
<button className={filter === 'all' ? 'selected' : ''}> all</button>
|
||||
<button className={filter === 'active' ? 'selected' : ''}>active</button>
|
||||
<button className={filter === 'completed' ? 'selected' : ''}>completed</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
|
@ -1,22 +1,21 @@
|
|||
import React from 'react';
|
||||
import { TodoListItem } from './TodoListItem';
|
||||
import { FilterTypes, Todos } from '../TodoApp.types';
|
||||
|
||||
export class TodoList extends React.Component<any, any> {
|
||||
render() {
|
||||
const { filter, todos, complete } = this.props;
|
||||
export const TodoList = (props) => {
|
||||
const { filter, todos } = props;
|
||||
|
||||
// filteredTodos returns an array of filtered todo keys [01,02,03]
|
||||
const filteredTodos = Object.keys(todos).filter(id => {
|
||||
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
|
||||
});
|
||||
const filteredTodos = todos.filter((todo) => {
|
||||
if (todo.status === 'cleared') return false;
|
||||
return filter === 'all' ||
|
||||
(filter === 'completed' && todo.status === 'completed') ||
|
||||
(filter === 'active' && todo.status === 'active');
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="todos">
|
||||
{filteredTodos.map(id => (
|
||||
<TodoListItem key={id} id={id} complete={complete} {...todos[id]} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ul className="todos">
|
||||
{filteredTodos.map((todo) => (
|
||||
<TodoListItem key={todo.id} {...todo} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import React from 'react';
|
||||
import { TodoItem } from '../TodoApp.types';
|
||||
|
||||
export class TodoListItem extends React.Component<any, any> {
|
||||
render() {
|
||||
const { label, completed } = this.props;
|
||||
export const TodoListItem = (props) => {
|
||||
const { label, status, id } = props;
|
||||
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" checked={completed} onChange={() => undefined} /> {label}
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" checked={status === 'completed'} /> {label}
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,28 +4,22 @@ If you don't already have the app running, start it by running `npm start` from
|
|||
|
||||
## TodoFooter
|
||||
|
||||
1. Open TodoFooter and write a `TodoFooterProps` interface. It should include two values, a `clear` and `todos`. Use this interface in the function props like this: `(props: TodoFooterProps)`
|
||||
1. Open TodoFooter and write a `TodoFooterProps` interface. It should include two values, a `clearCompleted` and `todos`. Use this interface in the function props like this: `(props: TodoFooterProps)`
|
||||
|
||||
2. Write an `_onClick` function that calls `props.clear`.
|
||||
2. Write an `handleClick` function that calls `props.clear`.
|
||||
|
||||
- Since TodoFooter is not a class, the `_onClick` function needs to be stored in a const placed before the `return`.
|
||||
- Remember to use an arrow function to define this click handler.
|
||||
|
||||
3. Assign `_onClick` to the button's `onClick` prop. You won't need to use `this` since the component isn't a class.
|
||||
3. Assign `handleClick` to the button's `onClick` prop.
|
||||
|
||||
4. Test out this functionality. Check a few todos complete and click the `Clear Completed` button.
|
||||
|
||||
## TodoHeader
|
||||
|
||||
1. Open TodoHeader and write `TodoHeaderProps` which will include `addTodo`, `setFilter` and `filter`. Replace the first `any` in the class declaration with this interface.
|
||||
1. Open TodoHeader then write and use the `TodoHeaderProps` which will include `addTodo`, `changeFilter` and `filter`.
|
||||
|
||||
2. This component also has state. Write `TodoHeaderState` (there's just one value), and add this where the second `any` was.
|
||||
2. Add `onFilter` to each of the filter buttons
|
||||
|
||||
3. Add `_onFilter` to each of the filter buttons
|
||||
- Note that we can't add new parameters to onClick, but we can pull information from the event target!
|
||||
|
||||
- Note that we can't add new parameters to onClick, but we can pull information from the event target!
|
||||
- Remember to use an arrow function for this one too
|
||||
|
||||
4. Call `_onAdd` from the submit button
|
||||
4. Call `onSubmit` from the submit button
|
||||
|
||||
5. Check out this new functionality! We can now add and filter todos!
|
||||
|
|
|
@ -2,86 +2,75 @@ import React from 'react';
|
|||
import { TodoFooter } from './components/TodoFooter';
|
||||
import { TodoHeader } from './components/TodoHeader';
|
||||
import { TodoList } from './components/TodoList';
|
||||
import { Todos, FilterTypes } from './TodoApp.types';
|
||||
import { Todo, Todos, FilterTypes } from './TodoApp.types';
|
||||
|
||||
let index = 0;
|
||||
const defaultTodos: Todos = [
|
||||
{
|
||||
id: '04',
|
||||
label: 'Todo 4',
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
id: '03',
|
||||
label: 'Todo 3',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '02',
|
||||
label: 'Todo 2',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '01',
|
||||
label: 'Todo 1',
|
||||
status: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
export class TodoApp extends React.Component<any, { todos: Todos; filter: FilterTypes }> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
todos: {
|
||||
'04': {
|
||||
label: 'Todo 4',
|
||||
completed: true
|
||||
},
|
||||
'03': {
|
||||
label: 'Todo 3',
|
||||
completed: false
|
||||
},
|
||||
'02': {
|
||||
label: 'Todo 2',
|
||||
completed: false
|
||||
},
|
||||
'01': {
|
||||
label: 'Todo 1',
|
||||
completed: false
|
||||
}
|
||||
},
|
||||
filter: 'all'
|
||||
export const TodoApp = () => {
|
||||
const [filter, setFilter] = React.useState('all');
|
||||
const [todos, setTodos] = React.useState<Todos>(defaultTodos);
|
||||
|
||||
const addTodo = (label: string): void => {
|
||||
const getId = () => Date.now().toString();
|
||||
const newTodo: Todo = {
|
||||
id: getId(),
|
||||
label: label,
|
||||
status: 'active',
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filter, todos } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader addTodo={this._addTodo} setFilter={this._setFilter} filter={filter} />
|
||||
<TodoList complete={this._complete} todos={todos} filter={filter} />
|
||||
<TodoFooter clear={this._clear} todos={todos} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// business logic
|
||||
|
||||
private _addTodo = label => {
|
||||
const { todos } = this.state;
|
||||
const id = index++;
|
||||
|
||||
this.setState({
|
||||
todos: { ...todos, [id]: { label, completed: false } }
|
||||
});
|
||||
setTodos([...todos, newTodo]);
|
||||
};
|
||||
|
||||
private _complete = id => {
|
||||
const { todos } = this.state;
|
||||
const todo = todos[id];
|
||||
const newTodos = { ...todos, [id]: { ...todo, completed: !todo.completed } };
|
||||
|
||||
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];
|
||||
const toggleCompleted = (id: string) => {
|
||||
const newTodos = todos.map((todo): Todo => {
|
||||
if (todo.id === id) {
|
||||
return { ...todo, status: todo.status === 'active' ? 'completed' : 'active' };
|
||||
} else {
|
||||
return todo;
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
todos: newTodos
|
||||
});
|
||||
setTodos(newTodos);
|
||||
};
|
||||
|
||||
private _setFilter = filter => {
|
||||
this.setState({
|
||||
filter: filter
|
||||
const clearCompleted = () => {
|
||||
const updatedTodos = todos.map((todo): Todo => {
|
||||
if (todo.status === 'completed') {
|
||||
return { ...todo, status: 'cleared' };
|
||||
} else {
|
||||
return todo;
|
||||
}
|
||||
});
|
||||
setTodos(updatedTodos);
|
||||
};
|
||||
|
||||
const changeFilter = (filter: FilterTypes) => {
|
||||
setFilter(filter);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader filter={filter} changeFilter={changeFilter} addTodo={addTodo} />
|
||||
<TodoList todos={todos} filter={'all'} toggleCompleted={toggleCompleted} />
|
||||
<TodoFooter todos={todos} clearCompleted={clearCompleted} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
export type FilterTypes = 'all' | 'active' | 'completed';
|
||||
export type TodoType = 'active' | 'completed' | 'cleared';
|
||||
|
||||
export type CompleteTodo = (id) => null;
|
||||
|
||||
export interface TodoItem {
|
||||
export interface Todo {
|
||||
id: string;
|
||||
label: string;
|
||||
completed: boolean;
|
||||
status: TodoType;
|
||||
}
|
||||
|
||||
export interface Todos {
|
||||
[id: string]: TodoItem;
|
||||
}
|
||||
export type Todos = Todo[];
|
||||
|
||||
export type AddTodo = (label: string) => void;
|
||||
export type ToggleCompleted = (id: string) => void;
|
||||
export type ClearCompleted = () => void;
|
||||
export type ChangeFilter = (filter: FilterTypes) => void;
|
||||
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import React from 'react';
|
||||
import { Todos } from '../TodoApp.types';
|
||||
|
||||
export const TodoFooter = props => {
|
||||
const itemCount = Object.keys(props.todos).filter(id => !props.todos[id].completed).length;
|
||||
export const TodoFooter = (props) => {
|
||||
const { clearCompleted, todos } = props;
|
||||
|
||||
const itemCount = todos.filter((todo) => todo.status === 'active').length;
|
||||
|
||||
|
||||
return (
|
||||
<footer>
|
||||
<span>
|
||||
{itemCount} item{itemCount === 1 ? '' : 's'} left
|
||||
</span>
|
||||
<button className="submit">Clear Completed</button>
|
||||
<button className="submit">
|
||||
Clear Completed
|
||||
</button>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,40 +1,34 @@
|
|||
import React from 'react';
|
||||
import { FilterTypes } from '../TodoApp.types';
|
||||
|
||||
export class TodoHeader extends React.Component<any, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { labelInput: '' };
|
||||
}
|
||||
export const TodoHeader = (props) => {
|
||||
const [inputText, setInputText] = React.useState<string>('');
|
||||
const { filter, addTodo, changeFilter } = props;
|
||||
|
||||
render() {
|
||||
const { filter } = this.props;
|
||||
return (
|
||||
<header>
|
||||
<h1>todos <small>(1.7 exercise)</small></h1>
|
||||
<div className="addTodo">
|
||||
<input value={this.state.labelInput} onChange={this._onChange} className="textfield" placeholder="add todo" />
|
||||
<button className="submit">Add</button>
|
||||
</div>
|
||||
<nav className="filter">
|
||||
<button className={filter === 'all' ? 'selected' : ''}>all</button>
|
||||
<button className={filter === 'active' ? 'selected' : ''}>active</button>
|
||||
<button className={filter === 'completed' ? 'selected' : ''}>completed</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
_onFilter = evt => {
|
||||
this.props.setFilter(evt.target.innerText);
|
||||
const onInput = (e) => {
|
||||
setInputText(e.target.value);
|
||||
};
|
||||
|
||||
_onChange = evt => {
|
||||
this.setState({ labelInput: evt.target.value });
|
||||
const onSubmit = () => {
|
||||
if (inputText.length > 0) addTodo(inputText);
|
||||
setInputText('');
|
||||
};
|
||||
|
||||
_onAdd = () => {
|
||||
this.props.addTodo(this.state.labelInput);
|
||||
this.setState({ labelInput: '' });
|
||||
const onFilter = (e) => {
|
||||
changeFilter(e.currentTarget.textContent)
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<header>
|
||||
<h1>todos <small>(1.6 exercise)</small></h1>
|
||||
<div className="addTodo">
|
||||
<input value={inputText} onChange={onInput} className="textfield" placeholder="add todo" />
|
||||
<button className="submit">Add</button>
|
||||
</div>
|
||||
<nav className="filter">
|
||||
<button className={filter === 'all' ? 'selected' : ''}> all</button>
|
||||
<button className={filter === 'active' ? 'selected' : ''}>active</button>
|
||||
<button className={filter === 'completed' ? 'selected' : ''}>completed</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
|
@ -3,26 +3,26 @@ import { TodoListItem } from './TodoListItem';
|
|||
import { FilterTypes, Todos } from '../TodoApp.types';
|
||||
|
||||
interface TodoListProps {
|
||||
complete: (id: string) => void;
|
||||
todos: Todos;
|
||||
filter: FilterTypes;
|
||||
toggleCompleted: (id: string) => void;
|
||||
todos: Todos;
|
||||
}
|
||||
|
||||
export class TodoList extends React.Component<TodoListProps, any> {
|
||||
render() {
|
||||
const { filter, todos, complete } = this.props;
|
||||
export const TodoList = (props: TodoListProps) => {
|
||||
const { filter, todos, toggleCompleted } = props;
|
||||
|
||||
// filteredTodos returns an array of filtered todo keys [01,02,03]
|
||||
const filteredTodos = Object.keys(todos).filter(id => {
|
||||
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
|
||||
});
|
||||
const filteredTodos = todos.filter((todo) => {
|
||||
if (todo.status === 'cleared') return false;
|
||||
return filter === 'all' ||
|
||||
(filter === 'completed' && todo.status === 'completed') ||
|
||||
(filter === 'active' && todo.status === 'active');
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="todos">
|
||||
{filteredTodos.map(id => (
|
||||
<TodoListItem key={id} id={id} complete={complete} {...todos[id]} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ul className="todos">
|
||||
{filteredTodos.map((todo) => (
|
||||
<TodoListItem key={todo.id} {...todo} toggleCompleted={toggleCompleted} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
import React from 'react';
|
||||
import { TodoItem } from '../TodoApp.types';
|
||||
import { Todo, ToggleCompleted } from '../TodoApp.types';
|
||||
|
||||
interface TodoListItemProps extends TodoItem {
|
||||
id: string;
|
||||
complete: (id: string) => void;
|
||||
interface TodoListItemProps extends Todo {
|
||||
toggleCompleted: ToggleCompleted;
|
||||
}
|
||||
|
||||
export class TodoListItem extends React.Component<TodoListItemProps, any> {
|
||||
render() {
|
||||
const { label, completed, complete, id } = this.props;
|
||||
export const TodoListItem = (props: TodoListItemProps) => {
|
||||
const { label, status, id, toggleCompleted } = props;
|
||||
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" checked={completed} onChange={() => complete(id)} /> {label}
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
const handleCheck = () => toggleCompleted(id);
|
||||
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" checked={status === 'completed'} onChange={handleCheck} /> {label}
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,67 +2,94 @@ import React from 'react';
|
|||
import { TodoFooter } from './components/TodoFooter';
|
||||
import { TodoHeader } from './components/TodoHeader';
|
||||
import { TodoList } from './components/TodoList';
|
||||
import { Todos, FilterTypes } from './TodoApp.types';
|
||||
import { Todo, Todos, FilterTypes, AppContextProps } from './TodoApp.types';
|
||||
|
||||
let index = 0;
|
||||
export const AppContext = React.createContext<AppContextProps>(undefined);
|
||||
|
||||
export class TodoApp extends React.Component<{}, { todos: Todos; filter: FilterTypes }> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
todos: {},
|
||||
filter: 'all'
|
||||
const defaultTodos: Todos = [
|
||||
{
|
||||
id: '04',
|
||||
label: 'Todo 4',
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
id: '03',
|
||||
label: 'Todo 3',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '02',
|
||||
label: 'Todo 2',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '01',
|
||||
label: 'Todo 1',
|
||||
status: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
export const TodoApp = () => {
|
||||
const [filter, setFilter] = React.useState<FilterTypes>('all');
|
||||
const [todos, setTodos] = React.useState<Todos>(defaultTodos);
|
||||
|
||||
// TODO Convert to useReducer
|
||||
const addTodo = (label: string): void => {
|
||||
const getId = () => Date.now().toString();
|
||||
const newTodo: Todo = {
|
||||
id: getId(),
|
||||
label: label,
|
||||
status: 'active',
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filter, todos } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader addTodo={this._addTodo} setFilter={this._setFilter} filter={filter} />
|
||||
<TodoList complete={this._complete} todos={todos} filter={filter} />
|
||||
<TodoFooter clear={this._clear} todos={todos} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _addTodo = label => {
|
||||
const { todos } = this.state;
|
||||
const id = index++;
|
||||
|
||||
this.setState({
|
||||
todos: { ...todos, [id]: { label, completed: false } }
|
||||
});
|
||||
setTodos([...todos, newTodo]);
|
||||
};
|
||||
|
||||
private _complete = id => {
|
||||
const { todos } = this.state;
|
||||
const todo = todos[id];
|
||||
const newTodos = { ...todos, [id]: { ...todo, completed: !todo.completed } };
|
||||
|
||||
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];
|
||||
const toggleCompleted = (id: string) => {
|
||||
const newTodos = todos.map((todo): Todo => {
|
||||
if (todo.id === id) {
|
||||
return { ...todo, status: todo.status === 'active' ? 'completed' : 'active' };
|
||||
} else {
|
||||
return todo;
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
todos: newTodos
|
||||
});
|
||||
setTodos(newTodos);
|
||||
};
|
||||
|
||||
private _setFilter = filter => {
|
||||
this.setState({
|
||||
filter: filter
|
||||
const clearCompleted = () => {
|
||||
const updatedTodos = todos.map((todo): Todo => {
|
||||
if (todo.status === 'completed') {
|
||||
return { ...todo, status: 'cleared' };
|
||||
} else {
|
||||
return todo;
|
||||
}
|
||||
});
|
||||
setTodos(updatedTodos);
|
||||
};
|
||||
}
|
||||
|
||||
const changeFilter = (filter: FilterTypes) => {
|
||||
setFilter(filter);
|
||||
};
|
||||
|
||||
const getFilter = () => {
|
||||
return filter;
|
||||
}
|
||||
|
||||
const getTodos = () => {
|
||||
return todos;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{
|
||||
addTodo,
|
||||
toggleCompleted,
|
||||
clearCompleted,
|
||||
changeFilter,
|
||||
getFilter,
|
||||
getTodos
|
||||
}}>
|
||||
<TodoHeader />
|
||||
<TodoList />
|
||||
<TodoFooter />
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
export type FilterTypes = 'all' | 'active' | 'completed';
|
||||
export type TodoType = 'active' | 'completed' | 'cleared';
|
||||
|
||||
export interface TodoItem {
|
||||
export interface Todo {
|
||||
id: string;
|
||||
label: string;
|
||||
completed: boolean;
|
||||
status: TodoType;
|
||||
}
|
||||
|
||||
export interface Todos {
|
||||
[id: string]: TodoItem;
|
||||
export type Todos = Todo[];
|
||||
|
||||
|
||||
export interface AppContextProps {
|
||||
addTodo: (label: string) => void;
|
||||
toggleCompleted: (id: string) => void;
|
||||
clearCompleted: () => void;
|
||||
changeFilter: (filter: FilterTypes) => void;
|
||||
getFilter: () => FilterTypes;
|
||||
getTodos: () => Todos;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import React from 'react';
|
||||
import { Todos } from '../TodoApp.types';
|
||||
interface TodoFooterProps {
|
||||
clear: () => void;
|
||||
todos: Todos;
|
||||
}
|
||||
import { AppContext } from '../TodoApp';
|
||||
|
||||
export const TodoFooter = (props: TodoFooterProps) => {
|
||||
const itemCount = Object.keys(props.todos).filter(id => !props.todos[id].completed).length;
|
||||
const _onClick = () => {
|
||||
props.clear();
|
||||
export const TodoFooter = () => {
|
||||
const { clearCompleted, getTodos } = React.useContext(AppContext);
|
||||
|
||||
const itemCount = getTodos().filter((todo) => todo.status === 'active').length;
|
||||
const handleClick = () => {
|
||||
clearCompleted();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -16,7 +14,7 @@ export const TodoFooter = (props: TodoFooterProps) => {
|
|||
<span>
|
||||
{itemCount} item{itemCount === 1 ? '' : 's'} left
|
||||
</span>
|
||||
<button onClick={_onClick} className="submit">
|
||||
<button onClick={handleClick} className="submit">
|
||||
Clear Completed
|
||||
</button>
|
||||
</footer>
|
||||
|
|
|
@ -1,58 +1,39 @@
|
|||
import React from 'react';
|
||||
import React, { ChangeEventHandler, MouseEventHandler, useState, useContext } from 'react';
|
||||
import { FilterTypes } from '../TodoApp.types';
|
||||
import { AppContext } from '../TodoApp';
|
||||
|
||||
interface TodoHeaderProps {
|
||||
addTodo: (label: string) => void;
|
||||
setFilter: (filter: FilterTypes) => void;
|
||||
filter: FilterTypes;
|
||||
}
|
||||
export const TodoHeader = () => {
|
||||
const { changeFilter, addTodo, getFilter } = useContext(AppContext);
|
||||
const [inputText, setInputText] = useState<string>('');
|
||||
|
||||
interface TodoHeaderState {
|
||||
labelInput: string;
|
||||
}
|
||||
|
||||
export class TodoHeader extends React.Component<TodoHeaderProps, TodoHeaderState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { labelInput: '' };
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filter, setFilter } = this.props;
|
||||
return (
|
||||
<header>
|
||||
<h1>todos <small>(1.7 final)</small></h1>
|
||||
<div className="addTodo">
|
||||
<input value={this.state.labelInput} onChange={this._onChange} className="textfield" placeholder="add todo" />
|
||||
<button onClick={this._onAdd} className="submit">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<nav className="filter">
|
||||
<button onClick={this._onFilter} className={filter === 'all' ? 'selected' : ''}>
|
||||
all
|
||||
</button>
|
||||
<button onClick={this._onFilter} className={filter === 'active' ? 'selected' : ''}>
|
||||
active
|
||||
</button>
|
||||
<button onClick={this._onFilter} className={filter === 'completed' ? 'selected' : ''}>
|
||||
completed
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
_onFilter = evt => {
|
||||
this.props.setFilter(evt.target.innerText);
|
||||
const onInput: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setInputText(e.target.value);
|
||||
};
|
||||
|
||||
_onChange = evt => {
|
||||
this.setState({ labelInput: evt.target.value });
|
||||
const onSubmit = () => {
|
||||
if (inputText.length > 0) addTodo(inputText);
|
||||
setInputText('');
|
||||
};
|
||||
|
||||
_onAdd = () => {
|
||||
this.props.addTodo(this.state.labelInput);
|
||||
this.setState({ labelInput: '' });
|
||||
const onFilter: MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
changeFilter(e.currentTarget.textContent as FilterTypes)
|
||||
};
|
||||
}
|
||||
return (
|
||||
<header>
|
||||
<h1>
|
||||
todos <small>(1.7 final)</small>
|
||||
</h1>
|
||||
<div className="addTodo">
|
||||
<input value={inputText} onChange={onInput} className="textfield" placeholder="add todo" />
|
||||
<button onClick={onSubmit} className="submit">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<nav className="filter">
|
||||
<button onClick={onFilter} className={getFilter() === 'all' ? 'selected' : ''}> all</button>
|
||||
<button onClick={onFilter} className={getFilter() === 'active' ? 'selected' : ''}>active</button>
|
||||
<button onClick={onFilter} className={getFilter() === 'completed' ? 'selected' : ''}>completed</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,27 +1,22 @@
|
|||
import React from 'react';
|
||||
import { TodoListItem } from './TodoListItem';
|
||||
import { FilterTypes, Todos } from '../TodoApp.types';
|
||||
import { AppContext } from '../TodoApp';
|
||||
|
||||
interface TodoListProps {
|
||||
complete: (id: string) => void;
|
||||
todos: Todos;
|
||||
filter: FilterTypes;
|
||||
}
|
||||
export const TodoList = () => {
|
||||
const { getFilter, getTodos } = React.useContext(AppContext);
|
||||
|
||||
export class TodoList extends React.Component<TodoListProps, any> {
|
||||
render() {
|
||||
const { filter, todos, complete } = this.props;
|
||||
const filteredTodos = getTodos().filter((todo) => {
|
||||
if (todo.status === 'cleared') return false;
|
||||
return getFilter() === 'all' ||
|
||||
(getFilter() === 'completed' && todo.status === 'completed') ||
|
||||
(getFilter() === 'active' && todo.status === 'active');
|
||||
});
|
||||
|
||||
const filteredTodos = Object.keys(todos).filter(id => {
|
||||
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="todos">
|
||||
{filteredTodos.map(id => (
|
||||
<TodoListItem key={id} id={id} complete={complete} {...todos[id]} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ul className="todos">
|
||||
{filteredTodos.map((todo) => (
|
||||
<TodoListItem key={todo.id} {...todo} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
import React from 'react';
|
||||
import { TodoItem } from '../TodoApp.types';
|
||||
import { Todo } from '../TodoApp.types';
|
||||
import { AppContext } from '../TodoApp';
|
||||
|
||||
interface TodoListItemProps extends TodoItem {
|
||||
id: string;
|
||||
complete: (id: string) => void;
|
||||
}
|
||||
export const TodoListItem = (props: Todo) => {
|
||||
const { label, status, id } = props;
|
||||
const { toggleCompleted } = React.useContext(AppContext);
|
||||
|
||||
export class TodoListItem extends React.Component<TodoListItemProps, any> {
|
||||
render() {
|
||||
const { label, completed, complete, id } = this.props;
|
||||
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" checked={completed} onChange={() => complete(id)} /> {label}
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" checked={status === 'completed'} onChange={() => toggleCompleted(id)} /> {label}
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,7 +15,7 @@ function isValidLessonFolder(folder) {
|
|||
|
||||
function* getEntryPoint(step) {
|
||||
if (isValidLessonFolder(step)) {
|
||||
for (let prefix of ['', 'demo/', 'exercise/', 'final/']) {
|
||||
for (let prefix of ['', 'demo/', 'exercise/', 'final/', 'lesson/']) {
|
||||
for (let suffix of ['.js', '.jsx', '.ts', '.tsx']) {
|
||||
const entryRequest = `./${step}/${prefix}src/index${suffix}`;
|
||||
if (fs.existsSync(entryRequest)) {
|
||||
|
|
Загрузка…
Ссылка в новой задаче