├── .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 |
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 |
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 |
15 |
Party at Aperture Laboratories
16 |
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 |
);
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 |
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 |
--------------------------------------------------------------------------------