├── .gitignore
├── .jshintrc
├── README.md
├── Schedule
├── excercises
├── 1-your-first-component
│ ├── 10
│ ├── app.js
│ ├── index.html
│ ├── notes.js
│ ├── solution.js
│ └── tests.js
├── 2-props
│ ├── app.js
│ ├── index.html
│ ├── notes.js
│ ├── solution.js
│ ├── styles.css
│ ├── tests.js
│ └── validateEmail.js
├── 3-events-and-state
│ ├── app.js
│ ├── index.html
│ ├── notes.js
│ ├── solution.js
│ └── styles.css
├── 4-props-v-state
│ ├── app.js
│ ├── data.js
│ ├── index.html
│ ├── notes.js
│ ├── solution.js
│ ├── styles.css
│ └── styles.js
├── 5-flux
│ ├── app.js
│ ├── app
│ │ ├── AppDispatcher.js
│ │ ├── Constants.js
│ │ ├── actions
│ │ │ ├── ServerActionCreators.js
│ │ │ └── ViewActionCreators.js
│ │ ├── components
│ │ │ └── App.js
│ │ ├── lib
│ │ │ └── xhr.js
│ │ ├── stores
│ │ │ └── ContactsStore.js
│ │ └── utils
│ │ │ └── ApiUtil.js
│ ├── index.html
│ ├── solution
│ │ ├── AppDispatcher.js
│ │ ├── Constants.js
│ │ ├── actions
│ │ │ ├── ServerActionCreators.js
│ │ │ └── ViewActionCreators.js
│ │ ├── components
│ │ │ └── App.js
│ │ ├── lib
│ │ │ └── xhr.js
│ │ ├── stores
│ │ │ └── ContactsStore.js
│ │ └── utils
│ │ │ └── ApiUtil.js
│ └── styles.css
├── 6-routing
│ ├── ContactStore.js
│ ├── app.js
│ ├── index.html
│ ├── solution.js
│ └── styles.css
├── 7-migrating-to-react
│ ├── app.js
│ ├── backbone-todomvc
│ │ ├── .gitignore
│ │ ├── index.html
│ │ ├── js
│ │ │ ├── app.js
│ │ │ ├── collections
│ │ │ │ └── todos.js
│ │ │ ├── models
│ │ │ │ └── todo.js
│ │ │ ├── routers
│ │ │ │ └── router.js
│ │ │ └── views
│ │ │ │ ├── app-view.js
│ │ │ │ └── todo-view.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── index.html
│ └── styles.css
├── assert.js
├── index.html
└── shared.css
├── flux-diagram-white-background.png
├── package.json
├── props-v-state.png
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esnext": true,
3 | "eqnull": true
4 | }
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | React.js Workshop with Ryan Florence
2 | ====================================
3 |
4 | You will need
5 |
6 | ```
7 | git clone https://github.com/FrontendMasters/2015-02-13-React.git
8 | cd 2015-02-13-React
9 | npm install
10 | npm start
11 | ```
12 |
13 | Then visit http://localhost:8080.
14 |
15 | This runs the webpack dev server, any changes you make to javascript
16 | files in `excercises` will cause the browser to reload live.
17 |
18 | Links mentioned in the workshop
19 | -------------------------------
20 |
21 | https://github.com/ryanflorence/react-training/blob/gh-pages/lessons/05-wrapping-dom-libs.md
22 |
23 | http://react-router-mega-demo.herokuapp.com/
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Schedule:
--------------------------------------------------------------------------------
1 | 9:15 (10) introduction
2 | 9:25 (20) lets build an app
3 | 9:45 (10) clone/download the training repo
4 |
5 | 9:55 (10) your first component
6 | 10:05 (5) excercise
7 | 10:10 (5) discussion
8 |
9 | 10:15 (5) break
10 |
11 | 10:20 (25) props
12 | 10:45 (15) excercise
13 | 11:00 (5) discussion
14 |
15 | 11:05 (5) break
16 |
17 | 11:10 (20) events and state
18 | 11:30 (20) excercise
19 | 11:50 (10) discussion
20 |
21 | 12:00 (60) lunch
22 |
23 | 1:00 (10) props v. state
24 | 1:15 (15) excercise
25 | 1:30 (5) discussion
26 |
27 | 1:35 (30) flux
28 | 2:05 (15) excercise
29 | 2:20 (5) discussion
30 |
31 | 2:25 (5) break
32 |
33 | 2:30 (20) routing
34 | 2:50 (15) excercise
35 | 3:05 (5) discussion
36 |
37 | 3:10 (30) migrating to React
38 | 3:40 (15) excercise
39 | 3:55 (5) discussion
40 |
41 | 4:00 (30) What's next
42 |
43 |
--------------------------------------------------------------------------------
/excercises/1-your-first-component/10:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontendMasters/2015-02-13-React/20a2ebebc2318d6754b7080e49751fe50401a199/excercises/1-your-first-component/10
--------------------------------------------------------------------------------
/excercises/1-your-first-component/app.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Excercise:
3 | //
4 | // Render `DATA` to the page
5 | // - put the title in an h1
6 | // - only render mexican food (hint: arrays have a "filter" method)
7 | // - sort the items in alphabetical order by name
8 | // (might want to use `sort-by` https://github.com/staygrimm/sort-by#example)
9 | ////////////////////////////////////////////////////////////////////////////////
10 |
11 | var React = require('react');
12 | var sortBy = require('sort-by');
13 |
14 | var DATA = {
15 | title: 'Menu',
16 | items: [
17 | { id: 1, name: 'tacos', type: 'mexican' },
18 | { id: 2, name: 'burrito', type: 'mexican' },
19 | { id: 3, name: 'tostada', type: 'mexican' },
20 | { id: 4, name: 'hush puppies', type: 'southern' }
21 | ]
22 | };
23 |
24 | var Menu = React.createClass({
25 | render () {
26 | return null;
27 | }
28 | });
29 |
30 | React.render(
, document.body, () => {
31 | require('./tests').run();
32 | });
33 |
34 |
--------------------------------------------------------------------------------
/excercises/1-your-first-component/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Your First Component
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/excercises/1-your-first-component/notes.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 |
3 | ////////////////////////////////////////////////////////////////////////////////
4 | React.render(React.DOM.div({}, 'hello'), document.body);
5 |
6 | ////////////////////////////////////////////////////////////////////////////////
7 | var { div } = React.DOM;
8 | var element = div({}, 'hello');
9 | React.render(element, document.body);
10 |
11 | ////////////////////////////////////////////////////////////////////////////////
12 | var { div, ul, li } = React.DOM;
13 | var element = div({},
14 | h1({}, 'hello!'),
15 | ul({},
16 | li({}, 'one'),
17 | li({}, 'two'),
18 | li({}, 'three')
19 | )
20 | );
21 |
22 | ////////////////////////////////////////////////////////////////////////////////
23 | var items = ['one', 'two', 'three'];
24 | var element = div({},
25 | h1({}, 'hello!'),
26 | ul({}, items.map(item => li({}, item)))
27 | );
28 |
29 |
30 | ////////////////////////////////////////////////////////////////////////////////
31 | var element = (
32 |
33 |
hello!
34 |
35 | one
36 | two
37 | three
38 |
39 |
40 | );
41 |
42 | ////////////////////////////////////////////////////////////////////////////////
43 | var element = (
44 |
45 |
hello!
46 |
47 | {items.map((item) => {item} )}
48 |
49 |
50 | );
51 |
52 | ////////////////////////////////////////////////////////////////////////////////
53 | var items = items.sort().map((item) => {item} );
54 | var element = (
55 |
59 | );
60 |
61 |
62 | ////////////////////////////////////////////////////////////////////////////////
63 | var App = React.createClass({
64 | render () {
65 | var items = items.sort().map((item) => {item} );
66 | return (
67 |
71 | );
72 | }
73 | });
74 |
75 |
--------------------------------------------------------------------------------
/excercises/1-your-first-component/solution.js:
--------------------------------------------------------------------------------
1 | require('./tests');
2 | var React = require('react');
3 | var sortBy = require('sort-by');
4 |
5 | // Render this data to the page
6 | // - put the title in an h1
7 | // - only render mexican food (hint: DATA.items.filter())
8 | // - sort the items in alphabetical order by name
9 | // (hint: use sort-by https://github.com/staygrimm/sort-by#example)
10 |
11 | var DATA = {
12 | title: 'Menu',
13 | items: [
14 | { id: 1, name: 'tacos', type: 'mexican' },
15 | { id: 2, name: 'burrito', type: 'mexican' },
16 | { id: 3, name: 'tostada', type: 'mexican' },
17 | { id: 4, name: 'hush puppies', type: 'southern' }
18 | ]
19 | };
20 |
21 | var Menu = React.createClass({
22 | render () {
23 | var items = DATA.items.filter((item) => {
24 | return item.type === 'mexican';
25 | })
26 | .sort(sortBy('name'))
27 | .map((item) => {
28 | return {item.name} ;
29 | });
30 | return (
31 |
32 |
{DATA.title}
33 |
34 |
35 | );
36 | }
37 | });
38 |
39 | React.render( , document.body, () => {
40 | require('./tests').run();
41 | });
42 |
43 |
--------------------------------------------------------------------------------
/excercises/1-your-first-component/tests.js:
--------------------------------------------------------------------------------
1 | var assert = require('../assert');
2 |
3 | exports.run = () => {
4 | var html = document.body.innerHTML;
5 | assert(!!html.match(/burrito/), 'found burrito');
6 | assert(!!html.match(/tacos/), 'found tacos');
7 | assert(!!html.match(/tostada/), 'found tostada');
8 | assert(!html.match(/hush puppies/), 'did not find hush puppies');
9 | assert(html.indexOf('burrito') < html.indexOf('tacos'), 'burrito first');
10 | assert(html.indexOf('tacos') < html.indexOf('tostada'), 'tacos second');
11 | };
12 |
13 |
14 |
--------------------------------------------------------------------------------
/excercises/2-props/app.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Excercise:
3 | //
4 | // http://facebook.github.io/react/docs/reusable-components.html#prop-validation
5 | //
6 | // - Don't access `USERS` directly in the app, use a prop
7 | // - Validate Gravatar's `size` property, allow it to be a
8 | // a number, or a string that can be converted to a number,
9 | // ie: `size="asdf"` should warn (hint: parseInt)
10 | // - in emailType, what if the prop name isn't email? what if we wanted
11 | // the prop to be "userEmail" or "loginId"? Switch the Gravatar
12 | // prop name from "email" to "loginId", send a bad value, and then
13 | // fix the code to make the warning make sense.
14 | // - how many times does `getDefaultProps` get called?
15 | // - experiment with some of the other propTypes, send improper values
16 | // and look at the messages you get
17 | ////////////////////////////////////////////////////////////////////////////////
18 |
19 | var React = require('react');
20 | var md5 = require('MD5');
21 | var validateEmail = require('./validateEmail');
22 | var warning = require('react/lib/warning');
23 |
24 | var GRAVATAR_URL = "http://gravatar.com/avatar";
25 |
26 | var USERS = [
27 | { id: 1, name: 'Ryan Florence', email: 'rpflorencegmail.com' },
28 | { id: 2, name: 'Michael Jackson', email: 'mjijackson@gmail.com' }
29 | ];
30 |
31 | var emailType = (props, propName, componentName) => {
32 | warning(
33 | validateEmail(props.email),
34 | `Invalid email '${props.email}' sent to 'Gravatar'. Check the render method of '${componentName}'.`
35 | );
36 | };
37 |
38 | var Gravatar = React.createClass({
39 | propTypes: {
40 | email: emailType
41 | },
42 |
43 | getDefaultProps () {
44 | return {
45 | size: 16
46 | };
47 | },
48 |
49 | render () {
50 | var { email, size } = this.props;
51 | var hash = md5(email);
52 | var url = `${GRAVATAR_URL}/${hash}?s=${size*2}`;
53 | return ;
54 | }
55 | });
56 |
57 | var App = React.createClass({
58 | render () {
59 | var users = USERS.map((user) => {
60 | return (
61 |
62 | {user.name}
63 |
64 | );
65 | });
66 | return (
67 |
71 | );
72 | }
73 | });
74 |
75 | React.render( , document.body);
76 |
77 | //require('./tests').run(Gravatar, emailType);
78 |
79 |
--------------------------------------------------------------------------------
/excercises/2-props/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Frontend Masters
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/excercises/2-props/notes.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var md5 = require('MD5');
3 | var validateEmail = require('./validateEmail');
4 | var warning = require('react/lib/warning');
5 |
6 | var GRAVATAR_URL = "http://gravatar.com/avatar";
7 | var USERS = [
8 | { id: 1, name: 'Ryan Florence', email: 'rpflorencegmail.com' },
9 | { id: 2, name: 'Michael Jackson', email: 'mjijackson@gmail.com' }
10 | ];
11 |
12 | ////////////////////////////////////////////////////////////////////////////////
13 | // Props are a lot like element attributes
14 | var element = ;
15 | var element = React.DOM.input({type: 'checkbox', required: true});
16 |
17 | ////////////////////////////////////////////////////////////////////////////////
18 | // we use them in images
19 | var App = React.createClass({
20 | render () {
21 | var users = USERS.map((user) => {
22 | var size = 36;
23 | var hash = md5(user.email);
24 | var url = `${GRAVATAR_URL}/${hash}?s=${size*2}`;
25 | return (
26 |
27 | {user.name}
28 |
29 | );
30 | });
31 | return (
32 |
36 | );
37 | }
38 | });
39 |
40 | ////////////////////////////////////////////////////////////////////////////////
41 | // sometimes componets get too big, refactor just like functions
42 | var Gravatar = React.createClass({
43 | render () {
44 | var { user, size } = this.props;
45 | var hash = md5(user.email);
46 | var url = `${GRAVATAR_URL}/${hash}?s=${size*2}`;
47 | return ;
48 | }
49 | });
50 |
51 | var App = React.createClass({
52 | render () {
53 | var users = USERS.map((user) => {
54 | return {user.name}
55 | });
56 | return (
57 |
61 | );
62 | }
63 | });
64 |
65 | ////////////////////////////////////////////////////////////////////////////////
66 | // we can validate propTypes to help others consume our components
67 | var Gravatar = React.createClass({
68 | propTypes: {
69 | user: React.PropTypes.shape({
70 | email: React.PropTypes.string.isRequired,
71 | name: React.PropTypes.string.isRequired,
72 | id: React.PropTypes.number.isRequired
73 | }).isRequired
74 | },
75 |
76 | render () {
77 | var { user, size } = this.props;
78 | var hash = md5(user.email);
79 | var url = `${GRAVATAR_URL}/${hash}?s=${size*2}`;
80 | return ;
81 | }
82 | });
83 |
84 | var App = React.createClass({
85 | render () {
86 | var users = USERS.map((user) => {
87 | return {user.name}
88 | });
89 | return (
90 |
94 | );
95 | }
96 | });
97 |
98 | ////////////////////////////////////////////////////////////////////////////////
99 | // we can create our own propTypes
100 | var emailType = (props, propName, componentName) => {
101 | warning(
102 | validateEmail(props.email),
103 | `Invalid email '${props.email}' sent to 'Gravatar'. Check the render method of '${componentName}'.`
104 | );
105 | };
106 |
107 | var Gravatar = React.createClass({
108 | propTypes: {
109 | user: React.PropTypes.shape({
110 | email: emailType,
111 | name: React.PropTypes.string.isRequired,
112 | id: React.PropTypes.number.isRequired
113 | }).isRequired
114 | },
115 |
116 | render () {
117 | var { user, size } = this.props;
118 | var hash = md5(user.email);
119 | var url = `${GRAVATAR_URL}/${hash}?s=${size*2}`;
120 | return ;
121 | }
122 | });
123 |
124 | var App = React.createClass({
125 | render () {
126 | var users = USERS.map((user) => {
127 | return {user.name} ;
128 | });
129 | return (
130 |
134 | );
135 | }
136 | });
137 |
138 | React.render( , document.body);
139 |
140 |
--------------------------------------------------------------------------------
/excercises/2-props/solution.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var md5 = require('MD5');
3 | var validateEmail = require('./validateEmail');
4 | var warning = require('react/lib/warning');
5 |
6 | var GRAVATAR_URL = "http://gravatar.com/avatar";
7 |
8 | var USERS = [
9 | { id: 1, name: 'Ryan Florence', email: 'rpflorence@gmail.com' },
10 | { id: 2, name: 'Michael Jackson', email: 'mjijackson@gmail.com' }
11 | ];
12 |
13 | var emailType = (props, propName, componentName) => {
14 | warning(
15 | validateEmail(props.email),
16 | `Invalid email '${props[propName]}' sent to 'Gravatar'. Check the render method of '${componentName}'.`
17 | );
18 | };
19 |
20 | var sizeType = (props, propName, componentName) => {
21 | warning(
22 | !isNaN(parseInt(props[propName])),
23 | `Invalid prop "${propName}", can't convert "${props[propName]}" to number. Check the render method of "${componentName}".`
24 | );
25 | };
26 |
27 | var Gravatar = React.createClass({
28 | propTypes: {
29 | user: React.PropTypes.shape({
30 | email: emailType,
31 | name: React.PropTypes.string.isRequired,
32 | id: React.PropTypes.number.isRequired
33 | }).isRequired,
34 | size: sizeType
35 | },
36 |
37 | getDefaultProps () {
38 | return {
39 | size: 16
40 | };
41 | },
42 |
43 | render () {
44 | var { user, size } = this.props;
45 | var hash = md5(user.email);
46 | var url = `${GRAVATAR_URL}/${hash}?s=${size*2}`;
47 | return ;
48 | }
49 | });
50 |
51 | var App = React.createClass({
52 | render () {
53 | var users = this.props.users.map((user) => {
54 | return {user.name} ;
55 | });
56 | return (
57 |
61 | );
62 | }
63 | });
64 |
65 | React.render(, document.body);
66 |
67 |
--------------------------------------------------------------------------------
/excercises/2-props/styles.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontendMasters/2015-02-13-React/20a2ebebc2318d6754b7080e49751fe50401a199/excercises/2-props/styles.css
--------------------------------------------------------------------------------
/excercises/2-props/tests.js:
--------------------------------------------------------------------------------
1 | // TODO!
2 | //var React = require('react');
3 | //var assert = require('../assert');
4 |
5 | //var captureWarnings = (fn) => {
6 | //var _warn = console.warn;
7 | //var msgs = {};
8 | //console.warn = function (msg) {
9 | //msgs[msg] = true;
10 | //_warn.apply(console, arguments);
11 | //};
12 | //fn();
13 | //console.warn = _warn;
14 | //return msgs;
15 | //};
16 |
17 | //var expectWarning = (expected, fn) => {
18 | //var msgs = captureWarnings(fn);
19 | //assert(msgs[expected], `got expected warning "${expected}"`);
20 | //};
21 |
22 | //var doNotExpectWarning = (notExpected, fn) => {
23 | //var msgs = captureWarnings(fn);
24 | //assert(msgs[notExpected] == null, `Did not get warning "${notExpected}"`);
25 | //};
26 |
27 | //var noWarningsMatch = (regex, fn) => {
28 | //var msgs = captureWarnings(fn);
29 | //var failed = false;
30 | //Object.keys(msgs).forEach((msg) => {
31 | //if (regex.test(msg)) {
32 | //assert(false, `did not expect to match warning ${msg}`);
33 | //failed = true;
34 | //}
35 | //});
36 | //if (failed === false)
37 | //assert(true, `no warnings matched ${regex}`);
38 | //};
39 |
40 | //exports.run = (Gravatar, emailType) => {
41 | //var el = ;
42 |
43 | //var Foo = React.createClass({
44 | //propTypes: {
45 | //loginId: emailType
46 | //},
47 | //render () { return null; }
48 | //});
49 |
50 | //noWarningsMatch(/undefined/, () => {
51 | //
52 | //});
53 |
54 | //};
55 |
56 |
--------------------------------------------------------------------------------
/excercises/2-props/validateEmail.js:
--------------------------------------------------------------------------------
1 | var regex = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|”(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*”)@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;
2 | module.exports = (email) => {
3 | return regex.test(email);
4 | };
5 |
--------------------------------------------------------------------------------
/excercises/3-events-and-state/app.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Excercise:
3 | // - make these tabs work when you click them
4 | ////////////////////////////////////////////////////////////////////////////////
5 | var React = require('react');
6 | var assign = require('react/lib/Object.assign');
7 |
8 | var DATA = [
9 | { name: 'USA', description: 'Land of the Free, Home of the brave' },
10 | { name: 'China', description: 'Lots of concrete' },
11 | { name: 'Russia', description: 'World Cup 2018!' },
12 | ];
13 |
14 | var App = React.createClass({
15 |
16 | renderTabs () {
17 | return this.props.countries.map((country, index) => {
18 | return (
19 |
20 | {country.name}
21 |
22 | );
23 | });
24 | },
25 |
26 | renderPanel () {
27 | var country = this.props.countries[0];
28 | return (
29 |
30 |
{country.description}
31 |
32 | );
33 | },
34 |
35 | render () {
36 | return (
37 |
38 |
39 | {this.renderTabs()}
40 |
41 |
42 | {this.renderPanel()}
43 |
44 |
45 | );
46 |
47 | }
48 | });
49 |
50 | var styles = {};
51 |
52 | styles.tab = {
53 | display: 'inline-block',
54 | padding: 10,
55 | margin: 10,
56 | borderBottom: '4px solid',
57 | borderBottomColor: '#ccc',
58 | cursor: 'pointer'
59 | };
60 |
61 | styles.activeTab = assign({}, styles.tab, {
62 | borderBottomColor: '#000'
63 | });
64 |
65 | styles.tabPanels = {
66 | padding: 10
67 | };
68 |
69 | React.render(, document.body);
70 |
71 |
--------------------------------------------------------------------------------
/excercises/3-events-and-state/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Frontend Masters
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/excercises/3-events-and-state/notes.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 |
3 | var alertStuff = () => {
4 | alert('STUFF!');
5 | };
6 |
7 | var App = React.createClass({
8 | render () {
9 | return (
10 |
11 |
Events and State
12 | Alert!
13 |
14 | );
15 | }
16 | });
17 |
18 | ////////////////////////////////////////////////////////////////////////////////
19 | // usually put it on the component
20 | var App = React.createClass({
21 | alertStuff () {
22 | alert('more stuff');
23 | },
24 |
25 | render () {
26 | return (
27 |
28 |
Events and State
29 | Alert!
30 |
31 | );
32 | }
33 | });
34 |
35 | ////////////////////////////////////////////////////////////////////////////////
36 | // can bind args
37 | var App = React.createClass({
38 | alertStuff (msg) {
39 | alert(msg);
40 | },
41 |
42 | render () {
43 | return (
44 |
45 |
Events and State
46 | Alert!
47 | Other Alert!
48 |
49 | );
50 | }
51 | });
52 |
53 |
54 | ////////////////////////////////////////////////////////////////////////////////
55 | // lets make a content toggler
56 | var ContentToggle = React.createClass({
57 |
58 | getInitialState: function() {
59 | return {
60 | showDetails: false
61 | };
62 | },
63 |
64 | toggle: function() {
65 | this.setState({
66 | showDetails: !this.state.showDetails
67 | }, this.maybeFocus);
68 | },
69 |
70 | maybeFocus: function() {
71 | if (this.state.showDetails)
72 | this.refs.details.getDOMNode().focus();
73 | },
74 |
75 | handleKeyboard: function(event) {
76 | if (event.key === 'Enter' || event.key === ' ')
77 | this.toggle();
78 | },
79 |
80 | render: function() {
81 | var details;
82 | var summaryClassName = 'ContentToggle__Summary';
83 |
84 | if (this.state.showDetails) {
85 | details = this.props.children;
86 | summaryClassName += ' ContentToggle__Summary--open';
87 | }
88 |
89 | return (
90 |
91 |
97 | {this.props.summary}
98 |
99 |
100 |
105 | {details}
106 |
107 |
108 | );
109 | }
110 | });
111 |
112 | var App = React.createClass({
113 | render () {
114 | return (
115 |
116 |
Events and State
117 |
118 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
119 |
120 |
121 | );
122 | }
123 | });
124 |
125 | React.render( , document.body);
126 |
--------------------------------------------------------------------------------
/excercises/3-events-and-state/solution.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Excercise:
3 | // - make these tabs work when you click them
4 | ////////////////////////////////////////////////////////////////////////////////
5 | var React = require('react');
6 | var assign = require('react/lib/Object.assign');
7 |
8 | var DATA = [
9 | { name: 'USA', description: 'Land of the Free, Home of the brave' },
10 | { name: 'China', description: 'Lots of concrete' },
11 | { name: 'Russia', description: 'World Cup 2018!' },
12 | ];
13 |
14 | var App = React.createClass({
15 |
16 | getInitialState () {
17 | return {
18 | activeTabIndex: 0
19 | };
20 | },
21 |
22 | handleTabClick (activeTabIndex) {
23 | this.setState({ activeTabIndex });
24 | },
25 |
26 | renderTabs () {
27 | return this.props.countries.map((country, index) => {
28 | var style = this.state.activeTabIndex === index ?
29 | styles.activeTab : styles.tab;
30 | var clickHandler = this.handleTabClick.bind(this, index);
31 | return (
32 |
33 | {country.name}
34 |
35 | );
36 | });
37 | },
38 |
39 | renderPanel () {
40 | var country = this.props.countries[this.state.activeTabIndex];
41 | return (
42 |
43 |
{country.description}
44 |
45 | );
46 | },
47 |
48 | render () {
49 | return (
50 |
51 |
52 | {this.renderTabs()}
53 |
54 |
55 | {this.renderPanel()}
56 |
57 |
58 | );
59 |
60 | }
61 | });
62 |
63 | var styles = {};
64 |
65 | styles.tab = {
66 | display: 'inline-block',
67 | padding: 10,
68 | margin: 10,
69 | borderBottom: '4px solid',
70 | borderBottomColor: '#ccc',
71 | cursor: 'pointer'
72 | };
73 |
74 | styles.activeTab = assign({}, styles.tab, {
75 | borderBottomColor: '#000'
76 | });
77 |
78 | styles.tabPanels = {
79 | padding: 10
80 | };
81 |
82 | React.render(, document.body);
83 |
--------------------------------------------------------------------------------
/excercises/3-events-and-state/styles.css:
--------------------------------------------------------------------------------
1 | .ContentToggle__Summary {
2 | cursor: pointer;
3 | border-bottom: solid 1px transparent;
4 | }
5 |
6 | .ContentToggle__Summary:before {
7 | display: inline-block;
8 | width: 1em;
9 | content: '▸';
10 | }
11 |
12 | .ContentToggle__Summary--open:before {
13 | content: '▾';
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/excercises/4-props-v-state/app.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Excercise:
3 | //
4 | // make tabs a "pure component" by not managing any of its own state, instead
5 | // add a property to tell it which tab to show, and then have it communicate
6 | // with its owner to get rerendered with a new active tab.
7 | //
8 | // Why would you move that state up? you might have a workflow where they can't
9 | // progress from one step to the next until they've completed some sort of task
10 | // but they can go back if they'd like. If the tabs keep their own state you
11 | // can't control them with your application logic.
12 | ////////////////////////////////////////////////////////////////////////////////
13 | var React = require('react');
14 | var styles = require('./styles');
15 | var data = require('./data');
16 |
17 | var Tabs = React.createClass({
18 |
19 | propTypes: {
20 | data: React.PropTypes.array.isRequired
21 | },
22 |
23 | getInitialState () {
24 | return {
25 | activeTabIndex: 0
26 | };
27 | },
28 |
29 | handleTabClick (activeTabIndex) {
30 | this.setState({ activeTabIndex });
31 | },
32 |
33 | renderTabs () {
34 | return this.props.data.map((tab, index) => {
35 | var style = this.state.activeTabIndex === index ?
36 | styles.activeTab : styles.tab;
37 | var clickHandler = this.handleTabClick.bind(this, index);
38 | return (
39 |
40 | {tab.name}
41 |
42 | );
43 | });
44 | },
45 |
46 | renderPanel () {
47 | var tab = this.props.data[this.state.activeTabIndex];
48 | return (
49 |
50 |
{tab.description}
51 |
52 | );
53 | },
54 |
55 | render () {
56 | return (
57 |
58 |
59 | {this.renderTabs()}
60 |
61 |
62 | {this.renderPanel()}
63 |
64 |
65 | );
66 | }
67 | });
68 |
69 | var App = React.createClass({
70 | render () {
71 | return (
72 |
73 |
Props v. State
74 |
75 |
76 | );
77 | }
78 | });
79 |
80 | React.render(, document.body);
81 |
82 |
--------------------------------------------------------------------------------
/excercises/4-props-v-state/data.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | { name: 'Step 1', description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' },
3 | { name: 'Step 2', description: 'Eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui id est.' },
4 | { name: 'Step 3', description: 'Sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit.' },
5 | ];
6 |
7 |
--------------------------------------------------------------------------------
/excercises/4-props-v-state/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Frontend Masters
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/excercises/4-props-v-state/notes.js:
--------------------------------------------------------------------------------
1 | // add toggleAll
2 | // need isOpen
3 | // state gets out of whack
4 |
5 | var React = require('react');
6 |
7 | var ContentToggle = React.createClass({
8 |
9 | toggle () {
10 | this.props.onToggle();
11 | },
12 |
13 | componentDidUpdate () {
14 | if (this.props.isOpen)
15 | this.refs.details.getDOMNode().focus();
16 | },
17 |
18 | handleKeyboard (event) {
19 | if (event.key === 'Enter' || event.key === ' ')
20 | this.toggle();
21 | },
22 |
23 | render () {
24 | var details;
25 | var summaryClassName = 'ContentToggle__Summary';
26 |
27 | if (this.props.isOpen) {
28 | details = this.props.children;
29 | summaryClassName += ' ContentToggle__Summary--open';
30 | }
31 |
32 | return (
33 |
34 |
40 | {this.props.summary}
41 |
42 |
43 |
48 | {details}
49 |
50 |
51 | );
52 | }
53 | });
54 |
55 | var App = React.createClass({
56 | getInitialState () {
57 | return {
58 | openAll: false,
59 | toggleStates: {
60 | jerk: false,
61 | tacos: false
62 | }
63 | };
64 | },
65 |
66 | toggleAll () {
67 | var { toggleStates, openAll } = this.state;
68 | var newOpenAll = !openAll;
69 | var newStates = Object.keys(toggleStates).reduce((newStates, key) => {
70 | newStates[key] = newOpenAll;
71 | return newStates;
72 | }, {});
73 | this.setState({
74 | toggleStates: newStates,
75 | openAll: newOpenAll
76 | });
77 | },
78 |
79 | handleToggle (id) {
80 | var { toggleStates } = this.state;
81 | toggleStates[id] = !toggleStates[id];
82 | this.setState({ toggleStates });
83 | var keys = Object.keys(toggleStates);
84 | var areOpen = keys.filter(key => toggleStates[key]);
85 | if (areOpen.length === keys.length) {
86 | this.setState({ openAll: true });
87 | }
88 | else if (areOpen.length === 0) {
89 | this.setState({ openAll: false });
90 | }
91 | },
92 |
93 | render () {
94 | return (
95 |
96 |
Events and State
97 |
Toggle All
98 |
99 |
100 |
105 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
106 |
107 |
108 |
113 | Adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt.
114 |
115 |
116 |
117 | );
118 | }
119 | });
120 |
121 | React.render( , document.body);
122 |
123 |
--------------------------------------------------------------------------------
/excercises/4-props-v-state/solution.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Excercise:
3 | //
4 | // make tabs a "pure component" by not managing any of its own state, instead
5 | // add a property to tell it which tab to show, and then have it communicate
6 | // with its owner to get rerendered with a new active tab.
7 | ////////////////////////////////////////////////////////////////////////////////
8 | var React = require('react');
9 | var styles = require('./styles');
10 | var data = require('./data');
11 |
12 | var Tabs = React.createClass({
13 |
14 | propTypes: {
15 | data: React.PropTypes.array.isRequired,
16 | activeTabIndex: React.PropTypes.number.isRequired,
17 | onActivateTab: React.PropTypes.func.isRequired,
18 | },
19 |
20 | handleTabClick (activeTabIndex) {
21 | this.props.onActivateTab(activeTabIndex);
22 | },
23 |
24 | renderTabs () {
25 | return this.props.data.map((tab, index) => {
26 | var style = this.props.activeTabIndex === index ?
27 | styles.activeTab : styles.tab;
28 | var clickHandler = this.handleTabClick.bind(this, index);
29 | return (
30 |
31 | {tab.name}
32 |
33 | );
34 | });
35 | },
36 |
37 | renderPanel () {
38 | var tab = this.props.data[this.props.activeTabIndex];
39 | return (
40 |
41 |
{tab.description}
42 |
43 | );
44 | },
45 |
46 | render () {
47 | return (
48 |
49 |
50 | {this.renderTabs()}
51 |
52 |
53 | {this.renderPanel()}
54 |
55 |
56 | );
57 | }
58 | });
59 |
60 | var App = React.createClass({
61 | getInitialState () {
62 | return {
63 | activeTabIndex: 0
64 | };
65 | },
66 |
67 | handleActivateTab (activeTabIndex) {
68 | this.setState({ activeTabIndex });
69 | },
70 |
71 | render () {
72 | return (
73 |
74 |
Props v. State
75 |
80 |
81 | );
82 | }
83 | });
84 |
85 | React.render(, document.body);
86 |
87 |
--------------------------------------------------------------------------------
/excercises/4-props-v-state/styles.css:
--------------------------------------------------------------------------------
1 | .ContentToggle__Summary {
2 | cursor: pointer;
3 | border-bottom: solid 1px transparent;
4 | }
5 |
6 | .ContentToggle__Summary:before {
7 | display: inline-block;
8 | width: 1em;
9 | content: '▸';
10 | }
11 |
12 | .ContentToggle__Summary--open:before {
13 | content: '▾';
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/excercises/4-props-v-state/styles.js:
--------------------------------------------------------------------------------
1 | var assign = require('react/lib/Object.assign');
2 | var styles = {};
3 |
4 | styles.tab = {
5 | display: 'inline-block',
6 | padding: 10,
7 | margin: 10,
8 | borderBottom: '4px solid',
9 | borderBottomColor: '#ccc',
10 | cursor: 'pointer'
11 | };
12 |
13 | styles.activeTab = assign({}, styles.tab, {
14 | borderBottomColor: '#000'
15 | });
16 |
17 | styles.tabPanels = {
18 | padding: 10
19 | };
20 |
21 | module.exports = styles;
22 |
23 |
--------------------------------------------------------------------------------
/excercises/5-flux/app.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Excercise:
3 | //
4 | // Add the ability to delete a contact. Make sure to follow the
5 | // flux flow.
6 | //
7 | // hint: use `xhr.deleteJSON(url)` where the url is `/contacts/:id`
8 | ////////////////////////////////////////////////////////////////////////////////
9 | var React = require('react');
10 | var App = require('./app/components/App');
11 | React.render( , document.body);
12 |
13 |
--------------------------------------------------------------------------------
/excercises/5-flux/app/AppDispatcher.js:
--------------------------------------------------------------------------------
1 | var { Dispatcher } = require('flux');
2 | var { PayloadSources } = require('./Constants');
3 | var assign = require('react/lib/Object.assign');
4 |
5 | var AppDispatcher = assign(new Dispatcher(), {
6 |
7 | handleServerAction (action) {
8 | var payload = {
9 | source: PayloadSources.SERVER_ACTION,
10 | action: action
11 | };
12 | this.dispatch(payload);
13 | },
14 |
15 | handleViewAction (action) {
16 | var payload = {
17 | source: PayloadSources.VIEW_ACTION,
18 | action: action
19 | };
20 | this.dispatch(payload);
21 | }
22 | });
23 |
24 | module.exports = AppDispatcher;
25 |
26 |
27 |
--------------------------------------------------------------------------------
/excercises/5-flux/app/Constants.js:
--------------------------------------------------------------------------------
1 | var keyMirror = require('react/lib/keyMirror');
2 |
3 | module.exports = {
4 | //API: 'http://localhost:3000',
5 | API: 'http://addressbook-api.herokuapp.com',
6 |
7 | ActionTypes: keyMirror({
8 | CONTACTS_LOADED: null,
9 | LOAD_CONTACTS: null
10 | }),
11 |
12 | PayloadSources: keyMirror({
13 | SERVER_ACTION: null,
14 | VIEW_ACTION: null
15 | })
16 | };
17 |
18 |
--------------------------------------------------------------------------------
/excercises/5-flux/app/actions/ServerActionCreators.js:
--------------------------------------------------------------------------------
1 | var { ActionTypes } = require('../Constants');
2 | var AppDispatcher = require('../AppDispatcher');
3 |
4 | var ServerActionCreators = {
5 | loadedContacts (contacts) {
6 | AppDispatcher.handleServerAction({
7 | type: ActionTypes.CONTACTS_LOADED,
8 | contacts: contacts
9 | });
10 | }
11 | };
12 |
13 | module.exports = ServerActionCreators;
14 |
15 |
--------------------------------------------------------------------------------
/excercises/5-flux/app/actions/ViewActionCreators.js:
--------------------------------------------------------------------------------
1 | var { ActionTypes } = require('../Constants');
2 | var AppDispatcher = require('../AppDispatcher');
3 | var ApiUtil = require('../utils/ApiUtil');
4 |
5 | var ViewActionCreators = {
6 | loadContacts () {
7 | AppDispatcher.handleViewAction({
8 | type: ActionTypes.LOAD_CONTACTS
9 | });
10 | ApiUtil.loadContacts();
11 | }
12 | };
13 |
14 | module.exports = ViewActionCreators;
15 |
16 |
--------------------------------------------------------------------------------
/excercises/5-flux/app/components/App.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var ContactsStore = require('../stores/ContactsStore');
3 | var ViewActionCreators = require('../actions/ViewActionCreators');
4 |
5 | var App = React.createClass({
6 | getInitialState () {
7 | return ContactsStore.getState();
8 | },
9 |
10 | componentDidMount () {
11 | ContactsStore.addChangeListener(this.handleStoreChange);
12 | ViewActionCreators.loadContacts();
13 | },
14 |
15 | componentWillUnmount () {
16 | ContactsStore.removeChangeListener(this.handleStoreChange);
17 | },
18 |
19 | handleStoreChange () {
20 | this.setState(ContactsStore.getState());
21 | },
22 |
23 | renderContacts () {
24 | return this.state.contacts.map((contact) => {
25 | return {contact.first} {contact.last} ;
26 | });
27 | },
28 |
29 | render () {
30 | if (!this.state.loaded) {
31 | return Loading...
;
32 | }
33 |
34 | return (
35 |
38 | );
39 | }
40 | });
41 |
42 | module.exports = App;
43 |
44 |
--------------------------------------------------------------------------------
/excercises/5-flux/app/lib/xhr.js:
--------------------------------------------------------------------------------
1 | exports.getJSON = (url, cb) => {
2 | var req = new XMLHttpRequest();
3 | req.onload = function () {
4 | if (req.status === 404) {
5 | cb(new Error('not found'));
6 | } else {
7 | cb(null, JSON.parse(req.response));
8 | }
9 | };
10 | req.open('GET', url);
11 | req.send();
12 | };
13 |
14 | exports.postJSON = (url, obj, cb) => {
15 | var req = new XMLHttpRequest();
16 | req.onload = function () {
17 | cb(JSON.parse(req.response));
18 | };
19 | req.open('POST', url);
20 | req.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
21 | req.send(JSON.stringify(obj));
22 | };
23 |
24 | exports.deleteJSON = (url, cb) => {
25 | var req = new XMLHttpRequest();
26 | req.onload = cb;
27 | req.open('DELETE', url);
28 | req.send();
29 | };
30 |
31 |
--------------------------------------------------------------------------------
/excercises/5-flux/app/stores/ContactsStore.js:
--------------------------------------------------------------------------------
1 | var AppDispatcher = require('../AppDispatcher');
2 | var { EventEmitter } = require('events');
3 | var { ActionTypes } = require('../Constants');
4 | var assign = require('react/lib/Object.assign');
5 |
6 | var events = new EventEmitter();
7 | var CHANGE_EVENT = 'CHANGE';
8 |
9 | var state = {
10 | contacts: [],
11 | loaded: false
12 | };
13 |
14 | var setState = (newState) => {
15 | assign(state, newState);
16 | events.emit(CHANGE_EVENT);
17 | };
18 |
19 | var ContactsStore = {
20 | addChangeListener (fn) {
21 | events.addListener(CHANGE_EVENT, fn);
22 | },
23 |
24 | removeChangeListener (fn) {
25 | events.removeListener(CHANGE_EVENT, fn);
26 | },
27 |
28 | getState () {
29 | return state;
30 | }
31 | };
32 |
33 | ContactsStore.dispatchToken = AppDispatcher.register((payload) => {
34 | var { action } = payload;
35 | console.log(action.type);
36 | if (action.type === ActionTypes.CONTACTS_LOADED) {
37 | setState({
38 | loaded: true,
39 | contacts: action.contacts
40 | });
41 | }
42 | });
43 |
44 | module.exports = ContactsStore;
45 |
46 |
--------------------------------------------------------------------------------
/excercises/5-flux/app/utils/ApiUtil.js:
--------------------------------------------------------------------------------
1 | var xhr = require('../lib/xhr');
2 | var { API, ActionTypes } = require('../Constants');
3 | var ServerActionCreators = require('../actions/ServerActionCreators');
4 |
5 | var ApiUtils = {
6 | loadContacts () {
7 | xhr.getJSON(`${API}/contacts`, (err, res) => {
8 | ServerActionCreators.loadedContacts(res.contacts);
9 | });
10 | }
11 | };
12 |
13 | module.exports = ApiUtils;
14 |
15 |
--------------------------------------------------------------------------------
/excercises/5-flux/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Frontend Masters
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/excercises/5-flux/solution/AppDispatcher.js:
--------------------------------------------------------------------------------
1 | var { Dispatcher } = require('flux');
2 | var { PayloadSources } = require('./Constants');
3 | var assign = require('react/lib/Object.assign');
4 |
5 | var AppDispatcher = assign(new Dispatcher(), {
6 |
7 | handleServerAction (action) {
8 | var payload = {
9 | source: PayloadSources.SERVER_ACTION,
10 | action: action
11 | };
12 | this.dispatch(payload);
13 | },
14 |
15 | handleViewAction (action) {
16 | var payload = {
17 | source: PayloadSources.VIEW_ACTION,
18 | action: action
19 | };
20 | this.dispatch(payload);
21 | }
22 | });
23 |
24 | module.exports = AppDispatcher;
25 |
26 |
27 |
--------------------------------------------------------------------------------
/excercises/5-flux/solution/Constants.js:
--------------------------------------------------------------------------------
1 | var keyMirror = require('react/lib/keyMirror');
2 |
3 | module.exports = {
4 | //API: 'http://localhost:3000',
5 | API: 'http://addressbook-api.herokuapp.com',
6 |
7 | ActionTypes: keyMirror({
8 | CONTACTS_LOADED: null,
9 | LOAD_CONTACTS: null,
10 | CONTACT_DELETED: null
11 | }),
12 |
13 | PayloadSources: keyMirror({
14 | SERVER_ACTION: null,
15 | VIEW_ACTION: null
16 | })
17 | };
18 |
19 |
--------------------------------------------------------------------------------
/excercises/5-flux/solution/actions/ServerActionCreators.js:
--------------------------------------------------------------------------------
1 | var { ActionTypes } = require('../Constants');
2 | var AppDispatcher = require('../AppDispatcher');
3 |
4 | var ServerActionCreators = {
5 | loadedContacts (contacts) {
6 | AppDispatcher.handleServerAction({
7 | type: ActionTypes.CONTACTS_LOADED,
8 | contacts: contacts
9 | });
10 | },
11 |
12 | deletedContact (contact) {
13 | AppDispatcher.handleServerAction({
14 | type: ActionTypes.CONTACT_DELETED,
15 | contact: contact
16 | });
17 | }
18 |
19 | };
20 |
21 | module.exports = ServerActionCreators;
22 |
23 |
--------------------------------------------------------------------------------
/excercises/5-flux/solution/actions/ViewActionCreators.js:
--------------------------------------------------------------------------------
1 | var { ActionTypes } = require('../Constants');
2 | var AppDispatcher = require('../AppDispatcher');
3 | var ApiUtil = require('../utils/ApiUtil');
4 |
5 | var ViewActionCreators = {
6 | loadContacts () {
7 | AppDispatcher.handleViewAction({
8 | type: ActionTypes.LOAD_CONTACTS
9 | });
10 | ApiUtil.loadContacts();
11 | },
12 |
13 | deleteContact (contact) {
14 | AppDispatcher.handleViewAction({
15 | type: ActionTypes.CONTACT_DELETED,
16 | contact: contact
17 | });
18 | ApiUtil.deleteContact(contact);
19 | }
20 | };
21 |
22 | module.exports = ViewActionCreators;
23 |
24 |
--------------------------------------------------------------------------------
/excercises/5-flux/solution/components/App.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var ContactsStore = require('../stores/ContactsStore');
3 | var ViewActionCreators = require('../actions/ViewActionCreators');
4 |
5 | var App = React.createClass({
6 | getInitialState () {
7 | return ContactsStore.getState();
8 | },
9 |
10 | componentDidMount () {
11 | ContactsStore.addChangeListener(this.handleStoreChange);
12 | ViewActionCreators.loadContacts();
13 | },
14 |
15 | componentWillUnmount () {
16 | ContactsStore.removeChangeListener(this.handleStoreChange);
17 | },
18 |
19 | handleStoreChange () {
20 | this.setState(ContactsStore.getState());
21 | },
22 |
23 | deleteContact (contact) {
24 | ViewActionCreators.deleteContact(contact);
25 | },
26 |
27 | renderContacts () {
28 | return this.state.contacts.map((contact) => {
29 | return
30 | {contact.first} {contact.last}
31 |
32 | delete
33 |
34 | ;
35 | });
36 | },
37 |
38 | render () {
39 | if (!this.state.loaded) {
40 | return Loading...
;
41 | }
42 |
43 | return (
44 |
47 | );
48 | }
49 | });
50 |
51 | module.exports = App;
52 |
53 |
--------------------------------------------------------------------------------
/excercises/5-flux/solution/lib/xhr.js:
--------------------------------------------------------------------------------
1 | localStorage.token = localStorage.token || (Date.now()*Math.random());
2 |
3 | exports.getJSON = (url, cb) => {
4 | var req = new XMLHttpRequest();
5 | req.onload = function () {
6 | if (req.status === 404) {
7 | cb(new Error('not found'));
8 | } else {
9 | cb(null, JSON.parse(req.response));
10 | }
11 | };
12 | req.open('GET', url);
13 | setToken(req);
14 | req.send();
15 | };
16 |
17 | exports.postJSON = (url, obj, cb) => {
18 | var req = new XMLHttpRequest();
19 | req.onload = function () {
20 | cb(JSON.parse(req.response));
21 | };
22 | req.open('POST', url);
23 | req.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
24 | setToken(req);
25 | req.send(JSON.stringify(obj));
26 | };
27 |
28 | exports.deleteJSON = (url, cb) => {
29 | var req = new XMLHttpRequest();
30 | req.onload = cb;
31 | req.open('DELETE', url);
32 | setToken(req);
33 | req.send();
34 | };
35 |
36 | function setToken (req) {
37 | req.setRequestHeader('authorization', localStorage.token);
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/excercises/5-flux/solution/stores/ContactsStore.js:
--------------------------------------------------------------------------------
1 | var AppDispatcher = require('../AppDispatcher');
2 | var { EventEmitter } = require('events');
3 | var { ActionTypes } = require('../Constants');
4 | var assign = require('react/lib/Object.assign');
5 |
6 | var events = new EventEmitter();
7 | var CHANGE_EVENT = 'CHANGE';
8 |
9 | var state = {
10 | contacts: [],
11 | loaded: false
12 | };
13 |
14 | var setState = (newState) => {
15 | assign(state, newState);
16 | events.emit(CHANGE_EVENT);
17 | };
18 |
19 | var ContactsStore = {
20 | addChangeListener (fn) {
21 | events.addListener(CHANGE_EVENT, fn);
22 | },
23 |
24 | removeChangeListener (fn) {
25 | events.removeListener(CHANGE_EVENT, fn);
26 | },
27 |
28 | getState () {
29 | return state;
30 | }
31 | };
32 |
33 | ContactsStore.dispatchToken = AppDispatcher.register((payload) => {
34 | var { action } = payload;
35 |
36 | if (action.type === ActionTypes.CONTACTS_LOADED) {
37 | setState({
38 | loaded: true,
39 | contacts: action.contacts
40 | });
41 | }
42 |
43 | if (action.type === ActionTypes.CONTACT_DELETED) {
44 | var newContacts = state.contacts.filter((contact) => {
45 | return contact.id !== action.contact.id;
46 | });
47 | setState({ contacts: newContacts });
48 | }
49 |
50 | });
51 |
52 | module.exports = ContactsStore;
53 |
54 |
--------------------------------------------------------------------------------
/excercises/5-flux/solution/utils/ApiUtil.js:
--------------------------------------------------------------------------------
1 | var xhr = require('../lib/xhr');
2 | var { API, ActionTypes } = require('../Constants');
3 | var ServerActionCreators = require('../actions/ServerActionCreators');
4 |
5 | var ApiUtils = {
6 | loadContacts () {
7 | xhr.getJSON(`${API}/contacts`, (err, res) => {
8 | ServerActionCreators.loadedContacts(res.contacts);
9 | });
10 | },
11 |
12 | deleteContact (contact) {
13 | xhr.deleteJSON(`${API}/contacts/${contact.id}`, (err, res) => {
14 | ServerActionCreators.deletedContact(contact);
15 | });
16 | }
17 | };
18 |
19 | module.exports = ApiUtils;
20 |
21 |
--------------------------------------------------------------------------------
/excercises/5-flux/styles.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontendMasters/2015-02-13-React/20a2ebebc2318d6754b7080e49751fe50401a199/excercises/5-flux/styles.css
--------------------------------------------------------------------------------
/excercises/6-routing/ContactStore.js:
--------------------------------------------------------------------------------
1 | //var API = 'http://localhost:3000/contacts';
2 | var API = 'http://addressbook-api.herokuapp.com/contacts';
3 |
4 | var _contacts = {};
5 | var _changeListeners = [];
6 | var _initCalled = false;
7 |
8 | var ContactStore = module.exports = {
9 |
10 | init: function () {
11 | if (_initCalled)
12 | return;
13 |
14 | _initCalled = true;
15 |
16 | getJSON(API, function (err, res) {
17 | res.contacts.forEach(function (contact) {
18 | _contacts[contact.id] = contact;
19 | });
20 |
21 | ContactStore.notifyChange();
22 | });
23 | },
24 |
25 | addContact: function (contact, cb) {
26 | postJSON(API, { contact: contact }, function (res) {
27 | _contacts[res.contact.id] = res.contact;
28 | ContactStore.notifyChange();
29 | if (cb) cb(res.contact);
30 | });
31 | },
32 |
33 | removeContact: function (id, cb) {
34 | deleteJSON(API + '/' + id, cb);
35 | delete _contacts[id];
36 | ContactStore.notifyChange();
37 | },
38 |
39 | getContacts: function () {
40 | var array = [];
41 |
42 | for (var id in _contacts)
43 | array.push(_contacts[id]);
44 |
45 | return array;
46 | },
47 |
48 | getContact: function (id) {
49 | return _contacts[id];
50 | },
51 |
52 | notifyChange: function () {
53 | _changeListeners.forEach(function (listener) {
54 | listener();
55 | });
56 | },
57 |
58 | addChangeListener: function (listener) {
59 | _changeListeners.push(listener);
60 | },
61 |
62 | removeChangeListener: function (listener) {
63 | _changeListeners = _changeListeners.filter(function (l) {
64 | return listener !== l;
65 | });
66 | }
67 |
68 | };
69 |
70 | function getJSON(url, cb) {
71 | var req = new XMLHttpRequest();
72 | req.onload = function () {
73 | if (req.status === 404) {
74 | cb(new Error('not found'));
75 | } else {
76 | cb(null, JSON.parse(req.response));
77 | }
78 | };
79 | req.open('GET', url);
80 | req.send();
81 | }
82 |
83 | function postJSON(url, obj, cb) {
84 | var req = new XMLHttpRequest();
85 | req.onload = function () {
86 | cb(JSON.parse(req.response));
87 | };
88 | req.open('POST', url);
89 | req.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
90 | req.send(JSON.stringify(obj));
91 | }
92 |
93 | function deleteJSON(url, cb) {
94 | var req = new XMLHttpRequest();
95 | req.onload = cb;
96 | req.open('DELETE', url);
97 | req.send();
98 | }
99 |
100 |
--------------------------------------------------------------------------------
/excercises/6-routing/app.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Excercise:
3 | //
4 | // Add a route to "about" and link to it above the "New Contact"
5 | // link on the left sidebar.
6 | ////////////////////////////////////////////////////////////////////////////////
7 |
8 | var React = require('react');
9 | var Router = require('react-router');
10 | var ContactStore = require('./ContactStore');
11 | var {
12 | Route,
13 | DefaultRoute,
14 | NotFoundRoute,
15 | RouteHandler,
16 | Link
17 | } = Router;
18 |
19 | var App = React.createClass({
20 | getInitialState: function () {
21 | return {
22 | contacts: ContactStore.getContacts(),
23 | loading: true
24 | };
25 | },
26 |
27 | componentWillMount: function () {
28 | ContactStore.init();
29 | },
30 |
31 | componentDidMount: function () {
32 | ContactStore.addChangeListener(this.updateContacts);
33 | },
34 |
35 | componentWillUnmount: function () {
36 | ContactStore.removeChangeListener(this.updateContacts);
37 | },
38 |
39 | updateContacts: function () {
40 | this.setState({
41 | contacts: ContactStore.getContacts(),
42 | loading: false
43 | });
44 | },
45 |
46 | render: function () {
47 | var contacts = this.state.contacts.map(function (contact) {
48 | return {contact.first} ;
49 | });
50 | return (
51 |
52 |
53 |
New Contact
54 |
57 |
Invalid Link (not found)
58 |
59 |
60 |
61 |
62 |
63 | );
64 | }
65 | });
66 |
67 | var Index = React.createClass({
68 | render: function () {
69 | return Address Book ;
70 | }
71 | });
72 |
73 | var Contact = React.createClass({
74 |
75 | mixins: [ Router.Navigation, Router.State ],
76 |
77 | getStateFromStore: function (id) {
78 | id = this.getParams().id;
79 | return {
80 | contact: ContactStore.getContact(id)
81 | };
82 | },
83 |
84 | getInitialState: function () {
85 | return this.getStateFromStore();
86 | },
87 |
88 | componentDidMount: function () {
89 | ContactStore.addChangeListener(this.updateContact);
90 | },
91 |
92 | componentWillUnmount: function () {
93 | ContactStore.removeChangeListener(this.updateContact);
94 | },
95 |
96 | componentWillReceiveProps: function () {
97 | this.setState(this.getStateFromStore());
98 | },
99 |
100 | updateContact: function () {
101 | if (!this.isMounted())
102 | return;
103 |
104 | this.setState(this.getStateFromStore());
105 | },
106 |
107 | destroy: function () {
108 | var id = this.getParams().id;
109 | ContactStore.removeContact(id);
110 | this.transitionTo('/');
111 | },
112 |
113 | render: function () {
114 | var contact = this.state.contact || {};
115 | var name = contact.first + ' ' + contact.last;
116 | var avatar = contact.avatar;
117 | return (
118 |
119 |
120 |
{name}
121 |
Delete
122 |
123 | );
124 | }
125 | });
126 |
127 | var NewContact = React.createClass({
128 |
129 | mixins: [ Router.Navigation ],
130 |
131 | createContact: function (event) {
132 | event.preventDefault();
133 | ContactStore.addContact({
134 | first: this.refs.first.getDOMNode().value,
135 | last: this.refs.last.getDOMNode().value
136 | }, function (contact) {
137 | this.transitionTo('contact', { id: contact.id });
138 | }.bind(this));
139 | },
140 |
141 | render: function () {
142 | return (
143 |
152 | );
153 | }
154 | });
155 |
156 | var NotFound = React.createClass({
157 | render: function () {
158 | return Not found ;
159 | }
160 | });
161 |
162 | var routes = (
163 |
164 |
165 |
166 |
167 |
168 |
169 | );
170 |
171 | Router.run(routes, function (Handler) {
172 | React.render( , document.body);
173 | });
174 |
175 |
--------------------------------------------------------------------------------
/excercises/6-routing/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Frontend Masters
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/excercises/6-routing/solution.js:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Excercise:
3 | //
4 | // Add a route to "about" and link to it above the "New Contact"
5 | // link on the left sidebar.
6 | ////////////////////////////////////////////////////////////////////////////////
7 |
8 | var React = require('react');
9 | var Router = require('react-router');
10 | var ContactStore = require('./ContactStore');
11 | var {
12 | Route,
13 | DefaultRoute,
14 | NotFoundRoute,
15 | RouteHandler,
16 | Link
17 | } = Router;
18 |
19 | var App = React.createClass({
20 | getInitialState: function () {
21 | return {
22 | contacts: ContactStore.getContacts(),
23 | loading: true
24 | };
25 | },
26 |
27 | componentWillMount: function () {
28 | ContactStore.init();
29 | },
30 |
31 | componentDidMount: function () {
32 | ContactStore.addChangeListener(this.updateContacts);
33 | },
34 |
35 | componentWillUnmount: function () {
36 | ContactStore.removeChangeListener(this.updateContacts);
37 | },
38 |
39 | updateContacts: function () {
40 | this.setState({
41 | contacts: ContactStore.getContacts(),
42 | loading: false
43 | });
44 | },
45 |
46 | render: function () {
47 | var contacts = this.state.contacts.map(function (contact) {
48 | return {contact.first} ;
49 | });
50 | return (
51 |
52 |
53 |
About
54 |
New Contact
55 |
58 |
Invalid Link (not found)
59 |
60 |
61 |
62 |
63 |
64 | );
65 | }
66 | });
67 |
68 | var Index = React.createClass({
69 | render: function () {
70 | return Address Book ;
71 | }
72 | });
73 |
74 | var Contact = React.createClass({
75 |
76 | mixins: [ Router.Navigation, Router.State ],
77 |
78 | getStateFromStore: function (id) {
79 | id = this.getParams().id;
80 | return {
81 | contact: ContactStore.getContact(id)
82 | };
83 | },
84 |
85 | getInitialState: function () {
86 | return this.getStateFromStore();
87 | },
88 |
89 | componentDidMount: function () {
90 | ContactStore.addChangeListener(this.updateContact);
91 | },
92 |
93 | componentWillUnmount: function () {
94 | ContactStore.removeChangeListener(this.updateContact);
95 | },
96 |
97 | componentWillReceiveProps: function () {
98 | this.setState(this.getStateFromStore());
99 | },
100 |
101 | updateContact: function () {
102 | if (!this.isMounted())
103 | return;
104 |
105 | this.setState(this.getStateFromStore());
106 | },
107 |
108 | destroy: function () {
109 | var id = this.getParams().id;
110 | ContactStore.removeContact(id);
111 | this.transitionTo('/');
112 | },
113 |
114 | render: function () {
115 | var contact = this.state.contact || {};
116 | var name = contact.first + ' ' + contact.last;
117 | var avatar = contact.avatar;
118 | return (
119 |
120 |
121 |
{name}
122 |
Delete
123 |
124 | );
125 | }
126 | });
127 |
128 | var NewContact = React.createClass({
129 |
130 | mixins: [ Router.Navigation ],
131 |
132 | createContact: function (event) {
133 | event.preventDefault();
134 | ContactStore.addContact({
135 | first: this.refs.first.getDOMNode().value,
136 | last: this.refs.last.getDOMNode().value
137 | }, function (contact) {
138 | this.transitionTo('contact', { id: contact.id });
139 | }.bind(this));
140 | },
141 |
142 | render: function () {
143 | return (
144 |
153 | );
154 | }
155 | });
156 |
157 | var NotFound = React.createClass({
158 | render: function () {
159 | return Not found ;
160 | }
161 | });
162 |
163 | var About = React.createClass({
164 | render: function () {
165 | return About ;
166 | }
167 | });
168 |
169 | var routes = (
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 | );
178 |
179 | Router.run(routes, function (Handler) {
180 | React.render( , document.body);
181 | });
182 |
183 |
--------------------------------------------------------------------------------
/excercises/6-routing/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Helvetica Neue", Arial;
3 | font-weight: 200;
4 | }
5 |
6 | a {
7 | color: hsl(200, 50%, 50%);
8 | }
9 |
10 | a.active {
11 | color: hsl(20, 50%, 50%);
12 | }
13 |
14 | .App {
15 | position: absolute;
16 | top: 0;
17 | left: 0;
18 | right: 0;
19 | bottom: 0;
20 | width: 500px;
21 | height: 500px;
22 | }
23 |
24 | .ContactList {
25 | position: absolute;
26 | left: 0;
27 | top: 0;
28 | bottom: 0;
29 | width: 300px;
30 | overflow: auto;
31 | padding: 20px;
32 | }
33 |
34 | .Content {
35 | position: absolute;
36 | left: 300px;
37 | top: 0;
38 | bottom: 0;
39 | right: 0;
40 | border-left: 1px solid #ccc;
41 | overflow: auto;
42 | padding: 40px;
43 | }
44 |
45 |
46 |
--------------------------------------------------------------------------------
/excercises/7-migrating-to-react/app.js:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/excercises/7-migrating-to-react/backbone-todomvc/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/backbone/*
2 | !node_modules/backbone/backbone.js
3 |
4 | node_modules/backbone.localstorage/*
5 | !node_modules/backbone.localstorage/backbone.localStorage.js
6 |
7 | node_modules/jquery/*
8 | !node_modules/jquery/dist
9 | node_modules/jquery/dist/*
10 | !node_modules/jquery/dist/jquery.js
11 |
12 | node_modules/todomvc-app-css/*
13 | !node_modules/todomvc-app-css/index.css
14 |
15 | node_modules/todomvc-common/*
16 | !node_modules/todomvc-common/base.css
17 | !node_modules/todomvc-common/base.js
18 |
19 | node_modules/underscore/*
20 | !node_modules/underscore/underscore.js
21 |
--------------------------------------------------------------------------------
/excercises/7-migrating-to-react/backbone-todomvc/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Backbone.js • TodoMVC
6 |
7 |
8 |
9 |
10 |
22 |
27 |
35 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/excercises/7-migrating-to-react/backbone-todomvc/js/app.js:
--------------------------------------------------------------------------------
1 | /*global $ */
2 | /*jshint unused:false */
3 | var app = app || {};
4 | var ENTER_KEY = 13;
5 | var ESC_KEY = 27;
6 |
7 | $(function () {
8 | 'use strict';
9 |
10 | // sorry, there's no build, jsx loader is async,
11 | // so we're just hacking for now to get the point across
12 | setTimeout(function () {
13 | // kick things off by creating the `App`
14 | new app.AppView();
15 | }, 100);
16 | });
17 |
--------------------------------------------------------------------------------
/excercises/7-migrating-to-react/backbone-todomvc/js/collections/todos.js:
--------------------------------------------------------------------------------
1 | /*global Backbone */
2 | var app = app || {};
3 |
4 | (function () {
5 | 'use strict';
6 |
7 | // Todo Collection
8 | // ---------------
9 |
10 | // The collection of todos is backed by *localStorage* instead of a remote
11 | // server.
12 | var Todos = Backbone.Collection.extend({
13 | // Reference to this collection's model.
14 | model: app.Todo,
15 |
16 | // Save all of the todo items under the `"todos"` namespace.
17 | localStorage: new Backbone.LocalStorage('todos-backbone'),
18 |
19 | // Filter down the list of all todo items that are finished.
20 | completed: function () {
21 | return this.where({completed: true});
22 | },
23 |
24 | // Filter down the list to only todo items that are still not finished.
25 | remaining: function () {
26 | return this.where({completed: false});
27 | },
28 |
29 | // We keep the Todos in sequential order, despite being saved by unordered
30 | // GUID in the database. This generates the next order number for new items.
31 | nextOrder: function () {
32 | return this.length ? this.last().get('order') + 1 : 1;
33 | },
34 |
35 | // Todos are sorted by their original insertion order.
36 | comparator: 'order'
37 | });
38 |
39 | // Create our global collection of **Todos**.
40 | app.todos = new Todos();
41 | })();
42 |
--------------------------------------------------------------------------------
/excercises/7-migrating-to-react/backbone-todomvc/js/models/todo.js:
--------------------------------------------------------------------------------
1 | /*global Backbone */
2 | var app = app || {};
3 |
4 | (function () {
5 | 'use strict';
6 |
7 | // Todo Model
8 | // ----------
9 |
10 | // Our basic **Todo** model has `title`, `order`, and `completed` attributes.
11 | app.Todo = Backbone.Model.extend({
12 | // Default attributes for the todo
13 | // and ensure that each todo created has `title` and `completed` keys.
14 | defaults: {
15 | title: '',
16 | completed: false
17 | },
18 |
19 | // Toggle the `completed` state of this todo item.
20 | toggle: function () {
21 | this.save({
22 | completed: !this.get('completed')
23 | });
24 | }
25 | });
26 | })();
27 |
--------------------------------------------------------------------------------
/excercises/7-migrating-to-react/backbone-todomvc/js/routers/router.js:
--------------------------------------------------------------------------------
1 | /*global Backbone */
2 | var app = app || {};
3 |
4 | (function () {
5 | 'use strict';
6 |
7 | // Todo Router
8 | // ----------
9 | var TodoRouter = Backbone.Router.extend({
10 | routes: {
11 | '*filter': 'setFilter'
12 | },
13 |
14 | setFilter: function (param) {
15 | // Set the current filter to be used
16 | app.TodoFilter = param || '';
17 |
18 | // Trigger a collection filter event, causing hiding/unhiding
19 | // of Todo view items
20 | app.todos.trigger('filter');
21 | }
22 | });
23 |
24 | app.TodoRouter = new TodoRouter();
25 | Backbone.history.start();
26 | })();
27 |
--------------------------------------------------------------------------------
/excercises/7-migrating-to-react/backbone-todomvc/js/views/app-view.js:
--------------------------------------------------------------------------------
1 | /*global Backbone, jQuery, _, ENTER_KEY */
2 | var app = app || {};
3 |
4 | (function ($) {
5 | 'use strict';
6 |
7 | // The Application
8 | // ---------------
9 |
10 | // Our overall **AppView** is the top-level piece of UI.
11 | app.AppView = Backbone.View.extend({
12 |
13 | // Instead of generating a new element, bind to the existing skeleton of
14 | // the App already present in the HTML.
15 | el: '#todoapp',
16 |
17 | // Our template for the line of statistics at the bottom of the app.
18 | statsTemplate: _.template($('#stats-template').html()),
19 |
20 | // Delegated events for creating new items, and clearing completed ones.
21 | events: {
22 | 'keypress #new-todo': 'createOnEnter',
23 | 'click #clear-completed': 'clearCompleted',
24 | 'click #toggle-all': 'toggleAllComplete'
25 | },
26 |
27 | // At initialization we bind to the relevant events on the `Todos`
28 | // collection, when items are added or changed. Kick things off by
29 | // loading any preexisting todos that might be saved in *localStorage*.
30 | initialize: function () {
31 | this.allCheckbox = this.$('#toggle-all')[0];
32 | this.$input = this.$('#new-todo');
33 | this.$footer = this.$('#footer');
34 | this.$main = this.$('#main');
35 | this.$list = $('#todo-list');
36 |
37 | this.listenTo(app.todos, 'add', this.addOne);
38 | this.listenTo(app.todos, 'reset', this.addAll);
39 | this.listenTo(app.todos, 'change:completed', this.filterOne);
40 | this.listenTo(app.todos, 'filter', this.filterAll);
41 | this.listenTo(app.todos, 'all', this.render);
42 |
43 | // Suppresses 'add' events with {reset: true} and prevents the app view
44 | // from being re-rendered for every model. Only renders when the 'reset'
45 | // event is triggered at the end of the fetch.
46 | app.todos.fetch({reset: true});
47 | },
48 |
49 | // Re-rendering the App just means refreshing the statistics -- the rest
50 | // of the app doesn't change.
51 | render: function () {
52 | var completed = app.todos.completed().length;
53 | var remaining = app.todos.remaining().length;
54 |
55 | if (app.todos.length) {
56 | this.$main.show();
57 | this.$footer.show();
58 |
59 | this.$footer.html(this.statsTemplate({
60 | completed: completed,
61 | remaining: remaining
62 | }));
63 |
64 | this.$('#filters li a')
65 | .removeClass('selected')
66 | .filter('[href="#/' + (app.TodoFilter || '') + '"]')
67 | .addClass('selected');
68 | } else {
69 | this.$main.hide();
70 | this.$footer.hide();
71 | }
72 |
73 | this.allCheckbox.checked = !remaining;
74 | },
75 |
76 | // Add a single todo item to the list by creating a view for it, and
77 | // appending its element to the ``.
78 | addOne: function (todo) {
79 | var view = new app.TodoView({ model: todo });
80 | this.$list.append(view.render().el);
81 | },
82 |
83 | // Add all items in the **Todos** collection at once.
84 | addAll: function () {
85 | this.$list.html('');
86 | app.todos.each(this.addOne, this);
87 | },
88 |
89 | filterOne: function (todo) {
90 | todo.trigger('visible');
91 | },
92 |
93 | filterAll: function () {
94 | app.todos.each(this.filterOne, this);
95 | },
96 |
97 | // Generate the attributes for a new Todo item.
98 | newAttributes: function () {
99 | return {
100 | title: this.$input.val().trim(),
101 | order: app.todos.nextOrder(),
102 | completed: false
103 | };
104 | },
105 |
106 | // If you hit return in the main input field, create new **Todo** model,
107 | // persisting it to *localStorage*.
108 | createOnEnter: function (e) {
109 | if (e.which === ENTER_KEY && this.$input.val().trim()) {
110 | app.todos.create(this.newAttributes());
111 | this.$input.val('');
112 | }
113 | },
114 |
115 | // Clear all completed todo items, destroying their models.
116 | clearCompleted: function () {
117 | _.invoke(app.todos.completed(), 'destroy');
118 | return false;
119 | },
120 |
121 | toggleAllComplete: function () {
122 | var completed = this.allCheckbox.checked;
123 |
124 | app.todos.each(function (todo) {
125 | todo.save({
126 | completed: completed
127 | });
128 | });
129 | }
130 | });
131 | })(jQuery);
132 |
--------------------------------------------------------------------------------
/excercises/7-migrating-to-react/backbone-todomvc/js/views/todo-view.js:
--------------------------------------------------------------------------------
1 | /*global Backbone, jQuery, _, ENTER_KEY, ESC_KEY */
2 | var app = app || {};
3 |
4 | (function ($) {
5 | 'use strict';
6 |
7 | // Todo Item View
8 | // --------------
9 |
10 | // The DOM element for a todo item...
11 | app.TodoView = Backbone.View.extend({
12 | //... is a list tag.
13 | tagName: 'li',
14 |
15 | // Cache the template function for a single item.
16 | template: _.template($('#item-template').html()),
17 |
18 | // The DOM events specific to an item.
19 | events: {
20 | 'click .toggle': 'toggleCompleted',
21 | 'dblclick label': 'edit',
22 | 'click .destroy': 'clear',
23 | 'keypress .edit': 'updateOnEnter',
24 | 'keydown .edit': 'revertOnEscape',
25 | 'blur .edit': 'close'
26 | },
27 |
28 | // The TodoView listens for changes to its model, re-rendering. Since
29 | // there's a one-to-one correspondence between a **Todo** and a
30 | // **TodoView** in this app, we set a direct reference on the model for
31 | // convenience.
32 | initialize: function () {
33 | this.listenTo(this.model, 'change', this.render);
34 | this.listenTo(this.model, 'destroy', this.remove);
35 | this.listenTo(this.model, 'visible', this.toggleVisible);
36 | },
37 |
38 | // Re-render the titles of the todo item.
39 | render: function () {
40 | // Backbone LocalStorage is adding `id` attribute instantly after
41 | // creating a model. This causes our TodoView to render twice. Once
42 | // after creating a model and once on `id` change. We want to
43 | // filter out the second redundant render, which is caused by this
44 | // `id` change. It's known Backbone LocalStorage bug, therefore
45 | // we've to create a workaround.
46 | // https://github.com/tastejs/todomvc/issues/469
47 | if (this.model.changed.id !== undefined) {
48 | return;
49 | }
50 |
51 | this.$el.html(this.template(this.model.toJSON()));
52 | this.$el.toggleClass('completed', this.model.get('completed'));
53 | this.toggleVisible();
54 | this.$input = this.$('.edit');
55 | return this;
56 | },
57 |
58 | toggleVisible: function () {
59 | this.$el.toggleClass('hidden', this.isHidden());
60 | },
61 |
62 | isHidden: function () {
63 | return this.model.get('completed') ?
64 | app.TodoFilter === 'active' :
65 | app.TodoFilter === 'completed';
66 | },
67 |
68 | // Toggle the `"completed"` state of the model.
69 | toggleCompleted: function () {
70 | this.model.toggle();
71 | },
72 |
73 | // Switch this view into `"editing"` mode, displaying the input field.
74 | edit: function () {
75 | this.$el.addClass('editing');
76 | this.$input.focus();
77 | },
78 |
79 | // Close the `"editing"` mode, saving changes to the todo.
80 | close: function () {
81 | var value = this.$input.val();
82 | var trimmedValue = value.trim();
83 |
84 | // We don't want to handle blur events from an item that is no
85 | // longer being edited. Relying on the CSS class here has the
86 | // benefit of us not having to maintain state in the DOM and the
87 | // JavaScript logic.
88 | if (!this.$el.hasClass('editing')) {
89 | return;
90 | }
91 |
92 | if (trimmedValue) {
93 | this.model.save({ title: trimmedValue });
94 |
95 | if (value !== trimmedValue) {
96 | // Model values changes consisting of whitespaces only are
97 | // not causing change to be triggered Therefore we've to
98 | // compare untrimmed version with a trimmed one to check
99 | // whether anything changed
100 | // And if yes, we've to trigger change event ourselves
101 | this.model.trigger('change');
102 | }
103 | } else {
104 | this.clear();
105 | }
106 |
107 | this.$el.removeClass('editing');
108 | },
109 |
110 | // If you hit `enter`, we're through editing the item.
111 | updateOnEnter: function (e) {
112 | if (e.which === ENTER_KEY) {
113 | this.close();
114 | }
115 | },
116 |
117 | // If you're pressing `escape` we revert your change by simply leaving
118 | // the `editing` state.
119 | revertOnEscape: function (e) {
120 | if (e.which === ESC_KEY) {
121 | this.$el.removeClass('editing');
122 | // Also reset the hidden input back to the original value.
123 | this.$input.val(this.model.get('title'));
124 | }
125 | },
126 |
127 | // Remove the item, destroy the model from *localStorage* and delete its view.
128 | clear: function () {
129 | this.model.destroy();
130 | }
131 | });
132 | })(jQuery);
133 |
--------------------------------------------------------------------------------
/excercises/7-migrating-to-react/backbone-todomvc/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "dependencies": {
4 | "react": "0.12.2",
5 | "backbone": "^1.1.2",
6 | "backbone.localstorage": "^1.1.16",
7 | "jquery": "^2.1.3",
8 | "todomvc-app-css": "^1.0.0",
9 | "todomvc-common": "^1.0.1",
10 | "underscore": "^1.7.0"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/excercises/7-migrating-to-react/backbone-todomvc/readme.md:
--------------------------------------------------------------------------------
1 | # Backbone.js TodoMVC Example
2 |
3 | > Backbone.js gives structure to web applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing API over a RESTful JSON interface.
4 |
5 | > _[Backbone.js - backbonejs.org](http://backbonejs.org)_
6 |
7 |
8 | ## Learning Backbone.js
9 |
10 | The [Backbone.js website](http://backbonejs.org) is a great resource for getting started.
11 |
12 | Here are some links you may find helpful:
13 |
14 | * [Annotated source code](http://backbonejs.org/docs/backbone.html)
15 | * [Applications built with Backbone.js](http://backbonejs.org/#examples)
16 | * [FAQ](http://backbonejs.org/#faq)
17 |
18 | Articles and guides from the community:
19 |
20 | * [Developing Backbone.js Applications](http://addyosmani.github.io/backbone-fundamentals)
21 | * [Collection of tutorials, blog posts, and example sites](https://github.com/documentcloud/backbone/wiki/Tutorials%2C-blog-posts-and-example-sites)
22 |
23 | Get help from other Backbone.js users:
24 |
25 | * [Backbone.js on StackOverflow](http://stackoverflow.com/questions/tagged/backbone.js)
26 | * [Google Groups mailing list](https://groups.google.com/forum/#!forum/backbonejs)
27 | * [Backbone.js on Twitter](http://twitter.com/documentcloud)
28 |
29 | _If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues)._
30 |
--------------------------------------------------------------------------------
/excercises/7-migrating-to-react/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Frontend Masters
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/excercises/7-migrating-to-react/styles.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontendMasters/2015-02-13-React/20a2ebebc2318d6754b7080e49751fe50401a199/excercises/7-migrating-to-react/styles.css
--------------------------------------------------------------------------------
/excercises/assert.js:
--------------------------------------------------------------------------------
1 | module.exports = (pass, description) => {
2 | if (pass === true) {
3 | console.log('%c✔︎ ok', 'color: green', description);
4 | }
5 | else {
6 | console.assert(pass, description);
7 | }
8 | };
9 |
10 |
--------------------------------------------------------------------------------
/excercises/index.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/excercises/shared.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Helvetica Neue";
3 | }
4 |
5 | #index {
6 | text-align: center;
7 | font-size: 40;
8 | font-weight: 200;
9 | line-height: 1.7;
10 | }
11 |
12 | #index ul {
13 | list-style: none;
14 | padding: 0;
15 | margin: 0;
16 | }
17 |
18 | #index a {
19 | background: #eee;
20 | color: #000;
21 | padding: 0.125em 0.25em;
22 | text-decoration: none;
23 | }
24 |
25 | #index p {
26 | font-size: 50%;
27 | margin: 0;
28 | color: #ccc;
29 | }
30 |
31 | #index a:hover {
32 | background: #aaa;
33 | }
34 |
35 | #index a:visited {
36 | color: #fff;
37 | }
38 |
--------------------------------------------------------------------------------
/flux-diagram-white-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontendMasters/2015-02-13-React/20a2ebebc2318d6754b7080e49751fe50401a199/flux-diagram-white-background.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontendmasters",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "start": "webpack-dev-server --inline --no-info --content-base excercises"
7 | },
8 | "author": "Ryan Florence ",
9 | "license": "ISC",
10 | "dependencies": {
11 | "MD5": "1.2.1",
12 | "bluebird": "2.9.2",
13 | "flux": "2.0.1",
14 | "jquery": "2.1.3",
15 | "jquery-ui": "1.10.5",
16 | "jsx-loader": "0.12.2",
17 | "react": "0.12.2",
18 | "react-router": "0.11.6",
19 | "sort-by": "1.1.0",
20 | "webpack": "1.4.15",
21 | "webpack-dev-server": "1.7.0"
22 | },
23 | "devDependencies": {
24 | "6to5-core": "3.6.0",
25 | "6to5-loader": "3.0.0",
26 | "expect": "1.6.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/props-v-state.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontendMasters/2015-02-13-React/20a2ebebc2318d6754b7080e49751fe50401a199/props-v-state.png
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var path = require('path');
3 | var webpack = require('webpack');
4 | var CODE = __dirname+'/excercises';
5 | var React = require('react');
6 |
7 | makeIndex();
8 |
9 | module.exports = {
10 |
11 | devtool: 'eval',
12 |
13 | entry: fs.readdirSync(CODE).reduce(function (entries, dir) {
14 | if (isDirectory(path.join(CODE, dir)))
15 | entries[dir] = path.join(CODE, dir, 'app.js');
16 | return entries;
17 | }, {}),
18 |
19 | output: {
20 | path: 'excercises/__build__',
21 | filename: '[name].js',
22 | chunkFilename: '[id].chunk.js',
23 | publicPath: '/__build__/'
24 | },
25 |
26 | module: {
27 | loaders: [
28 | { test: /\.json$/, loader: 'json-loader' },
29 | { test: /\.js$/, loader: 'jsx-loader?harmony' }
30 | ]
31 | },
32 |
33 | plugins: [
34 | new webpack.optimize.CommonsChunkPlugin('shared.js')
35 | ]
36 |
37 | };
38 |
39 | function makeIndex () {
40 | var list = fs.readdirSync(CODE).filter(function(dir) {
41 | return isDirectory(path.join(CODE, dir));
42 | }).map(function (dir) {
43 | return React.DOM.li({}, React.DOM.a({href: '/'+dir}, dir.replace(/-/g, ' ')));
44 | });
45 | var markup = React.renderToStaticMarkup((
46 | React.DOM.html({},
47 | React.DOM.link({rel: 'stylesheet', href: '/shared.css'}),
48 | React.DOM.body({id: "index"},
49 | React.DOM.ul({}, list)
50 | )
51 | )
52 | ));
53 | fs.writeFileSync('./excercises/index.html', markup);
54 | }
55 |
56 | function isDirectory(dir) {
57 | return fs.lstatSync(dir).isDirectory();
58 | }
59 |
60 |
--------------------------------------------------------------------------------