├── .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 | 39 |
40 | ); 41 | 42 | //////////////////////////////////////////////////////////////////////////////// 43 | var element = ( 44 |
45 |

hello!

46 | 49 |
50 | ); 51 | 52 | //////////////////////////////////////////////////////////////////////////////// 53 | var items = items.sort().map((item) =>
  • {item}
  • ); 54 | var element = ( 55 |
    56 |

    hello!

    57 | 58 |
    59 | ); 60 | 61 | 62 | //////////////////////////////////////////////////////////////////////////////// 63 | var App = React.createClass({ 64 | render () { 65 | var items = items.sort().map((item) =>
  • {item}
  • ); 66 | return ( 67 |
    68 |

    hello!

    69 |
    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 |
    68 |

    Users

    69 |
      {users}
    70 |
    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 |
    33 |

    Users

    34 |
      {users}
    35 |
    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 |
    58 |

    Users

    59 |
      {users}
    60 |
    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 |
    91 |

    Users

    92 |
      {users}
    93 |
    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 |
    131 |

    Users

    132 |
      {users}
    133 |
    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 |
    58 |

    Users

    59 |
      {users}
    60 |
    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 | 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 | 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 | 47 | 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 | 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 |
    36 |
      {this.renderContacts()}
    37 |
    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 | 34 |
  • ; 35 | }); 36 | }, 37 | 38 | render () { 39 | if (!this.state.loaded) { 40 | return
    Loading...
    ; 41 | } 42 | 43 | return ( 44 |
    45 |
      {this.renderContacts()}
    46 |
    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 |
      55 | {contacts} 56 |
    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 | 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 |
    144 |

    145 | 146 | 147 |

    148 |

    149 | Cancel 150 |

    151 |
    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 |
      56 | {contacts} 57 |
    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 | 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 |
    145 |

    146 | 147 | 148 |

    149 |

    150 | Cancel 151 |

    152 |
    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 |
    11 | 15 |
    16 | 17 | 18 |
      19 |
      20 |
      21 |
      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 | --------------------------------------------------------------------------------