├── .gitignore ├── LICENSE ├── README.md ├── build ├── index.html └── stylesheets │ ├── elements.css │ ├── style.css │ └── tabs.css ├── exercises └── .gitkeep ├── koans ├── 01-HelloWorld.jsx ├── 02-PartiesList.jsx ├── 03-WhatsYourName.jsx ├── 04-Quiz.jsx ├── 05-Challenge-GroceryList-part-1.jsx ├── 05-Challenge-GroceryList-part-2.jsx ├── 05-Challenge-GroceryList-part-3.jsx ├── 05-Challenge-GroceryList-part-4.jsx ├── 06-RenderComponent.jsx ├── 07-LifecycleMethods.js.jsx └── main.jsx ├── package.json ├── server.js ├── test ├── .gitkeep ├── 01-HelloWorld.js ├── 02-PartiesList.js ├── 03-WhatsYourName.js ├── 04-Quiz.js ├── 05-Challenge-GroceryList.js ├── 06-RenderComponent.js ├── 07-LifecycleMethods.js └── helpers.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | pids 4 | *.pid 5 | *.seed 6 | lib-cov 7 | coverage 8 | .grunt 9 | .lock-wscript 10 | build/Release 11 | node_modules 12 | *.module-cache 13 | src 14 | exercises 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Arkency 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React.js Koans 2 | 3 | If you want to learn React.js you came to the right place. We prepared a set of practical exercises that will help you learn React.js from square one. The only thing you need to know is JavaScript. Here we are using [ECMAScript 2015 standard](https://babeljs.io/docs/learn-es2015/). 4 | 5 | The Koans are a set of tasks to complete. Prepared tests checks if they are done correctly. 6 | 7 | ## Installation 8 | 9 | Make sure you have Node.js and Python 2 installed. 10 | 11 | 1. `git clone https://github.com/arkency/reactjs_koans.git` 12 | 2. `cd reactjs_koans` 13 | 3. `npm run setup` 14 | 15 | ## Koans structure 16 | 17 | * Edit the files found in the **`exercises`** directory. 18 | * The `koans` directory contains the source of all the exercises. `test` contains the tests. 19 | * `src` contains files compiled from `exercises`. 20 | * `build` contains sources launched in the web browser version of Koans. 21 | 22 | ## Start a local web server (optional) 23 | 24 | You can run a webserver and see your changes live in your web browser: 25 | 26 | 1. Run command `npm run start` 27 | 2. Visit http://localhost:8080/ 28 | 29 | ## Instructions 30 | 31 | 1. Remember about running the setup script before you start working on Koans! 32 | 2. Work on the code found in the `exercises` directory and run the tests to see if you did everything right. 33 | 3. You need to do the exercises in the given order. 34 | 4. Try to not peek at the test files! They contain spoilers. 35 | 5. To run the tests, use `npm run test`. To automatically run tests when your code changes, use `npm run watch`. 36 | 37 | ## Video walkthrough 38 | 39 | * [Koans Installation](https://www.youtube.com/watch?v=Csf909B5_Qg) 40 | * [Walkthrough - Exercise #1](https://www.youtube.com/watch?v=wSC2Jqy3xLU) 41 | * [Walkthrough - Exercise #2](https://www.youtube.com/watch?v=2iuVq17YQxM) 42 | 43 | ## More than just Koans 44 | 45 | ### Blog 46 | 47 | If you want to read more about React.js and modern JavaScript applications, check out our [React Kung Fu blog](http://reactkungfu.com/). 48 | 49 | ### The book 50 | 51 | For people who finished Koans, we prepared something to go continue: the *React by example book*. In this book, we explain how to create common widgets like password-strength meter or credit card input. 52 | 53 | For the price of the ebook, you get: 54 | 55 | * Over 140 pages of React content. From fast introduction to React to example Todo app; 56 | * 11 practical real-world examples; 57 | * Repositories with code for most of the examples; 58 | 59 | It's an early version of the book. It means some wording in book may change and there will be more examples later. All updates for the book are free. 60 | 61 | You can use special **20% discount code: REACTKOANS**. 62 | [Grab your copy today](https://arkency.dpdcart.com/cart/add?product_id=113689&method_id=120078) or [download a free chapter](http://reactkungfu.com/assets/misc/sample.pdf) 63 | 64 | ## Additional resources 65 | 66 | * [React docs](https://facebook.github.io/react/docs/getting-started.html) - it's a great source of in-depth information about React. 67 | * [Why keys are important in React](http://blog.arkency.com/2014/10/react-dot-js-and-dynamic-children-why-the-keys-are-important/) - great reading explaining the reason for React's keys. 68 | * [Reactiflux](http://www.reactiflux.com/). User group on Slack. You can meet a lot of people using React there. There's a channel for beginners `needhelp`. 69 | 70 | 71 | ## About 72 | 73 | Arkency 74 | 75 | React Koans is funded and maintained by Arkency. Check out our other [open-source projects](https://github.com/arkency). 76 | 77 | You can also [hire us](http://arkency.com) or [read our blog](http://blog.arkency.com). 78 | -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ReactJS Koans 8 | 9 | 10 |
11 |

12 | React Koans by 13 | Arkency 14 |

15 |
16 |
17 | 59 |
60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /build/stylesheets/elements.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | text-align: center; 3 | text-transform: uppercase; 4 | font-family: "Open Sans", sans-serif; 5 | font-size: 27px; 6 | color: #444444; 7 | font-weight: 700; 8 | } 9 | h1 span { 10 | font-weight: 400; 11 | } 12 | 13 | input[type="text"] { 14 | padding: 10px; 15 | border: solid 1px #dcdcdc; 16 | transition: box-shadow 0.3s, border 0.3s; 17 | } 18 | input[type="text"]:focus, 19 | input[type="text"].focus { 20 | border: solid 1px #707070; 21 | box-shadow: 0 0 5px 1px #969696; 22 | } 23 | 24 | button { 25 | border: 0 none; 26 | margin-left: 5px; 27 | border-radius: 2px 2px 2px 2px; 28 | color: #FFFFFF; 29 | cursor: pointer; 30 | display: inline-block; 31 | font-size: 12px; 32 | font-weight: bold; 33 | line-height: 20px; 34 | margin-bottom: 0; 35 | margin-top: 10px; 36 | padding: 7px 10px; 37 | text-transform: none; 38 | transition: all 0.3s ease 0s; 39 | -moz-transition: all 0.3s ease 0s; 40 | -webkit-transition: all 0.3s ease 0s; 41 | width: 16.795%; 42 | text-align: center; 43 | background: none repeat scroll 0 0 #999999; 44 | color: #FFFFFF; 45 | } 46 | button:hover { 47 | background: none repeat scroll 0 0 #444444; 48 | color: #FFFFFF; 49 | } 50 | button[disabled] { 51 | opacity: 0.5; 52 | } 53 | -------------------------------------------------------------------------------- /build/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Open+Sans:400,700); 2 | @import url(elements.css); 3 | @import url(tabs.css); 4 | 5 | body, html { 6 | height: 100%; 7 | margin: 0; 8 | -webkit-font-smoothing: antialiased; 9 | font-weight: 100; 10 | background: white; 11 | text-align: center; 12 | font-family: 'Open Sans', Helvetica, Arial sans-serif; 13 | } 14 | 15 | header { 16 | margin-top: 40px; 17 | } 18 | -------------------------------------------------------------------------------- /build/stylesheets/tabs.css: -------------------------------------------------------------------------------- 1 | .tabs input[type=radio] { 2 | position: absolute; 3 | top: -9999px; 4 | left: -9999px; 5 | } 6 | .tabs { 7 | width: 80%; 8 | position: relative; 9 | padding: 0; 10 | margin: 40px auto; 11 | } 12 | .exercise{ 13 | float: left; 14 | padding-top: 5px; 15 | margin-right: 3px; 16 | } 17 | ul.tabs>li { 18 | list-style-type: none; 19 | } 20 | .tabs label { 21 | display: block; 22 | padding: 10px 20px; 23 | border-radius: 2px 2px 0 0; 24 | color: white; 25 | font-size: 15px; 26 | font-weight: normal; 27 | background: #C41D47; 28 | cursor: pointer; 29 | position: relative; 30 | top: 3px; 31 | -webkit-transition: all 0.2s ease-in-out; 32 | -moz-transition: all 0.2s ease-in-out; 33 | -o-transition: all 0.2s ease-in-out; 34 | transition: all 0.2s ease-in-out; 35 | } 36 | .tabs label:hover { 37 | background: #444444; 38 | top: 0; 39 | } 40 | 41 | [id^=tab]:checked + label { 42 | background: #444444; 43 | color: white; 44 | top: 0; 45 | } 46 | 47 | [id^=tab]:checked ~ [id^=tab-content] { 48 | display: block; 49 | } 50 | .tab-content{ 51 | z-index: 2; 52 | display: none; 53 | text-align: left; 54 | width: 100%; 55 | font-size: 20px; 56 | line-height: 140%; 57 | padding-top: 10px; 58 | background: #AB1C40; 59 | padding: 12px; 60 | color: white; 61 | position: absolute; 62 | top: 53px; 63 | margin-top: 50px; 64 | left: 0; 65 | box-sizing: border-box; 66 | -webkit-animation-duration: 0.5s; 67 | -o-animation-duration: 0.5s; 68 | -moz-animation-duration: 0.5s; 69 | animation-duration: 0.5s; 70 | } 71 | .tab-content ul li { 72 | cursor: pointer; 73 | } 74 | .tab-content ul li.selected { 75 | color: #444444; 76 | } 77 | .tab-content ul li.completed { 78 | color: #440000; 79 | text-decoration: line-through; 80 | } 81 | @media (min-width: 1565px) { 82 | .tab-content{ 83 | top:0px; 84 | margin-top:53px; 85 | } 86 | } 87 | @media (max-width: 840px) { 88 | .tab-content{ 89 | top:0px; 90 | margin-top:153px; 91 | } 92 | } 93 | @media (max-width: 653px) { 94 | .tab-content{ 95 | top:0px; 96 | margin-top:203px; 97 | } 98 | } 99 | @media (max-width: 443px) { 100 | .tab-content{ 101 | top:0px; 102 | margin-top:353px; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /exercises/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arkency/reactjs_koans/5de91ac29494e4edd1d999ff7b49efc9715dd182/exercises/.gitkeep -------------------------------------------------------------------------------- /koans/01-HelloWorld.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | // This is really simple React Component. 4 | // It has its own name (HelloWorld) it will be used for things like error display. 5 | // 6 | // Task: Render HTML span with "Hello World" text. 7 | 8 | class HelloWorld extends React.Component { 9 | // All components *must* have a `render` method defined. 10 | // 11 | // To define a component's render method, we use syntax called JSX. As you 12 | // can see it looks similar to HTML. You can use normal JavaScript too, but 13 | // JSX is much more popular, so we will stick to it. JSX gets converted to 14 | // JavaScript code. It is here just for readability purposes. 15 | // 16 | // Note: You can read about `render` syntax here: 17 | // https://facebook.github.io/react/docs/displaying-data.html 18 | // 19 | // Warning! JSX is not HTML - in the following lessons you will notice the differences. 20 | // 21 | // React delivers a big set of standard HTML elements like `div`, `p`, 22 | // `canvas` etc. Here you can see usage of a `div` element. 23 | 24 | render() { 25 | return ( 26 |
FILL ME IN!
27 | ); 28 | } 29 | } 30 | 31 | // Note: 32 | // You can use the official Google Chrome extension to browse all ReactJS 33 | // components rendered on a single page. See the description here: 34 | // https://facebook.github.io/react/blog/2014/01/02/react-chrome-developer-tools.html 35 | 36 | export default HelloWorld; 37 | -------------------------------------------------------------------------------- /koans/02-PartiesList.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | // We will create dynamic list of parties nearby. Let's see how to render 4 | // a list of items. 5 | // 6 | // Task #1: Add another party (or parties) to the list. 7 | // Task #2: Change `ul` HTML attribute `class` to 'parties-list'. 8 | 9 | class PartiesList extends React.Component { 10 | // We can put DOM elements delivered by React just like HTML elements. 11 | // Doesn't this code look familiar to you? 12 | render() { 13 | return ( 14 | 17 | ); 18 | } 19 | // Think about it: Why is that `className` attribute not called just `class`? 20 | // Hint: Some words in JavaScript/JSX are reserved... 21 | } 22 | 23 | export default PartiesList; 24 | -------------------------------------------------------------------------------- /koans/03-WhatsYourName.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | // Let's get to the most important feature of React.js - reactive state. 4 | // 5 | // Each React component contains 2 important hashes: 6 | // * Properties - we pass these values when we create a component. They don't 7 | // change during the component's lifecycle. 8 | // * State - these values can change over entire life of the component. 9 | // When you change some value in `state` object, React will 10 | // re-calculate the `render` method and update the HTML to match 11 | // the new state (in this case, updating the class). 12 | // 13 | // You can pass properties to components using JSX attributes. You did it in 14 | // the last exercise! See the example below. 15 | // 16 | // ``` 17 | //
[...]
18 | // ``` 19 | // 20 | // We created `React.DOM.div` component with properties: 21 | // `{className: "example-class", attr1: "ugabuga"}` 22 | // 23 | // You have two tasks to complete in this exercise: 24 | // Task #1: Someone left broken code in the `onNameChange` method. It is always triggered 25 | // after changing the value of `name` input. This method takes 26 | // event as its only argument. You need to retrieve the input value from 27 | // that object and update the `name` field in the `state` object. 28 | // Warning: Do not try to change the `render` structure. 29 | // 30 | // Task #2: If there is no name given, we should display a message encouraging 31 | // the user to insert their name. 32 | // Display text: "Hey there. Enter your name." if name.length is 0. 33 | // 34 | // Hint: Use temporary variables to achieve the goal. See the example below. 35 | // ``` 36 | // render() { 37 | // var numberToDisplay; 38 | // if (this.state.number > 9000) { 39 | // numberToDisplay = "IT'S OVER 9000!"; 40 | // } else { 41 | // numberToDisplay = this.state.number; 42 | // } 43 | // return( 44 | // {numberToDisplay} 45 | // ) 46 | // ``` 47 | // 48 | // Further reading on task #2: https://facebook.github.io/react/tips/if-else-in-JSX.html 49 | 50 | class WhatsYourName extends React.Component { 51 | // By default `this.state` is `null`. In `render` we are referring to 52 | // a specific element from the `state` object - `this.state.name`. 53 | // If we don't set an initial state, we will get an error. It's impossible to fetch 54 | // an object key from `null`. 55 | // 56 | // Think about it: you can set name from a cookie on component initialization! 57 | // What else could you do here? 58 | 59 | constructor(props) { 60 | // Properties object is called `props`. You can access it with `this.props`. 61 | // We won't use it in this exercise. 62 | super(props); 63 | this.state = { name: "" }; 64 | 65 | // Warning! If we don't bind this method - we would not be able to update state. 66 | this.onNameChange = this.onNameChange.bind(this); 67 | } 68 | 69 | // `event` is the only argument passed to that method. It will be an event 70 | // object thrown by React on actions like `click`, `change` etc. 71 | // 72 | // You need to correct the call of `setState` method. Just try to set 73 | // the `name` field to the value passed in event. 74 | // 75 | // Hint: use `console.log` to check `event.target`. You will find text 76 | // entered to the input there. 77 | onNameChange(event) { 78 | // Huh... There's something wrong here... 79 | this.setState({bad_attribute: "ChangeME!"}); 80 | } 81 | 82 | render() { 83 | return ( 84 |
85 |

Hello {this.state.name}

86 | 87 |
88 | ); 89 | } 90 | } 91 | 92 | // Notice some details here: 93 | // 1. `onChange` attribute isn't placed between `" "`, but `{ }` - we want to 94 | // reference function, not string. 95 | // 2. You must be very careful on methods binding. You can do it in the constructor. 96 | // 3. `state` object is `null` by default! If you want to display initial 97 | // value from state object, you should initialize state object. 98 | 99 | // ProTip: Always specify input's `name` attribute: React uses it to identify 100 | // inputs on page. Not doing so may cause you to waste a long time 101 | // debugging your program. 102 | 103 | export default WhatsYourName; 104 | -------------------------------------------------------------------------------- /koans/04-Quiz.jsx: -------------------------------------------------------------------------------- 1 | // Try to answer those questions without peeking into tests. 2 | 3 | var Answers = { 4 | // Question #1: What is the name of the class that we extend to create components class? 5 | // Tip: See any of the exercises 6 | answer1: "Enter your answer here", 7 | 8 | // Question #2: JSX is converted directly into JavaScript. True or false? 9 | // Tip: See the first exercise. 10 | answer2: null, // true/false? 11 | 12 | // Question #3: What's the name of the method that must be defined in every component? 13 | answer3: "FILL ME IN", 14 | 15 | // Question #4: If I want to set `div` component's HTML class, what attribute 16 | // should I declare in JSX? 17 | // Tip: See exercise #2. 18 | answer4: "class", 19 | 20 | // Question #5: A component's properties can be changed after its initialization. True or false? 21 | // Tip: See exercise #3 22 | answer5: null, // true/false? 23 | 24 | // Question #6: What's the name of the method for changing the component's state? 25 | // Tip: See exercise #3 26 | answer6: 'stateSet', 27 | 28 | // Question #7: You must bind a component's methods (except `render`) 29 | // to make it possible to change the state. True or false? 30 | // Tip: See exercise #3 31 | answer7: null // true/false? 32 | }; 33 | 34 | export default Answers; 35 | -------------------------------------------------------------------------------- /koans/05-Challenge-GroceryList-part-1.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | // This exercise is a bit bigger than the previous ones. 4 | // We will make a reactive grocery list. 5 | // 6 | // Task: Fill the `return` of `GroceryList` render method. It should return 7 | // a list of `GroceryListItem`. You need to display the groceries names 8 | // using `this.props` in `GroceryListItem`. We already prepared the variable 9 | // `groceriesComponents` inside `render` method containing a list of these items for you. 10 | 11 | class GroceryList extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | groceries: [ { name: "Apples" } ] 16 | }; 17 | } 18 | 19 | render() { 20 | let groceriesComponents = []; 21 | // Properties are a way to pass parameters to your React components. 22 | // We mentioned this in the third exercise. Properties are to React 23 | // components what attributes are to HTML elements. 24 | // 25 | // Below you can see how to pass properties to child components. 26 | // We have defined a `grocery` property for each `GroceryListItem`. 27 | for(var index = 0; index < this.state.groceries.length; index++) { 28 | groceriesComponents.push( 29 | 32 | ); 33 | } 34 | 35 | // Hint: Don't forget about putting items into `ul` 36 | return ( 37 |
38 | // Put your code here 39 |
40 | ); 41 | } 42 | } 43 | 44 | // Render grocery name from component's properties. 45 | // If you have a problem, check `this.props` in the console. 46 | class GroceryListItem extends React.Component { 47 | constructor(props) { 48 | super(props); 49 | } 50 | 51 | render() { 52 | return ( 53 |
  • 54 | // Put your code here. 55 |
  • 56 | ); 57 | } 58 | } 59 | 60 | export default GroceryList; 61 | -------------------------------------------------------------------------------- /koans/05-Challenge-GroceryList-part-2.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | // We continue our journey with reactive grocery list. We implemented the previous 4 | // task ourselves. You can see our example implementation. 5 | // 6 | // Task: You have to implement adding items to the list. Create the implementation 7 | // of the `addGroceryItem` method. This method should modify the `GroceryList` 8 | // component's state and by that re-render this component. 9 | // 10 | // You need to render an input and button in `GroceryList` (after the 11 | // groceries list). Users will use them as an input for new groceries. 12 | // 13 | // We prepared two components for you. Please render the button and 14 | // input under your list and define the `onClick` handler for the button. 15 | // 16 | // Hint: See `render` method body. Look for `newProductInput` and 17 | // `newProductAddButton` variables 18 | // 19 | // Do you remember how we used `onChange` event in the third exercise? 20 | 21 | 22 | class GroceryList extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | this.state = { 26 | groceries: [ 27 | { name: "Apples" } 28 | ], 29 | newGroceryName: "" 30 | }; 31 | 32 | this.addGroceryItem = this.addGroceryItem.bind(this); 33 | this.inputChanged = this.inputChanged.bind(this); 34 | } 35 | 36 | // Warning: You shouldn't change this method 37 | inputChanged(event) { 38 | this.setState({newGroceryName: event.target.value}); 39 | } 40 | 41 | // Fill the definition of the following method to allow adding new items to the list 42 | // Hint #1: You should use the `concat` function to modify groceries list. 43 | // Hint #2: Remember about the case where input is empty. 44 | // Hint #3: Name of the new grocery item will be stored in `this.state.newGroceryName`. 45 | addGroceryItem() { 46 | // Put your code here 47 | } 48 | 49 | render() { 50 | let groceriesComponents = [], 51 | newProductInput, 52 | newProductAddButton; 53 | 54 | for(var index = 0; index < this.state.groceries.length; index++) { 55 | groceriesComponents.push( 56 | 59 | ); 60 | } 61 | 62 | // Here are components for task #2. 63 | newProductInput = ; 64 | // Something is missing here... Will anything happen if you click this button now? 65 | newProductAddButton = ; 66 | 67 | return ( 68 |
    69 |
      70 | {groceriesComponents} 71 |
    72 |
    73 | ); 74 | } 75 | } 76 | 77 | class GroceryListItem extends React.Component { 78 | constructor(props) { 79 | super(props); 80 | } 81 | 82 | render() { 83 | return (
  • {this.props.grocery.name}
  • ); 84 | } 85 | } 86 | 87 | export default GroceryList; 88 | -------------------------------------------------------------------------------- /koans/05-Challenge-GroceryList-part-3.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | // Task: User needs to be able to clear whole grocery list in one click. 4 | // Render a proper button under your list and implement the `clearList` method. 5 | // This method should clear the `groceries` array placed in your state. 6 | // This is similar to the previous task so I don't want to say any more. 7 | // Have fun. 8 | // 9 | // Caution: Remember that you should change state only using `setState` 10 | // method. The only exception of that rule is state definition 11 | // in a component's class constructor. 12 | // 13 | // Hint: Don't forget about adding the clearing list button to the 14 | // `GroceryList` rendering method. 15 | 16 | class GroceryList extends React.Component { 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | groceries: [ 21 | { name: "Apples" } 22 | ], 23 | newGroceryName: "" 24 | }; 25 | 26 | this.addGroceryItem = this.addGroceryItem.bind(this); 27 | this.clearList = this.clearList.bind(this); 28 | this.inputChanged = this.inputChanged.bind(this); 29 | } 30 | 31 | inputChanged(event) { 32 | this.setState({ newGroceryName: event.target.value }); 33 | } 34 | 35 | addGroceryItem() { 36 | if(this.state.newGroceryName) { 37 | let newGroceryItem = { name: this.state.newGroceryName }; 38 | this.setState({ 39 | groceries: this.state.groceries.concat([newGroceryItem]) 40 | }); 41 | } 42 | } 43 | 44 | // Fill the definition of the following method to allow clearing the list 45 | // Hint: You can just simply set the groceries to an empty array. 46 | clearList() { 47 | // Put your code here 48 | } 49 | 50 | render() { 51 | let groceriesComponents = [], 52 | newProductInput, 53 | newProductAddButton, 54 | clearListButton; 55 | 56 | for(var index = 0; index < this.state.groceries.length; index++) { 57 | groceriesComponents.push( 58 | 61 | ); 62 | } 63 | 64 | newProductInput = ; 65 | newProductAddButton = ; 66 | clearListButton = ; 67 | 68 | return ( 69 |
    70 |
      71 | {groceriesComponents} 72 |
    73 | {newProductInput} 74 | {newProductAddButton} 75 |
    76 | ); 77 | } 78 | } 79 | 80 | class GroceryListItem extends React.Component { 81 | constructor(props) { 82 | super(props); 83 | } 84 | 85 | render() { 86 | return (
  • {this.props.grocery.name}
  • ); 87 | } 88 | } 89 | 90 | export default GroceryList; 91 | -------------------------------------------------------------------------------- /koans/05-Challenge-GroceryList-part-4.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | // Task: Ok, now the last exercise. You have to implement toggling 4 | // completeness of the each grocery list's item. Make each item reactive. 5 | // 6 | // This is why we prepared the declaration of the `toggleGroceryCompleteness` 7 | // method in the `GroceryList` component. 8 | // 9 | // WARNING: You should remember that we create a `grocery` item in the 10 | // `addGroceryItem` method. You need to add a `completed` field to 11 | // the `grocery` objects created there. 12 | // 13 | // === Tasks below aren't required for proceeding in Koans, but we encourage you to try === 14 | // 15 | // Extra Task: As you can see in the `GroceryList` component - it has more than one 16 | // responsiblity. It displays groceries list, handles state 17 | // modification and handles the display and logic of new grocery 18 | // addition. The last of these responsibilities can be easily 19 | // extracted to another component. The new component should handle 20 | // only text input and the submit button. 21 | // 22 | // Hint: You can pass a callback to the component's method 23 | // (like `addGroceryItem`) as an attribute in the `render` method. 24 | // 25 | // Warning: This task doesn't have its own tests, but current ones 26 | // should be enough to cover it. The behaviour of whole 27 | // app should not change. 28 | // 29 | // Extra Task: You can try to disable submit button for `newGrocery` if 30 | // `newGroceryName` state is empty. You can just use property 31 | // `disabled` for submit button component in `render` method. 32 | // 33 | // Hint: There are no tests for this extra task. You need to do them 34 | // yourself. You can perform manual-testing (meh.) 35 | // Or try to create your own tests. 36 | // Check out `test/05-Challange-GroceryList.js` for tests to this part. 37 | 38 | class GroceryList extends React.Component { 39 | constructor(props) { 40 | super(props); 41 | this.state = { 42 | groceries: [ 43 | { 44 | name: "Apples", 45 | completed: false 46 | } 47 | ], 48 | newGroceryName: "" 49 | }; 50 | 51 | this.addGroceryItem = this.addGroceryItem.bind(this); 52 | this.clearList = this.clearList.bind(this); 53 | this.inputChanged = this.inputChanged.bind(this); 54 | } 55 | 56 | inputChanged(event) { 57 | this.setState({ newGroceryName: event.target.value }); 58 | } 59 | 60 | addGroceryItem() { 61 | if(this.state.newGroceryName) { 62 | let newGroceryItem = { name: this.state.newGroceryName }; 63 | this.setState({ 64 | groceries: this.state.groceries.concat([newGroceryItem]) 65 | }); 66 | } 67 | } 68 | 69 | clearList(event) { 70 | this.setState({groceries: []}); 71 | } 72 | 73 | // Fill the definition of the following method to allow completing each item 74 | // Hint 1: Pay attention to the element's index on the list. 75 | toggleGroceryCompleteness(groceryIndex) { 76 | // Put your code here 77 | } 78 | 79 | render() { 80 | let groceriesComponents = [], 81 | newProductInput, 82 | newProductAddButton, 83 | clearListButton; 84 | for(var index = 0; index < this.state.groceries.length; index++) { 85 | groceriesComponents.push( 86 | 90 | ); 91 | } 92 | 93 | newProductInput = ; 94 | newProductAddButton = ; 95 | clearListButton = ; 96 | 97 | return ( 98 |
    99 |
      100 | {groceriesComponents} 101 |
    102 | {newProductInput} 103 | {newProductAddButton} 104 | {clearListButton} 105 |
    106 | ); 107 | } 108 | } 109 | 110 | class GroceryListItem extends React.Component { 111 | constructor(props) { 112 | super(props); 113 | } 114 | 115 | render() { 116 | let completed = this.props.grocery.completed ? "completed" : ''; 117 | return ( 118 |
  • 119 | {this.props.grocery.name} 120 |
  • 121 | ); 122 | } 123 | } 124 | 125 | export default GroceryList; 126 | -------------------------------------------------------------------------------- /koans/06-RenderComponent.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | // Congratulations for completing your first React challenge! 4 | // You have already learned how to create React's components and how they affect 5 | // each other. In this exercise you will learn how to render components on the web page. 6 | // 7 | // Task #1: Fill the `renderNameComponent` function. It should render ReactElement 8 | // into the DOM. 9 | class Name extends React.Component { 10 | render() { 11 | return (

    Bazinga!

    ); 12 | } 13 | } 14 | 15 | // See you got a domNode passed as a `domNode` argument. 16 | function renderNameComponent(domNode) { 17 | // Put your code here 18 | } 19 | 20 | // Hint: You have to use the `React.render(ReactElement element, DOMNode node)` method. 21 | // As you can see, this method takes two parameters: 22 | // `ReactElement` and DOM node. 23 | // 24 | // Don't mistake `React.render` with `render` method in a component class. 25 | // They are completely different methods! 26 | // 27 | // Notice that class `Name` has type `React.Component`. 28 | // It's a ReactComponent, not a ReactElement! You need to create an 29 | // element from the component before putting it into DOM. One way of doing 30 | // that is using `React.createElement(ReactComponent component)`. E.g. 31 | // 32 | // `let element = React.createElement(Name);` 33 | 34 | export default renderNameComponent; 35 | -------------------------------------------------------------------------------- /koans/07-LifecycleMethods.js.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Every React component exposes special methods that allow you to plug in logic 4 | // when certain events occur during the component's life. They are called 5 | // 'lifecycle methods', and they can be used in variety of ways. 6 | // They are used mostly to integrate non-React code manipulating the DOM with 7 | // your components - like autocomplete, jQuery plugins etc. 8 | // 9 | // There are three methods that are widely used: 10 | // * componentDidMount - this method fires when React component is rendered for 11 | // the first time in the web browser. It does not run when 12 | // you render component using server-side rendering. 13 | // A render can be caused by an explicit React.render 14 | // call or when a child component is rendered within a render 15 | // method of its parent component. 16 | // 17 | // * componentDidUpdate - this method fires when a component is updated - 18 | // when state changes or the forceUpdate method 19 | // is called explicitly. 20 | // 21 | // * componentWillUnmount - this method fires before the component 'dies'. You 22 | // can unmount the component directly using the 23 | // React.unmountComponentAtNode method. A component can 24 | // be also unmounted during re-rendering of the parent component. 25 | // 26 | // Tasks for this exercise are in comments inside the component class code. 27 | // 28 | // In this exercise lifecycle methods will be used to provide convenient debug 29 | // messages in developer's console. 30 | // There are more lifecycle methods available. 31 | // Those three presented are commonly used. 32 | // 33 | // Extra task: Learn about componentWillUpdate. What's the difference between 34 | // this and the componentDidUpdate method? Think about the possible 35 | // use cases of this lifecycle method. 36 | // Extra task: Learn about componentWillMount. How can it be useful? 37 | // (Hint: Think about server-side rendering of React components) 38 | // Extra task: Learn about componentWillReceiveProps. How it can be used? 39 | // Is it fired when you render a component for the first time? 40 | // Extra task: There is a method which directly modifies behavior of React 41 | // itself - it's called shouldComponentUpdate. 42 | // How can you use it to optimise rendering cycle of your 43 | // React components? Learn about PureRenderMixin. 44 | // 45 | // All lifecycle methods are described here: 46 | // http://facebook.github.io/react/docs/component-specs.html 47 | class LifecycleMethodsComponent extends React.Component { 48 | constructor(props) { 49 | super(props); 50 | this.state = { name: "Bob" }; 51 | } 52 | 53 | // This code will be called when the component finishes mounting 54 | // (so it is visible for a user). 55 | componentDidMount() { 56 | // Task 1: Display a message "I'm mounted!" in developer's console when the 57 | // component finishes mounting. 58 | // Use `console.log` function for it. 59 | } 60 | 61 | componentDidUpdate(prevProps, prevState) { 62 | // Task 2: Display a message "Updated!" in developer's console 63 | // when the component updates. 64 | // Here you also need to use the console.log function. 65 | // Notice that in this lifecycle method you have an access 66 | // to previous values of properties and state. 67 | // Think about it: Could you find a possible use case 68 | // for using previous state and properties values? 69 | } 70 | 71 | componentWillUnmount() { 72 | // Task 3: Display a message "Goodbye, cruel world! :(" in developer's 73 | // console when the component unmounts. 74 | // In the real world this lifecycle method is often used to 75 | // 'clean up' external integrations from the component. 76 | // Think about the use case like this: You have an event bus and 77 | // you are listening for events. Your event listeners use setState 78 | // directly. What will happen if you unmount the component? 79 | // How can this lifecycle method help you to avoid such problems? 80 | } 81 | 82 | render() { 83 | return (

    Whatever, {this.state.name}!

    ); 84 | } 85 | } 86 | 87 | export default LifecycleMethodsComponent; 88 | -------------------------------------------------------------------------------- /koans/main.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | import HelloWorld from './01-HelloWorld.jsx'; 4 | import PartiesList from './02-PartiesList.jsx'; 5 | import WhatsYourName from './03-WhatsYourName.jsx'; 6 | import GroceryListPart1 from './05-Challenge-GroceryList-part-1.jsx'; 7 | import GroceryListPart2 from './05-Challenge-GroceryList-part-2.jsx'; 8 | import GroceryListPart3 from './05-Challenge-GroceryList-part-3.jsx'; 9 | import GroceryListPart4 from './05-Challenge-GroceryList-part-4.jsx'; 10 | import renderName from './06-RenderComponent.jsx'; 11 | 12 | React.render(, document.getElementById("tab-content1")); 13 | React.render(, document.getElementById("tab-content2")); 14 | React.render(, document.getElementById("tab-content3")); 15 | React.render(, document.getElementById("tab-content4")); 16 | React.render(, document.getElementById("tab-content5")); 17 | React.render(, document.getElementById("tab-content6")); 18 | React.render(, document.getElementById("tab-content7")); 19 | renderName(document.getElementById("tab-content8")); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs_koans", 3 | "version": "1.0.0", 4 | "description": "React.js Koans", 5 | "main": "index.js", 6 | "scripts": { 7 | "compile": "babel exercises --out-dir src", 8 | "setup": "npm install && ncp koans/ exercises/", 9 | "start": "npm run compile && nodemon server.js", 10 | "test": "npm run compile && mocha -b --compilers js:babel/register --require test/helpers.js test/**/*.js || echo", 11 | "watch": "onchange exercises/*.jsx -- npm run test" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/arkency/reactjs_koans.git" 16 | }, 17 | "keywords": [ 18 | "react.js", 19 | "kata", 20 | "koans", 21 | "tests" 22 | ], 23 | "author": "Arkency", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/arkency/reactjs_koans/issues" 27 | }, 28 | "homepage": "https://github.com/arkency/reactjs_koans", 29 | "dependencies": { 30 | "babel": "^5.6.14", 31 | "babel-core": "^5.6.15", 32 | "babel-loader": "^5.3.1", 33 | "express": "^4.12.4", 34 | "jsdom": "^6.5.1", 35 | "lodash": "^3.8.0", 36 | "mocha": "^2.2.4", 37 | "ncp": "^2.0.0", 38 | "nodemon": "^1.3.7", 39 | "onchange": "^1.1.0", 40 | "react": "^0.13.3", 41 | "react-hot-loader": "^1.2.3", 42 | "react-tools": "^0.13.3", 43 | "sinon": "^1.15.4", 44 | "webpack": "^1.9.10", 45 | "webpack-dev-server": "^1.9.0", 46 | "node-libs-browser": ">=0.4.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var webpack = require('webpack'); 4 | var WebpackDevServer = require('webpack-dev-server'); 5 | var config = require('./webpack.config'); 6 | 7 | app.get('/app.js', function (req, res) { 8 | res.redirect('//localhost:9090/build/app.js'); 9 | }); 10 | 11 | app.get('/', function (req, res) { 12 | res.sendFile(__dirname + '/build/index.html'); 13 | }); 14 | 15 | app.use(express.static(__dirname + '/build/stylesheets')); 16 | 17 | new WebpackDevServer(webpack(config), { 18 | publicPath: config.output.publicPath, 19 | hot: true, 20 | noInfo: true, 21 | historyApiFallback: true 22 | }).listen(9090, 'localhost', function (err, result) { 23 | if (err) { 24 | console.log(err); 25 | } 26 | }); 27 | 28 | var server = app.listen(8080, function () { 29 | console.log('Listening at http://localhost:%s', server.address().port); 30 | }); -------------------------------------------------------------------------------- /test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arkency/reactjs_koans/5de91ac29494e4edd1d999ff7b49efc9715dd182/test/.gitkeep -------------------------------------------------------------------------------- /test/01-HelloWorld.js: -------------------------------------------------------------------------------- 1 | import HelloWorld from '../src/01-HelloWorld.js'; 2 | 3 | describe("01 - HelloWorld", () => { 4 | var component; 5 | 6 | beforeEach( () => { 7 | var elem = document.createElement('div'); 8 | elem = document.body.appendChild(elem); 9 | return component = React.render(React.createElement(HelloWorld), elem); 10 | }); 11 | 12 | afterEach( () => { 13 | React.unmountComponentAtNode(React.findDOMNode(component)); 14 | }); 15 | 16 | it("should complete all tasks", () => { 17 | var divTags = React.addons.TestUtils.scryRenderedDOMComponentsWithTag(component, 'div'); 18 | var spanTags = React.addons.TestUtils.scryRenderedDOMComponentsWithTag(component, 'span'); 19 | 20 | assert.equal(divTags.length == 0, true, "Your React component shouldn't render any `div` HTML elements") 21 | assert.equal(spanTags.length == 1, true, "You need to render exactly one `span` HTML element") 22 | 23 | assert.equal(spanTags[0].props.children, "Hello World", "You have rendered wrong message in your `span` element"); 24 | }) 25 | }); 26 | -------------------------------------------------------------------------------- /test/02-PartiesList.js: -------------------------------------------------------------------------------- 1 | import PartiesList from '../src/02-PartiesList.js'; 2 | 3 | describe("02 - Parties List", () => { 4 | var component; 5 | 6 | beforeEach( () => { 7 | var elem = document.createElement('div'); 8 | elem = document.body.appendChild(elem); 9 | return component = React.render(React.createElement(PartiesList), elem); 10 | }); 11 | 12 | afterEach( () => { 13 | React.unmountComponentAtNode(React.findDOMNode(component)); 14 | }); 15 | 16 | describe("should complete all tasks", () => { 17 | it("Task #1: Party hard - have more than 1 party on party list", () => { 18 | var lists = React.addons.TestUtils.scryRenderedDOMComponentsWithTag(component, 'ul'); 19 | assert.equal(lists.length, 1, "You must render an `ul` HTML list"); 20 | 21 | var list = lists[0]; 22 | var parties = React.addons.TestUtils.scryRenderedDOMComponentsWithTag(list, 'li'); 23 | 24 | assert.equal(parties.length > 1, true, "You have only one party on the list. Add one more party to the list."); 25 | }); 26 | 27 | it("Task #2: List has proper class attribute", () => { 28 | var list = React.addons.TestUtils.findRenderedDOMComponentWithTag(component, 'ul'); 29 | assert.equal(list.props.className, 'parties-list', "`ul` element rendered by React should have `className` attribute `parties-list`"); 30 | }); 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /test/03-WhatsYourName.js: -------------------------------------------------------------------------------- 1 | import WhatsYourName from '../src/03-WhatsYourName.js'; 2 | 3 | describe("03 - What's Your Name?", () => { 4 | var component; 5 | var paragraphs; 6 | var inputs; 7 | 8 | var nameInParagraphEqualsTo = (paragraph, name) => { 9 | var children = paragraph.props.children; 10 | var text = _.isArray(children) ? children.join("") : children; 11 | return _.isEqual(text, `Hello ${ name }`); 12 | } 13 | 14 | beforeEach( () => { 15 | var elem = document.createElement('div'); 16 | elem = document.body.appendChild(elem); 17 | component = React.render(React.createElement(WhatsYourName), elem); 18 | paragraphs = React.addons.TestUtils.scryRenderedDOMComponentsWithTag(component, 'p'); 19 | inputs = React.addons.TestUtils.scryRenderedDOMComponentsWithTag(component, 'input'); 20 | }); 21 | 22 | afterEach( () => { 23 | React.unmountComponentAtNode(React.findDOMNode(component)); 24 | }); 25 | 26 | describe("should complete all tasks", () => { 27 | describe("Task #1 - greet user", () => { 28 | it("Should change name in paragraph on name change in input", () =>{ 29 | var input = _.first(inputs); 30 | var paragraph = _.first(paragraphs); 31 | 32 | assert.equal(paragraphs.length, 1, "There should be only one `p` element rendered by the component"); 33 | assert.equal(inputs.length, 1, "There should be only one `input` rendered by the component"); 34 | 35 | React.addons.TestUtils.Simulate.change(input, {target: { value: "XYZ" } }); 36 | assert.equal(nameInParagraphEqualsTo(paragraph, 'XYZ'), true, "After changing name in input, I should see the change in `p` element's content. In other words: `this.state.name` should change."); 37 | 38 | React.addons.TestUtils.Simulate.change(input, {target: { value: "ZYX" } }); 39 | assert.equal(nameInParagraphEqualsTo(paragraph, 'ZYX'), true, "After changing name in input for the second time, we should see the change in `p` element. In other words: `this.state.name` should change."); 40 | }); 41 | }); 42 | 43 | describe("Task #2 - don't greet user if name wasn't provided", () => { 44 | it("Should display welcoming message if user didn't provide their name", () => { 45 | var input = _.first(inputs); 46 | var paragraph = _.first(paragraphs); 47 | 48 | React.addons.TestUtils.Simulate.change(input, {target: { value: "" } }); 49 | assert.equal(paragraph.props.children, "Hey there. Enter your name.", 50 | "If user didn't enter the name (`this.state.name` length is 0), show \"Hey there. Enter your name.\". See the hint in task's description." 51 | ); 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/04-Quiz.js: -------------------------------------------------------------------------------- 1 | import Answers from '../src/04-Quiz.js'; 2 | 3 | describe("04 - Quiz", () => { 4 | it("Question #1", () => { 5 | assert.equal(Answers.answer1 === "Component", true, "Check the line that contains `extends` word in each exercise.") 6 | }); 7 | 8 | it("Question #2", () => { 9 | assert.equal(Answers.answer2, true, "JSX is not an HTML (so what is it?)"); 10 | }); 11 | 12 | it("Question #3", () => { 13 | assert.equal(Answers.answer3 === 'render', true, "In every component we created, there was one method. It contained JSX code."); 14 | }); 15 | 16 | it("Question #4", () => { 17 | assert.equal(Answers.answer4 === 'className', true, "`class` is a reserved word in JavaScript. See JSX code in exercise #2."); 18 | }); 19 | 20 | it("Question #5", () => { 21 | assert.equal(Answers.answer5, false, "See the comments about properties in exercise #3"); 22 | }); 23 | 24 | it("Question #6", () => { 25 | assert.equal(Answers.answer6 === 'setState', true, "See the `onNameChange` method from exercise #3"); 26 | }); 27 | 28 | it("Question #7", () => { 29 | assert.equal(Answers.answer7, true, "I'll make it easier for you: yes you have to. Why? See exercise #3! It's very important."); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/05-Challenge-GroceryList.js: -------------------------------------------------------------------------------- 1 | import GroceryListPart1 from '../src/05-Challenge-GroceryList-part-1.js'; 2 | import GroceryListPart2 from '../src/05-Challenge-GroceryList-part-2.js'; 3 | import GroceryListPart3 from '../src/05-Challenge-GroceryList-part-3.js'; 4 | import GroceryListPart4 from '../src/05-Challenge-GroceryList-part-4.js'; 5 | 6 | describe("05 - Challenge - Grocery List", () => { 7 | var component; 8 | 9 | describe("Task #1 - display a list of groceries", () => { 10 | 11 | beforeEach( () => { 12 | var elem = document.createElement('div'); 13 | elem = document.body.appendChild(elem); 14 | component = React.render(React.createElement(GroceryListPart1), elem); 15 | }); 16 | 17 | it('There should be an unordered list of groceries', () => { 18 | let groceryListItems = React.addons.TestUtils.scryRenderedDOMComponentsWithTag(component, "li"); 19 | assert.equal(groceryListItems.length, 1, "There should be exactly one element on the groceries list"); 20 | 21 | let groceryItem = _.first(groceryListItems); 22 | assert.equal(groceryItem.props.children, "Apples", "GroceryItem should render only text inside
  • tag. This text should contain only grocery item name."); 23 | }); 24 | }); 25 | 26 | describe("Task #2 - add a new product to list", () => { 27 | 28 | beforeEach( () => { 29 | var elem = document.createElement('div'); 30 | elem = document.body.appendChild(elem); 31 | component = React.render(React.createElement(GroceryListPart2), elem); 32 | }); 33 | 34 | it('Should render required tags like additional button and input', () => { 35 | try { React.addons.TestUtils.findRenderedDOMComponentWithClass(component, "new-item");} 36 | catch(err){ 37 | throw new Error("I can't find new item input"); 38 | } 39 | try { React.addons.TestUtils.findRenderedDOMComponentWithClass(component, "add-product");} 40 | catch(err){ 41 | throw new Error("I can't find 'Add new Product' button"); 42 | } 43 | }); 44 | 45 | it('Should be possible to add a new product to list', () => { 46 | let newProductInput = React.addons.TestUtils.findRenderedDOMComponentWithClass(component, "new-item"); 47 | let newProductAddButton = React.addons.TestUtils.findRenderedDOMComponentWithClass(component, "add-product"); 48 | React.addons.TestUtils.Simulate.change(newProductInput.getDOMNode(), { target: {value: 'Oranges' }}); 49 | React.addons.TestUtils.Simulate.click(newProductAddButton.getDOMNode()); 50 | 51 | let groceryListItems = React.addons.TestUtils.scryRenderedDOMComponentsWithTag(component, "li"); 52 | assert.equal(groceryListItems.length, 2, "There should be exactly two elements on the groceries list"); 53 | 54 | let groceryItem = _.last(groceryListItems); 55 | assert.equal(groceryItem.props.children, "Oranges", "GroceriesListItem should display a grocery name"); 56 | }); 57 | 58 | it('Should not be possible to add empty element', () => { 59 | let newProductAddButton = React.addons.TestUtils.findRenderedDOMComponentWithClass(component, "add-product"); 60 | React.addons.TestUtils.Simulate.click(newProductAddButton.getDOMNode()); 61 | let groceryListItems = React.addons.TestUtils.scryRenderedDOMComponentsWithTag(component, "li"); 62 | 63 | assert.equal(groceryListItems.length, 1, "There should be exactly one element on the groceries list"); 64 | }); 65 | }); 66 | 67 | describe("Task #3 - clearing groceries list", () => { 68 | 69 | beforeEach( () => { 70 | var elem = document.createElement('div'); 71 | elem = document.body.appendChild(elem); 72 | component = React.render(React.createElement(GroceryListPart3), elem); 73 | }); 74 | 75 | it('Should render required tags', () => { 76 | try { React.addons.TestUtils.findRenderedDOMComponentWithClass(component, "clear-list");} 77 | catch(err){ 78 | throw new Error("I can't find 'Clear the List' button"); 79 | } 80 | }); 81 | 82 | it('Should be possible to remove all list items', () => { 83 | let clearListButton = React.addons.TestUtils.findRenderedDOMComponentWithClass(component, "clear-list"); 84 | React.addons.TestUtils.Simulate.click(clearListButton.getDOMNode()); 85 | let groceryListItems = React.addons.TestUtils.scryRenderedDOMComponentsWithTag(component, "li"); 86 | 87 | assert.equal(groceryListItems.length, 0, "There should be no elements on the groceries list"); 88 | }); 89 | }); 90 | 91 | describe("Task #4 - collecting groceries items", () => { 92 | 93 | beforeEach( () => { 94 | var elem = document.createElement('div'); 95 | elem = document.body.appendChild(elem); 96 | component = React.render(React.createElement(GroceryListPart4), elem); 97 | }); 98 | 99 | it('Should be possible to make the grocery item complete', () => { 100 | let groceryListItems = React.addons.TestUtils.scryRenderedDOMComponentsWithTag(component, "li"); 101 | let groceryItem = _.last(groceryListItems); 102 | React.addons.TestUtils.Simulate.click(groceryItem.getDOMNode()); 103 | 104 | groceryListItems = React.addons.TestUtils.scryRenderedDOMComponentsWithTag(component, "li"); 105 | assert.equal(groceryListItems.length, 1, "There should be exactly one element on the groceries list"); 106 | 107 | groceryItem = _.last(groceryListItems); 108 | assert.equal(groceryItem.props.className, "completed", "GroceriesListItem should be completed"); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/06-RenderComponent.js: -------------------------------------------------------------------------------- 1 | import renderNameComponent from '../src/06-RenderComponent.js'; 2 | 3 | describe("06 - RenderComponent", () => { 4 | 5 | beforeEach( () => { 6 | renderNameComponent(document.body); 7 | }); 8 | 9 | describe("Task #1 - render Name component", () => { 10 | it('There should be a rendered React component', () => { 11 | var element = document.getElementById('hello'); 12 | assert.notEqual(element, null, "There should be a paragraph with id `hello` rendered on site."); 13 | assert.equal(element.innerHTML, "Bazinga!", "Rendered paragraph should contain `Bazinga!`"); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/07-LifecycleMethods.js: -------------------------------------------------------------------------------- 1 | import LifecycleMethodsComponent from '../src/07-LifecycleMethods.js'; 2 | import sinon from 'sinon'; 3 | 4 | describe("07 - Lifecycle methods", () => { 5 | var component; 6 | 7 | describe("Task #1 - emit a console log when the component mounts", () => { 8 | it("should emit 'I'm mounted!' in developer's console", () => { 9 | var mock = sinon.mock(console); 10 | mock.expects("log").once().withArgs("I'm mounted!"); 11 | 12 | var rootNode = document.body.appendChild(document.createElement('div')); 13 | React.render(React.createElement(LifecycleMethodsComponent), rootNode); 14 | mock.verify(); 15 | }); 16 | }); 17 | 18 | describe("Task #2 - emit a console log when the component updates", () => { 19 | it("should emit 'Updated!' in developer's console", () => { 20 | var rootNode = document.body.appendChild(document.createElement('div')); 21 | var component = React.render(React.createElement(LifecycleMethodsComponent), rootNode); 22 | 23 | var mock = sinon.mock(console); 24 | mock.expects("log").exactly(2).withArgs("Updated!"); 25 | 26 | component.setState({ name: "Victor" }); 27 | component.forceUpdate(); 28 | 29 | mock.verify(); 30 | }); 31 | }); 32 | 33 | describe("Task #3 - emit a console log when the component unmounts", () => { 34 | it("should emit 'Goodbye, cruel world! :(' in developer's console", () => { 35 | var rootNode = document.body.appendChild(document.createElement('div')); 36 | var component = React.render(React.createElement(LifecycleMethodsComponent), rootNode); 37 | 38 | var mock = sinon.mock(console); 39 | mock.expects("log").once().withArgs("Goodbye, cruel world! :("); 40 | React.unmountComponentAtNode(rootNode); 41 | mock.verify(); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | let assert = require('assert'); 2 | let jsdom = require('jsdom'); 3 | 4 | global.document = jsdom.jsdom(undefined, { 5 | virtualConsole: jsdom.createVirtualConsole().sendTo(console) 6 | }); 7 | global.window = global.document.defaultView; 8 | global.navigator = { userAgent: "Node.JS" }; 9 | 10 | let React = require('react/addons'); 11 | let _ = require('lodash'); 12 | 13 | global.React = React; 14 | global.assert = assert; 15 | global._ = _; 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | 6 | entry: [ 7 | 'webpack-dev-server/client?http://localhost:9090', 8 | 'webpack/hot/only-dev-server', 9 | './exercises/main' 10 | ], 11 | 12 | output: { 13 | path: __dirname + '/build/', 14 | filename: 'app.js', 15 | publicPath: 'http://localhost:9090/build/' 16 | }, 17 | 18 | module: { 19 | loaders: [ 20 | { test: path.join(__dirname, 'exercises'), loaders: ['react-hot', 'babel-loader']} 21 | ] 22 | }, 23 | 24 | resolve: { 25 | extensions: ['', '.js', '.jsx'] 26 | }, 27 | 28 | plugins: [ 29 | new webpack.HotModuleReplacementPlugin(), 30 | new webpack.NoErrorsPlugin() 31 | ] 32 | } 33 | --------------------------------------------------------------------------------