diff --git a/.babelrc b/.babelrc
deleted file mode 100644
index 68ffd3bce..000000000
--- a/.babelrc
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "presets": [
- "es2015",
- "stage-1",
- "react"
- ],
- "plugins": [
- "transform-object-assign"
- ]
-}
diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index 4a7ea3036..000000000
--- a/.editorconfig
+++ /dev/null
@@ -1,12 +0,0 @@
-root = true
-
-[*]
-indent_style = space
-indent_size = 2
-end_of_line = lf
-charset = utf-8
-trim_trailing_whitespace = true
-insert_final_newline = true
-
-[*.md]
-trim_trailing_whitespace = false
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index 63585f08e..000000000
--- a/.eslintignore
+++ /dev/null
@@ -1 +0,0 @@
-subjects/migrating-to-react/backbone-todomvc
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index 45ec85f00..000000000
--- a/.eslintrc
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "parser": "babel-eslint",
- "env": {
- "browser": true,
- "node": true
- },
- "plugins": [
- "import",
- "react"
- ],
- "extends": [
- "eslint:recommended",
- "plugin:import/errors",
- "plugin:react/recommended"
- ],
- "settings": {
- "import/resolver": "webpack"
- },
- "rules": {
- "array-bracket-spacing": [ 1, "always" ],
- "comma-dangle": [ 1, "never" ],
- "eqeqeq": [ 2, "smart" ],
- "jsx-quotes": [ 1, "prefer-double" ],
- "no-unused-vars": 0,
- "object-curly-spacing": [ 1, "always" ],
- "quotes": [ 1, "single", "avoid-escape" ],
- "react/jsx-space-before-closing": [ 1, "never" ],
- "react/no-did-mount-set-state": 0,
- "react/prop-types": 1,
- "semi": [ 1, "never" ],
- "space-before-blocks": [ 1, "always" ]
- }
-}
diff --git a/.gitignore b/.gitignore
index 00026d63b..ded057a5e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,3 @@
-subjects/index.html
-subjects/**/lecture.html
-subjects/**/exercise.html
-subjects/**/solution.html
-!subjects/**/backbone-todomvc/index.html
node_modules
+yarn-error.log
+public/**/*.html
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index d2091e4df..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-language: node_js
-node_js:
-- stable
-branches:
- only:
- - master
- - /^greenkeeper-/
diff --git a/JavaScriptPrimer.md b/JavaScriptPrimer.md
index 221fb98e9..3372bbcfd 100644
--- a/JavaScriptPrimer.md
+++ b/JavaScriptPrimer.md
@@ -7,44 +7,44 @@ Note: All of the syntax discussed here is actually part of the JavaScript spec,
JavaScript has always had `var`:
```js
-var name = 'Ryan'
+var name = "Michael";
```
`var` can be hard to manage especially because of it's "function scoping", so now we've got two other ways to define values that have "block scope":
```js
// var does not have block scope
-var name = 'Ryan'
+var name = "Michael";
if (true) {
- var name = 'Michael'
- name // 'Michael'
+ var name = "Bruce";
+ name; // 'Bruce'
}
-name // 'Michael'
+name; // 'Bruce'
// let has block scope
-let name = 'Ryan'
+let name = "Michael";
if (true) {
- let name = 'Michael'
- name // 'Michael'
+ let name = "Bruce";
+ name; // 'Bruce'
}
-name // 'Ryan'
+name; // 'Michael'
// const has block scope too
-const name = 'Ryan'
+const name = "Michael";
if (true) {
- const name = 'Michael'
- name // 'Michael'
+ const name = "Bruce";
+ name; // 'Bruce'
}
-name // 'Ryan'
+name; // 'Michael'
// let can be reassigned
-let isOpen = true
-isOpen = false
-isOpen // false
+let isOpen = true;
+isOpen = false;
+isOpen; // false
// const cannot be reassigned
-const isOpen = true
-isOpen = false // throws error
+const isOpen = true;
+isOpen = false; // throws error
```
We find block scope to make more sense to people and is generally more useful, therefore we don't use `var`.
@@ -56,17 +56,17 @@ In practice, nearly everything is `const`.
## String templates
```js
-const something = 'ugly stuff'
-const str = 'instead of ' + something + ' like this'
+const something = "ugly stuff";
+const str = "instead of " + something + " like this";
-const something = 'lovely stuff'
-const str = `you can do ${something} like this`
+const something = "lovely stuff";
+const str = `you can do ${something} like this`;
const str = `
also
multiline
is totally cool
-`
+`;
```
## Concise object methods
@@ -82,7 +82,7 @@ const obj = {
youCanDoThis() {
// do stuff
}
-}
+};
```
## Arrow functions
@@ -91,33 +91,32 @@ Arrow functions remove the context from a function, meaning the function has no
```js
const obj = {
- url: '/api/stuff',
+ url: "/api/stuff",
fetch(users) {
- users.forEach((user) => {
+ users.forEach(user => {
// `this` is the `this` from outside this function because
// there is no context inside an arrow function
- getUser(`${this.url}/${user.id}`)
- })
+ getUser(`${this.url}/${user.id}`);
+ });
}
-}
+};
```
Also, if the other side of an arrow function is an expression, it acts like an implicit return:
```js
-const add = function(x, y) { return x + y }
+const add = function(x, y) {
+ return x + y;
+};
// becomes
-const add = (x, y) => { return x + y }
+const add = (x, y) => {
+ return x + y;
+};
// which can be shorter with explicit expression return
-const add = (x, y) => x + y
-
-// if we want multiline, we can create an expression with ()
-const add = (x, y) => (
- x + y
-)
+const add = (x, y) => x + y;
```
## Arrays
@@ -125,55 +124,57 @@ const add = (x, y) => (
We do a lot with arrays, here are a few methods we use often:
```js
-const numbers = [ 1, 2, 3, 4, 5 ]
+const numbers = [1, 2, 3, 4, 5];
// map converts an array to a new, transformed array
-const doubled = numbers.map((number) => {
- return number * 2
-})
-doubled // [ 2, 4, 6, 8, 10 ]
+const doubled = numbers.map(number => {
+ return number * 2;
+});
+doubled; // [ 2, 4, 6, 8, 10 ]
// filter, return false to remove from an array
-const lessThan3 = numbers.filter((n) => {
- return n < 3
-})
-lessThan3 // [ 1, 2 ]
+const lessThan3 = numbers.filter(n => {
+ return n < 3;
+});
+lessThan3; // [ 1, 2 ]
// remember, that can be super short
-const lessThan3 = numbers.filter(n => n < 3)
+const lessThan3 = numbers.filter(n => n < 3);
```
## Destructuring
```js
-const obj = { x: 1, y: 2 }
+const obj = { x: 1, y: 2 };
// instead of:
-const x = obj.x
-const y = obj.y
+const x = obj.x;
+const y = obj.y;
// we can "destructure" the values off
-const { x, y } = obj
-x // 1
-y // 2
+const { x, y } = obj;
+x; // 1
+y; // 2
// you can use this all over the place, like function parameters
-function add({x, y}) { return x + y}
-add({x: 3, y: 4}) // 7
+function add({ x, y }) {
+ return x + y;
+}
+add({ x: 3, y: 4 }); // 7
```
## Modules
```js
// instead of cjs
-var React = require('react')
+var React = require("react");
// we use ES modules
-import React from 'react'
-import ReactDOM from 'react-dom'
+import React from "react";
+import ReactDOM from "react-dom";
// and with destructuring to boot!
-import { render } from 'react-dom'
+import { render } from "react-dom";
```
Our training material then uses [Webpack](https://webpack.github.io/), a module bundler, to graph the dependencies and create a build so that this works in browsers that don't yet support some of these features natively.
diff --git a/README.md b/README.md
index 44678f4b5..7ad0968c8 100644
--- a/README.md
+++ b/README.md
@@ -5,13 +5,13 @@ This repo contains the course material for [React Training](https://reacttrainin
First, install [git](http://git-scm.com/downloads) and the latest stable version of [node](https://nodejs.org/). Then:
```sh
-$ git clone https://github.com/ReactTraining/react-subjects.git
-$ cd react-subjects
+$ git clone https://github.com/ReactTraining/react-workshop.git
+$ cd react-workshop
$ npm install
$ npm start
```
-Then open your browser to [http://localhost:8080](http://localhost:8080). You should see a list of subjects.
+Your web browser should open to [http://localhost:8080](http://localhost:8080)where you'll see a list of subjects.
**IMPORTANT:** Please read the [JavaScript Primer](https://github.com/ReactTraining/react-subjects/blob/master/JavaScriptPrimer.md) before attending the workshop. It's a refresher on some of the newer bits of JavaScript you'll want to be familiar with in order to get the most out of the experience.
@@ -19,7 +19,7 @@ Then open your browser to [http://localhost:8080](http://localhost:8080). You sh
A few common problems:
-- **You're having problems cloning the repository.** Some corporate networks block port 22, which git uses to communicate with GitHub over SSH. Instead of using SSH, clone the repo over HTTPS. Use the following command to tell git to always use `https` instead of `git`:
+* **You're having problems cloning the repository.** Some corporate networks block port 22, which git uses to communicate with GitHub over SSH. Instead of using SSH, clone the repo over HTTPS. Use the following command to tell git to always use `https` instead of `git`:
```sh
$ git config --global url."https://".insteadOf git://
@@ -29,19 +29,19 @@ $ git config --global url."https://".insteadOf git://
insteadOf = git://
```
-- **You're having trouble installing node.** We recommend using [nvm](https://github.com/creationix/nvm). nvm makes it really easy to use multiple versions of node on the same machine painlessly. After you install nvm, install the latest stable version of node with the following command:
+* **You're having trouble installing node.** We recommend using [nvm](https://github.com/creationix/nvm). nvm makes it really easy to use multiple versions of node on the same machine painlessly. After you install nvm, install the latest stable version of node with the following command:
```sh
$ nvm use default stable
```
-- **You don't have permissions to install stuff.** You might see an error like `EACCES` during the `npm install` step. If that's the case, it probably means that at some point you did an `sudo npm install` and installed some stuff with root permissions. To fix this, you need to forcefully remove all files that npm caches on your machine and re-install without sudo.
+* **You don't have permissions to install stuff.** You might see an error like `EACCES` during the `npm install` step. If that's the case, it probably means that at some point you did an `sudo npm install` and installed some stuff with root permissions. To fix this, you need to forcefully remove all files that npm caches on your machine and re-install without sudo.
```sh
$ sudo rm -rf node_modules
# If you installed node with nvm (suggested):
-$ sudo rm -rf ~/.npm
+$ sudo rm -rf ~/.npm
# If you installed node with Homebrew:
$ sudo rm -rf /usr/local/lib/node_modules
diff --git a/Syllabus.md b/Syllabus.md
deleted file mode 100644
index 6795dba81..000000000
--- a/Syllabus.md
+++ /dev/null
@@ -1,209 +0,0 @@
-# Syllabus
-
-This document contains a list of subjects we cover in our React training workshops. The stuff near the top is generally part of our "React Fundamentals" workshop and the stuff further down we generally cover in "Advanced React" workshops.
-
-For any workshop, please be sure you are familiar with the material in our [JavaScript primer](https://github.com/ReactJSTraining/react-subjects/blob/master/JavaScriptPrimer.md) before attending.
-
-## Eliminating Time
-
-Solutions are nonsense without understanding the problems they solve. We'll take a trip down memory lane to shine a light on the stuff that led to all the bugs we've written in the past to prime us to see clearly how React is going to help.
-
-Objectives:
-- Understand the problems React solves
-- Get people thinking about [the conceptual gap between the static program and the dynamic process][Dijkstra], and what it means to "eliminate time" in their apps
-
-[Dijkstra]: https://en.wikiquote.org/wiki/Edsger_W._Dijkstra
-
-
-## Rendering with React
-
-We'll explore the lowest level of React: rendering UI. We'll take a look at what the "Virtual DOM" and JSX are, brush up on some JavaScript array methods, and do our first coding exercise.
-
-Objectives:
-- Render UI with React
-- Transform and massage data into UI
-- Become familiar with JSX
-- Brush up on JavaScript expressions and scope
-
-
-## Components
-
-Components are the building blocks of UI in React. We'll discuss a few parts of a component's lifecycle, it's interface with the rest of the application, component state, encapsulation, and how to make components reusable.
-
-Objectives:
-- Define components
-- Determine good values for component state
-- Interface with the rest of the world (props)
-- Use propTypes as "runnable documentation"
-- Handle user interaction
-
-
-## Props vs. State
-
-One of the first questions that comes up when you start using React is "when do I use state, and when do I use props?" We'll explore an app with changing requirements that lets us experience state moving to props and why. We'll discuss how data flows, or rather, how components communicate with each other. Finally, we'll explore how component composition helps answer the "props vs. state" question.
-
-Objectives:
-- Understand the difference between props and state
-- Know when to use props vs. state
-- Pass data up and down the view hierarchy
-- Compose generic components into specialized components
-
-
-## Imperative to Declarative
-
-React is "declarative". What does that mean? What is imperative code? We'll answer these questions, and show the power that comes from shifting your imperative code into declarative components using more of the React component lifecycle.
-
-Objectives:
-- Turn imperative code into declarative code
-- Experience the benefits that come from declarative programming
-- Learn to use existing JS libraries declaratively
-- Use more of the React component lifecycle
-- Understand how and when to use `ref`s
-
-
-## Forms
-
-Solidify your [declarative](#imperative-to-declarative) understanding by working with forms in React. We'll discuss the various use-cases for forms and how to handle them in React.
-
-Objectives:
-- Understand [controlled vs. uncontrolled](https://facebook.github.io/react/docs/forms.html) components
-- Work with forms
-
-
-## Testing
-
-Testing UI has never been this straightforward. You'll learn how to test any UI interaction your app has.
-
-Objectives:
-- Test stateless components
-- Test stateful components
-- Simulate events
-
-
-## Compound Components
-
-Some components get really, really big. Not only do their render methods get large, but as more people try to use the component, the props it takes grow as well. Eventually you end up with way too many properties and a really difficult component to work with that has to change with every new use-case you throw at it. There's a better way with compound components.
-
-Objectives:
-- Create reusable components by compounding related components together
-- Dynamically flow data between components
-
-
-## Controlled Compound Components
-
-Some reusable components are a lot like form elements and need to be controlled or uncontrolled. We'll examine techniques to implement components with this kind of behavior.
-
-Objectives:
-- Create a component that can be controlled or uncontrolled
-
-Prerequisites:
-- [Compound Components](#compound-components)
-- [Forms](#forms)
-
-
-## Context
-
-Context is an advanced, slightly-unstable, and powerful tool that solves a handful of use-cases really elegantly. We'll discuss when and why it's a good solution, and when it's not.
-
-Objectives:
-- Learn to use context
-- Learn when it's useful and when to use a different pattern
-
-Prerequisites:
-- [Compound Components](#compound-components)
-
-
-## Higher Order Components
-
-Higher order components allow you to compose behavior into components without inheritence or modifying the component itself with a wide range of use-cases.
-
-Objectives:
-- Create a higher order component
-- Understand when to use a higher order component
-
-
-## Render Props
-
-As we begin to make more things declarative we run into code that seems unlikely to be made declarative. We'll explore this pattern and see how it allows us to make anything in our app declarative.
-
-Objectives:
-- Learn to use render props
-- Learn when they are most useful
-
-
-## Performance and Render Optimizations
-
-As your app grows you'll eventually hit some slow interactions. We'll explore how to identify the bottlenecks and how to fix them. We'll also see how React can help us avoid the bottlenecks in the first place.
-
-Objectives:
-- Identify performance bottlenecks
-- Learn how to do "windowing" with React
-
-
-## Animation
-
-We'll showcase several ways to do animation including imperatively with 3rd-party animation libs like jQuery and declaratively with [react-motion](https://github.com/chenglou/react-motion).
-
-Objectives:
-- Animate elements
-
-Prerequisites:
-- [Imperative to Declarative](#imperative-to-declarative)
-- [Render Props](#render-props)
-
-
-## Routing
-
-Keep your application UI and the URL in sync, no more broken back buttons. We'll introduce some basic uses of [React Router](https://github.com/ReactTraining/react-router), and how it helps you weave together your components into an application with multiple screens.
-
-Objectives:
-- Understand principles of client-side "routing"
-- Understand use-cases for routing
-- Create UI at specific URLs
-- Transition between screens
-
-Prerequisites:
-- [Render Props](#render-props)
-
-
-## Server Rendering
-
-React lets you render your components server-side for improved SEO and performance. In this section we'll discuss the techniques and trade-offs of rendering your React components on the server. We'll also transition a fully client-side app to server-side rendering, and discuss the use cases we solve each step of the way.
-
-Objectives:
-- Understand principles of server-side rendering and when it's useful
-- Render components server-side
-- Fetch data before rendering server-side
-- Seamlessly transition to the client page without replacing HTML already in the page
-
-
-## Redux
-
-Redux is a library for managing application state that can be useful when using React. We'll work on a real Redux app that makes requests to a real server and explore some techniques for managing shared state client-side.
-
-Objectives:
-- Understand one-way data flow
-- Understand Redux concepts like stores and actions
-- Implement a feature using Redux
-- Handle latency and errors using Redux
-
-
-## Implementing Redux
-
-The react-redux bindings make use of several advanced features of React including [context](#context) and [higher-order components](#higher-order-components). We'll use both of these concepts to build our own "mini Redux" and discover how Redux really works behind the scenes.
-
-Objectives:
-- Use higher-order components to put stuff on context
-- Use context to pass state down the hierarchy
-
-Prerequisites:
-- [Context](#context)
-- [Higher Order Components](#higher-order-components)
-
-
-## Migrating to React
-
-This section is designed for teams that want to migrate, instead of rewrite, their existing app to React in a way that won't block everyone else on the team. You'll be writing and shipping your new code incrementally. We'll also discuss how to integrate with existing JS libs as you encounter them in your app.
-
-Objectives:
-- Learn how to integrate React into an existing code-base
diff --git a/package.json b/package.json
index f3def1cc2..c55dc9b99 100644
--- a/package.json
+++ b/package.json
@@ -1,71 +1,62 @@
{
- "description": "Course material for http://reacttraining.com",
- "repository": "ReactTraining/react-subjects",
+ "private": true,
+ "name": "react-workshop",
+ "description": "Lectures and exercises for React Training workshops",
+ "repository": "ReactTraining/react-workshop",
+ "homepage": "https://reacttraining.com",
+ "author": "React Training LLC",
"license": "GPL-3.0",
- "authors": [
- "Ryan Florence",
- "Michael Jackson"
- ],
"scripts": {
- "start": "node ./scripts/build.js && webpack-dev-server --inline --content-base subjects",
- "server-exercise": "supervisor -- -r babel-register subjects/ServerRendering/exercise/server.js",
- "server-solution": "supervisor -- -r babel-register subjects/ServerRendering/solution/server.js",
- "start-api": "node api.js",
- "lint": "eslint subjects/**/*.js",
- "test": "npm run lint"
+ "start": "node ./scripts/build.js && webpack-dev-server --inline --content-base public",
+ "ssr-exercise": "supervisor -- -r babel-register 'subjects/14-Server-Rendering/exercise/server.js'",
+ "ssr-solution": "supervisor -- -r babel-register 'subjects/14-Server-Rendering/solution/server.js'"
},
"dependencies": {
"angular": "1.5.8",
- "assert": "1.4.1",
+ "babel-core": "^6.23.1",
"babel-loader": "^6.2.4",
- "babel-plugin-transform-object-assign": "^6.5.0",
- "babel-preset-es2015": "^6.6.0",
+ "babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.5.0",
- "babel-preset-stage-1": "^6.5.0",
+ "babel-preset-stage-2": "^6.24.1",
"babel-register": "^6.9.0",
"backbone": "^1.2.3",
"body-parser": "^1.15.2",
"bootstrap": "^3.3.4",
"bootstrap-webpack": "0.0.5",
+ "create-react-class": "^15.6.3",
"css-loader": "^0.23.1",
"events": "^1.0.2",
- "expect": "^1.13.4",
+ "expect": "^23.0.0",
"exports-loader": "^0.6.3",
- "expose-loader": "^0.7.1",
+ "expose-loader": "0.7.1",
"express": "^4.14.0",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.9.0",
"firebase": "^2.4.2",
- "flux": "^2.0.1",
"form-serialize": "0.7.1",
- "immstruct": "^2.0.0",
- "immutable": "^3.7.3",
"imports-loader": "0.6.5",
"invariant": "^2.1.0",
"isomorphic-fetch": "^2.2.1",
"jquery": "^3.1.0",
"jsonp": "0.2.0",
- "key-mirror": "^1.0.1",
"less": "^2.7.1",
"less-loader": "^2.2.3",
- "match-sorter": "^1.5.0",
"md5": "2.1.0",
- "mkdirp": "^0.5.0",
+ "mkdirp": "^0.5.1",
"mocha": "3.0.2",
"mustache": "^2.2.1",
+ "open-browser-webpack-plugin": "^0.0.5",
+ "prop-types": "^15.5.10",
"purecss": "0.6.0",
- "react": "^15.1.0",
+ "react": "^16.3.2",
"react-addons-css-transition-group": "^15.1.0",
- "react-addons-perf": "^15.1.0",
- "react-addons-test-utils": "^15.1.0",
"react-addons-transition-group": "^15.1.0",
- "react-dom": "^15.1.0",
+ "react-dom": "^16.3.2",
"react-motion": "^0.4.2",
- "react-point": "^2.1.0",
- "react-redux": "^4.4.5",
- "react-router": "^4.0.0-alpha.4",
+ "react-redux": "^5.0.2",
+ "react-router-dom": "^4.0.0",
"react-tween-state": "0.1.5",
- "redux": "3.0.4",
+ "redux": "^3.6.0",
"redux-logger": "2.2.1",
"redux-thunk": "1.0.0",
"sort-by": "^1.1.0",
@@ -76,11 +67,7 @@
"webpack": "^1.7.3",
"webpack-dev-server": "^1.8.0"
},
- "devDependencies": {
- "babel-eslint": "^6.1.2",
- "eslint": "^3.2.2",
- "eslint-import-resolver-webpack": "^0.5.1",
- "eslint-plugin-import": "^1.13.0",
- "eslint-plugin-react": "^6.0.0"
+ "prettier": {
+ "printWidth": 72
}
}
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 000000000..9298d05ce
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/shared.css b/public/shared.css
new file mode 100644
index 000000000..6aaf9ab10
--- /dev/null
+++ b/public/shared.css
@@ -0,0 +1,27 @@
+html {
+ box-sizing: border-box;
+}
+*,
+*:before,
+*:after {
+ box-sizing: inherit;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+ Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
+ background: white;
+}
+
+a:link,
+a:visited {
+ color: rebeccapurple;
+ text-decoration: none;
+}
+a:link:hover {
+ text-decoration: underline;
+}
+
+.hot {
+ color: red;
+}
diff --git a/scripts/build.js b/scripts/build.js
index 21f2a12ff..7d1fd6a07 100644
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -1,87 +1,90 @@
-const fs = require('fs')
-const path = require('path')
-const React = require('react')
-const ReactDOMServer = require('react-dom/server')
+const fs = require("fs");
+const path = require("path");
+const mkdirp = require("mkdirp");
+const React = require("react");
+const ReactDOMServer = require("react-dom/server");
-function createMarkup(mainBundle) {
- return ReactDOMServer.renderToStaticMarkup(
- React.DOM.html({},
- React.DOM.head({},
- React.DOM.link({ rel: 'stylesheet', href: '/shared.css' })
- ),
- React.DOM.body({},
- React.DOM.div({ id: 'app' }),
- React.DOM.script({ src: '/__build__/shared.js' }),
- React.DOM.script({ src: '/__build__/' + mainBundle + '.js' })
- )
- )
- )
+function writeFile(file, contents) {
+ mkdirp.sync(path.dirname(file));
+ fs.writeFileSync(file, contents);
}
-const RootDir = path.resolve(__dirname, '..')
-const SubjectsDir = path.join(RootDir, 'subjects')
-
-const Subjects = {
- HelloWorld: 'Hello World',
- Rendering: 'Rendering',
- Components: 'Components',
- PropsVsState: 'Props vs. State',
- ImperativeToDeclarative: 'Imperative to Declarative',
- Forms: 'Forms',
- Testing: 'Testing',
- CompoundComponents: 'Compound Components',
- Context: 'Context',
- HigherOrderComponents: 'Higher Order Components',
- RenderProps: 'Render Props',
- RenderOptimizations: 'Performance and Render Optimizations',
- TweenState: 'TweenState',
- Animation: 'Animation',
- Routing: 'Routing',
- ServerRendering: 'Server Rendering',
- JSONTable: 'JSON Table',
- Select: 'Select',
- Slider: 'Slider',
- Calculator: 'Calculator',
- ChatApp: 'Chat App',
- Cursors: 'Cursors',
- MigratingToReact: 'Migrating to React',
- Redux: 'Redux',
- MiniRedux: 'Implementing Redux'
+function renderPage(element) {
+ return (
+ "" + ReactDOMServer.renderToStaticMarkup(element)
+ );
}
-const SubjectDirNames = Object.keys(Subjects)
+const e = React.createElement;
-const markup = ReactDOMServer.renderToStaticMarkup(
- React.DOM.html({},
- React.DOM.head({},
- React.DOM.link({ rel: 'stylesheet', href: '/shared.css' })
+function HostPage({ chunk, data, title = "React Training" }) {
+ return e(
+ "html",
+ null,
+ e(
+ "head",
+ null,
+ e("meta", { charSet: "utf-8" }),
+ e("meta", {
+ name: "viewport",
+ content: "width=device-width, initial-scale=1"
+ }),
+ e("title", null, title),
+ e("link", { rel: "icon", href: "/favicon.ico?react-workshop" }),
+ e("link", { rel: "stylesheet", href: "/shared.css" }),
+ data &&
+ e("script", {
+ dangerouslySetInnerHTML: {
+ __html: `window.__DATA__ = ${JSON.stringify(data)}`
+ }
+ })
),
- React.DOM.body({ id: 'index' },
- React.DOM.table({ cellSpacing: 0, cellPadding: 0 },
- React.DOM.tbody({},
- SubjectDirNames.map(function (dir, index) {
- return React.DOM.tr({ key: dir, className: (index % 2) ? 'odd' : 'even' },
- React.DOM.td({ className: 'lecture-link' },
- React.DOM.a({ href: '/' + dir + '/lecture.html' }, Subjects[dir])
- ),
- React.DOM.td({ className: 'exercise-link' },
- React.DOM.a({ href: '/' + dir + '/exercise.html' }, 'exercise')
- ),
- React.DOM.td({ className: 'solution-link' },
- React.DOM.a({ href: '/' + dir + '/solution.html' }, 'solution')
- )
- )
- })
- )
- )
+ e(
+ "body",
+ null,
+ e("div", { id: "app" }),
+ e("script", { src: "/shared.js" }),
+ e("script", { src: `/${chunk}.js` })
)
- )
-)
+ );
+}
+
+const publicDir = path.resolve(__dirname, "..", "public");
+const subjectsDir = path.resolve(__dirname, "..", "subjects");
+const subjectDirs = fs
+ .readdirSync(subjectsDir)
+ .map(file => path.join(subjectsDir, file))
+ .filter(file => fs.statSync(file).isDirectory());
+
+const subjects = [];
+
+subjectDirs.forEach(dir => {
+ const base = path.basename(dir);
+ const match = base.match(/^(\d+)-(.+)$/);
+ const subject = {
+ number: match[1],
+ name: match[2].replace(/-/g, " ")
+ };
+
+ ["lecture", "exercise", "solution"].forEach(name => {
+ if (fs.existsSync(path.join(dir, `${name}.js`))) {
+ console.log(`Building /${base}/${name}.html...`);
+
+ writeFile(
+ path.join(publicDir, base, `${name}.html`),
+ renderPage(e(HostPage, { chunk: `${base}-${name}` }))
+ );
+
+ subject[name] = `/${base}/${name}.html`;
+ }
+ });
+
+ subjects.push(subject);
+});
-fs.writeFileSync(path.join(SubjectsDir, 'index.html'), markup)
+console.log(`Building /index.html...`);
-SubjectDirNames.forEach(function (dir) {
- fs.writeFileSync(path.join(SubjectsDir, dir, 'lecture.html'), createMarkup(dir + '-lecture'))
- fs.writeFileSync(path.join(SubjectsDir, dir, 'exercise.html'), createMarkup(dir + '-exercise'))
- fs.writeFileSync(path.join(SubjectsDir, dir, 'solution.html'), createMarkup(dir + '-solution'))
-})
+writeFile(
+ path.join(publicDir, "index.html"),
+ renderPage(e(HostPage, { chunk: "index", data: { subjects } }))
+);
diff --git a/subjects/Flux/Flux.png b/slides/Flux.png
similarity index 100%
rename from subjects/Flux/Flux.png
rename to slides/Flux.png
diff --git a/slides/Redux.gif b/slides/Redux.gif
new file mode 100644
index 000000000..6da63cddc
Binary files /dev/null and b/slides/Redux.gif differ
diff --git a/subjects/.babelrc b/subjects/.babelrc
new file mode 100644
index 000000000..4e73ad6ca
--- /dev/null
+++ b/subjects/.babelrc
@@ -0,0 +1,7 @@
+{
+ "presets": [
+ "env",
+ "stage-2",
+ "react"
+ ]
+}
diff --git a/subjects/HelloWorld/exercise.js b/subjects/00-Hello-World/exercise.js
similarity index 68%
rename from subjects/HelloWorld/exercise.js
rename to subjects/00-Hello-World/exercise.js
index 7c11b17ae..68f67358a 100644
--- a/subjects/HelloWorld/exercise.js
+++ b/subjects/00-Hello-World/exercise.js
@@ -4,11 +4,11 @@
// - change the contents of the render function and save the file
// - see the updates automatically in your browser without refreshing!
////////////////////////////////////////////////////////////////////////////////
-import React from 'react'
-import ReactDOM from 'react-dom'
+import React from "react";
+import ReactDOM from "react-dom";
function App() {
- return
Hello world!
+ return
Hello world!
;
}
-ReactDOM.render(, document.getElementById('app'))
+ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/00-Hello-World/lecture.js b/subjects/00-Hello-World/lecture.js
new file mode 100644
index 000000000..e73e51b73
--- /dev/null
+++ b/subjects/00-Hello-World/lecture.js
@@ -0,0 +1,382 @@
+import $ from "jquery";
+import { search } from "./utils/searchWikipedia";
+
+const html = `
+
+
Wikipedia
+
+
Loading...
+
+
Results for:
+
+
+
+
+
+
+`;
+
+$("#app").html(html); // <-- component
+
+$("#form")
+ .on("submit", event => {
+ // <-- state change
+ event.preventDefault();
+ const term = $("#input").val(); // <-- state
+ $("#loading").show(); // <-- time
+ $("#meta").hide(); // <-- time
+ $("#results").empty(); // <-- time
+ search(term, (err, results) => {
+ $("#loading").hide(); // <-- time
+ $("#meta").show(); // <-- time
+ $("#title").html(term); // <-- time
+ results.forEach(result => {
+ const li = $("");
+ const html = `
+
+ ${result.title}
+
+
+
+
${result.description}
+
+`;
+ li.html(html); // <-- time
+ if ($("#descending").is(":checked")) {
+ // <-- state
+ li.prependTo($("#results")); // <-- time
+ } else {
+ li.appendTo($("#results")); // <-- time
+ }
+ li.find("button").on("click", () => {
+ // <-- component
+ li.find(".toggler").toggle(); // <-- time
+ const isHidden = li.find(".toggler").is(":hidden"); // <-- state
+ li.find("button").html(isHidden ? "show more" : "hide"); // <-- time
+ });
+ });
+ });
+ })
+ .trigger("submit"); // <-- state change
+
+$("#descending").on("click", event => {
+ // <-- state change
+ $("#results li").each((i, li) => {
+ $("#results").prepend(li); // <-- time
+ });
+});
+
+// What's awesome:
+//
+// I can still bang out this code even after not using jQuery for
+// 4 years.
+//
+// What's not awesome:
+//
+// When our code...
+//
+// - is written as flows
+// - doesn't call out state
+// - has no entry point to change state
+//
+// ...it gets really hard to deal with. After you identify state,
+// and how to change it, you must write code to connect every state
+// to nearly every other state. Every feature requires changes to code
+// in multiple places. Also, it's just too hard to think about for most
+// of us, leading to lots of bugs.
+
+////////////////////////////////////////////////////////////////////////////////
+
+// import Backbone from "backbone";
+// import $ from "jquery";
+// import _ from "underscore";
+// import { search } from "./utils/searchWikipedia";
+
+// const appTemplate = _.template(`
+//
+// <% } %>
+// `),
+
+// events: {
+// "click button": "toggle"
+// },
+
+// initialize() {
+// this.model.set("isOpen", false, { silent: true }); // <-- model ownership?
+// this.listenTo(this.model, "change:isOpen", this.render);
+// },
+
+// toggle() {
+// this.model.set("isOpen", !this.model.get("isOpen")); // <-- KVO web
+// },
+
+// render() {
+// this.$el.html(this.template(this.model.toJSON()));
+// }
+// });
+
+// new AppView({
+// el: "#app",
+// model: new Backbone.Model({
+// title: "Wikipedia",
+// loading: false,
+// term: "tacos",
+// descending: false,
+// results: []
+// })
+// });
+
+// What's awesome
+//
+// - Moved state to models so we can identify what state changes
+// the app.
+// - Moved creating UI into templates, one step closer to being
+// declarative.
+//
+// What's not so awesome
+//
+// - DOM Bombs
+// - kill focus for assistive devices
+// - non-performant
+//
+// - KVO Web
+// - can't predict what will happen if you change state
+// > Events complect communication and flow of control.
+// > ... their fundamental nature, ... is that upon an event
+// > an arbitrary amount of other code is run
+// > http://clojure.com/blog/2013/06/28/clojure-core-async-channels.html
+//
+// - leads to cascading updates
+// - non-performant
+// - to fix leads to knowing how your app changes over time intimately
+//
+// - imperative composition
+// - non-performant
+// - to fix
+// - have to know how your app changes over time intimately
+// - lots of code to manage instances
+// - lots of mistakes
+
+////////////////////////////////////////////////////////////////////////////////
+
+// import angular from "angular";
+// import { search } from "./utils/searchWikipedia";
+
+// document.documentElement.setAttribute("ng-app", "wikipedia");
+
+// document.getElementById("app").innerHTML = `
+//
+// `
+// };
+// });
+
+// What's awesome
+//
+// - fully declarative templates
+// - declarative component composition
+//
+// What's not so awesome
+//
+// - directives and filters are globals
+// - have to think about time with $apply/$watch, etc.
+// - rendering assumptions require you to keep object identity
+// and therefore think about time
+// - and the real kicker: shared mutable state
+//
+// > July 7, 2014
+// >
+// > Vojta brought up some points that we don’t yet have plans to solve
+// > some problems we see in larger apps. In particular, how developers
+// > can reason about data flow within an app.
+// >
+// > Key points: scope hierarchy is a huge pile of shared state that many
+// > components from the application because of two way data-binding it's
+// > not clear what how the data flows because it can flow in all
+// > directions (including from child components to parents) - this makes
+// > it hard to understand the app and understand of impact of model
+// > changes in one part of the app on another (seemingly unrelated) part
+// > of it.
+// https://twitter.com/teozaurus/status/518071391959388160
diff --git a/subjects/HelloWorld/notes.md b/subjects/00-Hello-World/notes.md
similarity index 100%
rename from subjects/HelloWorld/notes.md
rename to subjects/00-Hello-World/notes.md
diff --git a/subjects/00-Hello-World/utils/searchWikipedia.js b/subjects/00-Hello-World/utils/searchWikipedia.js
new file mode 100644
index 000000000..67bc87171
--- /dev/null
+++ b/subjects/00-Hello-World/utils/searchWikipedia.js
@@ -0,0 +1,24 @@
+import jsonp from "jsonp";
+
+const API =
+ "https://en.wikipedia.org/w/api.php?action=opensearch&format=json";
+
+export function search(term, cb) {
+ jsonp(`${API}&search=${term}`, (err, data) => {
+ if (err) {
+ cb(err);
+ } else {
+ const [searchTerm, titles, descriptions, urls] = data;
+ cb(
+ null,
+ titles.sort().map((title, index) => {
+ return {
+ title,
+ description: descriptions[index],
+ url: urls[index]
+ };
+ })
+ );
+ }
+ });
+}
diff --git a/subjects/01-Rendering/exercise.js b/subjects/01-Rendering/exercise.js
new file mode 100644
index 000000000..3cd78f0bb
--- /dev/null
+++ b/subjects/01-Rendering/exercise.js
@@ -0,0 +1,38 @@
+////////////////////////////////////////////////////////////////////////////////
+// Exercise:
+//
+// - Render `DATA.title` in an
+// - Render a
with each of `DATA.items` as an
+// - Now only render an
for mexican food (hint: use DATA.items.filter(...))
+// - Sort the items in alphabetical order by name (hint: use sort-by https://github.com/staygrimm/sort-by#example)
+//
+// Got extra time?
+//
+// - Add a
- )
+ );
}
}
const DATA = [
- { id: 1, name: 'USA', description: 'Land of the Free, Home of the brave' },
- { id: 2, name: 'Brazil', description: 'Sunshine, beaches, and Carnival' },
- { id: 3, name: 'Russia', description: 'World Cup 2018!' }
-]
+ {
+ id: 1,
+ name: "USA",
+ description: "Land of the Free, Home of the brave"
+ },
+ {
+ id: 2,
+ name: "Brazil",
+ description: "Sunshine, beaches, and Carnival"
+ },
+ { id: 3, name: "Russia", description: "World Cup 2018!" }
+];
+
+ReactDOM.render(
+ ,
+ document.getElementById("app")
+);
-ReactDOM.render(, document.getElementById('app'), function () {
- require('./tests').run(this)
-})
+require("./tests").run();
diff --git a/subjects/02-Components/lecture.js b/subjects/02-Components/lecture.js
new file mode 100644
index 000000000..ffb9909bc
--- /dev/null
+++ b/subjects/02-Components/lecture.js
@@ -0,0 +1,70 @@
+import "./styles.css";
+
+import React from "react";
+import ReactDOM from "react-dom";
+
+let isOpen = false;
+
+function handleClick() {
+ isOpen = !isOpen;
+ updateThePage();
+}
+
+function ContentToggle() {
+ let summaryClassName = "content-toggle-summary";
+
+ if (isOpen) {
+ summaryClassName += " content-toggle-summary-open";
+ }
+
+ return (
+
+
+ Tacos
+
+ {isOpen && (
+
+
+ A taco is a traditional Mexican dish composed of a corn or
+ wheat tortilla folded or rolled around a filling.
+
+
+ )}
+
+ );
+}
+
+function updateThePage() {
+ ReactDOM.render(, document.getElementById("app"));
+}
+
+updateThePage();
+
+////////////////////////////////////////////////////////////////////////////////
+// What happens when we want to render 2 s? Shared mutable state!
+
+////////////////////////////////////////////////////////////////////////////////
+// React gives us a component model we can use to encapsulate state at the
+// instance level, so each component instance has its own state. Let's refactor
+// this code to use React components.
+
+////////////////////////////////////////////////////////////////////////////////
+// First, encapsulate the state in an object. Then, add a setState function that
+// we can use to update state and automatically update the page any time it
+// changes.
+
+//////////////////////////////////////////////////////////////////////////////////
+// React gives us setState and automatically re-renders as the state changes.
+
+////////////////////////////////////////////////////////////////////////////////
+// Let's make re-usable and render a few of them. Title and
+// children are properties we can pass in from the parent component.
+
+////////////////////////////////////////////////////////////////////////////////
+// Wrap a few s in a that tracks the # of times
+// it has been toggled and shows a counter. gets an onToggle
+// handler. This is like a "custom event".
+
+////////////////////////////////////////////////////////////////////////////////
+// We can use propTypes to declare the name, type, and even default value of
+// our props. These are like "runnable docs" for our code.
diff --git a/subjects/02-Components/solution.js b/subjects/02-Components/solution.js
new file mode 100644
index 000000000..04311dee9
--- /dev/null
+++ b/subjects/02-Components/solution.js
@@ -0,0 +1,110 @@
+////////////////////////////////////////////////////////////////////////////////
+// Exercise:
+//
+// - Render a tab for each country with its name in the tab
+// - When you click on a tab, make it appear to be active while the others
+// appear inactive
+// - Render the correct content for the selected tab in the panel
+//
+// Got extra time?
+//
+// - Make generic so that it doesn't know anything about
+// country data (Hint: good propTypes help)
+////////////////////////////////////////////////////////////////////////////////
+import React from "react";
+import ReactDOM from "react-dom";
+
+const styles = {};
+
+styles.tab = {
+ display: "inline-block",
+ padding: 10,
+ margin: 10,
+ borderBottom: "4px solid",
+ borderBottomColor: "#ccc",
+ cursor: "pointer"
+};
+
+styles.activeTab = {
+ ...styles.tab,
+ borderBottomColor: "#000"
+};
+
+styles.panel = {
+ padding: 10
+};
+
+class Tabs extends React.Component {
+ state = {
+ activeIndex: 0
+ };
+
+ selectTab(activeIndex) {
+ this.setState({ activeIndex });
+ }
+
+ render() {
+ const { data } = this.props;
+ const { activeIndex } = this.state;
+
+ const tabs = data.map((country, index) => {
+ const isActive = index === activeIndex;
+ const style = isActive ? styles.activeTab : styles.tab;
+
+ return (
+
+// );
+// }
+// }
+
+// ReactDOM.render(, document.getElementById("app"));
+
+//////////////////////////////////////////////////////////////////////////////
+// This is cool, until we screw up the state by clicking the button, then
+// clicking every item to the other state, and then clicking the button again,
+// now that the parent owns the toggle state, we need it know each toggler's
+// state and synchronize it
+
+// class ContentToggle extends React.Component {
+// state = { isOpen: this.props.isOpen };
+
+// handleClick = () => {
+// this.setState({ isOpen: !this.state.isOpen }, () => {
+// if (this.props.onToggle) {
+// this.props.onToggle(this.state.isOpen);
+// }
+// });
+// };
+
+// componentWillReceiveProps(nextProps) {
+// this.setState({ isOpen: nextProps.isOpen });
+// }
+
+// render() {
+// let summaryClassName = "content-toggle-summary";
+
+// if (this.state.isOpen) {
+// summaryClassName += " content-toggle-summary-open";
+// }
+
+// return (
+//
+// );
+// }
+
+//////////////////////////////////////////////////////////////////////////////
+// - We didn't really get rid of state, we just pushed it up a level
+// - got rid of synchronizing state :)
+// - component is super simple, just a function of its props
+//
+// But its not as portable anymore
+// - Must implement `onToggle` :\
+// - Must manage state in the owner, always :\
+//
+// We can create a controlled component that wraps our pure component.
+
+// class StatefulContentToggle extends React.Component {
+// state = { isOpen: false };
+// render() {
+// return (
+// this.setState({ isOpen })}
+// />
+// );
+// }
+// }
+
+// ReactDOM.render(, document.getElementById("app"));
+
+////////////////////////////////////////////////////////////////////////////////
+// You don't inherit from base classes, you compose by wrapping, just like you
+// compose functions, call one inside of another
diff --git a/subjects/03-Props-vs-State/solution.js b/subjects/03-Props-vs-State/solution.js
new file mode 100644
index 000000000..a205a0bb3
--- /dev/null
+++ b/subjects/03-Props-vs-State/solution.js
@@ -0,0 +1,94 @@
+////////////////////////////////////////////////////////////////////////////////
+// Exercise:
+//
+// Make the "Go to Step 2" button work.
+//
+// In order to do this, you'll have to make tabs a "pure component" so that it
+// no longer manages its own state. Instead add a prop to tell it which tab to
+// show, and then move the state up to the .
+//
+// Also, be sure that clicking on the individual tabs still works.
+////////////////////////////////////////////////////////////////////////////////
+import React from "react";
+import ReactDOM from "react-dom";
+import PropTypes from "prop-types";
+
+import * as styles from "./styles";
+import data from "./data";
+
+class Tabs extends React.Component {
+ static propTypes = {
+ data: PropTypes.array.isRequired,
+ activeIndex: PropTypes.number.isRequired,
+ onChange: PropTypes.func.isRequired
+ };
+
+ selectTab(index) {
+ this.props.onChange(index);
+ }
+
+ renderTabs() {
+ return this.props.data.map((tab, index) => {
+ const style =
+ this.props.activeIndex === index
+ ? styles.activeTab
+ : styles.tab;
+
+ return (
+
+ );
+ }
+}
+
+ReactDOM.render(, document.getElementById("app"));
+
+////////////////////////////////////////////////////////////////////////////////
+// Give the a default value.
+
+// class App extends React.Component {
+// render() {
+// return (
+//
+//
Forms
+//
+//
+// );
+// }
+// }
+
+////////////////////////////////////////////////////////////////////////////////
+// Access the value using event.target.
+
+// class App extends React.Component {
+// handleChange = event => {
+// console.log(event.target.value);
+// };
+
+// render() {
+// return (
+//
+//
Forms
+//
+//
+// );
+// }
+// }
+
+////////////////////////////////////////////////////////////////////////////////
+// Or use a ref.
+
+// class App extends React.Component {
+// handleChange = () => {
+// console.log(this.input.value);
+// };
+
+// render() {
+// return (
+//
+//
Forms
+//
+//
+// );
+// }
+// }
+
+////////////////////////////////////////////////////////////////////////////////
+// Or you can "control" the and have its value in state.
+// What happens if we don't have an `onChange` but provide a value?
+
+// class App extends React.Component {
+// state = {
+// inputValue: "cupcakes"
+// };
+
+// handleChange = () => {
+// this.setState({
+// inputValue: this.input.value
+// });
+// };
+
+// render() {
+// return (
+//
+//
Forms
+//
+//
{JSON.stringify(this.state, null, 2)}
+//
+// );
+// }
+// }
+
+////////////////////////////////////////////////////////////////////////////////
+// When it's controlled, we can setState elsewhere and it stays in sync.
+
+// class App extends React.Component {
+// state = {
+// inputValue: "cupcakes"
+// };
+
+// handleChange = () => {
+// this.setState({
+// inputValue: this.input.value
+// });
+// };
+
+// render() {
+// return (
+//
+//
Forms
+//
+//
{JSON.stringify(this.state, null, 2)}
+//
+// );
+// }
+// }
+
+////////////////////////////////////////////////////////////////////////////////
+// Some forms are transactional, so modeling in state isn't necessary, just
+// use DOM APIs to access the data, like when you need to save off some data
+// somewhere and reset the form, but the values in the form are never
+// important to `render`.
+
+// class App extends React.Component {
+// handleSubmit = event => {
+// event.preventDefault();
+// const values = serializeForm(event.target, { hash: true });
+// console.log(values);
+// };
+
+// render() {
+// return (
+//
+//
Forms
+//
+//
+// );
+// }
+// }
+
+////////////////////////////////////////////////////////////////////////////////
+// If we did want it all in state, we don't have to link up every single
+// element to state, can use
and DOM APIs to slurp form values into
+// state
+// 3. Multiple parts of the app manipulate the state, changes need
+// to be reflected in the input (two-way), use `value` and
+// `onChange`
diff --git a/subjects/04-Controlled-vs-Uncontrolled/solution.js b/subjects/04-Controlled-vs-Uncontrolled/solution.js
new file mode 100644
index 000000000..a5474c8c4
--- /dev/null
+++ b/subjects/04-Controlled-vs-Uncontrolled/solution.js
@@ -0,0 +1,151 @@
+////////////////////////////////////////////////////////////////////////////////
+// Exercise
+//
+// - When the checkbox is checked:
+// - Fill in the shipping fields with the values from billing
+// - Disable the shipping fields so they are not directly editable
+// - Keep the shipping fields up to date as billing fields change
+// - Hint: you can get the checkbox value from `event.target.checked`
+// - When the form submits, console.log the values
+//
+// Got extra time?
+//
+// - If there are more than two characters in the "state" field, let the user
+// know they should use the two-character abbreviation
+// - Save the state of the form and restore it when the page first loads, in
+// case the user accidentally closes the tab before the form is submitted
+////////////////////////////////////////////////////////////////////////////////
+import React from "react";
+import ReactDOM from "react-dom";
+import serializeForm from "form-serialize";
+
+class CheckoutForm extends React.Component {
+ state = {
+ billingName: "Michael Jackson",
+ billingState: "CA",
+ shippingName: "",
+ shippingState: "",
+ shippingSameAsBilling: false
+ };
+
+ handleSubmit = event => {
+ event.preventDefault();
+
+ const values = serializeForm(event.target, { hash: true });
+
+ console.log(values);
+ };
+
+ render() {
+ const {
+ billingName,
+ billingState,
+ shippingName,
+ shippingState,
+ shippingSameAsBilling
+ } = this.state;
+
+ return (
+
+
Checkout
+
+
+
+
+
+
+
+
+ Submit
+
+
+
+ );
+ }
+}
+
+ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/ImperativeToDeclarative/exercise-no-bootstrap.js b/subjects/05-Imperative-to-Declarative/exercise-no-bootstrap.js
similarity index 53%
rename from subjects/ImperativeToDeclarative/exercise-no-bootstrap.js
rename to subjects/05-Imperative-to-Declarative/exercise-no-bootstrap.js
index c1b7a418d..492a4b235 100644
--- a/subjects/ImperativeToDeclarative/exercise-no-bootstrap.js
+++ b/subjects/05-Imperative-to-Declarative/exercise-no-bootstrap.js
@@ -5,78 +5,78 @@
// - even though this Modal is a React component, the author created an
// imperative API D: Make it declarative!
////////////////////////////////////////////////////////////////////////////////
-import React from 'react'
-import ReactDOM from 'react-dom'
+import React from "react";
+import ReactDOM from "react-dom";
class Modal extends React.Component {
- state = { isOpen: false }
+ state = { isOpen: false };
containerStyle = {
- position: 'fixed',
+ position: "fixed",
left: 0,
right: 0,
top: 0,
bottom: 0,
- background: 'rgba(255, 255, 255, 0.75)'
- }
+ background: "rgba(255, 255, 255, 0.75)"
+ };
contentStyle = {
- position: 'fixed',
+ position: "fixed",
padding: 10,
left: 50,
right: 50,
top: 50,
bottom: 50,
- background: '#fff',
- border: '1px solid',
- textAlign: 'center'
- }
+ background: "#fff",
+ border: "1px solid",
+ textAlign: "center"
+ };
open() {
- this.setState({ isOpen: true })
+ this.setState({ isOpen: true });
}
close() {
- this.setState({ isOpen: false })
+ this.setState({ isOpen: false });
}
render() {
- if (this.state.isOpen === false)
- return null
+ if (this.state.isOpen === false) return null;
return (
- Your bones don’t break, mine do. That’s clear. Your cells react to
- bacteria and viruses differently than mine. You don’t get sick, I do.
- That’s also clear. But for some reason, you and I react the exact
- same way to water. We swallow it too fast, we choke. We get some in
- our lungs, we drown. However unreal it may seem, we are connected,
- you and I. We’re on the same curve, just on opposite ends.
+ Your bones don’t break, mine do. That’s clear. Your cells
+ react to bacteria and viruses differently than mine. You don’t
+ get sick, I do. That’s also clear. But for some reason, you
+ and I react the exact same way to water. We swallow it too
+ fast, we choke. We get some in our lungs, we drown. However
+ unreal it may seem, we are connected, you and I. We’re on the
+ same curve, just on opposite ends.
@@ -84,8 +84,8 @@ class App extends React.Component {
Are you sure?
- )
+ );
}
}
-ReactDOM.render(, document.getElementById('app'))
+ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/ImperativeToDeclarative/exercise.js b/subjects/05-Imperative-to-Declarative/exercise.js
similarity index 53%
rename from subjects/ImperativeToDeclarative/exercise.js
rename to subjects/05-Imperative-to-Declarative/exercise.js
index c1ec4f0d9..cc5678c14 100644
--- a/subjects/ImperativeToDeclarative/exercise.js
+++ b/subjects/05-Imperative-to-Declarative/exercise.js
@@ -4,68 +4,84 @@
// This Modal, even though its a React component, has an imperative API to
// open and close it. Can you convert it to a declarative API?
////////////////////////////////////////////////////////////////////////////////
-import React, { PropTypes } from 'react'
-import ReactDOM, { findDOMNode } from 'react-dom'
-import $ from 'jquery'
-import 'bootstrap-webpack'
+import React from "react";
+import ReactDOM from "react-dom";
+import PropTypes from "prop-types";
+import $ from "jquery";
+import "bootstrap-webpack";
class Modal extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
children: PropTypes.node
+ };
+
+ open() {
+ $(this.node).modal("show");
+ }
+
+ close() {
+ $(this.node).modal("hide");
}
render() {
return (
-
- )
+ );
}
}
-ReactDOM.render(, document.getElementById('app'))
+ReactDOM.render(, document.getElementById("app"));
////////////////////////////////////////////////////////////////////////////////
// Can't predict what the sound is going to be by looking at state or the render
diff --git a/subjects/ImperativeToDeclarative/solution.js b/subjects/05-Imperative-to-Declarative/solution.js
similarity index 51%
rename from subjects/ImperativeToDeclarative/solution.js
rename to subjects/05-Imperative-to-Declarative/solution.js
index 6fd139ff8..204fa715c 100644
--- a/subjects/ImperativeToDeclarative/solution.js
+++ b/subjects/05-Imperative-to-Declarative/solution.js
@@ -4,103 +4,96 @@
// This Modal, even though its a React component, has an imperative API to
// open and close it. Can you convert it to a declarative API?
////////////////////////////////////////////////////////////////////////////////
-import React, { PropTypes } from 'react'
-import ReactDOM, { findDOMNode } from 'react-dom'
-import $ from 'jquery'
-import 'bootstrap-webpack'
+import React from "react";
+import ReactDOM from "react-dom";
+import PropTypes from "prop-types";
+import $ from "jquery";
+import "bootstrap-webpack";
class Modal extends React.Component {
static propTypes = {
- isOpen: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
- onClose: PropTypes.func,
- children: PropTypes.node
- }
+ children: PropTypes.node,
+ isOpen: PropTypes.bool.isRequired
+ };
componentDidMount() {
- this.doImperativeWork()
-
- // This is only necessary to keep state in sync
- // with the DOM. Since we're keeping state now,
- // we should make sure it's accurate.
- $(findDOMNode(this)).on('hidden.bs.modal', () => {
- if (this.props.onClose)
- this.props.onClose()
- })
+ this.doImperativeWork();
}
componentDidUpdate(prevProps) {
- if (prevProps.isOpen !== this.props.isOpen)
- this.doImperativeWork()
+ if (prevProps.isOpen !== this.props.isOpen) {
+ this.doImperativeWork();
+ }
}
doImperativeWork() {
- if (this.props.isOpen === true) {
- $(findDOMNode(this)).modal('show')
- } else {
- $(findDOMNode(this)).modal('hide')
- }
+ $(this.node).modal(this.props.isOpen ? "show" : "hide");
}
render() {
return (
-
- )
+ );
}
}
-ReactDOM.render(, document.getElementById('app'))
+ReactDOM.render(, document.getElementById("app"));
///////////////////////////////////////////////////////////////////////////////
// We can wrap up the scroll listening into a component with
diff --git a/subjects/07-Render-Props/solution.js b/subjects/07-Render-Props/solution.js
new file mode 100644
index 000000000..e20bfac51
--- /dev/null
+++ b/subjects/07-Render-Props/solution.js
@@ -0,0 +1,133 @@
+////////////////////////////////////////////////////////////////////////////////
+// Exercise:
+//
+// - Create a component that encapsulates the geo state and
+// watching logic and uses a render prop to pass the coordinates back to
+// the
+//
+// Got extra time?
+//
+// - Create a component that translates the geo coordinates to a
+// physical address and prints it to the screen (hint: use
+// `getAddressFromCoords`)
+// - You should be able to compose and beneath it to
+// naturally compose both the UI and the state needed to render it
+////////////////////////////////////////////////////////////////////////////////
+import React from "react";
+import ReactDOM from "react-dom";
+import PropTypes from "prop-types";
+
+import getAddressFromCoords from "./utils/getAddressFromCoords";
+import LoadingDots from "./LoadingDots";
+
+class GeoPosition extends React.Component {
+ static propTypes = {
+ children: PropTypes.func.isRequired
+ };
+
+ state = {
+ coords: {
+ latitude: null,
+ longitude: null
+ },
+ error: null
+ };
+
+ componentDidMount() {
+ this.geoId = navigator.geolocation.watchPosition(
+ position => {
+ this.setState({
+ coords: {
+ latitude: position.coords.latitude,
+ longitude: position.coords.longitude
+ }
+ });
+ },
+ error => {
+ this.setState({ error });
+ }
+ );
+ }
+
+ componentWillUnmount() {
+ navigator.geolocation.clearWatch(this.geoId);
+ }
+
+ render() {
+ return this.props.children(this.state);
+ }
+}
+
+class GeoAddress extends React.Component {
+ static propTypes = {
+ latitude: PropTypes.number,
+ longitude: PropTypes.number,
+ children: PropTypes.func.isRequired
+ };
+
+ state = { address: null };
+
+ componentDidMount() {
+ if (this.props.latitude && this.props.longitude) this.fetch();
+ }
+
+ componentDidUpdate(prevProps) {
+ if (
+ prevProps.longitude !== this.props.longitude ||
+ prevProps.latitude !== this.props.latitude
+ )
+ this.fetch();
+ }
+
+ fetch() {
+ const { latitude, longitude } = this.props;
+
+ getAddressFromCoords(latitude, longitude).then(address => {
+ this.setState({ address });
+ });
+ }
+
+ render() {
+ return this.props.children(this.state);
+ }
+}
+
+class App extends React.Component {
+ render() {
+ return (
+
+
Geolocation
+
+
GeoPosition
+
+ {state =>
+ state.error ? (
+
Error: {state.error.message}
+ ) : (
+
+
Latitude
+
{state.coords.latitude || }
+
Longitude
+
{state.coords.longitude || }
+
+ )
+ }
+
+
+
GeoAddress Composition
+
+ {({ coords }) => (
+
+ {({ address }) =>
{address || }
}
+
+ )}
+
+
+ );
+ }
+}
+
+ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/07-Render-Props/utils/getAddressFromCoords.js b/subjects/07-Render-Props/utils/getAddressFromCoords.js
new file mode 100644
index 000000000..5087f3df8
--- /dev/null
+++ b/subjects/07-Render-Props/utils/getAddressFromCoords.js
@@ -0,0 +1,60 @@
+const GoogleMapsAPI = "https://maps.googleapis.com/maps/api";
+
+function wait(timeout, work) {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ try {
+ resolve(work());
+ } catch (error) {
+ reject(error);
+ }
+ }, timeout);
+ });
+}
+
+const retryTimeout = 5000;
+
+function getAddressFromCoords(latitude, longitude) {
+ const url = `${GoogleMapsAPI}/geocode/json?latlng=${latitude},${longitude}`;
+
+ return fetch(url)
+ .then(res => res.json())
+ .then(
+ json =>
+ json.status === "OVER_QUERY_LIMIT"
+ ? // Wait for the query limit to reset.
+ wait(retryTimeout, () =>
+ getAddressFromCoords(latitude, longitude)
+ )
+ : json.results[0].formatted_address
+ );
+}
+
+let lastCallTime = 0;
+let alreadyWarned = false;
+let promise = null;
+
+function throttledGetAddressFromCoords(latitude, longitude) {
+ if (latitude == null || longitude == null) {
+ return Promise.resolve(null);
+ }
+
+ const currentTime = Date.now();
+
+ if (lastCallTime + retryTimeout < currentTime) {
+ lastCallTime = currentTime;
+ promise = getAddressFromCoords(latitude, longitude);
+ } else if (!alreadyWarned) {
+ alreadyWarned = true;
+
+ window.alert(
+ "It looks like you're calling getAddressFromCoords many times " +
+ "quickly in a loop. Take a closer look at the componentDidUpdate " +
+ "function in ..."
+ );
+ }
+
+ return promise;
+}
+
+export default throttledGetAddressFromCoords;
diff --git a/subjects/CompoundComponents/exercise.js b/subjects/08-Compound-Components/exercise.js
similarity index 51%
rename from subjects/CompoundComponents/exercise.js
rename to subjects/08-Compound-Components/exercise.js
index d79cba950..2868a34c3 100644
--- a/subjects/CompoundComponents/exercise.js
+++ b/subjects/08-Compound-Components/exercise.js
@@ -7,75 +7,64 @@
// - The selected should pass the correct value to its
// - The `defaultValue` should be set on first render
//
-// Hints to get started:
-//
-// - will need some state
-// - It then needs to pass that state to the s so they know
-// whether or not they are active
-//
// Got extra time?
//
-// Implement a `value` prop and allow this to work like a "controlled input"
-// (https://facebook.github.io/react/docs/forms.html#controlled-components)
-//
-// - Add a button to that sets `this.state.radioValue` to a pre-determined
-// value, like "tape"
-// - Make the update accordingly
-//
-// Implement keyboard controls on the (you'll need tabIndex="0" on
-// the s so the keyboard will work)
-//
-// - Enter and space bar should select the option
-// - Arrow right, arrow down should select the next option
-// - Arrow left, arrow up should select the previous option
+// - Implement an `onChange` prop that communicates the 's state
+// back to the so it can use it to render something
+// - Implement keyboard controls on the
+// - Hint: Use tabIndex="0" on the s so the keyboard will work
+// - Enter and space bar should select the option
+// - Arrow right, arrow down should select the next option
+// - Arrow left, arrow up should select the previous option
////////////////////////////////////////////////////////////////////////////////
-import React, { PropTypes } from 'react'
-import ReactDOM from 'react-dom'
+import React from "react";
+import ReactDOM from "react-dom";
+import PropTypes from "prop-types";
class RadioGroup extends React.Component {
static propTypes = {
defaultValue: PropTypes.string
- }
+ };
render() {
- return
+// );
+// }
+
+// ReactDOM.render(, document.getElementById("app"));
+
+////////////////////////////////////////////////////////////////////////////////
+// Now this is really flexible
+//
+// - can change order of panels v. tabs
+// - can pass in our own styles to tabs
+// - can even have unrelated elements inside
+// - in other words, we now have control over rendering while
+// Tabs handles the interaction
+//
+// Oh but you really loved the old tabs yeah?
+
+// class DataTabs extends React.Component {
+// static defaultProps = {
+// disabled: []
+// };
+
+// render() {
+// return (
+//
+//
+// {this.props.data.map((item, index) => (
+//
+// {item.label}
+//
+// ))}
+//
+//
+// {this.props.data.map(item => (
+// {item.description}
+// ))}
+//
+//
+// );
+// }
+// }
+
+// function App() {
+// const tabData = [
+// {
+// label: "Tacos",
+// description:
+// );
+// }
+
+// ReactDOM.render(, document.getElementById("app"));
+
+////////////////////////////////////////////////////////////////////////////////
+// Instead of creating a handful of options, compose several components together
+// and then compose them together into their own components.
+//
+// A really awesome library that does this is react-soundplayer
diff --git a/subjects/08-Compound-Components/solution-extra.js b/subjects/08-Compound-Components/solution-extra.js
new file mode 100644
index 000000000..13476a7d3
--- /dev/null
+++ b/subjects/08-Compound-Components/solution-extra.js
@@ -0,0 +1,173 @@
+////////////////////////////////////////////////////////////////////////////////
+// Exercise:
+//
+// Implement a radio group form control with the API found in .
+//
+// - Clicking a should update the value of
+// - The selected should pass the correct value to its
+// - The `defaultValue` should be set on first render
+//
+// Got extra time?
+//
+// - Implement an `onChange` prop that communicates the 's state
+// back to the parent so it can use it to render
+// - Implement keyboard controls on the
+// - Hint: Use tabIndex="0" on the s so the keyboard will work
+// - Enter and space bar should select the option
+// - Arrow right, arrow down should select the next option
+// - Arrow left, arrow up should select the previous option
+////////////////////////////////////////////////////////////////////////////////
+import React from "react";
+import ReactDOM from "react-dom";
+import PropTypes from "prop-types";
+
+class RadioGroup extends React.Component {
+ static propTypes = {
+ defaultValue: PropTypes.string,
+ onChange: PropTypes.func
+ };
+
+ state = { value: this.props.defaultValue };
+
+ updateFocus(index) {
+ // Temporarily set this instance variable so we know to update
+ // the focus on the next render. It's safe to do this here since
+ // we know that we already have the focus since we just received
+ // a keyboard event.
+ this._focusIndex = index;
+
+ this.forceUpdate(() => {
+ // Immediately unset the variable after the next render so we
+ // don't inadvertently steal the focus from other elements when
+ // we update but we don't have the focus.
+ delete this._focusIndex;
+ });
+ }
+
+ render() {
+ const length = React.Children.toArray(this.props.children).length;
+
+ return (
+
+ );
+ }
+}
+
+ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/CompoundComponents/solution.js b/subjects/08-Compound-Components/solution.js
similarity index 52%
rename from subjects/CompoundComponents/solution.js
rename to subjects/08-Compound-Components/solution.js
index 3f59c311e..28bb264bb 100644
--- a/subjects/CompoundComponents/solution.js
+++ b/subjects/08-Compound-Components/solution.js
@@ -7,99 +7,87 @@
// - The selected should pass the correct value to its
// - The `defaultValue` should be set on first render
//
-// Hints to get started:
-//
-// - will need some state
-// - It then needs to pass that state to the s so they know
-// whether or not they are active
-//
// Got extra time?
//
-// Implement a `value` prop and allow this to work like a "controlled input"
-// (https://facebook.github.io/react/docs/forms.html#controlled-components)
-//
-// - Add a button to that sets `this.state.radioValue` to a pre-determined
-// value, like "tape"
-// - Make the update accordingly
-//
-// Implement keyboard controls on the (you'll need tabIndex="0" on
-// the s so the keyboard will work)
-//
-// - Enter and space bar should select the option
-// - Arrow right, arrow down should select the next option
-// - Arrow left, arrow up should select the previous option
+// - Implement an `onChange` prop that communicates the 's state
+// back to the so it can use it to render something
+// - Implement keyboard controls on the
+// - Hint: Use tabIndex="0" on the s so the keyboard will work
+// - Enter and space bar should select the option
+// - Arrow right, arrow down should select the next option
+// - Arrow left, arrow up should select the previous option
////////////////////////////////////////////////////////////////////////////////
-import React, { PropTypes } from 'react'
-import ReactDOM from 'react-dom'
-
-class RadioIcon extends React.Component {
- static propTypes = {
- isSelected: PropTypes.bool.isRequired
- }
-
- render() {
- return (
-
- )
- }
-}
+import React from "react";
+import ReactDOM from "react-dom";
+import PropTypes from "prop-types";
class RadioGroup extends React.Component {
static propTypes = {
defaultValue: PropTypes.string
- }
+ };
- state = {
- value: this.props.defaultValue
- }
+ state = { value: this.props.defaultValue };
select(value) {
this.setState({ value }, () => {
- this.props.onChange(this.state.value)
- })
+ this.props.onChange(this.state.value);
+ });
}
render() {
- const children = React.Children.map(this.props.children, (child) => (
+ const children = React.Children.map(this.props.children, child =>
React.cloneElement(child, {
isSelected: child.props.value === this.state.value,
onClick: () => this.select(child.props.value)
})
- ))
+ );
- return
, , and
// components such that:
//
-// - Clicking the "submits" the form
+// - Clicking the calls the form's `onSubmit` handler
// - Hitting "Enter" while in a submits the form
// - Don't use a
element, we're intentionally recreating the
// browser's built-in behavior
//
// Got extra time?
//
-// - Send the values of all the s to the
handler
+// - Send the values of all the s to the form's `onSubmit` handler
// without using DOM traversal APIs
// - Implement a that resets the s in the form
-//
////////////////////////////////////////////////////////////////////////////////
-import React from 'react'
-import ReactDOM from 'react-dom'
+import React from "react";
+import ReactDOM from "react-dom";
+import PropTypes from "prop-types";
class Form extends React.Component {
render() {
- return
+ );
+}
+
+ReactDOM.render(, document.getElementById("app"));
+
+////////////////////////////////////////////////////////////////////////////////
+// Sometimes you don't want to specify how deep in the view tree the child
+// components need to be, our current implementation expects TabList/TabPanels
+// to be immediate children of Tabs, also Tab and TabPanel are required to be
+// immediate children of their parent components. We really only care about the
+// interactivity between the components, not their hierarchy.
+//
+// We could recursively check children with each render, which seems like a bad
+// plan, so instead we can use a feature called "context".
+//
+// Wrapping in a div breaks everything! Instead of using
+// cloneElement, let's use context to pass the activeIndex down.
+
+// const TabsContext = React.createContext();
+
+// function TabList({ children, _activeIndex, _onTabSelect }) {
+// return (
+//
+// );
+// }
+
+// ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/09-Context/solution-extra.js b/subjects/09-Context/solution-extra.js
new file mode 100644
index 000000000..19b38e38d
--- /dev/null
+++ b/subjects/09-Context/solution-extra.js
@@ -0,0 +1,136 @@
+////////////////////////////////////////////////////////////////////////////////
+// Exercise:
+//
+// Using context, implement the
, , and
+// components such that:
+//
+// - Clicking the calls the form's `onSubmit` handler
+// - Hitting "Enter" while in a submits the form
+// - Don't use a
element, we're intentionally recreating the
+// browser's built-in behavior
+//
+// Got extra time?
+//
+// - Send the values of all the s to the form's `onSubmit` handler
+// without using DOM traversal APIs
+// - Implement a that resets the s in the form
+////////////////////////////////////////////////////////////////////////////////
+import React from "react";
+import ReactDOM from "react-dom";
+import PropTypes from "prop-types";
+
+const FormContext = React.createContext();
+
+class Form extends React.Component {
+ state = { values: {} };
+
+ handleChange = (name, value) => {
+ this.setState(state => ({
+ values: {
+ ...state.values,
+ [name]: value
+ }
+ }));
+ };
+
+ handleReset = () => {
+ this.setState({ values: {} });
+ };
+
+ handleSubmit = () => {
+ if (this.props.onSubmit) this.props.onSubmit(this.state.values);
+ };
+
+ render() {
+ return (
+
+
+ );
+ }
+}
+
+ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/09-Context/solution.js b/subjects/09-Context/solution.js
new file mode 100644
index 000000000..d97c018b6
--- /dev/null
+++ b/subjects/09-Context/solution.js
@@ -0,0 +1,101 @@
+////////////////////////////////////////////////////////////////////////////////
+// Exercise:
+//
+// Using context, implement the
, , and
+// components such that:
+//
+// - Clicking the calls the form's `onSubmit` handler
+// - Hitting "Enter" while in a submits the form
+// - Don't use a
element, we're intentionally recreating the
+// browser's built-in behavior
+//
+// Got extra time?
+//
+// - Send the values of all the s to the form's `onSubmit` handler
+// without using DOM traversal APIs
+// - Implement a that resets the s in the form
+////////////////////////////////////////////////////////////////////////////////
+import React from "react";
+import ReactDOM from "react-dom";
+import PropTypes from "prop-types";
+
+const FormContext = React.createContext();
+
+class Form extends React.Component {
+ handleSubmit = () => {
+ if (this.props.onSubmit) this.props.onSubmit();
+ };
+
+ render() {
+ return (
+
+
+ );
+ }
+}
+
+ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/09-Context/styles.js b/subjects/09-Context/styles.js
new file mode 100644
index 000000000..df662c5d1
--- /dev/null
+++ b/subjects/09-Context/styles.js
@@ -0,0 +1,25 @@
+export const tabList = {};
+
+export const tab = {
+ display: "inline-block",
+ padding: 10,
+ margin: 10,
+ borderBottom: "4px solid",
+ borderBottomColor: "#ccc",
+ cursor: "pointer"
+};
+
+export const activeTab = {
+ ...tab,
+ borderBottomColor: "#000"
+};
+
+export const disabledTab = {
+ ...tab,
+ opacity: 0.25,
+ cursor: "default"
+};
+
+export const tabPanels = {
+ padding: 10
+};
diff --git a/subjects/10-Routing/Gravatar.js b/subjects/10-Routing/Gravatar.js
new file mode 100644
index 000000000..723ac1790
--- /dev/null
+++ b/subjects/10-Routing/Gravatar.js
@@ -0,0 +1,32 @@
+import React from "react";
+import PropTypes from "prop-types";
+import md5 from "md5";
+
+const GravatarURL = "http://gravatar.com/avatar";
+
+class Gravatar extends React.Component {
+ static propTypes = {
+ email: PropTypes.string.isRequired,
+ size: PropTypes.number.isRequired
+ };
+
+ static defaultProps = {
+ size: 80
+ };
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default Gravatar;
diff --git a/subjects/10-Routing/exercise.js b/subjects/10-Routing/exercise.js
new file mode 100644
index 000000000..fea49c4d9
--- /dev/null
+++ b/subjects/10-Routing/exercise.js
@@ -0,0 +1,95 @@
+////////////////////////////////////////////////////////////////////////////////
+// Exercise:
+//
+// - Wrap the component in 's render method in a . This
+// provides the right context for the s and s we will create
+// - Create a to /user/:userId for each person in
+// - When a person's is clicked, render instead of .
+// Hints: You can get the person's userId from 's "match" prop
+// which it gets from the router. Also, you'll probably want to wrap
+// and in a in 's render method
+//
+// Got extra time?
+//
+// - Render a when the URL is something other than / or a user profile
+// URL. Manually type in a bad URL to get the component to show up.
+// - Add a to the profile page that links back to Home so users don't
+// have to use the Back button to get back to the home page
+// - Add a from "/users/:userID" to "/profile/:userID", then type in
+// the url "users/1" into the url and hit enter
+////////////////////////////////////////////////////////////////////////////////
+import React from "react";
+import ReactDOM from "react-dom";
+import {
+ HashRouter as Router,
+ Switch,
+ Route,
+ Link,
+ Redirect
+} from "react-router-dom";
+import Gravatar from "./Gravatar";
+
+const USERS = [
+ {
+ id: 1,
+ name: "Michael Jackson",
+ email: "michael@reacttraining.com"
+ },
+ { id: 2, name: "React Training", email: "hello@reacttraining.com" }
+];
+
+function getUserByID(id) {
+ for (let i = 0; i < USERS.length; ++i) {
+ if (USERS[i].id === parseInt(id, 10)) return USERS[i];
+ }
+
+ return null;
+}
+
+function Home() {
+ const contactItems = USERS.map(user => (
+
{user.name}
+ ));
+
+ return (
+
+
Home
+
{contactItems}
+
+ );
+}
+
+function Profile() {
+ const userId = 1; // TODO: Get this from the URL!
+ const user = getUserByID(userId);
+
+ if (user == null) return
Cannot find user with id {userId}
;
+
+ return (
+
+ {user.name}
+
+ );
+}
+
+function NoMatch() {
+ return (
+
+
No routes matched...
+
+ Go home
+
+
+ );
+}
+
+function App() {
+ return (
+
+
People Viewer
+
+
+ );
+}
+
+ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/10-Routing/lecture.js b/subjects/10-Routing/lecture.js
new file mode 100644
index 000000000..2ea9fbf81
--- /dev/null
+++ b/subjects/10-Routing/lecture.js
@@ -0,0 +1,135 @@
+import React from "react";
+import ReactDOM from "react-dom";
+
+function About() {
+ return
+ );
+ }
+}
+
+ReactDOM.render(, document.getElementById("app"));
+
+////////////////////////////////////////////////////////////////////////////////
+// Setup a hashchange listener so we know when the URL changes. When it does,
+// update state and pick which child component we're going to render.
+
+// function About() {
+// return
+//
+// );
+// }
+// }
+
+// ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/10-Routing/solution.js b/subjects/10-Routing/solution.js
new file mode 100644
index 000000000..585271970
--- /dev/null
+++ b/subjects/10-Routing/solution.js
@@ -0,0 +1,110 @@
+////////////////////////////////////////////////////////////////////////////////
+// Exercise:
+//
+// - Wrap the component in 's render method in a . This
+// provides the right context for the s and s we will create
+// - Create a to /user/:userId for each person in
+// - When a person's is clicked, render instead of .
+// Hints: You can get the person's userId from 's "match" prop
+// which it gets from the router. Also, you'll probably want to wrap
+// and in a in 's render method
+//
+// Got extra time?
+//
+// - Render a when the URL is something other than / or a user profile
+// URL. Manually type in a bad URL to get the component to show up.
+// - Add a to the profile page that links back to Home so users don't
+// have to use the Back button to get back to the home page
+// - Add a from "/users/:userID" to "/profile/:userID", then type in
+// the url "users/1" into the url and hit enter
+////////////////////////////////////////////////////////////////////////////////
+import React from "react";
+import ReactDOM from "react-dom";
+import {
+ HashRouter as Router,
+ Switch,
+ Route,
+ Link,
+ Redirect
+} from "react-router-dom";
+import Gravatar from "./Gravatar";
+
+const USERS = [
+ {
+ id: 1,
+ name: "Michael Jackson",
+ email: "michael@reacttraining.com"
+ },
+ { id: 2, name: "React Training", email: "hello@reacttraining.com" }
+];
+
+function getUserByID(id) {
+ for (let i = 0; i < USERS.length; ++i) {
+ if (USERS[i].id === parseInt(id, 10)) return USERS[i];
+ }
+
+ return null;
+}
+
+function Home() {
+ const contactItems = USERS.map(user => (
+
+ {user.name}
+
+ ));
+
+ return (
+
+
Home
+
{contactItems}
+
+ );
+}
+
+function Profile({ match }) {
+ const { userId } = match.params;
+ const user = getUserByID(userId);
+
+ if (user == null) return
Cannot find user with id {userId}
;
+
+ return (
+
+ {user.name}
+
+ );
+}
+
+function NoMatch() {
+ return (
+
+
No routes matched...
+
+ Go home
+
+
+ );
+}
+
+function App() {
+ return (
+
+
People Viewer
+
+
+
+
+
+ (
+
+ )}
+ />
+
+
+
+
+ );
+}
+
+ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/10-Routing/tests.js b/subjects/10-Routing/tests.js
new file mode 100644
index 000000000..4604677e8
--- /dev/null
+++ b/subjects/10-Routing/tests.js
@@ -0,0 +1,36 @@
+import ReactDOM from "react-dom";
+import { Simulate } from "react-dom/test-utils";
+import assert from "../assert";
+
+function toArray(nodeList) {
+ return Array.prototype.slice.call(nodeList, 0);
+}
+
+export function run(component) {
+ window.location.hash = "";
+
+ const node = ReactDOM.findDOMNode(component);
+ const peopleList = node.querySelector(".people-list");
+ const profileLinks = toArray(node.querySelectorAll(".people-list a"));
+ const profileHrefFormat = /\/profile\/\d+$/;
+
+ console.log("on first render");
+
+ assert(peopleList, "render the list of people");
+ assert(
+ profileLinks.length &&
+ profileLinks.every(
+ link =>
+ link && profileHrefFormat.test(link.getAttribute("href"))
+ ),
+ "render links to the profile page"
+ );
+
+ console.log("after clicking on a profile link...");
+
+ if (profileLinks.length) {
+ Simulate.click(profileLinks[1], { button: 0 });
+ }
+
+ assert(node.querySelector(".profile"), "show the profile page");
+}
diff --git a/subjects/11-Motion/components/Draggable.js b/subjects/11-Motion/components/Draggable.js
new file mode 100644
index 000000000..1acba0724
--- /dev/null
+++ b/subjects/11-Motion/components/Draggable.js
@@ -0,0 +1,61 @@
+import React from "react";
+import PropTypes from "prop-types";
+
+class Draggable extends React.Component {
+ static propTypes = {
+ component: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
+ .isRequired,
+ onDragStart: PropTypes.func,
+ onDrag: PropTypes.func,
+ onDrop: PropTypes.func
+ };
+
+ static defaultProps = {
+ component: "div"
+ };
+
+ componentDidMount() {
+ this.isDragging = false;
+ document.addEventListener("mouseup", this.handleMouseUp);
+ document.addEventListener("mousemove", this.handleMouseMove);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("mousemove", this.handleMouseMove);
+ document.removeEventListener("mouseup", this.handleMouseUp);
+ }
+
+ handleMouseDown = event => {
+ if (!this.isDragging) {
+ this.isDragging = true;
+
+ // Prevent Chrome from displaying a text cursor
+ event.preventDefault();
+
+ if (this.props.onDragStart) this.props.onDragStart(event);
+ }
+ };
+
+ handleMouseMove = event => {
+ if (this.isDragging && this.props.onDrag) this.props.onDrag(event);
+ };
+
+ handleMouseUp = event => {
+ if (this.isDragging) {
+ this.isDragging = false;
+
+ if (this.props.onDrop) this.props.onDrop(event);
+ }
+ };
+
+ render() {
+ const { component, ...otherProps } = this.props;
+
+ return React.createElement(component, {
+ ...otherProps,
+ onMouseDown: this.handleMouseDown
+ });
+ }
+}
+
+export default Draggable;
diff --git a/subjects/11-Motion/components/Tone.js b/subjects/11-Motion/components/Tone.js
new file mode 100644
index 000000000..e7c34f104
--- /dev/null
+++ b/subjects/11-Motion/components/Tone.js
@@ -0,0 +1,37 @@
+import React from "react";
+import PropTypes from "prop-types";
+import createOscillator from "../utils/createOscillator";
+
+class Tone extends React.Component {
+ static propTypes = {
+ isPlaying: PropTypes.bool.isRequired,
+ pitch: PropTypes.number.isRequired,
+ volume: PropTypes.number.isRequired
+ };
+
+ componentDidMount() {
+ this.oscillator = createOscillator();
+ this.doImperativeWork();
+ }
+
+ componentDidUpdate() {
+ this.doImperativeWork();
+ }
+
+ doImperativeWork() {
+ if (this.props.isPlaying) {
+ this.oscillator.play();
+ } else {
+ this.oscillator.stop();
+ }
+
+ this.oscillator.setPitchBend(this.props.pitch);
+ this.oscillator.setVolume(this.props.volume);
+ }
+
+ render() {
+ return null;
+ }
+}
+
+export default Tone;
diff --git a/subjects/11-Motion/exercise.js b/subjects/11-Motion/exercise.js
new file mode 100644
index 000000000..27e5c4768
--- /dev/null
+++ b/subjects/11-Motion/exercise.js
@@ -0,0 +1,117 @@
+///////////////////////////////////////////////////////////////////////////////
+// Exercise:
+//
+// - Use a to animate the transition of the red "marker" to its
+// destination when it is dropped
+//
+// Got extra time?
+//
+// - If you didn't already, use a custom spring to give the animation
+// an elastic, bouncy feel
+// - Add a "drop hint" element that indicates which element will receive
+// the marker when it is dropped to improve usability
+////////////////////////////////////////////////////////////////////////////////
+import "./styles.css";
+
+import React from "react";
+import ReactDOM from "react-dom";
+import PropTypes from "prop-types";
+import { Motion, spring } from "react-motion";
+import Draggable from "./components/Draggable";
+
+class DropGrid extends React.Component {
+ state = {
+ isDraggingMarker: false,
+ startX: 0,
+ startY: 0,
+ mouseX: 0,
+ mouseY: 0
+ };
+
+ getRelativeXY({ clientX, clientY }) {
+ const { offsetLeft, offsetTop } = this.node;
+
+ return {
+ x: clientX - offsetLeft,
+ y: clientY - offsetTop
+ };
+ }
+
+ handleDragStart = event => {
+ const { x, y } = this.getRelativeXY(event);
+ const { offsetLeft, offsetTop } = event.target;
+
+ // Prevent Chrome from displaying a text cursor
+ event.preventDefault();
+
+ this.setState({
+ isDraggingMarker: true,
+ startX: x - offsetLeft,
+ startY: y - offsetTop,
+ mouseX: x,
+ mouseY: y
+ });
+ };
+
+ handleDrag = event => {
+ const { x, y } = this.getRelativeXY(event);
+
+ this.setState({
+ mouseX: x,
+ mouseY: y
+ });
+ };
+
+ handleDrop = () => {
+ this.setState({ isDraggingMarker: false });
+ };
+
+ render() {
+ const {
+ isDraggingMarker,
+ startX,
+ startY,
+ mouseX,
+ mouseY
+ } = this.state;
+
+ let markerLeft, markerTop;
+ if (isDraggingMarker) {
+ markerLeft = mouseX - startX;
+ markerTop = mouseY - startY;
+ } else {
+ markerLeft =
+ Math.floor(Math.max(0, Math.min(449, mouseX)) / 150) * 150;
+ markerTop =
+ Math.floor(Math.max(0, Math.min(449, mouseY)) / 150) * 150;
+ }
+
+ const markerStyle = {
+ left: markerLeft,
+ top: markerTop
+ };
+
+ return (
+
(this.node = node)}>
+
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
+ );
+ }
+}
+
+ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/Animation/lecture.js b/subjects/11-Motion/lecture.js
similarity index 88%
rename from subjects/Animation/lecture.js
rename to subjects/11-Motion/lecture.js
index 4dc182e55..413765a41 100644
--- a/subjects/Animation/lecture.js
+++ b/subjects/11-Motion/lecture.js
@@ -1,34 +1,31 @@
-import React from 'react'
-import ReactDOM, { findDOMNode } from 'react-dom'
-import { Motion, StaggeredMotion, spring, presets } from 'react-motion'
-import $ from 'jquery'
+import "./styles.css";
-import './styles'
+import React from "react";
+import ReactDOM from "react-dom";
+import { Motion, StaggeredMotion, spring, presets } from "react-motion";
+import $ from "jquery";
class ToggleSwitch extends React.Component {
state = {
isActive: false
- }
+ };
toggle = () => {
- this.setState({ isActive: !this.state.isActive })
- }
+ this.setState({ isActive: !this.state.isActive });
+ };
render() {
- const x = this.state.isActive ? 400 : 0
+ const x = this.state.isActive ? 400 : 0;
return (
-
+
- )
+ );
}
}
-ReactDOM.render(
- ,
- document.getElementById('app')
-)
+ReactDOM.render(, document.getElementById("app"));
///////////////////////////////////////////////////////////////////////////////
// We can integrate with other DOM animation libraries by doing imperative work
diff --git a/subjects/11-Motion/solution.js b/subjects/11-Motion/solution.js
new file mode 100644
index 000000000..997c9d486
--- /dev/null
+++ b/subjects/11-Motion/solution.js
@@ -0,0 +1,124 @@
+///////////////////////////////////////////////////////////////////////////////
+// Exercise:
+//
+// - Use a to animate the transition of the red "marker" to its
+// destination when it is dropped
+//
+// Got extra time?
+//
+// - If you didn't already, use a custom spring to give the animation
+// an elastic, bouncy feel
+// - Add a "drop hint" element that indicates which element will receive
+// the marker when it is dropped to improve usability
+////////////////////////////////////////////////////////////////////////////////
+import "./styles.css";
+
+import React from "react";
+import ReactDOM from "react-dom";
+import PropTypes from "prop-types";
+import { Motion, spring } from "react-motion";
+import Draggable from "./components/Draggable";
+
+class DropGrid extends React.Component {
+ state = {
+ isDraggingMarker: false,
+ startX: 0,
+ startY: 0,
+ mouseX: 0,
+ mouseY: 0
+ };
+
+ getRelativeXY({ clientX, clientY }) {
+ const { offsetLeft, offsetTop } = this.node;
+
+ return {
+ x: clientX - offsetLeft,
+ y: clientY - offsetTop
+ };
+ }
+
+ handleDragStart = event => {
+ const { x, y } = this.getRelativeXY(event);
+ const { offsetLeft, offsetTop } = event.target;
+
+ // Prevent Chrome from displaying a text cursor
+ event.preventDefault();
+
+ this.setState({
+ isDraggingMarker: true,
+ startX: x - offsetLeft,
+ startY: y - offsetTop,
+ mouseX: x,
+ mouseY: y
+ });
+ };
+
+ handleDrag = event => {
+ const { x, y } = this.getRelativeXY(event);
+
+ this.setState({
+ mouseX: x,
+ mouseY: y
+ });
+ };
+
+ handleDrop = () => {
+ this.setState({ isDraggingMarker: false });
+ };
+
+ render() {
+ const {
+ isDraggingMarker,
+ startX,
+ startY,
+ mouseX,
+ mouseY
+ } = this.state;
+
+ let markerLeft, markerTop;
+ if (isDraggingMarker) {
+ markerLeft = mouseX - startX;
+ markerTop = mouseY - startY;
+ } else {
+ markerLeft =
+ Math.floor(Math.max(0, Math.min(449, mouseX)) / 150) * 150;
+ markerTop =
+ Math.floor(Math.max(0, Math.min(449, mouseY)) / 150) * 150;
+ }
+
+ const bouncySpring = style =>
+ spring(style, { stiffness: 170, damping: 8 });
+
+ const markerStyle = {
+ left: isDraggingMarker ? markerLeft : bouncySpring(markerLeft),
+ top: isDraggingMarker ? markerTop : bouncySpring(markerTop)
+ };
+
+ return (
+
(this.node = node)}>
+
+ {style => (
+
+ )}
+
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
+ );
+ }
+}
+
+ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/Animation/styles.css b/subjects/11-Motion/styles.css
similarity index 84%
rename from subjects/Animation/styles.css
rename to subjects/11-Motion/styles.css
index 49b8b4ffc..19fe728b2 100644
--- a/subjects/Animation/styles.css
+++ b/subjects/11-Motion/styles.css
@@ -1,12 +1,9 @@
-html {
- box-sizing: border-box;
-}
-*, *:before, *:after {
- box-sizing: inherit;
-}
-
body {
- padding: 10px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ margin: 0;
}
.toggle-switch {
@@ -20,7 +17,6 @@ body {
-ms-user-select: none;
-webkit-user-select: none;
}
-
.toggle-switch-knob {
position: absolute;
width: 50px;
@@ -39,10 +35,6 @@ body {
width: 350px;
}
-
-
-
-
.grid {
position: absolute;
margin: auto;
@@ -55,7 +47,7 @@ body {
width: 451px;
height: 451px;
}
-.grid .cell {
+.grid-cell {
border: 1px solid #999;
border-width: 0 0 1px 1px;
width: 150px;
@@ -67,11 +59,12 @@ body {
-webkit-user-select: none;
user-select: none;
}
-.grid .marker, .grid .drop-hint {
+
+.grid-marker, .grid .drop-hint {
position: absolute;
background: red;
- width: 150;
- height: 150;
+ width: 150px;
+ height: 150px;
opacity: 0.6;
}
.grid .drop-hint {
diff --git a/subjects/ImperativeToDeclarative/utils/AudioContextMonkeyPatch.js b/subjects/11-Motion/utils/AudioContextMonkeyPatch.js
similarity index 59%
rename from subjects/ImperativeToDeclarative/utils/AudioContextMonkeyPatch.js
rename to subjects/11-Motion/utils/AudioContextMonkeyPatch.js
index 524b20bc0..d03436608 100644
--- a/subjects/ImperativeToDeclarative/utils/AudioContextMonkeyPatch.js
+++ b/subjects/11-Motion/utils/AudioContextMonkeyPatch.js
@@ -13,16 +13,16 @@
limitations under the License.
*/
-/*
+/*
This monkeypatch library is intended to be included in projects that are
-written to the proper AudioContext spec (instead of webkitAudioContext),
-and that use the new naming and proper bits of the Web Audio API (e.g.
+written to the proper AudioContext spec (instead of webkitAudioContext),
+and that use the new naming and proper bits of the Web Audio API (e.g.
using BufferSourceNode.start() instead of BufferSourceNode.noteOn()), but may
have to run on systems that only support the deprecated bits.
-This library should be harmless to include if the browser supports
-unprefixed "AudioContext", and/or if it supports the new names.
+This library should be harmless to include if the browser supports
+unprefixed "AudioContext", and/or if it supports the new names.
The patches this library handles:
if window.AudioContext is unsupported, it will be aliased to webkitAudioContext().
@@ -39,66 +39,74 @@ OscillatorNode.start() is aliased to noteOn()
OscillatorNode.stop() is aliased to noteOff()
AudioParam.setTargetAtTime() is aliased to setTargetValueAtTime()
-This library does NOT patch the enumerated type changes, as it is
+This library does NOT patch the enumerated type changes, as it is
recommended in the specification that implementations support both integer
-and string types for AudioPannerNode.panningModel, AudioPannerNode.distanceModel
+and string types for AudioPannerNode.panningModel, AudioPannerNode.distanceModel
BiquadFilterNode.type and OscillatorNode.type.
*/
-(function (global, exports, perf) {
- 'use strict';
+(function(global, exports, perf) {
+ "use strict";
function fixSetTarget(param) {
- if (!param) // if NYI, just return
+ if (!param)
+ // if NYI, just return
return;
if (!param.setTargetAtTime)
- param.setTargetAtTime = param.setTargetValueAtTime;
+ param.setTargetAtTime = param.setTargetValueAtTime;
}
- if (window.hasOwnProperty('webkitAudioContext') &&
- !window.hasOwnProperty('AudioContext')) {
+ if (
+ window.hasOwnProperty("webkitAudioContext") &&
+ !window.hasOwnProperty("AudioContext")
+ ) {
window.AudioContext = webkitAudioContext;
- if (!AudioContext.prototype.hasOwnProperty('createGain'))
- AudioContext.prototype.createGain = AudioContext.prototype.createGainNode;
- if (!AudioContext.prototype.hasOwnProperty('createDelay'))
- AudioContext.prototype.createDelay = AudioContext.prototype.createDelayNode;
- if (!AudioContext.prototype.hasOwnProperty('createScriptProcessor'))
- AudioContext.prototype.createScriptProcessor = AudioContext.prototype.createJavaScriptNode;
-
- AudioContext.prototype.internal_createGain = AudioContext.prototype.createGain;
- AudioContext.prototype.createGain = function() {
+ if (!AudioContext.prototype.hasOwnProperty("createGain"))
+ AudioContext.prototype.createGain =
+ AudioContext.prototype.createGainNode;
+ if (!AudioContext.prototype.hasOwnProperty("createDelay"))
+ AudioContext.prototype.createDelay =
+ AudioContext.prototype.createDelayNode;
+ if (!AudioContext.prototype.hasOwnProperty("createScriptProcessor"))
+ AudioContext.prototype.createScriptProcessor =
+ AudioContext.prototype.createJavaScriptNode;
+
+ AudioContext.prototype.internal_createGain =
+ AudioContext.prototype.createGain;
+ AudioContext.prototype.createGain = function() {
const node = this.internal_createGain();
fixSetTarget(node.gain);
return node;
};
- AudioContext.prototype.internal_createDelay = AudioContext.prototype.createDelay;
- AudioContext.prototype.createDelay = function() {
+ AudioContext.prototype.internal_createDelay =
+ AudioContext.prototype.createDelay;
+ AudioContext.prototype.createDelay = function() {
const node = this.internal_createDelay();
fixSetTarget(node.delayTime);
return node;
};
- AudioContext.prototype.internal_createBufferSource = AudioContext.prototype.createBufferSource;
- AudioContext.prototype.createBufferSource = function() {
+ AudioContext.prototype.internal_createBufferSource =
+ AudioContext.prototype.createBufferSource;
+ AudioContext.prototype.createBufferSource = function() {
const node = this.internal_createBufferSource();
if (!node.start) {
- node.start = function ( when, offset, duration ) {
- if ( offset || duration )
- this.noteGrainOn( when, offset, duration );
- else
- this.noteOn( when );
- }
+ node.start = function(when, offset, duration) {
+ if (offset || duration)
+ this.noteGrainOn(when, offset, duration);
+ else this.noteOn(when);
+ };
}
- if (!node.stop)
- node.stop = node.noteoff;
+ if (!node.stop) node.stop = node.noteoff;
fixSetTarget(node.playbackRate);
return node;
};
- AudioContext.prototype.internal_createDynamicsCompressor = AudioContext.prototype.createDynamicsCompressor;
- AudioContext.prototype.createDynamicsCompressor = function() {
+ AudioContext.prototype.internal_createDynamicsCompressor =
+ AudioContext.prototype.createDynamicsCompressor;
+ AudioContext.prototype.createDynamicsCompressor = function() {
const node = this.internal_createDynamicsCompressor();
fixSetTarget(node.threshold);
fixSetTarget(node.knee);
@@ -109,8 +117,9 @@ BiquadFilterNode.type and OscillatorNode.type.
return node;
};
- AudioContext.prototype.internal_createBiquadFilter = AudioContext.prototype.createBiquadFilter;
- AudioContext.prototype.createBiquadFilter = function() {
+ AudioContext.prototype.internal_createBiquadFilter =
+ AudioContext.prototype.createBiquadFilter;
+ AudioContext.prototype.createBiquadFilter = function() {
const node = this.internal_createBiquadFilter();
fixSetTarget(node.frequency);
fixSetTarget(node.detune);
@@ -119,18 +128,17 @@ BiquadFilterNode.type and OscillatorNode.type.
return node;
};
- if (AudioContext.prototype.hasOwnProperty( 'createOscillator' )) {
- AudioContext.prototype.internal_createOscillator = AudioContext.prototype.createOscillator;
- AudioContext.prototype.createOscillator = function() {
+ if (AudioContext.prototype.hasOwnProperty("createOscillator")) {
+ AudioContext.prototype.internal_createOscillator =
+ AudioContext.prototype.createOscillator;
+ AudioContext.prototype.createOscillator = function() {
const node = this.internal_createOscillator();
- if (!node.start)
- node.start = node.noteOn;
- if (!node.stop)
- node.stop = node.noteOff;
+ if (!node.start) node.start = node.noteOn;
+ if (!node.stop) node.stop = node.noteOff;
fixSetTarget(node.frequency);
fixSetTarget(node.detune);
return node;
};
}
}
-}(window));
+})(window);
diff --git a/subjects/11-Motion/utils/createOscillator.js b/subjects/11-Motion/utils/createOscillator.js
new file mode 100644
index 000000000..8d0c41424
--- /dev/null
+++ b/subjects/11-Motion/utils/createOscillator.js
@@ -0,0 +1,63 @@
+import "./AudioContextMonkeyPatch";
+
+function Oscillator(audioContext) {
+ // TODO make more things not use this.
+ const oscillatorNode = audioContext.createOscillator();
+ oscillatorNode.start(0);
+
+ const gainNode = audioContext.createGain();
+ this.pitchBase = 50;
+ this.pitchBend = 0;
+ this.pitchRange = 2000;
+ this.volume = 0.5;
+ this.maxVolume = 1;
+ this.frequency = this.pitchBase;
+
+ let hasConnected = false;
+ let frequency = this.pitchBase;
+
+ this.play = function() {
+ oscillatorNode.connect(gainNode);
+ hasConnected = true;
+ };
+
+ this.stop = function() {
+ if (hasConnected) {
+ oscillatorNode.disconnect(gainNode);
+ hasConnected = false;
+ }
+ };
+
+ this.setType = function(type) {
+ oscillatorNode.type = type;
+ };
+
+ this.setPitchBend = function(v) {
+ this.pitchBend = v;
+ frequency = this.pitchBase + this.pitchBend * this.pitchRange;
+ oscillatorNode.frequency.value = frequency;
+ this.frequency = frequency;
+ };
+
+ this.setVolume = function(v) {
+ this.volume = this.maxVolume * v;
+ gainNode.gain.value = this.volume;
+ };
+
+ this.connect = function(output) {
+ gainNode.connect(output);
+ };
+
+ return this;
+}
+
+function createOscillator() {
+ const audioContext = new AudioContext();
+ const theremin = new Oscillator(audioContext);
+
+ theremin.connect(audioContext.destination);
+
+ return theremin;
+}
+
+export default createOscillator;
diff --git a/subjects/TweenState/components/HeightFader.js b/subjects/12-Transitions/components/HeightFader.js
similarity index 55%
rename from subjects/TweenState/components/HeightFader.js
rename to subjects/12-Transitions/components/HeightFader.js
index 79b29fea7..411ae940a 100644
--- a/subjects/TweenState/components/HeightFader.js
+++ b/subjects/12-Transitions/components/HeightFader.js
@@ -1,63 +1,61 @@
-import React from 'react'
-import { Mixin as TweenStateMixin } from 'react-tween-state'
+import React from "react";
+import { Mixin as TweenStateMixin } from "react-tween-state";
function getHeight(node) {
- return node.scrollHeight
+ return node.scrollHeight;
}
const HeightFader = React.createClass({
-
- mixins: [ TweenStateMixin ],
+ mixins: [TweenStateMixin],
getDefaultProps() {
return {
- component: 'li'
- }
+ component: "li"
+ };
},
getInitialState() {
return {
opacity: 0,
height: 0
- }
+ };
},
componentWillEnter(cb) {
- this.tweenState('opacity', {
+ this.tweenState("opacity", {
duration: 250,
endValue: 1
- })
+ });
- this.tweenState('height', {
+ this.tweenState("height", {
duration: 250,
endValue: getHeight(React.findDOMNode(this)),
onEnd: cb
- })
+ });
},
componentWillLeave(cb) {
- this.tweenState('opacity', {
+ this.tweenState("opacity", {
duration: 250,
endValue: 0
- })
+ });
- this.tweenState('height', {
+ this.tweenState("height", {
duration: 250,
endValue: 0,
onEnd: cb
- })
+ });
},
render() {
return React.createElement(this.props.component, {
...this.props,
style: {
- opacity: this.getTweeningValue('opacity'),
- height: this.getTweeningValue('height')
+ opacity: this.getTweeningValue("opacity"),
+ height: this.getTweeningValue("height")
}
- })
+ });
}
+});
-})
-
-export default HeightFader
+export default HeightFader;
diff --git a/subjects/TweenState/exercise.js b/subjects/12-Transitions/exercise.js
similarity index 71%
rename from subjects/TweenState/exercise.js
rename to subjects/12-Transitions/exercise.js
index e54588671..7f6e36b5b 100644
--- a/subjects/TweenState/exercise.js
+++ b/subjects/12-Transitions/exercise.js
@@ -5,11 +5,15 @@
// - Experiment with different types of easing (hint: use easingTypes at
// https://github.com/chenglou/tween-functions/blob/master/index.js)
////////////////////////////////////////////////////////////////////////////////
-import React, { PropTypes } from 'react'
-import { render } from 'react-dom'
-import { easingTypes, Mixin as TweenStateMixin } from 'react-tween-state'
+import "./styles.css";
-require('./styles')
+import React from "react";
+import ReactDOM from "react-dom";
+import PropTypes from "prop-types";
+import {
+ easingTypes,
+ Mixin as TweenStateMixin
+} from "react-tween-state";
const ToggleSwitch = React.createClass({
propTypes: {
@@ -19,37 +23,37 @@ const ToggleSwitch = React.createClass({
getDefaultProps() {
return {
animationDuration: 350
- }
+ };
},
getInitialState() {
return {
knobLeft: 0
- }
+ };
},
toggle() {
this.setState({
knobLeft: this.state.knobLeft === 0 ? 400 : 0
- })
+ });
},
handleClick() {
- this.toggle()
+ this.toggle();
},
render() {
const knobStyle = {
WebkitTransform: `translate3d(${this.state.knobLeft}px,0,0)`,
transform: `translate3d(${this.state.knobLeft}px,0,0)`
- }
+ };
return (
+ );
+ }
+}
+
+export default CreateContactForm;
diff --git a/subjects/Redux/solution.js b/subjects/15-Redux/solution/index.js
similarity index 74%
rename from subjects/Redux/solution.js
rename to subjects/15-Redux/solution/index.js
index ed67a13f5..d6b428599 100644
--- a/subjects/Redux/solution.js
+++ b/subjects/15-Redux/solution/index.js
@@ -3,9 +3,10 @@ The goal of this exercise is to gain some hands-on experience using Redux to
manage the state of a React application. In the process, we'll also learn how to
use Redux to communicate changes to a real API server.
- - Make the update its state so it displays the contacts when the store
- changes (hint: use store.subscribe). If you open the console, you'll see the
- contacts are already being loaded from the API.
+ - Get the the props it needs by using connect() to connect it to
+ the in its parent, the .
+ - Once it's connected, if you open the console you'll see the contacts being
+ loaded from the API.
- Add a delete next to each contact that dispatches a DELETE_CONTACT
action. Check the console to make sure the action is being dispatched.
- Add some logic to the contacts reducer to remove that contact when it sees
@@ -24,8 +25,8 @@ Got extra time?
connection. How does the UI respond to your actions? What can you do to improve
the UX?
*/
-import React from 'react'
-import ReactDOM from 'react-dom'
-import App from './solution/components/App'
+import React from "react";
+import ReactDOM from "react-dom";
+import App from "./components/App";
-ReactDOM.render(, document.getElementById('app'))
+ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/15-Redux/solution/logger.js b/subjects/15-Redux/solution/logger.js
new file mode 100644
index 000000000..793e4735f
--- /dev/null
+++ b/subjects/15-Redux/solution/logger.js
@@ -0,0 +1,13 @@
+const logger = store => next => action => {
+ console.group(action.type);
+ console.info("dispatching", action);
+
+ let result = next(action);
+
+ console.log("next state", store.getState());
+ console.groupEnd(action.type);
+
+ return result;
+};
+
+export default logger;
diff --git a/subjects/15-Redux/solution/reducers.js b/subjects/15-Redux/solution/reducers.js
new file mode 100644
index 000000000..eeb11a0d3
--- /dev/null
+++ b/subjects/15-Redux/solution/reducers.js
@@ -0,0 +1,44 @@
+import {
+ ADD_CONTACT,
+ CONTACTS_WERE_LOADED,
+ DELETE_CONTACT,
+ CONTACT_WAS_DELETED,
+ ERROR_DELETING_CONTACT
+} from "./actions";
+
+export function contacts(state = [], action) {
+ if (action.type === ADD_CONTACT) {
+ return state.concat([action.contact]);
+ } else if (action.type === CONTACTS_WERE_LOADED) {
+ return action.contacts;
+ } else if (action.type === CONTACT_WAS_DELETED) {
+ return state.filter(contact => contact.id !== action.contactId);
+ } else {
+ return state;
+ }
+}
+
+export function contactsBeingDeleted(state = {}, action) {
+ if (action.type === DELETE_CONTACT) {
+ return {
+ ...state,
+ [action.contactId]: true
+ };
+ } else if (action.type === ERROR_DELETING_CONTACT) {
+ delete state[action.contactId];
+ return { ...state };
+ } else {
+ return state;
+ }
+}
+
+export function contactsWithErrors(state = {}, action) {
+ if (action.type === ERROR_DELETING_CONTACT) {
+ return {
+ ...state,
+ [action.contactId]: action.message
+ };
+ } else {
+ return state;
+ }
+}
diff --git a/subjects/15-Redux/solution/store.js b/subjects/15-Redux/solution/store.js
new file mode 100644
index 000000000..62c558751
--- /dev/null
+++ b/subjects/15-Redux/solution/store.js
@@ -0,0 +1,16 @@
+import {
+ createStore,
+ combineReducers,
+ applyMiddleware,
+ compose
+} from "redux";
+import * as reducers from "./reducers";
+import logger from "./logger";
+
+const composeEnhancers =
+ window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
+
+export default createStore(
+ combineReducers(reducers),
+ composeEnhancers(applyMiddleware(logger))
+);
diff --git a/subjects/15-Redux/solution/utils/api.js b/subjects/15-Redux/solution/utils/api.js
new file mode 100644
index 000000000..b5e849604
--- /dev/null
+++ b/subjects/15-Redux/solution/utils/api.js
@@ -0,0 +1,13 @@
+import { getJSON, deleteJSON } from "./xhr";
+
+const serverURL = "http://addressbook-api.herokuapp.com";
+
+export function fetchContacts(cb) {
+ getJSON(`${serverURL}/contacts`, (error, res) => {
+ cb(error, res.contacts);
+ });
+}
+
+export function deleteContactById(contactId, cb) {
+ deleteJSON(`${serverURL}/contacts/${contactId}`, cb);
+}
diff --git a/subjects/15-Redux/solution/utils/xhr.js b/subjects/15-Redux/solution/utils/xhr.js
new file mode 100644
index 000000000..456f514d5
--- /dev/null
+++ b/subjects/15-Redux/solution/utils/xhr.js
@@ -0,0 +1,49 @@
+localStorage.token = localStorage.token || Date.now() * Math.random();
+
+function setToken(req) {
+ req.setRequestHeader("authorization", localStorage.token);
+}
+
+export function getJSON(url, callback) {
+ const req = new XMLHttpRequest();
+ req.onload = function() {
+ if (req.status === 404) {
+ callback(new Error("not found"));
+ } else {
+ callback(null, JSON.parse(req.response));
+ }
+ };
+ req.open("GET", url);
+ setToken(req);
+ req.send();
+}
+
+export function postJSON(url, obj, callback) {
+ const req = new XMLHttpRequest();
+ req.onload = function() {
+ callback(JSON.parse(req.response));
+ };
+ req.open("POST", url);
+ req.setRequestHeader(
+ "Content-Type",
+ "application/json;charset=UTF-8"
+ );
+ setToken(req);
+ req.send(JSON.stringify(obj));
+}
+
+export function deleteJSON(url, callback) {
+ const req = new XMLHttpRequest();
+ req.onload = function() {
+ setTimeout(() => {
+ if (req.status === 500) {
+ callback(new Error(req.responseText));
+ } else {
+ callback(null, req.responseText);
+ }
+ }, Math.random() * 5000);
+ };
+ req.open("DELETE", url);
+ setToken(req);
+ req.send();
+}
diff --git a/subjects/ChatApp/exercise.js b/subjects/16-Chat-App/exercise.js
similarity index 80%
rename from subjects/ChatApp/exercise.js
rename to subjects/16-Chat-App/exercise.js
index a747ee346..0fb69a503 100644
--- a/subjects/ChatApp/exercise.js
+++ b/subjects/16-Chat-App/exercise.js
@@ -5,21 +5,21 @@
//
// Need some ideas?
//
-// - Cause the message list to automatically scroll as new
-// messages come in
+// - Cause the message list to automatically scroll as new messages come in
// - Highlight messages from you to make them easy to find
// - Highlight messages that mention you by your GitHub username
// - Group subsequent messages from the same sender
// - Create a filter that lets you filter messages in the chat by
// sender and/or content
////////////////////////////////////////////////////////////////////////////////
-import React from 'react'
-import { render } from 'react-dom'
-import { login, sendMessage, subscribeToMessages } from './utils/ChatUtils'
-import './styles'
+import "./styles.css";
+
+import React from "react";
+import { render } from "react-dom";
+import { login, sendMessage, subscribeToMessages } from "./utils";
/*
-Here's how to use the ChatUtils:
+Here's how to use the utils:
login((error, auth) => {
// hopefully the error is `null` and you have a auth.github object
@@ -54,11 +54,13 @@ class Chat extends React.Component {
-
+
So, check it out:
-
QA Engineer walks into a bar.
+
+ QA Engineer walks into a bar.
+
Orders a beer.
Orders 0 beers.
Orders 999999999 beers.
@@ -69,16 +71,18 @@ class Chat extends React.Component {
-
+
Haha
-
Stop stealing other people's jokes :P
+
+ Stop stealing other people's jokes :P
+
-
+
:'(
@@ -88,12 +92,16 @@ class Chat extends React.Component {
-
+
- )
+ );
}
}
-render(, document.getElementById('app'))
+render(, document.getElementById("app"));
diff --git a/subjects/ChatApp/solution.js b/subjects/16-Chat-App/solution.js
similarity index 52%
rename from subjects/ChatApp/solution.js
rename to subjects/16-Chat-App/solution.js
index b7e225410..d4a305022 100644
--- a/subjects/ChatApp/solution.js
+++ b/subjects/16-Chat-App/solution.js
@@ -5,21 +5,21 @@
//
// Need some ideas?
//
-// - Cause the message list to automatically scroll as new
-// messages come in
+// - Cause the message list to automatically scroll as new messages come in
// - Highlight messages from you to make them easy to find
// - Highlight messages that mention you by your GitHub username
// - Group subsequent messages from the same sender
// - Create a filter that lets you filter messages in the chat by
// sender and/or content
////////////////////////////////////////////////////////////////////////////////
-import React from 'react'
-import { render } from 'react-dom'
-import { login, sendMessage, subscribeToMessages } from './utils/ChatUtils'
-import './styles'
+import "./styles.css";
+
+import React from "react";
+import { render } from "react-dom";
+import { login, sendMessage, subscribeToMessages } from "./utils";
/*
-Here's how to use the ChatUtils:
+Here's how to use the utils:
login((error, auth) => {
// hopefully the error is `null` and you have a auth.github object
@@ -44,33 +44,32 @@ The world is your oyster!
class SmartScroller extends React.Component {
componentDidMount() {
- this.autoScroll = true
- this.scrollToBottom()
+ this.autoScroll = true;
+ this.scrollToBottom();
}
componentDidUpdate() {
- if (this.autoScroll)
- this.scrollToBottom()
+ if (this.autoScroll) this.scrollToBottom();
}
scrollToBottom() {
- this.node.scrollTop = this.node.scrollHeight
+ this.node.scrollTop = this.node.scrollHeight;
}
- handleScroll() {
- const { scrollTop, scrollHeight, clientHeight } = this.node
- const distanceToBottom = scrollHeight - (scrollTop + clientHeight)
- this.autoScroll = distanceToBottom < 10
- }
+ handleScroll = () => {
+ const { scrollTop, scrollHeight, clientHeight } = this.node;
+ const distanceToBottom = scrollHeight - (scrollTop + clientHeight);
+ this.autoScroll = distanceToBottom < 10;
+ };
render() {
return (
- )
+ );
}
}
-render(, document.getElementById('app'))
+render(, document.getElementById("app"));
diff --git a/subjects/ChatApp/styles.css b/subjects/16-Chat-App/styles.css
similarity index 92%
rename from subjects/ChatApp/styles.css
rename to subjects/16-Chat-App/styles.css
index fcb917f7a..104215bfc 100644
--- a/subjects/ChatApp/styles.css
+++ b/subjects/16-Chat-App/styles.css
@@ -1,10 +1,6 @@
html {
- box-sizing: border-box;
overflow: hidden; /* prevent elastic scrolling in WebKit */
}
-*, *:before, *:after {
- box-sizing: inherit;
-}
body {
margin: 0;
@@ -12,7 +8,7 @@ body {
}
.chat {
- height: 100%;
+ height: 100vh;
display: flex;
flex-direction: column;
}
@@ -35,7 +31,6 @@ body {
font-size: 1.5em;
}
-
.messages {
overflow: auto;
flex: 1;
@@ -73,6 +68,7 @@ ol.messages {
.message-group-avatar {
position: absolute;
background-color: lightblue;
+ line-height: 0;
}
.message-group-avatar img {
height: 40px;
diff --git a/subjects/16-Chat-App/utils.js b/subjects/16-Chat-App/utils.js
new file mode 100644
index 000000000..49afa4370
--- /dev/null
+++ b/subjects/16-Chat-App/utils.js
@@ -0,0 +1,84 @@
+import Firebase from "firebase/lib/firebase-web";
+
+const baseRef = new Firebase("https://hip-react.firebaseio.com");
+const messagesRef = baseRef.child("messages");
+const usersRef = baseRef.child("users");
+
+const reservedRefNameChars = /[\.#\$\[\]]/g;
+
+function escapeKey(name) {
+ return name.replace(reservedRefNameChars, "_");
+}
+
+function escapeValue(rawValue) {
+ const value =
+ rawValue && typeof rawValue.toJSON === "function"
+ ? rawValue.toJSON()
+ : rawValue;
+
+ if (value == null) return null; // Remove undefined values
+
+ if (Array.isArray(value)) return value.map(escapeValue);
+
+ if (typeof value === "object") {
+ return Object.keys(value).reduce((memo, key) => {
+ memo[escapeKey(key)] = escapeValue(value[key]);
+ return memo;
+ }, {});
+ }
+
+ return value;
+}
+
+function saveAuth(auth) {
+ usersRef.child(auth.uid).set(escapeValue(auth));
+}
+
+export function login(callback) {
+ const auth = baseRef.getAuth();
+
+ if (auth) {
+ saveAuth(auth);
+ callback(null, auth);
+ } else {
+ baseRef.authWithOAuthPopup("github", (error, auth) => {
+ if (auth) saveAuth(auth);
+ callback(error, auth);
+ });
+ }
+}
+
+export function subscribeToMessages(callback) {
+ const handleValue = snapshot => {
+ const messages = [];
+
+ snapshot.forEach(s => {
+ const message = s.val();
+ message._key = s.key();
+ messages.push(message);
+ });
+
+ callback(messages);
+ };
+
+ messagesRef.on("value", handleValue);
+
+ return () => {
+ messagesRef.off("value", handleValue);
+ };
+}
+
+let serverTimeOffset = 0;
+baseRef.child(".info/serverTimeOffset").on("value", s => {
+ serverTimeOffset = s.val();
+});
+
+export function sendMessage(uid, username, avatarURL, text) {
+ messagesRef.push({
+ uid,
+ timestamp: Date.now() + serverTimeOffset,
+ username,
+ avatarURL,
+ text
+ });
+}
diff --git a/subjects/17-Mini-Router/exercise.js b/subjects/17-Mini-Router/exercise.js
new file mode 100644
index 000000000..fa30740af
--- /dev/null
+++ b/subjects/17-Mini-Router/exercise.js
@@ -0,0 +1,89 @@
+////////////////////////////////////////////////////////////////////////////////
+// Exercise:
+//
+// Implement the core components of React Router to make this app work:
+//
+//
+// 1. Add some state w/ `location` as a key
+// 2. Set `location`'s initial value to `this.history.location`
+// 3. Listen to `history` and put the location into state
+// 4. Use context to provide API to descendants
+//
+//
+// 1. Get the location from context
+// 2. Figure out if the path matches location.pathname
+// (hint: location.pathname.startsWith(...)
+// 3. If there is a match, figure out which prop to render
+// `component` or `render`
+// 4. If there is no match, render null
+//
+//
+// 1. Get a "push" method from context
+// 2. Use `push(...)` with the `to` prop
+//
+// Got extra time?
+//
+// - Implement or
+////////////////////////////////////////////////////////////////////////////////
+import React from "react";
+import ReactDOM from "react-dom";
+
+// You will be working in mini-router.js. This file is just an example app that
+// uses the components from that library.
+import { Router, Route, Link } from "./exercise/mini-router";
+
+function App() {
+ return (
+
+
+
+
+ Dashboard
+
+
+ About
+
+
+ Topics
+
+
+
+
+
+ (
+
+
Dashboard
+
+ )}
+ />
+
+
+
+
+ );
+}
+
+function About() {
+ return (
+
+
About
+
+ );
+}
+
+function Topics() {
+ return (
+
+
Topics
+
+
Rendering with React
+
Components
+
Props v. State
+
+
+ );
+}
+
+ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/17-Mini-Router/exercise/mini-router.js b/subjects/17-Mini-Router/exercise/mini-router.js
new file mode 100644
index 000000000..2bb8dccaf
--- /dev/null
+++ b/subjects/17-Mini-Router/exercise/mini-router.js
@@ -0,0 +1,49 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { createHashHistory } from "history";
+
+/*
+How to use the history library:
+
+// read the current URL
+history.location
+
+// listen for changes to the URL
+history.listen(() => {
+ history.location // is now different
+})
+
+// change the URL
+history.push('/something')
+*/
+
+class Router extends React.Component {
+ history = createHashHistory();
+
+ render() {
+ return this.props.children;
+ }
+}
+
+class Route extends React.Component {
+ render() {
+ const { path, render, component: Component } = this.props;
+ return null;
+ }
+}
+
+class Link extends React.Component {
+ handleClick = event => {
+ event.preventDefault();
+ };
+
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+export { Router, Route, Link };
diff --git a/subjects/17-Mini-Router/solution.js b/subjects/17-Mini-Router/solution.js
new file mode 100644
index 000000000..7fd225abf
--- /dev/null
+++ b/subjects/17-Mini-Router/solution.js
@@ -0,0 +1,89 @@
+////////////////////////////////////////////////////////////////////////////////
+// Exercise:
+//
+// Implement the core components of React Router to make this app work:
+//
+//
+// 1. Add some state w/ `location` as a key
+// 2. Set `location`'s initial value to `this.history.location`
+// 3. Listen to `history` and put the location into state
+// 4. Use context to provide API to descendants
+//
+//
+// 1. Get the location from context
+// 2. Figure out if the path matches location.pathname
+// (hint: location.pathname.startsWith(...)
+// 3. If there is a match, figure out which prop to render
+// `component` or `render`
+// 4. If there is no match, render null
+//
+//
+// 1. Get a "push" method from context
+// 2. Use `push(...)` with the `to` prop
+//
+// Got extra time?
+//
+// - Implement or
+////////////////////////////////////////////////////////////////////////////////
+import React from "react";
+import ReactDOM from "react-dom";
+
+// You will be working in mini-router.js. This file is just an example app that
+// uses the components from that library.
+import { Router, Route, Link } from "./solution/mini-router";
+
+function App() {
+ return (
+
+
+
+
+ Dashboard
+
+
+ About
+
+
+ Topics
+
+
+
+
+
+ (
+
+
Dashboard
+
+ )}
+ />
+
+
+
+
+ );
+}
+
+function About() {
+ return (
+
+
About
+
+ );
+}
+
+function Topics() {
+ return (
+
+
Topics
+
+
Rendering with React
+
Components
+
Props v. State
+
+
+ );
+}
+
+ReactDOM.render(, document.getElementById("app"));
diff --git a/subjects/17-Mini-Router/solution/mini-router.js b/subjects/17-Mini-Router/solution/mini-router.js
new file mode 100644
index 000000000..82b9d11d7
--- /dev/null
+++ b/subjects/17-Mini-Router/solution/mini-router.js
@@ -0,0 +1,90 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { createHashHistory } from "history";
+
+/*
+How to use the history library:
+
+// read the current URL
+history.location
+
+// listen for changes to the URL
+history.listen(() => {
+ history.location // is now different
+})
+
+// change the URL
+history.push('/something')
+*/
+
+const RouterContext = React.createContext();
+
+class Router extends React.Component {
+ history = createHashHistory();
+
+ state = {
+ location: this.history.location
+ };
+
+ componentDidMount() {
+ this.history.listen(location => {
+ this.setState({ location });
+ });
+ }
+
+ handlePush = to => {
+ this.history.push(to);
+ };
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+class Route extends React.Component {
+ render() {
+ return (
+
+ {router => {
+ const { path, render, component: Component } = this.props;
+
+ if (router.location.pathname.startsWith(path)) {
+ if (render) return render();
+ if (Component) return ;
+ }
+
+ return null;
+ }}
+
+ );
+ }
+}
+
+class Link extends React.Component {
+ handleClick = (event, router) => {
+ event.preventDefault();
+ router.push(this.props.to);
+ };
+
+ render() {
+ return (
+
+ {router => (
+ this.handleClick(event, router)}
+ >
+ {this.props.children}
+
+ )}
+
+ );
+ }
+}
+
+export { Router, Route, Link };
diff --git a/subjects/MiniRedux/exercise.js b/subjects/18-Mini-Redux/exercise.js
similarity index 97%
rename from subjects/MiniRedux/exercise.js
rename to subjects/18-Mini-Redux/exercise.js
index 1e927a3a2..69256c6f4 100644
--- a/subjects/MiniRedux/exercise.js
+++ b/subjects/18-Mini-Redux/exercise.js
@@ -28,4 +28,4 @@ If you run into problems, check the logger's output:
- Is the next state what you'd expect? Check the reducer.
*/
-import './exercise/index'
+import "./exercise/index";
diff --git a/subjects/18-Mini-Redux/exercise/components/App.js b/subjects/18-Mini-Redux/exercise/components/App.js
new file mode 100644
index 000000000..a24c197a4
--- /dev/null
+++ b/subjects/18-Mini-Redux/exercise/components/App.js
@@ -0,0 +1,30 @@
+import React from "react";
+import PropTypes from "prop-types";
+import connect from "../mini-redux/connect";
+
+class App extends React.Component {
+ increment = () => {
+ this.props.dispatch({ type: "INCREMENT" });
+ };
+
+ decrement = () => {
+ this.props.dispatch({ type: "DECREMENT" });
+ };
+
+ render() {
+ return (
+
',
+ test.title,
+ self.testURL(test)
+ );
+ var str = test.err.stack || test.err.toString();
+
+ // FF / Opera do not add the message
+ if (!~str.indexOf(test.err.message)) {
+ str = test.err.message + "\n" + str;
+ }
+
+ // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we
+ // check for the result of the stringifying.
+ if ("[object Error]" == str) str = test.err.message;
+
+ // Safari doesn't give you a stack. Let's at least provide a source line.
+ if (
+ !test.err.stack &&
+ test.err.sourceURL &&
+ test.err.line !== undefined
+ ) {
+ str +=
+ "\n(" + test.err.sourceURL + ":" + test.err.line + ")";
+ }
+
+ el.appendChild(fragment('
A taco is a traditional Mexican dish composed of a corn or wheat tortilla folded or rolled around a filling.
-
- )}
-
- )
-}
-
-function updateThePage() {
- ReactDOM.render(, document.getElementById('app'))
-}
-
-updateThePage()
-
-////////////////////////////////////////////////////////////////////////////////
-// Let's encapsulate state in an object and call it what it really is. Then, add
-// a setState function that we can use to update state and automatically update
-// the page any time the state changes.
-
-//////////////////////////////////////////////////////////////////////////////////
-// React gives us setState and automatically re-renders as the state changes.
-
-////////////////////////////////////////////////////////////////////////////////
-// Let's make re-usable and render a few of them. Title and
-// children are properties we can pass in from the parent component.
-
-////////////////////////////////////////////////////////////////////////////////
-// Wrap a few s in a that tracks the # of times
-// it has been toggled and shows a counter. gets an onToggle
-// handler, declared as a function in propTypes.
-
-////////////////////////////////////////////////////////////////////////////////
-// But we just got finished making generic, and now
-// is not! Can we make it generic as well? React.cloneElement
-// can help us pass props to elements that weren't initially provided.
-//
-// Side note: Be careful to use the React.Children utility methods.
-// this.props.children is opaque!
-
-//class ContentToggle extends React.Component {
-// state = {
-// isOpen: false
-// }
-//
-// handleClick() {
-// this.setState({
-// isOpen: !this.state.isOpen
-// })
-//
-// if (this.props.onToggle)
-// this.props.onToggle()
-// }
-//
-// render() {
-// let summaryClassName = 'ContentToggle__Summary'
-//
-// if (this.state.isOpen)
-// summaryClassName += ' ContentToggle__Summary--is-open'
-//
-// return (
-//
A taco is a traditional Mexican dish composed of a corn or wheat tortilla folded or rolled around a filling.
-//
-//
-//
A burrito is a type of Mexican and Tex-Mex food, consisting of a wheat flour tortilla wrapped or folded into a cylindrical shape to completely enclose the filling (in contrast to a taco, which is generally formed by simply folding a tortilla in half around a filling, leaving the semicircular perimeter open).
-//
-//
-//), document.getElementById('app'))
diff --git a/subjects/Components/solution.js b/subjects/Components/solution.js
deleted file mode 100644
index 10412bf61..000000000
--- a/subjects/Components/solution.js
+++ /dev/null
@@ -1,97 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Exercise:
-//
-// - Render a tab for each country with its name in the tab
-// - Make it so that you can click on a tab and it will appear active
-// while the others appear inactive
-// - Make it so the panel renders the correct content for the selected tab
-//
-// Got extra time?
-//
-// - Make generic so that it doesn't know anything about
-// country data (Hint: good propTypes help)
-////////////////////////////////////////////////////////////////////////////////
-import React from 'react'
-import ReactDOM from 'react-dom'
-
-const styles = {}
-
-styles.tab = {
- display: 'inline-block',
- padding: 10,
- margin: 10,
- borderBottom: '4px solid',
- borderBottomColor: '#ccc',
- cursor: 'pointer'
-}
-
-styles.activeTab = {
- ...styles.tab,
- borderBottomColor: '#000'
-}
-
-styles.panel = {
- padding: 10
-}
-
-class Tabs extends React.Component {
- state = {
- activeTabIndex: 0
- }
-
- selectTabIndex(activeTabIndex) {
- this.setState({ activeTabIndex })
- }
-
- render() {
- const { data } = this.props
- const { activeTabIndex } = this.state
-
- const tabs = data.map((country, index) => {
- const isActive = index === activeTabIndex
- const style = isActive ? styles.activeTab : styles.tab
-
- return (
-
-// )
-// }
-//}
-//
-//ReactDOM.render(, document.getElementById('app'))
-
-////////////////////////////////////////////////////////////////////////////////
-// That wasn't too bad, but it added a lot of complexity for something that
-// didn't seem to warrant that much of a change
-//
-// - render is less obvious
-// - have to use keys, or wrap stuff in extra divs
-// - adding another option that has to do with rendering will add even more
-// complexity
-
-////////////////////////////////////////////////////////////////////////////////
-// Lets add "disabled" to a tab, what does jQuery UI do?
-// https://api.jqueryui.com/tabs/#option-disabled
-
-//class Tabs extends React.Component {
-// static defaultProps = {
-// tabsPlacement: 'top',
-// disabled: []
-// }
-//
-// state = {
-// activeIndex: 0
-// }
-//
-// selectTabIndex(activeIndex) {
-// this.setState({ activeIndex })
-// }
-//
-// renderTabs() {
-// return this.props.data.map((tab, index) => {
-// const isActive = this.state.activeIndex === index
-// const isDisabled = this.props.disabled.indexOf(index) !== -1
-// const props = {
-// key: tab.label,
-// style: isDisabled ? styles.disabledTab : (
-// isActive ? styles.activeTab : styles.tab
-// )
-// }
-// if (!isDisabled)
-// props.onClick = () => this.selectTabIndex(index)
-// return
-// )
-// }
-//}
-//
-//ReactDOM.render(, document.getElementById('app'))
-
-////////////////////////////////////////////////////////////////////////////////
-// Instead of creating a handful of options, compose several components together
-// and then compose them together into their own components.
-//
-// A really awesome library that does this is react-soundplayer
diff --git a/subjects/CompoundComponents/lib/styles.js b/subjects/CompoundComponents/lib/styles.js
deleted file mode 100644
index fab419082..000000000
--- a/subjects/CompoundComponents/lib/styles.js
+++ /dev/null
@@ -1,25 +0,0 @@
-export const tabs = {}
-
-export const tab = {
- display: 'inline-block',
- padding: 10,
- margin: 10,
- borderBottom: '4px solid',
- borderBottomColor: '#ccc',
- cursor: 'pointer'
-}
-
-export const activeTab = {
- ...tab,
- borderBottomColor: '#000'
-}
-
-export const disabledTab = {
- ...tab,
- opacity: 0.25,
- cursor: 'default'
-}
-
-export const tabPanels = {
- padding: 10
-}
diff --git a/subjects/Context/lecture.js b/subjects/Context/lecture.js
deleted file mode 100644
index e8f277960..000000000
--- a/subjects/Context/lecture.js
+++ /dev/null
@@ -1,314 +0,0 @@
-import React, { PropTypes } from 'react'
-import ReactDOM from 'react-dom'
-import * as styles from './styles'
-
-////////////////////////////////////////////////////////////////////////////////
-// Sometimes you don't want to specify how deep in the view tree the child
-// components need to be, our current implementation expects TabList/TabPanels
-// to be immediate children of Tabs, also Tab and TabPanel are required to be
-// immediate children of their parent components. We really only care about the
-// interactivity between the components, not their hierarchy.
-//
-// We could recursively check children with each render, which seems like a bad
-// plan, so instead we can use a feature called "context".
-
-class TabList extends React.Component {
- render() {
- const children = React.Children.map(this.props.children, (child, index) => {
- return React.cloneElement(child, {
- isActive: index === this.props.activeIndex,
- onClick: () => this.props.onActivate(index)
- })
- })
-
- return
- )
- }
-}
-
-ReactDOM.render(, document.getElementById('app'))
-
-////////////////////////////////////////////////////////////////////////////////
-// Give the a default value.
-//class Forms extends React.Component {
-// render() {
-// return (
-//
-//
Forms
-//
-//
-//
-//
-// )
-// }
-//}
-
-////////////////////////////////////////////////////////////////////////////////
-// Access the value using event.target.
-//class Forms extends React.Component {
-// handleChange = (event) => {
-// console.log(event.target.value)
-// }
-//
-// render() {
-// return (
-//
-//
Forms
-//
-//
-//
-//
-// )
-// }
-//}
-
-////////////////////////////////////////////////////////////////////////////////
-// Or use a ref.
-//class Forms extends React.Component {
-// handleChange = () => {
-// console.log(findDOMNode(this.refs.someInput).value)
-// }
-//
-// render() {
-// return (
-//
-//
Forms
-//
-//
-//
-//
-// )
-// }
-//}
-
-////////////////////////////////////////////////////////////////////////////////
-// Or you can "control" the and have its value in state.
-// What happens if we don't have an `onChange` but provide a value?
-//class Forms extends React.Component {
-// state = {
-// someInputValue: 'lol'
-// }
-//
-// handleChange = () => {
-// this.setState({
-// someInputValue: findDOMNode(this.refs.someInput).value
-// })
-// }
-//
-// render() {
-// return (
-//
-//
Forms
-//
-//
-//
-//
{JSON.stringify(this.state, null, 2)}
-//
-// )
-// }
-//}
-
-////////////////////////////////////////////////////////////////////////////////
-// When it's controlled, we can set state elsewhere and it stays in sync
-//class Forms extends React.Component {
-// state = {
-// someInputValue: 'lol'
-// }
-//
-// handleChange = () => {
-// this.setState({
-// someInputValue: findDOMNode(this.refs.someInput).value
-// })
-// }
-//
-// render() {
-// return (
-//
-//
Forms
-//
-// this.setState({ someInputValue: 'changed!' })}>
-// Change the value
-//
-//
-//
-//
{JSON.stringify(this.state, null, 2)}
-//
-// )
-// }
-//}
-
-////////////////////////////////////////////////////////////////////////////////
-// Some forms are transactional, so modeling in state isn't necessary, just
-// use DOM APIs to access the data, like when you need to save off some data
-// somewhere and reset the form, but the values in the form are never
-// important to `render`.
-//class Forms extends React.Component {
-// handleSubmit = (event) => {
-// event.preventDefault()
-// const values = serializeForm(event.target, { hash: true })
-// console.log(values)
-// }
-//
-// render() {
-// return (
-//
-//
Forms
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-// Save
-//
-//
-//
-// )
-// }
-//}
-
-////////////////////////////////////////////////////////////////////////////////
-// If we did want it all in state, we don't have to link up every single
-// element to state, can use
. However, updates won't be
-// synchronized when other parts of the app manipulate the state like the
-// button we had earlier.
-//class Forms extends React.Component {
-// state = {
-// firstName: 'Ryan',
-// lastName: 'Florence'
-// }
-//
-// handleFormChange = (event) => {
-// event.preventDefault()
-// const form = findDOMNode(this.refs.form)
-// const values = serializeForm(form, { hash: true })
-// this.setState(values)
-// }
-//
-// render() {
-// return (
-//
-//
Forms
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-// Save
-//
-//
-//
{JSON.stringify(this.state, null, 2)}
-//
-// )
-// }
-//}
-
-////////////////////////////////////////////////////////////////////////////////
-// Use-cases:
-//
-// 1. Transactional forms, don't need anything in state, just use
-// `defaultValue` and `onSubmit`
-// 2. Some other part of the app needs the forms state to render,
-// but nothing else needs to manipulate that state (one-way),
-// use
and DOM APIs to slurp form values into
-// state
-// 3. Multiple parts of the app manipulate the state, changes need
-// to be reflected in the input (two-way), use `value` and
-// `onChange`
diff --git a/subjects/Forms/lib/swapi.js b/subjects/Forms/lib/swapi.js
deleted file mode 100644
index ea585bdb7..000000000
--- a/subjects/Forms/lib/swapi.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { getJSON } from './xhr'
-
-const SWAPI = 'http://swapi.co/api'
-
-export function getFilms(cb) {
- getJSON(`${SWAPI}/films/`, (err, data) => cb(err, data.results))
-}
-
-export function getPeople(filmURL, cb) {
- getJSON(filmURL, (err, film) => {
- const characters = film.characters.slice(0, 5)
- let data = []
- characters.forEach((url) => {
- getJSON(url, (err, person) => {
- data.push(person)
- if (data.length === characters.length) {
- cb(null, data)
- }
- })
- })
- })
-}
-
-export function getVehicles(personURL, cb) {
- getJSON(personURL, (err, person) => {
- const vehicles = person.vehicles.slice(0, 5)
- let data = []
- if (person.vehicles.length === 0) {
- cb(null, data)
- } else {
- vehicles.forEach((url) => {
- getJSON(url, (err, vehicle) => {
- data.push(vehicle)
- if (data.length === vehicles.length) {
- cb(null, data)
- }
- })
- })
- }
- })
-}
-
-
diff --git a/subjects/Forms/lib/xhr.js b/subjects/Forms/lib/xhr.js
deleted file mode 100644
index 1fde574c7..000000000
--- a/subjects/Forms/lib/xhr.js
+++ /dev/null
@@ -1,44 +0,0 @@
-localStorage.token = localStorage.token || (Date.now()*Math.random())
-
-function setToken(req) {
- req.setRequestHeader('authorization', localStorage.token)
-}
-
-export function getJSON(url, callback) {
- const req = new XMLHttpRequest()
- req.onload = function () {
- if (req.status === 404) {
- callback(new Error('not found'))
- } else {
- callback(null, JSON.parse(req.response))
- }
- }
- req.open('GET', url)
- setToken(req)
- req.send()
-}
-
-export function postJSON(url, obj, callback) {
- const req = new XMLHttpRequest()
- req.onload = function () {
- callback(JSON.parse(req.response))
- }
- req.open('POST', url)
- req.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')
- setToken(req)
- req.send(JSON.stringify(obj))
-}
-
-export function deleteJSON(url, callback) {
- const req = new XMLHttpRequest()
- req.onload = function () {
- if (req.status === 500) {
- callback(new Error(req.responseText))
- } else {
- callback(null, req.responseText)
- }
- }
- req.open('DELETE', url)
- setToken(req)
- req.send()
-}
diff --git a/subjects/Forms/solution.js b/subjects/Forms/solution.js
deleted file mode 100644
index 35b7262f8..000000000
--- a/subjects/Forms/solution.js
+++ /dev/null
@@ -1,109 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Exercise
-//
-// - When the checkbox is checked:
-// - Fill in the shipping fields with the values from billing
-// - Disable the shipping fields so they are not directly editable
-// - Keep the shipping fields up to date as billing fields change
-// - Hint: you can get the checkbox value from `event.target.checked`
-// - When the form submits, console.log the values
-//
-// Got extra time?
-//
-// - If there are more than two characters in the "state" field, let the user
-// know they should use the two-character abbreviation
-// - If the user types something into shipping, then checks the checkbox, then
-// unchecks the checkbox, ensure the field has the information from
-// before clicking the checkbox the first time
-import React from 'react'
-import ReactDOM from 'react-dom'
-import serializeForm from 'form-serialize'
-
-class CheckoutForm extends React.Component {
- state = {
- billingName: 'Michael Jackson',
- billingState: 'CA',
- shippingName: '',
- shippingState: '',
- shippingSameAsBilling: false
- }
-
- handleSubmit = (event) => {
- event.preventDefault()
-
- const values = serializeForm(event.target, { hash: true })
-
- console.log(values)
- }
-
- render() {
- const {
- billingName,
- billingState,
- shippingName,
- shippingState,
- shippingSameAsBilling
- } = this.state
-
- return (
-
-//`
-
-//$('#app').html(html) // <-- component
-
-//$('#form').on('submit', (event) => { // <-- state change
- //event.preventDefault()
- //const term = $('#input').val() // <-- state
- //$('#loading').show() // <-- time
- //$('#meta').hide() // <-- time
- //$('#results').empty() // <-- time
- //search(term, (err, results) => {
- //$('#loading').hide() // <-- time
- //$('#meta').show() // <-- time
- //$('#title').html(term) // <-- time
- //results.forEach((result) => {
- //const li = $('')
- //const html = `
- //
- //${result.title}
- //show more
- //
- //
- //
${result.description}
- //
- //`
- //li.html(html) // <-- time
- //if ($('#descending').is(':checked')) { // <-- state
- //li.prependTo($('#results')) // <-- time
- //} else {
- //li.appendTo($('#results')) // <-- time
- //}
- //li.find('button').on('click', () => { // <-- component
- //li.find('.toggler').toggle() // <-- time
- //const isHidden = li.find('.toggler').is(':hidden') // <-- state
- //li.find('button').html(isHidden ? 'show more' : 'hide') // <-- time
- //})
- //})
- //})
-//}).trigger('submit') // <-- state change
-
-//$('#descending').on('click', (event) => { // <-- state change
- //$('#results li').each((i, li) => {
- //$('#results').prepend(li) // <-- time
- //})
-//})
-
-// What's awesome:
-//
-// I can still bang out this code even after not using jQuery for
-// 4 years.
-//
-// What's not awesome:
-//
-// When our code...
-//
-// - is written as flows
-// - doesn't call out state
-// - has no entry point to change state
-//
-// ...it gets really hard to deal with. After you identify state,
-// and how to change it, you must write code to connect every state
-// to nearly every other state. Every feature requires changes to code
-// in multiple places. Also, it's just too hard to think about for must
-// of us leading to lot of bugs.
-
-
-////////////////////////////////////////////////////////////////////////////////
-//import Backbone from 'backbone'
-//import $ from 'jquery'
-//import _ from 'underscore'
-//import { search } from './lib/searchWikipedia'
-
-//const appTemplate = _.template(`
- //
- //<% } %>
- //`),
-
- //events: {
- //'click button': 'toggle'
- //},
-
- //initialize() {
- //this.model.set('isOpen', false, { silent: true }) // <-- model ownership?
- //this.listenTo(this.model, 'change:isOpen', this.render)
- //},
-
- //toggle() {
- //this.model.set('isOpen', !this.model.get('isOpen')) // <-- KVO web
- //},
-
- //render() {
- //this.$el.html(this.template(this.model.toJSON()))
- //}
-//})
-
-//new AppView({
- //el: '#app',
- //model: new Backbone.Model({
- //title: 'Wikipedia',
- //loading: false,
- //term: 'tacos',
- //descending: false,
- //results: []
- //})
-//})
-
-// What's awesome
-//
-// - Moved state to models so we can identify what state changes
-// the app.
-// - Moved creating UI into templates, one step closer to being
-// declarative
-//
-// What's not so awesome
-//
-// - DOM Bombs
-// - kill focus for assistive devices
-// - non-performant
-//
-// - KVO Web
-// - can't predict what will happen if you change state
-// > Events complect communication and flow of control.
-// > ... their fundamental nature, ... is that upon an event
-// > an arbitrary amount of other code is run
-// > http://clojure.com/blog/2013/06/28/clojure-core-async-channels.html
-//
-// - leads to cascading updates
-// - non-performant
-// - to fix leads to knowing how your app changes over time intimately
-//
-// - imperative composition
-// - non-performant
-// - to fix
-// - have to know how your app changes over time intimately
-// - lots of code to manage instances
-// - lots of mistakes
-
-////////////////////////////////////////////////////////////////////////////////
-//import angular from 'angular'
-//import { search } from './lib/searchWikipedia'
-
-//document.documentElement.setAttribute('ng-app', 'wikipedia')
-
-//document.getElementById('app').innerHTML = `
- //
- //`
- //}
-//})
-
-// What's awesome
-//
-// - fully declarative templates
-// - declarative component composition
-//
-// What's not so awesome
-//
-// - directives and filters are globals
-// - have to think about time with $apply/$watch, etc.
-// - rendering assumptions require you to keep object identity
-// and therefore think about time
-// - and the real kicker: shared mutable state
-//
-// > July 7, 2014
-// >
-// > Vojta brought up some points that we don’t yet have plans to solve
-// > some problems we see in larger apps. In particular, how developers
-// > can reason about data flow within an app.
-// >
-// > Key points: scope hierarchy is a huge pile of shared state that many
-// > components from the application because of two way data-binding it's
-// > not clear what how the data flows because it can flow in all
-// > directions (including from child components to parents) - this makes
-// > it hard to understand the app and understand of impact of model
-// > changes in one part of the app on another (seemingly unrelated) part
-// > of it.
-// https://twitter.com/teozaurus/status/518071391959388160
diff --git a/subjects/HelloWorld/lib/searchWikipedia.js b/subjects/HelloWorld/lib/searchWikipedia.js
deleted file mode 100644
index 006eb3f56..000000000
--- a/subjects/HelloWorld/lib/searchWikipedia.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import jsonp from 'jsonp'
-
-const API = 'https://en.wikipedia.org/w/api.php?action=opensearch&format=json'
-
-export function search(term, cb) {
- jsonp(`${API}&search=${term}`, (err, data) => {
- if (err) {
- cb(err)
- } else {
- const [ searchTerm, titles, descriptions, urls ] = data
- cb(null, titles.sort().map((title, index) => {
- return {
- title,
- description: descriptions[index],
- url: urls[index]
- }
- }))
- }
- })
-}
-
diff --git a/subjects/HigherOrderComponents/exercise.js b/subjects/HigherOrderComponents/exercise.js
deleted file mode 100644
index c7f970d9c..000000000
--- a/subjects/HigherOrderComponents/exercise.js
+++ /dev/null
@@ -1,40 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Exercise:
-//
-// Make `withMousePosition` a "higher-order component" that sends the mouse
-// position to the component as props.
-//
-// Hint: use `event.clientX` and `event.clientY`
-import React, { PropTypes } from 'react'
-import ReactDOM from 'react-dom'
-
-const withMousePosition = (Component) => {
- return Component
-}
-
-class App extends React.Component {
- static propTypes = {
- mouse: PropTypes.shape({
- x: PropTypes.number.isRequired,
- y: PropTypes.number.isRequired
- }).isRequired
- }
-
- render() {
- const { mouse } = this.props
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/subjects/MigratingToReact/backbone-todomvc/js/app.js b/subjects/MigratingToReact/backbone-todomvc/js/app.js
deleted file mode 100644
index cf6b8b78c..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/js/app.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/*global $ */
-/*jshint unused:false */
-var app = app || {};
-var ENTER_KEY = 13;
-var ESC_KEY = 27;
-
-$(function () {
- 'use strict';
-
- // sorry, there's no build, jsx loader is async,
- // so we're just hacking for now to get the point across
- setTimeout(function () {
- // kick things off by creating the `App`
- new app.AppView();
- }, 100);
-});
diff --git a/subjects/MigratingToReact/backbone-todomvc/js/collections/todos.js b/subjects/MigratingToReact/backbone-todomvc/js/collections/todos.js
deleted file mode 100644
index a0bd4170c..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/js/collections/todos.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/*global Backbone */
-var app = app || {};
-
-(function () {
- 'use strict';
-
- // Todo Collection
- // ---------------
-
- // The collection of todos is backed by *localStorage* instead of a remote
- // server.
- var Todos = Backbone.Collection.extend({
- // Reference to this collection's model.
- model: app.Todo,
-
- // Save all of the todo items under the `"todos"` namespace.
- localStorage: new Backbone.LocalStorage('todos-backbone'),
-
- // Filter down the list of all todo items that are finished.
- completed: function () {
- return this.where({completed: true});
- },
-
- // Filter down the list to only todo items that are still not finished.
- remaining: function () {
- return this.where({completed: false});
- },
-
- // We keep the Todos in sequential order, despite being saved by unordered
- // GUID in the database. This generates the next order number for new items.
- nextOrder: function () {
- return this.length ? this.last().get('order') + 1 : 1;
- },
-
- // Todos are sorted by their original insertion order.
- comparator: 'order'
- });
-
- // Create our global collection of **Todos**.
- app.todos = new Todos();
-})();
diff --git a/subjects/MigratingToReact/backbone-todomvc/js/models/todo.js b/subjects/MigratingToReact/backbone-todomvc/js/models/todo.js
deleted file mode 100644
index 3bc2c6253..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/js/models/todo.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/*global Backbone */
-var app = app || {};
-
-(function () {
- 'use strict';
-
- // Todo Model
- // ----------
-
- // Our basic **Todo** model has `title`, `order`, and `completed` attributes.
- app.Todo = Backbone.Model.extend({
- // Default attributes for the todo
- // and ensure that each todo created has `title` and `completed` keys.
- defaults: {
- title: '',
- completed: false
- },
-
- // Toggle the `completed` state of this todo item.
- toggle: function () {
- this.save({
- completed: !this.get('completed')
- });
- }
- });
-})();
diff --git a/subjects/MigratingToReact/backbone-todomvc/js/routers/router.js b/subjects/MigratingToReact/backbone-todomvc/js/routers/router.js
deleted file mode 100644
index f7d618f7a..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/js/routers/router.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/*global Backbone */
-var app = app || {};
-
-(function () {
- 'use strict';
-
- // Todo Router
- // ----------
- var TodoRouter = Backbone.Router.extend({
- routes: {
- '*filter': 'setFilter'
- },
-
- setFilter: function (param) {
- // Set the current filter to be used
- app.TodoFilter = param || '';
-
- // Trigger a collection filter event, causing hiding/unhiding
- // of Todo view items
- app.todos.trigger('filter');
- }
- });
-
- app.TodoRouter = new TodoRouter();
- Backbone.history.start();
-})();
diff --git a/subjects/MigratingToReact/backbone-todomvc/js/views/app-view.js b/subjects/MigratingToReact/backbone-todomvc/js/views/app-view.js
deleted file mode 100644
index 77f219446..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/js/views/app-view.js
+++ /dev/null
@@ -1,131 +0,0 @@
-/*global Backbone, jQuery, _, ENTER_KEY */
-var app = app || {};
-
-(function ($) {
- 'use strict';
-
- // The Application
- // ---------------
-
- // Our overall **AppView** is the top-level piece of UI.
- app.AppView = Backbone.View.extend({
-
- // Instead of generating a new element, bind to the existing skeleton of
- // the App already present in the HTML.
- el: '#todoapp',
-
- // Our template for the line of statistics at the bottom of the app.
- statsTemplate: _.template($('#stats-template').html()),
-
- // Delegated events for creating new items, and clearing completed ones.
- events: {
- 'keypress #new-todo': 'createOnEnter',
- 'click #clear-completed': 'clearCompleted',
- 'click #toggle-all': 'toggleAllComplete'
- },
-
- // At initialization we bind to the relevant events on the `Todos`
- // collection, when items are added or changed. Kick things off by
- // loading any preexisting todos that might be saved in *localStorage*.
- initialize: function () {
- this.allCheckbox = this.$('#toggle-all')[0];
- this.$input = this.$('#new-todo');
- this.$footer = this.$('#footer');
- this.$main = this.$('#main');
- this.$list = $('#todo-list');
-
- this.listenTo(app.todos, 'add', this.addOne);
- this.listenTo(app.todos, 'reset', this.addAll);
- this.listenTo(app.todos, 'change:completed', this.filterOne);
- this.listenTo(app.todos, 'filter', this.filterAll);
- this.listenTo(app.todos, 'all', this.render);
-
- // Suppresses 'add' events with {reset: true} and prevents the app view
- // from being re-rendered for every model. Only renders when the 'reset'
- // event is triggered at the end of the fetch.
- app.todos.fetch({reset: true});
- },
-
- // Re-rendering the App just means refreshing the statistics -- the rest
- // of the app doesn't change.
- render: function () {
- var completed = app.todos.completed().length;
- var remaining = app.todos.remaining().length;
-
- if (app.todos.length) {
- this.$main.show();
- this.$footer.show();
-
- this.$footer.html(this.statsTemplate({
- completed: completed,
- remaining: remaining
- }));
-
- this.$('#filters li a')
- .removeClass('selected')
- .filter('[href="#/' + (app.TodoFilter || '') + '"]')
- .addClass('selected');
- } else {
- this.$main.hide();
- this.$footer.hide();
- }
-
- this.allCheckbox.checked = !remaining;
- },
-
- // Add a single todo item to the list by creating a view for it, and
- // appending its element to the `
`.
- addOne: function (todo) {
- var view = new app.TodoView({ model: todo });
- this.$list.append(view.render().el);
- },
-
- // Add all items in the **Todos** collection at once.
- addAll: function () {
- this.$list.html('');
- app.todos.each(this.addOne, this);
- },
-
- filterOne: function (todo) {
- todo.trigger('visible');
- },
-
- filterAll: function () {
- app.todos.each(this.filterOne, this);
- },
-
- // Generate the attributes for a new Todo item.
- newAttributes: function () {
- return {
- title: this.$input.val().trim(),
- order: app.todos.nextOrder(),
- completed: false
- };
- },
-
- // If you hit return in the main input field, create new **Todo** model,
- // persisting it to *localStorage*.
- createOnEnter: function (e) {
- if (e.which === ENTER_KEY && this.$input.val().trim()) {
- app.todos.create(this.newAttributes());
- this.$input.val('');
- }
- },
-
- // Clear all completed todo items, destroying their models.
- clearCompleted: function () {
- _.invoke(app.todos.completed(), 'destroy');
- return false;
- },
-
- toggleAllComplete: function () {
- var completed = this.allCheckbox.checked;
-
- app.todos.each(function (todo) {
- todo.save({
- completed: completed
- });
- });
- }
- });
-})(jQuery);
diff --git a/subjects/MigratingToReact/backbone-todomvc/js/views/todo-view.js b/subjects/MigratingToReact/backbone-todomvc/js/views/todo-view.js
deleted file mode 100644
index 0b9e93d79..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/js/views/todo-view.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/*global Backbone, jQuery, _, ENTER_KEY, ESC_KEY */
-var app = app || {};
-
-(function ($) {
- 'use strict';
-
- // Todo Item View
- // --------------
-
- // The DOM element for a todo item...
- app.TodoView = Backbone.View.extend({
- //... is a list tag.
- tagName: 'li',
-
- // Cache the template function for a single item.
- template: _.template($('#item-template').html()),
-
- // The DOM events specific to an item.
- events: {
- 'click .toggle': 'toggleCompleted',
- 'dblclick label': 'edit',
- 'click .destroy': 'clear',
- 'keypress .edit': 'updateOnEnter',
- 'keydown .edit': 'revertOnEscape',
- 'blur .edit': 'close'
- },
-
- // The TodoView listens for changes to its model, re-rendering. Since
- // there's a one-to-one correspondence between a **Todo** and a
- // **TodoView** in this app, we set a direct reference on the model for
- // convenience.
- initialize: function () {
- this.listenTo(this.model, 'change', this.render);
- this.listenTo(this.model, 'destroy', this.remove);
- this.listenTo(this.model, 'visible', this.toggleVisible);
- },
-
- // Re-render the titles of the todo item.
- render: function () {
- // Backbone LocalStorage is adding `id` attribute instantly after
- // creating a model. This causes our TodoView to render twice. Once
- // after creating a model and once on `id` change. We want to
- // filter out the second redundant render, which is caused by this
- // `id` change. It's known Backbone LocalStorage bug, therefore
- // we've to create a workaround.
- // https://github.com/tastejs/todomvc/issues/469
- if (this.model.changed.id !== undefined) {
- return;
- }
-
- this.$el.html(this.template(this.model.toJSON()));
- this.$el.toggleClass('completed', this.model.get('completed'));
- this.toggleVisible();
- this.$input = this.$('.edit');
- return this;
- },
-
- toggleVisible: function () {
- this.$el.toggleClass('hidden', this.isHidden());
- },
-
- isHidden: function () {
- return this.model.get('completed') ?
- app.TodoFilter === 'active' :
- app.TodoFilter === 'completed';
- },
-
- // Toggle the `"completed"` state of the model.
- toggleCompleted: function () {
- this.model.toggle();
- },
-
- // Switch this view into `"editing"` mode, displaying the input field.
- edit: function () {
- this.$el.addClass('editing');
- this.$input.focus();
- },
-
- // Close the `"editing"` mode, saving changes to the todo.
- close: function () {
- var value = this.$input.val();
- var trimmedValue = value.trim();
-
- // We don't want to handle blur events from an item that is no
- // longer being edited. Relying on the CSS class here has the
- // benefit of us not having to maintain state in the DOM and the
- // JavaScript logic.
- if (!this.$el.hasClass('editing')) {
- return;
- }
-
- if (trimmedValue) {
- this.model.save({ title: trimmedValue });
-
- if (value !== trimmedValue) {
- // Model values changes consisting of whitespaces only are
- // not causing change to be triggered Therefore we've to
- // compare untrimmed version with a trimmed one to check
- // whether anything changed
- // And if yes, we've to trigger change event ourselves
- this.model.trigger('change');
- }
- } else {
- this.clear();
- }
-
- this.$el.removeClass('editing');
- },
-
- // If you hit `enter`, we're through editing the item.
- updateOnEnter: function (e) {
- if (e.which === ENTER_KEY) {
- this.close();
- }
- },
-
- // If you're pressing `escape` we revert your change by simply leaving
- // the `editing` state.
- revertOnEscape: function (e) {
- if (e.which === ESC_KEY) {
- this.$el.removeClass('editing');
- // Also reset the hidden input back to the original value.
- this.$input.val(this.model.get('title'));
- }
- },
-
- // Remove the item, destroy the model from *localStorage* and delete its view.
- clear: function () {
- this.model.destroy();
- }
- });
-})(jQuery);
diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.documentup.json b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.documentup.json
deleted file mode 100644
index 68ce04eb2..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.documentup.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "Backbone.localStorage",
- "twitter": "jeromegn"
-}
\ No newline at end of file
diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.npmignore b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.npmignore
deleted file mode 100644
index 6a626034c..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.npmignore
+++ /dev/null
@@ -1,6 +0,0 @@
-spec/
-examples/
-Makefile
-bower.json
-component.json
-CNAME
diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.travis.yml b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.travis.yml
deleted file mode 100644
index 7b74b286e..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.travis.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-language: node_js
-node_js:
- - 0.10
-before_script:
- - "export DISPLAY=:99.0"
- - "sh -e /etc/init.d/xvfb start"
-script: make test
\ No newline at end of file
diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/CHANGELOG.md b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/CHANGELOG.md
deleted file mode 100644
index b17fe803e..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/CHANGELOG.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# Changelog
-
-## 1.1.16 - January 22, 2015
-
-- Fix for #130. Uncaught TypeError: Cannot read property 'localStorage' of undefined
-
-## 1.1.15 - January 7, 2015
-
-- You can now pass `{ajaxSync: true}` as an option to `fetch` and `save` to force remote syncing. (@adamterlson)
-- Various fixes. Thanks to @nbergseng, @richardchen331, @eush77, @malept, @HenriqueSilverio
-
-## 1.1.14 - skipped due to some weirdness, inconsistent versioning.
-
-## 1.1.13 - August 5, 2014
-
-- Forgot a comma, broke it for bower.
-
-## 1.1.10 - August 4, 2014
-
-- Various fixes, thanks everybody
-
-## 1.1.7 - October 4, 2013
-
-- Fix CommonJS requiring (Brandon Dimcheff)
-
-## 1.1.6 - July 2, 2013
-
-- Don't assume `require` is defined when `exports` is defined (#100)
-
-## 1.1.5 - At some point...
-
-- Can't remember...
-
-## 1.1.4 - May 19, 2013
-
-- #90 Check for localStorage (throw an error if not available)
-
-## 1.1.3 - May 12, 2013
-
-- #79 Added CommonJS support
-
-## 1.1.2 - May 12, 2013
-
-- #82 Upgraded Backbone to version 1.0 in test suite
-- Fixed a few bugs after upgrading to 1.0... looks like Backbone is now triggering the events we were previously manually triggering.
-
-## 1.1.1 - May 11, 2013
-
-- #87 Fixes Lodash v1.0.0-rc.1 removal of _#chain
-- #86 Fix for when developer has specified in backbone which jQuery version to use by setting Backbone.$
-
-## 1.1.0 - prior to May 11, 2013
-
-- localStorage adapter for Backbone.js
\ No newline at end of file
diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/README.md b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/README.md
deleted file mode 100644
index 1991a3acd..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/README.md
+++ /dev/null
@@ -1,131 +0,0 @@
-# Backbone localStorage Adapter v1.1.16
-
-[](http://travis-ci.org/jeromegn/Backbone.localStorage)
-
-Quite simply a localStorage adapter for Backbone. It's a drop-in replacement for Backbone.Sync() to handle saving to a localStorage database.
-
-[](https://www.gittip.com/jeromegn/)
-
-## Usage
-
-Include Backbone.localStorage after having included Backbone.js:
-
-```html
-
-
-```
-
-Create your collections like so:
-
-```javascript
-window.SomeCollection = Backbone.Collection.extend({
-
- localStorage: new Backbone.LocalStorage("SomeCollection"), // Unique name within your app.
-
- // ... everything else is normal.
-
-});
-```
-
-If needed, you can use the default `Backbone.sync` (instead of local storage) by passing the `ajaxSync` option flag to any Backbone AJAX function, for example:
-
-```javascript
-var myModel = new SomeModel();
-myModel.fetch({ ajaxSync: true });
-myModel.save({ new: "value" }, { ajaxSync: true });
-```
-
-### RequireJS
-
-Include [RequireJS](http://requirejs.org):
-
-```html
-
-```
-
-RequireJS config:
-```javascript
-require.config({
- paths: {
- jquery: "lib/jquery",
- underscore: "lib/underscore",
- backbone: "lib/backbone",
- localstorage: "lib/backbone.localStorage"
- }
-});
-```
-
-Define your collection as a module:
-```javascript
-define("SomeCollection", ["localstorage"], function() {
- var SomeCollection = Backbone.Collection.extend({
- localStorage: new Backbone.LocalStorage("SomeCollection") // Unique name within your app.
- });
-
- return SomeCollection;
-});
-```
-
-Require your collection:
-```javascript
-require(["SomeCollection"], function(SomeCollection) {
- // ready to use SomeCollection
-});
-```
-
-### CommonJS
-
-If you're using [browserify](https://github.com/substack/node-browserify).
-
-Install using `npm install backbone.localstorage`, and require the module.
-
-```javascript
-Backbone.LocalStorage = require("backbone.localstorage");
-```
-
-##Support
-
-If you're having a problem with using the project, get help at CodersClan.
-
-
-
-## Contributing
-
-You'll need node and to `npm install` before being able to run the minification script.
-
-1. Fork;
-2. Write code, with tests;
-3. `make test` or `open spec/runner.html`;
-4. Create a pull request.
-
-Have fun!
-
-## Acknowledgments
-
-- [Mark Woodall](https://github.com/llad): initial tests (now refactored);
-- [Martin Häcker](https://github.com/dwt): many fixes and the test isolation.
-
-## License
-
-Licensed under MIT license
-
-Copyright (c) 2010 Jerome Gravel-Niquet
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/backbone.localStorage-min.js b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/backbone.localStorage-min.js
deleted file mode 100644
index 8ef0ccd71..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/backbone.localStorage-min.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/**
- * Backbone localStorage Adapter
- * Version 1.1.16
- *
- * https://github.com/jeromegn/Backbone.localStorage
- */(function(a,b){typeof exports=="object"&&typeof require=="function"?module.exports=b(require("backbone")):typeof define=="function"&&define.amd?define(["backbone"],function(c){return b(c||a.Backbone)}):b(Backbone)})(this,function(a){function b(){return((1+Math.random())*65536|0).toString(16).substring(1)}function c(){return b()+b()+"-"+b()+"-"+b()+"-"+b()+"-"+b()+b()+b()}function d(a){return a===Object(a)}function e(a,b){var c=a.length;while(c--)if(a[c]===b)return!0;return!1}function f(a,b){for(var c in b)a[c]=b[c];return a}function g(a,b){if(a==null)return void 0;var c=a[b];return typeof c=="function"?a[b]():c}return a.LocalStorage=window.Store=function(a,b){if(!this.localStorage)throw"Backbone.localStorage: Environment does not support localStorage.";this.name=a,this.serializer=b||{serialize:function(a){return d(a)?JSON.stringify(a):a},deserialize:function(a){return a&&JSON.parse(a)}};var c=this.localStorage().getItem(this.name);this.records=c&&c.split(",")||[]},f(a.LocalStorage.prototype,{save:function(){this.localStorage().setItem(this.name,this.records.join(","))},create:function(a){return!a.id&&a.id!==0&&(a.id=c(),a.set(a.idAttribute,a.id)),this.localStorage().setItem(this._itemName(a.id),this.serializer.serialize(a)),this.records.push(a.id.toString()),this.save(),this.find(a)},update:function(a){this.localStorage().setItem(this._itemName(a.id),this.serializer.serialize(a));var b=a.id.toString();return e(this.records,b)||(this.records.push(b),this.save()),this.find(a)},find:function(a){return this.serializer.deserialize(this.localStorage().getItem(this._itemName(a.id)))},findAll:function(){var a=[];for(var b=0,c,d;b=1.4.0"
- },
- "peerDependencies": {
- "backbone": "~1.x.x"
- },
- "keywords": [
- "backbone",
- "localstorage",
- "local",
- "storage",
- "cache",
- "sync"
- ],
- "main": "./backbone.localStorage.js",
- "engines": {
- "node": "*"
- },
- "gitHead": "728194adef60befefed1416825498d1b53cd517a",
- "description": "[](http://travis-ci.org/jeromegn/Backbone.localStorage)",
- "bugs": {
- "url": "https://github.com/jeromegn/Backbone.localStorage/issues"
- },
- "homepage": "https://github.com/jeromegn/Backbone.localStorage",
- "_id": "backbone.localstorage@1.1.16",
- "scripts": {},
- "_shasum": "3df5101d93abcee04f326035540f425dd0487916",
- "_from": "backbone.localstorage@>=1.1.16 <2.0.0",
- "_npmVersion": "2.1.17",
- "_nodeVersion": "0.10.35",
- "_npmUser": {
- "name": "jeromegn",
- "email": "jeromegn@gmail.com"
- },
- "maintainers": [
- {
- "name": "jeromegn",
- "email": "jeromegn@gmail.com"
- }
- ],
- "dist": {
- "shasum": "3df5101d93abcee04f326035540f425dd0487916",
- "tarball": "http://registry.npmjs.org/backbone.localstorage/-/backbone.localstorage-1.1.16.tgz"
- },
- "directories": {},
- "_resolved": "https://registry.npmjs.org/backbone.localstorage/-/backbone.localstorage-1.1.16.tgz"
-}
diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/.npmignore b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/.npmignore
deleted file mode 100644
index 6bcb69f50..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/.npmignore
+++ /dev/null
@@ -1,7 +0,0 @@
-test/
-Rakefile
-docs/
-raw/
-examples/
-index.html
-.jshintrc
diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/.travis.yml b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/.travis.yml
deleted file mode 100644
index 99dc7712c..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/.travis.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-language: node_js
-node_js:
- - 0.8
-notifications:
- email: false
diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/CNAME b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/CNAME
deleted file mode 100644
index dfadb7985..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/CNAME
+++ /dev/null
@@ -1,2 +0,0 @@
-backbonejs.org
-
diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/CONTRIBUTING.md b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/CONTRIBUTING.md
deleted file mode 100644
index 55403616b..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/CONTRIBUTING.md
+++ /dev/null
@@ -1,22 +0,0 @@
-## How to Open a Backbone.js Ticket
-
-* Do not use tickets to ask for help with (debugging) your application. Ask on
-the [mailing list](https://groups.google.com/forum/#!forum/backbonejs),
-in the IRC channel (`#documentcloud` on Freenode), or if you understand your
-specific problem, on [StackOverflow](http://stackoverflow.com/questions/tagged/backbone.js).
-
-* Before you open a ticket or send a pull request,
-[search](https://github.com/jashkenas/backbone/issues) for previous
-discussions about the same feature or issue. Add to the earlier ticket if you
-find one.
-
-* Before sending a pull request for a feature or bug fix, be sure to have
-[tests](http://backbonejs.org/test/).
-
-* Use the same coding style as the rest of the
-[codebase](https://github.com/jashkenas/backbone/blob/master/backbone.js).
-
-* In your pull request, do not add documentation or rebuild the minified
-`backbone-min.js` file. We'll do that before cutting a new release.
-
-* All pull requests should be made to the `master` branch.
diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/LICENSE b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/LICENSE
deleted file mode 100644
index 3ffd97de0..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/LICENSE
+++ /dev/null
@@ -1,22 +0,0 @@
-Copyright (c) 2010-2014 Jeremy Ashkenas, DocumentCloud
-
-Permission is hereby granted, free of charge, to any person
-obtaining a copy of this software and associated documentation
-files (the "Software"), to deal in the Software without
-restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following
-conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/README.md b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/README.md
deleted file mode 100644
index 7b0482df7..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/README.md
+++ /dev/null
@@ -1,29 +0,0 @@
- ____ __ __
- /\ _`\ /\ \ /\ \ __
- \ \ \ \ \ __ ___\ \ \/'\\ \ \____ ___ ___ __ /\_\ ____
- \ \ _ <' /'__`\ /'___\ \ , < \ \ '__`\ / __`\ /' _ `\ /'__`\ \/\ \ /',__\
- \ \ \ \ \/\ \ \.\_/\ \__/\ \ \\`\\ \ \ \ \/\ \ \ \/\ \/\ \/\ __/ __ \ \ \/\__, `\
- \ \____/\ \__/.\_\ \____\\ \_\ \_\ \_,__/\ \____/\ \_\ \_\ \____\/\_\_\ \ \/\____/
- \/___/ \/__/\/_/\/____/ \/_/\/_/\/___/ \/___/ \/_/\/_/\/____/\/_/\ \_\ \/___/
- \ \____/
- \/___/
- (_'_______________________________________________________________________________'_)
- (_.———————————————————————————————————————————————————————————————————————————————._)
-
-
-Backbone supplies structure to JavaScript-heavy applications by providing models 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 application over a RESTful JSON interface.
-
-For Docs, License, Tests, pre-packed downloads, and everything else, really, see:
-http://backbonejs.org
-
-To suggest a feature, report a bug, or general discussion:
-http://github.com/jashkenas/backbone/issues
-
-Backbone is an open-sourced component of DocumentCloud:
-https://github.com/documentcloud
-
-Many thanks to our contributors:
-http://github.com/jashkenas/backbone/contributors
-
-Special thanks to Robert Kieffer for the original philosophy behind Backbone.
-http://github.com/broofa
diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/backbone-min.js b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/backbone-min.js
deleted file mode 100644
index 8ea4b13d3..000000000
--- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/backbone-min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-(function(t,e){if(typeof define==="function"&&define.amd){define(["underscore","jquery","exports"],function(i,r,s){t.Backbone=e(t,s,i,r)})}else if(typeof exports!=="undefined"){var i=require("underscore");e(t,exports,i)}else{t.Backbone=e(t,{},t._,t.jQuery||t.Zepto||t.ender||t.$)}})(this,function(t,e,i,r){var s=t.Backbone;var n=[];var a=n.push;var o=n.slice;var h=n.splice;e.VERSION="1.1.2";e.$=r;e.noConflict=function(){t.Backbone=s;return this};e.emulateHTTP=false;e.emulateJSON=false;var u=e.Events={on:function(t,e,i){if(!c(this,"on",t,[e,i])||!e)return this;this._events||(this._events={});var r=this._events[t]||(this._events[t]=[]);r.push({callback:e,context:i,ctx:i||this});return this},once:function(t,e,r){if(!c(this,"once",t,[e,r])||!e)return this;var s=this;var n=i.once(function(){s.off(t,n);e.apply(this,arguments)});n._callback=e;return this.on(t,n,r)},off:function(t,e,r){var s,n,a,o,h,u,l,f;if(!this._events||!c(this,"off",t,[e,r]))return this;if(!t&&!e&&!r){this._events=void 0;return this}o=t?[t]:i.keys(this._events);for(h=0,u=o.length;h").attr(t);this.setElement(r,false)}else{this.setElement(i.result(this,"el"),false)}}});e.sync=function(t,r,s){var n=T[t];i.defaults(s||(s={}),{emulateHTTP:e.emulateHTTP,emulateJSON:e.emulateJSON});var a={type:n,dataType:"json"};if(!s.url){a.url=i.result(r,"url")||M()}if(s.data==null&&r&&(t==="create"||t==="update"||t==="patch")){a.contentType="application/json";a.data=JSON.stringify(s.attrs||r.toJSON(s))}if(s.emulateJSON){a.contentType="application/x-www-form-urlencoded";a.data=a.data?{model:a.data}:{}}if(s.emulateHTTP&&(n==="PUT"||n==="DELETE"||n==="PATCH")){a.type="POST";if(s.emulateJSON)a.data._method=n;var o=s.beforeSend;s.beforeSend=function(t){t.setRequestHeader("X-HTTP-Method-Override",n);if(o)return o.apply(this,arguments)}}if(a.type!=="GET"&&!s.emulateJSON){a.processData=false}if(a.type==="PATCH"&&k){a.xhr=function(){return new ActiveXObject("Microsoft.XMLHTTP")}}var h=s.xhr=e.ajax(i.extend(a,s));r.trigger("request",r,h,s);return h};var k=typeof window!=="undefined"&&!!window.ActiveXObject&&!(window.XMLHttpRequest&&(new XMLHttpRequest).dispatchEvent);var T={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};e.ajax=function(){return e.$.ajax.apply(e.$,arguments)};var $=e.Router=function(t){t||(t={});if(t.routes)this.routes=t.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var S=/\((.*?)\)/g;var H=/(\(\?)?:\w+/g;var A=/\*\w+/g;var I=/[\-{}\[\]+?.,\\\^$|#\s]/g;i.extend($.prototype,u,{initialize:function(){},route:function(t,r,s){if(!i.isRegExp(t))t=this._routeToRegExp(t);if(i.isFunction(r)){s=r;r=""}if(!s)s=this[r];var n=this;e.history.route(t,function(i){var a=n._extractParameters(t,i);n.execute(s,a);n.trigger.apply(n,["route:"+r].concat(a));n.trigger("route",r,a);e.history.trigger("route",n,r,a)});return this},execute:function(t,e){if(t)t.apply(this,e)},navigate:function(t,i){e.history.navigate(t,i);return this},_bindRoutes:function(){if(!this.routes)return;this.routes=i.result(this,"routes");var t,e=i.keys(this.routes);while((t=e.pop())!=null){this.route(t,this.routes[t])}},_routeToRegExp:function(t){t=t.replace(I,"\\$&").replace(S,"(?:$1)?").replace(H,function(t,e){return e?t:"([^/?]+)"}).replace(A,"([^?]*?)");return new RegExp("^"+t+"(?:\\?([\\s\\S]*))?$")},_extractParameters:function(t,e){var r=t.exec(e).slice(1);return i.map(r,function(t,e){if(e===r.length-1)return t||null;return t?decodeURIComponent(t):null})}});var N=e.History=function(){this.handlers=[];i.bindAll(this,"checkUrl");if(typeof window!=="undefined"){this.location=window.location;this.history=window.history}};var R=/^[#\/]|\s+$/g;var O=/^\/+|\/+$/g;var P=/msie [\w.]+/;var C=/\/$/;var j=/#.*$/;N.started=false;i.extend(N.prototype,u,{interval:50,atRoot:function(){return this.location.pathname.replace(/[^\/]$/,"$&/")===this.root},getHash:function(t){var e=(t||this).location.href.match(/#(.*)$/);return e?e[1]:""},getFragment:function(t,e){if(t==null){if(this._hasPushState||!this._wantsHashChange||e){t=decodeURI(this.location.pathname+this.location.search);var i=this.root.replace(C,"");if(!t.indexOf(i))t=t.slice(i.length)}else{t=this.getHash()}}return t.replace(R,"")},start:function(t){if(N.started)throw new Error("Backbone.history has already been started");N.started=true;this.options=i.extend({root:"/"},this.options,t);this.root=this.options.root;this._wantsHashChange=this.options.hashChange!==false;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.options.pushState&&this.history&&this.history.pushState);var r=this.getFragment();var s=document.documentMode;var n=P.exec(navigator.userAgent.toLowerCase())&&(!s||s<=7);this.root=("/"+this.root+"/").replace(O,"/");if(n&&this._wantsHashChange){var a=e.$('