├── .babelrc
├── .gitignore
├── .sass-lint.yml
├── .travis.yml
├── CNAME
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── ISSUE_TEMPLATE.md
├── LICENSE
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── SUMMARY.md
├── book.json
├── browserconfig.xml
├── deploy_key.enc
├── docs
├── COMMON.md
├── COMPLETING.md
├── COMPONENTS.md
├── CONTAINERS.md
├── EXTRAS.md
├── REDUCERS.md
├── REDUX.md
├── STRUCTURE.md
├── STYLES.md
├── TESTING.md
├── VIEWS.md
└── WEBPACK.md
├── index.html
├── manifest.json
├── package.json
├── src
├── common
│ └── Todo.ts
├── components
│ ├── Button.tsx
│ ├── Loader.tsx
│ ├── PageNotFound.tsx
│ ├── TodoComponent.tsx
│ └── __specs__
│ │ ├── Button.spec.tsx
│ │ ├── Loader.spec.tsx
│ │ ├── PageNotFound.spec.tsx
│ │ ├── TodoComponent.spec.tsx
│ │ └── __snapshots__
│ │ ├── Button.spec.tsx.snap
│ │ ├── Loader.spec.tsx.snap
│ │ ├── PageNotFound.spec.tsx.snap
│ │ └── TodoComponent.spec.tsx.snap
├── icons
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── mstile-150x150.png
│ └── safari-pinned-tab.svg
├── index.tsx
├── jest
│ └── setup.js
├── modules
│ ├── AppContainer.ts
│ ├── AppView.tsx
│ ├── __specs__
│ │ ├── AppView.spec.tsx
│ │ └── __snapshots__
│ │ │ └── AppView.spec.tsx.snap
│ └── index
│ │ ├── IndexContainer.ts
│ │ ├── IndexReducer.ts
│ │ ├── IndexView.tsx
│ │ └── __specs__
│ │ ├── IndexReducer.spec.ts
│ │ ├── IndexView.spec.tsx
│ │ └── __snapshots__
│ │ └── IndexView.spec.tsx.snap
├── redux
│ ├── guards.ts
│ ├── reducer.ts
│ └── store.ts
├── server.tsx
└── styles
│ ├── button.scss
│ ├── index.scss
│ ├── loader.scss
│ ├── pagenotfound.scss
│ ├── styles.scss
│ ├── todocomponent.scss
│ └── variables.scss
├── tsconfig.json
├── tslint.json
├── webpack.config.js
├── webpack.dev.js
├── webpack.prod.js
├── webpack.server.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["babel-preset-env"],
3 | "plugins": ["react-hot-loader/babel"]
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | node_modules/
3 | dist/
4 | **/.DS_store
5 | **/.vs
6 | **/.vscode
7 | !**/.vscode/tasks.json
8 | yarn-error.log
9 | _book/
10 | deploy_key
11 |
--------------------------------------------------------------------------------
/.sass-lint.yml:
--------------------------------------------------------------------------------
1 | options:
2 | merge-default-rules: false
3 | rules:
4 | bem-depth:
5 | - 4
6 | - max-depth: 4
7 | class-name-format:
8 | - allow-leading-underscore: false
9 | - convention: hyphenatedbem
10 | declarations-before-nesting: true
11 | extends-before-declarations: true
12 | hex-notation:
13 | - style: uppercase
14 | indentation:
15 | - size: 4
16 | nesting-depth:
17 | - max-depth: 4
18 | hex-length:
19 | - style: long
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - node
4 | cache: yarn
5 | before_script:
6 | - yarn global add greenkeeper-lockfile
7 | - greenkeeper-lockfile-update
8 | script: yarn run test:ci
9 | after_script:
10 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
11 | - greenkeeper-lockfile-upload
12 |
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | ts-react-boilerplate.js.org
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at laurilavanti@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribution guidelines
2 |
3 | All contributions are more than welcome!
4 |
5 | 1. Check the [issues](https://github.com/Lapanti/ts-react-boilerplate/issues) in case someone has already thought about the cool thing you want to contribute
6 | - If not, create an issue about it (*we will try to answer to your issue asap*)
7 | 2. Start working on a proposed solution by following the [development](https://github.com/Lapanti/ts-react-boilerplate#development) guide.
8 | 3. Wait for our comments and/or review
9 | 4. Thank you for contributing!
10 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:4-onbuild
2 |
3 | #TODO fill email below
4 | LABEL maintainer "your.email.here@domain.com"
5 |
6 | COPY dist/ /
7 |
8 | ENV NODE_ENV=production
9 |
10 | EXPOSE 8080
11 |
12 | CMD node /server.js
13 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | *Make sure the title is descriptive*
2 |
3 | This is a feature request/bug report/something else
4 |
5 | Add a short description
6 |
7 | I will create a PR with the solution / I need help to create a PR / I don't have the time/skills to create a PR
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Lauri Lavanti
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | *All PRs should be based on an issue*
2 | **WHY?**
3 | See issue # / A short description
4 | **WHAT?**
5 | A short description of the changes
6 |
7 | - [ ] I have considered (and possibly added) tests
8 | - [ ] I have updated the documentation (gitbook)
9 |
10 | [Optional] Resolves #
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # A very opinionated frontend boilerplate
2 |
3 | [](https://greenkeeper.io/)
4 | [](https://travis-ci.org/Lapanti/ts-react-boilerplate) [](https://opensource.org/licenses/MIT) [](https://david-dm.org/lapanti/ts-react-boilerplate) [](https://david-dm.org/lapanti/ts-react-boilerplate?type=dev) [](https://coveralls.io/github/Lapanti/ts-react-boilerplate?branch=master) [](https://codeclimate.com/github/Lapanti/ts-react-boilerplate/issues)
5 |
6 | ## Purpose
7 |
8 | This is all you need to get started in developing your own web application, using TypeScript, React, server-side rendering and all the other hip tools. If you know what you are doing, you can follow the [quick start guide](#quickstart) or you can go learn with the walk-through starting [here](/docs/STRUCTURE.md).
9 |
10 | ## Contents
11 | - [Quick start guide](#quickstart)
12 | - [Requirements](#requirements)
13 | - [Download the source code](#download)
14 | - [Starting development](#startingdevelopment)
15 | - [Tips and suggestions](#tipsandsuggestions)
16 | - [How to Docker](#dockerization)
17 | - [Dependencies](#dependencies)
18 | - [Contributing](#contributing)
19 | - [Development](#development)
20 | - [Testing](#testing)
21 | - [Roadmap](#roadmap)
22 | - [License and contact information](#license)
23 |
24 | ## Quick start guide
25 |
26 | ### Requirements
27 | - If you don't already have it, install [Node](https://nodejs.org/en/download/)
28 | - If you don't already have it, install [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
29 | - Install [Yarn](https://yarnpkg.com/lang/en/docs/install/)
30 |
31 | ### Download the source code
32 | 1. Open up your favorite kind of console
33 | 2. Navigate to the folder in which you want to store the source code
34 | 3. Run `git clone git@github.com:Lapanti/ts-react-boilerplate.git`
35 |
36 | ### Starting development
37 | 1. Open up the source code in your favorite TypeScript-capable editor (I recommend [Visual Studio Code](https://code.visualstudio.com/) if you don't have a preference)
38 | 2. Run `yarn` in the console to install dependencies (it'll take a while on the first run, so go on and read ahead while you wait)
39 | 3. Read through the comments in all the source files to get yourself acquinted with the ideas, concepts and patterns
40 | 4. Start the application by running `yarn develop` in your console (inside the folder you downloaded the code to) and open up your browser in the address it prints out
41 | 5. Create a deployable version of the application by running `yarn build`
42 | 6. Start the deployable version by running `yarn start` or read the [How to Docker](#dockerization) guide to Dockerize your application
43 | 7. To test your application, run `yarn test`
44 | 8. Start modifying the code to build your own application
45 |
46 | ## Tips and suggestions
47 | - Make sure everything has a type (the more you squeeze out of the compiler the easier you're going to have it while developing)
48 | - Follow [BEM](http://getbem.com/naming/)-naming with CSS
49 | - Follow [Redux-ducks pattern](https://github.com/erikras/ducks-modular-redux) except that name the reducers as according to the file (see [IndexReducer.tsx](/src/modules/index/IndexReducer.tsx) for an example)
50 |
51 | ## How to Docker
52 | The [Dockerfile](/Dockerfile) is where you can find the configuration to build a [Docker](https://www.docker.com/) image out of your application. The first line of the `Dockerfile` (starting with `FROM`) includes the base for your Dockerfile, feel free to change it if you want to.
53 | 1. Put your email to the [fourth line in the Dockerfile](/Dockerfile#L4)
54 | 2. In your console run `docker build .`
55 | 3. In your console run `docker run -d -p 8080:8080 bd9b1d6725bc` **but** replace `bd9b1d6725bc` with the image ID you received from the previous command
56 | 4. Host your Docker image in your favorite cloud or local server (the web is filled with guides for this)
57 |
58 | ## Dependencies
59 | The following are all the dependencies of the project, with the reasoning behind their inclusion:
60 | - :package: [Yarn](https://yarnpkg.com/lang/en/) for package management
61 | - :muscle: [TypeScript](https://www.typescriptlang.org/) for types
62 | - :computer: [Express](https://expressjs.com/) for server-side rendering
63 | - :eyes: [React](https://facebook.github.io/react/) to build the UI
64 | - :calling: [ReactDOM](https://facebook.github.io/react/docs/react-dom.html) to render the UI
65 | - :tada: [React-Redux](https://github.com/reactjs/react-redux) to bind Redux to React
66 | - :milky_way: [React-Router](https://github.com/ReactTraining/react-router) for routes on the client
67 | - :gift: [Redux](https://github.com/reactjs/redux) to handle state
68 | - :loop: [redux-observable](https://redux-observable.js.org/) to allow side-effects in Redux
69 | - :mag: [RxJs](https://github.com/ReactiveX/RxJS) for streams
70 | - :electric_plug: [webpack](https://webpack.js.org/) to bundle JS files
71 | - :flashlight: [webpack-dev-server](https://webpack.js.org/configuration/dev-server/#src/components/Sidebar/Sidebar.jsx) to host client while developing
72 | - :punch: [awesome-typescript-loader](https://github.com/s-panferov/awesome-typescript-loader) to compile TypeScript in the webpack pipe
73 | - :wave: [babel](https://babeljs.io) to transpile our compiled JavaScript to ES5 using [babel-loader](https://webpack.js.org/loaders/babel-loader/#src/components/Sidebar/Sidebar.jsx)
74 | - :tongue: [sass-loader](https://webpack.js.org/loaders/sass-loader/#src/components/Sidebar/Sidebar.jsx) to compile SASS into CSS
75 | - :pray: [Jest](https://facebook.github.io/jest/) for testing
76 | - :metal: [ts-jest](https://github.com/kulshekhar/ts-jest) to run Jest with TypeScript
77 | - :ok_hand: [TSlint](https://palantir.github.io/tslint/) for linting
78 | - :runner: [nock](https://github.com/node-nock/nock) to mock API calls
79 | - :question: [sass-lint](https://github.com/sasstools/sass-lint) to lint SASS
80 | - :bust_in_silhouette: [Enzyme](https://github.com/airbnb/enzyme) for snapshot and behavior testing
81 | - :cyclone: [Enzyme-to-JSON](https://github.com/adriantoine/enzyme-to-json) to enable Enzyme snapshots with Jest
82 | - :foggy: [enzyme-adapter-react-16](https://github.com/airbnb/enzyme/tree/master/packages/enzyme-adapter-react-16) to use Enzyme with React 16
83 | - :nail_care: [SASS](https://github.com/sass/node-sass) for styles
84 | - :two_hearts: [concurrently](https://github.com/kimmobrunfeldt/concurrently) to run multiple script concurrently
85 |
86 | ## Contributing
87 | Read the [contribution guidelines](./CONTRIBUTING.md)
88 |
89 | ### Development
90 | 1. Clone this repo (or fork and clone)
91 | 2. Navigate to the directory in console
92 | 3. Run `yarn` in console
93 | - [Optional] Install livereload extension to your browser in [Chrome](https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei?hl=en) or [Firefox](https://addons.mozilla.org/en-gb/firefox/addon/livereload/)
94 | 4. Run `yarn develop` in console
95 | 5. Open your browser in the address printed to the console
96 | 6. Modify the code with your favorite editor
97 |
98 | ### Testing
99 | - You can run all the tests with `yarn test`
100 | - *psst, you can update your snapshots with* `yarn test -- -u`
101 | - You can run Jest tests in watch mode with `yarn test:watch`
102 | - You can run all tests with coverage with `yarn test:ci`
103 |
104 | ### Roadmap
105 |
106 | - [x] TypeScript
107 | - [x] React
108 | - [x] Redux
109 | - [x] Server-side rendering
110 | - [x] Browserify
111 | - [x] SASS support
112 | - [x] Add a test framework
113 | - [x] Dockerize
114 | - [ ] Deployment scripts to AWS
115 | - [ ] `create-ts-react-boilerplate` scripts
116 |
117 | ## License and contact information
118 | You can contact me through here in Github or on [Twitter](https://twitter.com/laurilavanti)
119 |
120 | All of the code is licensed under the [MIT license](LICENSE)
121 |
--------------------------------------------------------------------------------
/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 |
3 | * [README](/README.md)
4 |
5 | ### Walk-through
6 |
7 | * [Application structure](/docs/STRUCTURE.md)
8 | * [Common](/docs/COMMON.md)
9 | * [Components](/docs/COMPONENTS.md)
10 | * [Redux](/docs/REDUX.md)
11 | * [Views](/docs/VIEWS.md)
12 | * [Reducers](/docs/REDUCERS.md)
13 | * [Containers](/docs/CONTAINERS.md)
14 | * [Completing React](/docs/COMPLETING.md)
15 | * [webpack](/docs/WEBPACK.md)
16 | * [Styles](/docs/STYLES.md)
17 | * [Testing](/docs/TESTING.md)
18 | * [Extras](/docs/EXTRAS.md)
19 |
--------------------------------------------------------------------------------
/book.json:
--------------------------------------------------------------------------------
1 | {
2 | "gitbook": "^3.1.1",
3 | "title": "TS-React-Boilerplate",
4 | "plugins": ["edit-link", "prism", "anker-enable", "github", "advanced-emoji", "-sharing"],
5 | "pluginsConfig": {
6 | "theme-default": {
7 | "showLevel": false
8 | },
9 | "edit-link": {
10 | "base": "https://github.com/Lapanti/ts-react-boilerplate/tree/master",
11 | "label": "Edit this page"
12 | },
13 | "github": {
14 | "url": "https://github.com/Lapanti/ts-react-boilerplate"
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #FF8041
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/deploy_key.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lapanti/ts-react-boilerplate/84f8192884d7407ced6e0261361736b664975b34/deploy_key.enc
--------------------------------------------------------------------------------
/docs/COMMON.md:
--------------------------------------------------------------------------------
1 | # Common
2 |
3 | We will begin with the simplest piece, common (or shared) code. This will also serve as a good introduction to [TypeScript](https://www.typescriptlang.org/).
4 |
5 | ### Initialize
6 |
7 | We will begin by creating the structure and installing the necessary dependencies (make sure you have [Yarn](https://yarnpkg.com/lang/en/) installed). Open up your console in the directory you want to build your application in and run the following commands:
8 |
9 | 1. Initialize the Yarn-project and answer the prompts according to your application:
10 | ```
11 | yarn init
12 | ```
13 | 2. Add the necessary dependencies, [TypeScript](https://www.typescriptlang.org/) and [TSLint](https://palantir.github.io/tslint/) (for code quality checks, a.k.a. linting). When you add dependencies in **Yarn** they will be saved to your `package.json` and **Yarn** will create a `yarn.lock` file to manage the versions of those dependencies. The flag `-D` saves them as `devDependencies` which will not be utilized when running the system in production.
14 | ```
15 | yarn add -D typescript tslint
16 | ```
17 | 3. Open your project in an editor like [Visual Studio Code](https://code.visualstudio.com/) or [Atom](https://atom.io/), though I recommend **Visual Studio Code** as it has [IntelliSense](https://en.wikipedia.org/wiki/Intelligent_code_completion).
18 | 4. Create the file `Todo.ts` inside a folder called `common` which will in turn be inside `src` that is located at the root of your application.
19 |
20 | ### Configuring TypeScript
21 |
22 | Next we will configure **TypeScript** by creating a file in the root folder called [tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). `tsconfig.json` indicates to **TypeScript** that the folder is a **TypeScript** project. Start by writing the following content into your `tsconfig.json`:
23 | ```json
24 | {
25 | "compilerOptions": {
26 | "noImplicitAny": true,
27 | "target": "es5",
28 | "jsx": "react",
29 | "moduleResolution": "node",
30 | "lib": ["dom", "es2016", "es2017"],
31 | "sourceMap": true
32 | },
33 | "include": [
34 | "src/**/*.ts",
35 | "src/**/*.tsx"
36 | ]
37 | }
38 |
39 | ```
40 |
41 | On the first row
42 | ```json
43 | "compilerOptions": {
44 | ```
45 | we start by defining the [compilerOptions](https://www.typescriptlang.org/docs/handbook/compiler-options.html).
46 |
47 | The first rule we set
48 | ```json
49 | "noImplicitAny": true,
50 | ```
51 | is `noImplicitAny` which will enforce the use of type declarations (more about those later) when the type would otherwise be inferred as `any`.
52 |
53 | The second rule
54 | ```json
55 | "target": "es5",
56 | ```
57 | defines the target ECMAScript version (in this case [ES5](https://kangax.github.io/compat-table/es5/)) for the compiled JavaScript files.
58 |
59 | In the third line
60 | ```json
61 | "moduleResolution": "node"
62 | ```
63 | we set the [moduleResolution](https://www.typescriptlang.org/docs/handbook/module-resolution.html) mode for the **TypeScript** compiler as `node`, which allows the importing of dependencies from the folder `node_modules` (*where Yarn saves them by default*) by using non-relative imports.
64 |
65 | On the fourth line
66 | ```json
67 | "lib": ["dom", "es2016", "es2017"]
68 | ```
69 | we add support for DOM APIs like [`Window`](https://developer.mozilla.org/en/docs/Web/API/Window) and the newest features from [es2016](http://2ality.com/2016/01/ecmascript-2016.html) and [es2017](http://2ality.com/2016/02/ecmascript-2017.html).
70 |
71 | On the fifth line
72 | ```json
73 | "sourceMap": true
74 | ```
75 | we tell the compiler to generate [source maps](https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Use_a_source_map).
76 |
77 | ---
78 |
79 | On the seventh line
80 | ```json
81 | "include": [
82 | ```
83 | we set the files **TypeScript** will compile.
84 |
85 | In this case we use a [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to indicate the **TypeScript** files on the eight and ninth lines.
86 | ```json
87 | "src/**/*.ts",
88 | "src/**/*.tsx"
89 | ```
90 | These globs will match all files in the `src` folder (recursively) with the extension `.ts` or `.tsx`.
91 |
92 | ### Start coding
93 |
94 | We will begin by creating a class that defines our Todo-items. A Todo should have an id (to differentiate between todos), a title (tha actual todo) and information on whether the todo has been completed or not. Here we see the benefit of using **TypeScript**, as it allows us to define the actual information contained in a Todo (in plain JavaScript and all other dynamically typed languages you as a developer have to keep track of such things). I'll first show you the class in its simplest form and then explain what each keyword means.
95 |
96 | ```typescript
97 | export default class Todo {
98 | readonly id: number;
99 | readonly title: string;
100 | readonly done: boolean;
101 | }
102 | ```
103 |
104 | ---
105 |
106 | On the first line
107 | ```typescript
108 | export default class Todo {
109 | ```
110 | we first define that this `class` is the default export (`export default`) of our module, which means that in other files we can write
111 | ```typescript
112 | import Todo from '../path/to/Todo.ts'
113 | ```
114 | instead of (just writing `export`)
115 | ```typescript
116 | import { Todo } from '../path/to/Todo.ts'
117 | ```
118 | to get a hold of our new `Todo`-class.
119 | > Remember that a module can only have one default export.
120 |
121 | After the export-clause we define the `class Todo`. In **TypeScript** a [class](https://www.typescriptlang.org/docs/handbook/classes.html) is something you can instantiate (create an instance of), and that can inherit other classes (have their properties as well).
122 |
123 | ---
124 |
125 | On the second line
126 | ```typescript
127 | readonly id: number;
128 | ```
129 | we define our first property for the `class Todo`. The first word [readonly](https://basarat.gitbooks.io/typescript/docs/types/readonly.html) defines the property as something that is [immutable](https://en.wikipedia.org/wiki/Immutable_object) and from now on **TypeScript** a `class` will give you an error if you try to change the value of a `Todo`'s `number`-field. The second word `id` is the name of the property (so later on we can access it by calling `myInstanceOfTodo.id`). The last word here is the [type](https://www.typescriptlang.org/docs/handbook/basic-types.html) of the property, meaning what kind of information the property can hold, in this case a `number` (this particular type doesn't care if it is a whole number or a floating one).
130 |
131 | ---
132 |
133 | The two following lines
134 | ```typescript
135 | readonly title: string;
136 | readonly done: boolean;
137 | ```
138 | are otherwise the same as the first one, except the property `title` has a **type** of `string`, meaning it's an arbitrary sequence of letters and characters and the property `done` has a **type** of `boolean`, meaning that its value is either `true` or `false`.
139 |
140 | ---
141 |
142 | Congratulations, you have now created your very first **TypeScript** `class`!
143 |
144 | ### Linting
145 |
146 | It's time to start linting your code by using [TSLint](https://palantir.github.io/tslint/). Let's begin by creating a [Yarn script](https://yarnpkg.com/lang/en/docs/cli/run/) to run **TSLint**:
147 | ```json
148 | // In package.json
149 | "scripts": {
150 | "lint:ts": "tslint --type-check --project tsconfig.json"
151 | }
152 | ```
153 | The lint command can now be run with `yarn run lint:ts`. This will now run **TSLint** with its default settings (*using tsconfig.json's settings*). However, that might not always be enough for you and if you want to define the rules for your own codebase more accurately, you can create a `tslint.json` in the root folder and populate it with rules according to [TSLint rules](https://palantir.github.io/tslint/rules/). For example in the boilerplate the `tslint.json` looks like that:
154 | ```json
155 | {
156 | "rules": {
157 | "no-any": false, // Allows use of any as a type declaration
158 | "no-magic-numbers": true, // Disallow magic numbers
159 | "only-arrow-functions": [true], // Enforces the use of arrow functions instead of the traditional syntax
160 | "curly": true, // Enforces curly braces in all ifs/fors/dos/whiles
161 | "no-var-keyword": true, // Enforces the use of the new keywords let and const instead of the old var
162 | "triple-equals": true, // Enforces the use of triple equals instead of double equals in conditionals
163 | "indent": ["spaces"], // Enforces indentation using spaces instead of tabs
164 | "prefer-const": true, // Enforces the use of const unless let is needed
165 | "semicolon": [true, "always"], // Enforces that all lines should end in a semicolon
166 | "eofline": true, // Enforces an empty line at the end of file
167 | "trailing-comma": [true, { "multiline": "always", "singleline": "never" }], // Enforces a comma at the end of all parameters that end in a new line
168 | "arrow-return-shorthand": [true], // Suggests one to use shorthand arrow functions when possible
169 | "class-name": true, // Enforces PascalCased class names
170 | "interface-name": [true, "always-prefix"], // Enforces all interfaces to follow PascalCasing and be prefixed with I
171 | "quotemark": [true, "single"], // Enforces the use of single quotation marks
172 | "no-unused-variable": [true, { "ignore-pattern": "^I"}], // Warns about unused variables, unless they start with a capital I (interfaces)
173 | "no-unused-expression": [true, "allow-fast-null-checks"] // Warns about unused expressions, unless it is a null check e.g. someVariable && someVariable.doSomething()
174 | }
175 | }
176 | ```
177 |
178 | ### Alternatives
179 |
180 | Below you can find alternatives to TypeScript, if you don't fancy it as much as I do:
181 | - [Flow](http://simplefocus.com/flowtype/)
182 | - [PureScript](http://www.purescript.org/)
183 |
--------------------------------------------------------------------------------
/docs/COMPLETING.md:
--------------------------------------------------------------------------------
1 | # Completing React
2 |
3 | Now we finally get to build our **React**-application into something that can be run and will actually do something! Here we make the assumption that you are going to build a medium- to large-sized application and show how to do these things in a more modular way, but for smaller applications some of these parts can be merged with the `IndexView` and some can be left out completely.
4 |
5 | ### Initialize
6 |
7 | We begin by installing new dependencies called [React Router](https://reacttraining.com/react-router/) and [ReactDOM](https://facebook.github.io/react/docs/react-dom.html)
8 | ```
9 | yarn add react-router-dom react-dom
10 | ```
11 | which adds the capability to define which URL equals which view and to render our **React** application to the [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction), respectively.
12 |
13 | And of course the types for them
14 | ```
15 | yarn add -D @types/react-router-dom @types/react-dom
16 | ```
17 |
18 | ### AppView
19 |
20 | We begin by writing an `AppView.ts` file into the `src/modules`-folder
21 | ```typescript
22 | import * as React from 'react';
23 | import { Route, Switch, RouteComponentProps } from 'react-router-dom';
24 | import IndexContainer from './index/IndexContainer';
25 | import PageNotFound from '../components/PageNotFound';
26 |
27 | export type IAppViewProps = RouteComponentProps;
28 |
29 | const AppView: React.StatelessComponent = () => (
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 |
38 | export default AppView;
39 | ```
40 | which is a fairly simple view, except for the ``-clause and `RouteComponentProps`.
41 | > All views that go through **React Router** get some extra properties, which are included in `RouteComponentProps`](https://reacttraining.com/react-router/web/api/Switch) element is used to render a single view out of all [`Route`s](https://reacttraining.com/react-router/web/api/Route) inside the ``. `Route`s have three main properties you should remember:
44 | 1. `path` which indicates what URL the view matches to (*it can be used to define parameters as well*)
45 | 2. `exact` which indicates that the view should only match if the URL matches `path` exactly
46 | 3. `component` which defines the actual view to render
47 |
48 | ### AppContainer
49 |
50 | Next we create a very simple **container** for `AppView` called `AppContainer`, which is situated in the same `src/modules`-folder
51 | ```typescript
52 | import { connect } from 'react-redux';
53 | import AppView, { IAppViewProps } from './AppView'; //tslint:disable-line:no-unused-variable
54 |
55 | export default connect<{}, undefined, IAppViewProps>(() => ({}))(AppView);
56 | ```
57 | which we use just to wrap `AppView` so that it can be used in routes.
58 |
59 | ### IndexView
60 |
61 | For `IndexView` we also need to add `RouteComponentProps`, so just add the following
62 | ```typescript
63 | import { RouteComponentProps } from 'react-router-dom';
64 | // ...
65 | export type IIndexProps = IIndexState & IIndexDispatch & RouteComponentProps;
66 | ```
67 |
68 | ### index
69 |
70 | Finally we create a file `index.ts` inside `src`
71 | ```typescript
72 | import * as React from 'react';
73 | import * as ReactDOM from 'react-dom';
74 | import { Provider } from 'react-redux';
75 | import { Route } from 'react-router-dom';
76 | import { ConnectedRouter } from 'react-router-redux';
77 | import { AppContainer as HotContainer } from 'react-hot-loader';
78 | import createHistory from 'history/createBrowserHistory';
79 | import configureStore from './redux/store';
80 | import AppContainer from './modules/AppContainer';
81 |
82 | const history = createHistory();
83 |
84 | const render = (container: React.ComponentClass) => ReactDOM.render(
85 |
86 |
87 |
88 |
89 |
90 |
91 | ,
92 | document.getElementById('app'),
93 | );
94 |
95 | render(AppContainer);
96 |
97 | if ((module as any).hot) {
98 | (module as any).hot.accept('./modules/AppContainer', () => render(AppContainer));
99 | }
100 |
101 | ```
102 | which is the entry file to our application that ties everything together.
103 |
104 | ---
105 |
106 | On the 14. line
107 | ```typescript
108 | import * as ReactDOM from 'react-dom';
109 |
110 | const render = (container: React.ComponentClass) => ReactDOM.render(
111 | // ...,
112 | document.getElementById('app'),
113 | ))
114 | ```
115 | we [render](https://facebook.github.io/react/docs/react-dom.html#render) our **React**-application to the **DOM** inside a div with the id `app` (*we'll come back to this*).
116 |
117 | ---
118 |
119 | On the 15. line
120 | ```typescript
121 | import { AppContainer as HotContainer } from 'react-hot-loader';
122 | // ...
123 |
124 | // ...
125 | ,
126 | ```
127 | we wrap our application into a container to enable **Hot Module Replacement** (more about it later in this section).
128 |
129 | ---
130 |
131 | On the 16. line
132 | ```typescript
133 | import * as React from 'react';
134 | import { Provider } from 'react-redux';
135 | import createHistory from 'history/createBrowserHistory';
136 | import configureStore from './redux/store';
137 | const history = createHistory();
138 | // ...
139 |
140 | // ...
141 |
142 | ```
143 | which wraps our **React** application with **Redux** using [`Provider`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#provider-store) from **react-redux**, which takes a single parameter `store`, for which we provide our store as we defined it in [Redux](/REDUX.md#store). `history/createBrowserHistory` is used to create a wrapper around the browser history we can use.
144 |
145 | ---
146 |
147 | On the 17. line
148 | ```typescript
149 | import * as React from 'react';
150 | import { Route } from 'react-router-dom';
151 | import { ConnectedRouter } from 'react-router-redux';
152 | import AppContainer from './modules/AppContainer';
153 | // ...
154 |
155 |
156 |
157 | // ...
158 | ```
159 | we keep the UI in sync with the URL using a [`ConnectedRouter`](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-redux) from **react-router-redux**, which takes as argument a [`history`](https://github.com/ReactTraining/react-router/blob/v3/docs/API.md#histories), where we give `history` we created previously. Here we define a single `Route` which renders `AppContainer` for all URL routes.
160 |
161 | ---
162 |
163 | Finally at the end
164 | ```typescript
165 | if ((module as any).hot) {
166 | (module as any).hot.accept('./modules/AppContainer', () => render(AppContainer));
167 | }
168 | ```
169 | we do a little configuration to allow our container to be loaded by the **Hot Module Replacement**-system.
170 |
171 | ### Index.html
172 |
173 | Finally we write an `index.html` in our root-folder
174 | ```html
175 |
176 |
177 |
178 |
179 | TS-React boilerplate
180 |
181 |
182 |
183 |
184 |
185 |
186 | ```
187 | which is just a very simple `HTML`-file, which imports our (*soon-to-be-bundled*) **JavaScript** from the current folder `/bundle.js` and contains a `div` with the id `app` so our `index.ts` works.
188 |
--------------------------------------------------------------------------------
/docs/COMPONENTS.md:
--------------------------------------------------------------------------------
1 | # Components
2 |
3 | Next we will start writing out our components. This will serve as a good introduction to [React](https://facebook.github.io/react/).
4 |
5 | ### Initialize
6 |
7 | 1. First we will add the needed dependencies for developing a **React** application:
8 | ```
9 | yarn add react
10 | ```
11 | 2. And the needed developer dependencies for **React** development (with [TypeScript](https://www.typescriptlang.org/)):
12 | ```
13 | yarn add -D @types/react tslint-react
14 | ```
15 | The package `@types/react` gives us type definitions for **React** and [tslint-react](https://github.com/palantir/tslint-react) allows us to use [TSLint](https://palantir.github.io/tslint/) to lint our **React** components.
16 |
17 | ### Configuring for React
18 |
19 | First we will configure **TypeScript** to work with [JSX](https://facebook.github.io/react/docs/jsx-in-depth.html) by adding the following line to our [tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html):
20 | ```json
21 | {
22 | "compilerOptions": {
23 | "jsx": "react",
24 | // Rest of the compiler options
25 | }
26 | // Rest of the config file
27 | }
28 | ```
29 | As we are not using another transformation step setting [jsx](https://www.typescriptlang.org/docs/handbook/jsx.html) to the value of `react` will emit the actual JavaScript after compilation.
30 |
31 | ---
32 |
33 | To use **tslint-react** we add the following lines to the file `tslint.json`:
34 | ```json
35 | {
36 | "extends": ["tslint-react"], // This will allow us to use tslint-react-specific rules
37 | "rules": {
38 | "jsx-no-lambda": false, // This disallows the definition of functions inside a React Component's render-function
39 | // The rest of the rules
40 | "quotemark": [true, "single", "jsx-double"] // We added "jsx-double" here to denote that in JSX one should use double quotation marks.
41 | }
42 | }
43 | ```
44 |
45 | ### Button
46 |
47 | We will start with the simplest component that utilizes all the tools we need for most components, a `Button`, so go ahead and create a file `Button.tsx` (*the extension defines the file as a TypeScript-file that has JSX in it*) in the folder `components`:
48 |
49 | ```typescript
50 | import * as React from 'react';
51 |
52 | interface IButtonProps {
53 | click(): void;
54 | readonly text: string;
55 | }
56 |
57 | const Button: React.StatelessComponent = ({ click, text }) => (
58 | click()} value={text} />
59 | );
60 |
61 | export default Button;
62 | ```
63 |
64 | In the first row
65 | ```typescript
66 | import * as React from 'react';
67 | ```
68 | we import all the functionalities provided by **React** under the name `React` (`* as React`), including the ability to write JSX (*those things that look like HTML*).
69 |
70 | ---
71 |
72 | On the third to sixth rows
73 | ```typescript
74 | interface IButtonProps {
75 | click(): void;
76 | readonly text: string;
77 | }
78 | ```
79 | we define an [interface](https://www.typescriptlang.org/docs/handbook/interfaces.html) to denote the [properties](https://facebook.github.io/react/docs/components-and-props.html) (*or props for short*) our **component** accepts (*and in this case needs*). `click()` defines a function, which will be known as `click` inside our `Button` and as we have only defined that it takes no arguments, we also tell the compiler that we don't care if it returns anything, just that it can be called, using the return type of `void`. `readonly text: string` on the other hand defines a [readonly](https://basarat.gitbooks.io/typescript/docs/types/readonly.html) (*an immutable property*) called `text` inside our `Button`, that is of the type [string](https://www.typescriptlang.org/docs/handbook/basic-types.html).
80 | > **Interfaces** are shapes we define, meaning that we do not care what the actual implementation is, just that it has those types of values with those names. They cannot be instantiated as such and thus cannot contain default values like [classes](https://www.typescriptlang.org/docs/handbook/classes.html).
81 |
82 | ---
83 |
84 | On the eighth to tenth rows
85 | ```typescript
86 | const Button: React.StatelessComponent = ({ click, text }) => (
87 | click()} value={text} />
88 | );
89 | ```
90 | we define a [constant](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const) (an immutable variable) called `Button` which is of the type [React.StatelessComponent](https://hackernoon.com/react-stateless-functional-components-nine-wins-you-might-have-overlooked-997b0d933dbc), meaning it is a [React component](https://facebook.github.io/react/docs/react-component.html), which does not have an internal [state](https://facebook.github.io/react-native/docs/state.html), but only **props** of type `IButtonProps`. [Stateless components](https://hackernoon.com/react-stateless-functional-components-nine-wins-you-might-have-overlooked-997b0d933dbc) in **React** only need to define their [render](https://facebook.github.io/react/docs/react-api.html)-method and using an [ES6 arrow function](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions) and a [destructuring assignment](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) we can return JSX with our **props** already defined as simple variables (*in this case "click" and "text"*). The actual return here is a simple HTML [input](https://facebook.github.io/react/docs/forms.html) element with a class of `btn` (*in React you define classes with the property "className"*), a type of `submit`, an [`onClick`-handler](https://facebook.github.io/react/docs/handling-events.html) that will simply call the `click`-property and a value of `text` (*in submit buttons the value is the text in the button*).
91 | > You need to surround JSX with parentheses.
92 |
93 | ---
94 |
95 | And finally on the twelfth row
96 | ```typescript
97 | export default Button;
98 | ```
99 | we export our `Button` as the default export of the module.
100 |
101 | ### TodoComponent
102 |
103 | Next we will define a component to visualize our `Todo`-class from the previous section, creating a file called `TodoComponent.tsx` in the `components`-folder:
104 | ```typescript
105 | import * as React from 'react';
106 | import Todo from '../common/Todo';
107 |
108 | export interface ITodoComponent {
109 | readonly todo: Todo;
110 | setDone(i: number): void;
111 | }
112 |
113 | const TodoComponent: React.StatelessComponent = ({ todo, setDone }) => (
114 |
124 | );
125 |
126 | export default TodoComponent;
127 | ```
128 | which is mostly very similar to our `Button`. The biggest differences here are the second import
129 | ```typescript
130 | import Todo from '../common/Todo';
131 | ```
132 | which imports our `Todo`-class using a relative path (*the TypeScript compiler will look for the file relative to the current file*) and the second property in our `interface`
133 | ```typescript
134 | setDone(i: number): void;
135 | ```
136 | which defines a function called `setDone` that takes one argument, which is a `number`.
137 |
138 | ### Loader
139 |
140 | Next we implement our third and simplest component, called a `Loader`, in a file called `Loader.tsx` in the `components`-directory
141 | ```typescript
142 | import * as React from 'react';
143 |
144 | const Loader: React.StatelessComponent = () => (
145 |
146 |
147 |
148 | );
149 |
150 | export default Loader;
151 | ```
152 | which uses all previously introduced tools, except this time we have defined the **props** it receives as [undefined](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html), meaning that it does not take any **props** at all.
153 |
154 | ### PageNotFound
155 |
156 | Lastly we create a page to be shown whenever user enters a URL that is not found (*a 404*), called `PageNotFound` in the `components`-directory
157 | ```typescript
158 | import * as React from 'react';
159 |
160 | const PageNotFound: React.StatelessComponent = () => (
161 |
162 | 404 - page not found
163 |
164 | );
165 |
166 | export default PageNotFound;
167 | ```
168 | which is again a very simple component, very similar to `Loader`.
169 |
170 | ### Alternatives
171 |
172 | Below you can find alternatives to **React**, although I would suggest **React** unless you have specific needs, which other frameworks solve better, as it also allows for [mobile development with React Native](https://facebook.github.io/react-native/).
173 | - [AngularJS](https://angularjs.org/), possibly the most popular framework after [React](https://facebook.github.io/react/)
174 | - [elm](http://elm-lang.org/), a functional alternative
175 | - [Cycle.js](https://cycle.js.org/), a functional reactive alternative
176 |
--------------------------------------------------------------------------------
/docs/CONTAINERS.md:
--------------------------------------------------------------------------------
1 | # Containers
2 |
3 | Next up we want to bind our [Views](/VIEWS.md) to our [Reducers](/REDUCERS.md), a.k.a. define how we get the **props** from our [state](http://redux.js.org/docs/basics/Reducers.html).
4 |
5 | ### Initialize
6 |
7 | First up we need to add a new dependency, [react-redux](https://github.com/reactjs/react-redux) to connect our **React** views to our **state**
8 | ```
9 | yarn add react-redux
10 | ```
11 | and the type definitions for it
12 | ```
13 | yarn add -D @types/react-redux
14 | ```
15 |
16 | ### IndexContainer
17 |
18 | We begin by creating a file called `IndexContainer.ts` inside our `index`-folder inside `src/modules`
19 | > All containers will have the same "prefix" as their accompanied **views**, a.k.a. `[Pagename]Container.ts`
20 |
21 | ```typescript
22 | import { connect } from 'react-redux';
23 | import { State } from '../../redux/reducer';
24 | import { setTitle, saveTodo, setDone } from './IndexReducer';
25 | import IndexView, { IIndexState, IIndexDispatch, IIndexProps } from './IndexView';
26 |
27 | const stateToProps = (state: State): IIndexState => ({
28 | title: state.index.title,
29 | todos: state.index.todos,
30 | loading: state.index.loading,
31 | });
32 |
33 | export default connect(stateToProps, {
34 | setTitle,
35 | saveTodo,
36 | setDone,
37 | })(IndexView);
38 | ```
39 |
40 | ---
41 |
42 | First we define a function to transform our [`State`](/REDUX.md#reducer) into the `IIndexState` required by our `IndexView`
43 | ```typescript
44 | import { State } from '../../redux/reducer';
45 | import { IIndexState } from './IndexView';
46 |
47 | const stateToProps = (state: State): IIndexState => ({
48 | title: state.index.title,
49 | todos: state.index.todos,
50 | loading: state.index.loading,
51 | });
52 | ```
53 | where we get the required information from the `State` and return it with the correct key.
54 |
55 | ---
56 |
57 | Thanks to a shorthand in `react-redux` we can just define the actions we need in the component by putting them in an object as the second argument for `connect`
58 | ```typescript
59 | import { connect } from 'react-redux';
60 | import { setTitle, saveTodo, setDone } from './IndexReducer';
61 | import { IIndexState, IIndexDispatch, IIndexProps } from './IndexView';
62 |
63 | export default connect(stateToProps, {
64 | setTitle,
65 | saveTodo,
66 | setDone,
67 | })
68 | ```
69 | where `connect` uses internally the function [`bindActionCreators`](http://redux.js.org/docs/api/bindActionCreators.html) for each action in the object and passes them on as functions that dispatch automatically. They will be named the same as they are in the object, so for example `setTitle` will be accessible as `this.props.setTitle` in the component.
70 |
71 | ---
72 |
73 | Finally we bind it all using **react-redux's** [`connect`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options)
74 | ```typescript
75 | import { connect } from 'react-redux';
76 | import { setTitle, saveTodo, setDone } from './IndexReducer';
77 | import IndexView, { IIndexState, IIndexDispatch, IIndexProps } from './IndexView';
78 |
79 | const stateToProps = ...
80 |
81 | export default connect(stateToProps, {
82 | setTitle,
83 | saveTodo,
84 | setDone,
85 | })(IndexView);
86 | ```
87 | where the first type argument is the type for the format we want to transform our **state** to (*in this case our `IIndexState`*), the second type argument is the format we want to transform the `dispatch`-function to (*in this case our `IIndexDispatch`*) and the last type argument is the type for the **props** our **view** expects. The first argument `connect` takes is a function to transform our **state** into the type of the first type argument and the second argument is a function to transform the `dispatch`-function into the second type argument (*here we also see for the first time a [curried function](https://en.wikipedia.org/wiki/Currying)*). The last argument is our **view** itself, which should be expecting **props** of the type of the third type argument.
88 |
--------------------------------------------------------------------------------
/docs/EXTRAS.md:
--------------------------------------------------------------------------------
1 | # Extras
2 |
3 | Here we go through things you might not need, but might want to include in your project.
4 |
5 | ### Favicon
6 |
7 | A recommended feature of all websites, especially now with so many mobile devices browsing the internet, is to have a [favicon](https://en.wikipedia.org/wiki/Favicon) in your website. A favicon is usually the logo of your company or website. In our case you can either use the ones you can find [here](https://github.com/Lapanti/ts-react-boilerplate/tree/master/src/icons) or make your own. If you want to make your own, I suggest creating a 260 x 260 pixel image, which you then generate into all the useful formats using [RealFaviconGenerator](https://realfavicongenerator.net/).
8 |
9 | If you used *RealFaviconGenerator* you should now have the following files:
10 | - `android-chrome-192x192.png`
11 | - `android-chrome-512x512.png`
12 | - `apple-touch-icon.png`
13 | - `favicon-16x16.png`
14 | - `favicon-32x32.png`
15 | - `favicon.ico`
16 | - `mstile-150x150.png`
17 | - `safari-pinned-tab.svg`
18 | - `manifest.json`
19 | - `browserconfig.xml`
20 | Now move `manifest.json` and `browserconfig.xml` to your root folder and the rest to `src/icons`.
21 |
22 | Now that we have everything in place, let's first update our `index.html` by adding the following parts to inside the `head` tag:
23 | ```html
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | ```
33 | where the first line is to have an icon present if an iPhone user [saves your website](https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html) to their Home screen. The second, third and sixth line are for different sizes of the traditional browser favicon. The fourth line is for a [manifest.json](https://developer.mozilla.org/en-US/docs/Web/Manifest) which does the same thing as the first line for Android users. The fifth line is an icon for Safari users when they [pin your website](https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariWebContent/pinnedTabs/pinnedTabs.html). The seventh line is to define a tile for Microsoft users when they [pin your website](https://msdn.microsoft.com/en-us/library/dn320426(v=vs.85).aspx). The final line is a theme color for your website which is used for example by [mobile browsers](https://developers.google.com/web/fundamentals/design-and-ux/browser-customization/).
34 |
35 | The contents of you `manifest.json` should look something like this:
36 | ```json
37 | {
38 | "name": "",
39 | "icons": [
40 | {
41 | "src": "/assets/icons/android-chrome-192x192.png",
42 | "sizes": "192x192",
43 | "type": "image/png"
44 | },
45 | {
46 | "src": "/assets/icons/android-chrome-512x512.png",
47 | "sizes": "512x512",
48 | "type": "image/png"
49 | }
50 | ],
51 | "theme_color": "#FF8041", // Change this and the following line to match your website
52 | "background_color": "#FFFFFF",
53 | "display": "standalone"
54 | }
55 | ```
56 |
57 | And the contents of your `browserconfig.xml` should look something like this:
58 | ```xml
59 |
60 |
61 |
62 |
63 |
64 | #FF8041
65 |
66 |
67 |
68 | ```
69 | where you should change the `TileColor` to match your icon's background.
70 |
71 | ---
72 |
73 | Now we also need to update our **webpack** configurations to include our new icons and manifests into the project, so add the following to your `webpack.dev.js`:
74 | ```javascript
75 | plugins: [
76 | new CopyWebpackPlugin([
77 | { from: path.resolve(__dirname, 'index.html') },
78 | { from: path.resolve(__dirname, 'manifest.json'), to: 'assets' },
79 | { from: path.resolve(__dirname, 'browserconfig.xml'), to: 'assets' },
80 | { from: path.resolve(__dirname, 'src/icons'), to: 'assets/icons' }
81 | ]),
82 | // ...
83 | ]
84 | ```
85 |
86 | And then add the following to your `webpack.prod.js`:
87 | ```javascript
88 | var CopyWebpackPlugin = require('copy-webpack-plugin');
89 | // ...
90 | plugins: [
91 | new CopyWebpackPlugin([
92 | { from: path.resolve(__dirname, 'manifest.json') },
93 | { from: path.resolve(__dirname, 'browserconfig.xml') },
94 | { from: path.resolve(__dirname, 'src/icons'), to: 'icons' }
95 | ]),
96 | // ...
97 | ]
98 | ```
99 |
100 | ### Server-side rendering
101 |
102 | Server-side rendering is the act of having a server render your **React**-application and sending it as an html file to the client, which can considerably reduce initial loading times and enables a lot of SEO. This is usually achieved by adding a [node](https://nodejs.org/en/)-server to your application and then hosting your code on a server.
103 |
104 | For our needs, we'll use [Express](https://expressjs.com/), starting with installing the new, required dependencies (*[http-status-enum](https://github.com/KyleNeedham/http-status-enum) is just a simple enumeration of HTTP Status Codes for TypeScript*)
105 | ```
106 | yarn add express http-status-enum
107 | yarn add -D @types/express
108 | ```
109 |
110 | ---
111 |
112 | Now the actual server we run will live in a file called `server.tsx` inside the `src`-folder
113 | ```typescript
114 | import * as path from 'path';
115 | import * as express from 'express';
116 | import * as React from 'react';
117 | import HttpStatus from 'http-status-enum';
118 | import { Provider } from 'react-redux';
119 | import { renderToString } from 'react-dom/server';
120 | import { StaticRouter, Route } from 'react-router-dom';
121 | import { routerMiddleware } from 'react-router-redux';
122 | import createHistory from 'history/createMemoryHistory';
123 | import { createStore, applyMiddleware } from 'redux';
124 | import { createEpicMiddleware } from 'redux-observable';
125 | import reducer, { epics, State } from './redux/reducer';
126 | import AppContainer from './modules/AppContainer';
127 |
128 | const normalizePort = (val: number | string): number | string | boolean => {
129 | const base = 10;
130 | const port: number = typeof val === 'string' ? parseInt(val, base) : val;
131 | return isNaN(port) ? val : port >= 0 ? port : false;
132 | };
133 |
134 | const renderHtml = (html: string, preloadedState: State) =>
135 | `
136 |
137 |
138 |
139 |
140 | Todo app
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
${html}
154 |
157 |
158 |
159 |
160 | `;
161 |
162 | const defaultPort = 8080;
163 | const port = normalizePort(process.env.PORT || defaultPort);
164 | const app = express();
165 |
166 | app.use('/assets', express.static(path.join('assets'), { redirect: false }));
167 |
168 | app.use((req: express.Request, res: express.Response) => {
169 | const store = createStore(
170 | reducer,
171 | applyMiddleware(routerMiddleware(createHistory()), createEpicMiddleware(epics)),
172 | );
173 | const context: { url?: string } = {};
174 | const html = renderToString(
175 |
176 |
177 |
178 |
179 | ,
180 | );
181 | if (context.url) {
182 | res.redirect(HttpStatus.MOVED_PERMANENTLY, context.url);
183 | } else {
184 | res.send(renderHtml(html, store.getState()));
185 | }
186 | });
187 |
188 | app.listen(port, () => console.log(`App is listening on ${port}`));
189 | ```
190 | which will serve the **React**-application on all other routes except `ROOT/assets`, where our assets are served from.
191 |
192 | ---
193 |
194 | First we made a simple function to normalize an incoming `PORT`-parameter
195 | ```typescript
196 | const normalizePort = (val: number | string): number | string | boolean => {
197 | const base = 10;
198 | const port: number = typeof val === 'string' ? parseInt(val, base) : val;
199 | return isNaN(port) ? val : port >= 0 ? port : false;
200 | };
201 | ```
202 | which tries to parse the incoming `val`-parameter.
203 |
204 | ---
205 |
206 | Next we create a function to render our `html` based on the rendered **React** application
207 | ```typescript
208 | const renderHtml = (html: string, preloadedState: State) => (
209 | `
210 |
211 |
212 |
213 |
214 | Todo app
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
${html}
228 |
231 |
232 |
233 |
234 | `
235 | );
236 | ```
237 | where the biggest point is `window.__PRELOADED_STATE__` which lets us set the **state** of the application in the server, although it requires the following modification to our `store.ts``
238 | ```typescript
239 | // Below is a necessary hack to access __PRELOADED_STATE__ on the global window object
240 | const preloadedState: State = (window).__PRELOADED_STATE__;
241 | delete (window).__PRELOADED_STATE__;
242 | const configureStore = (history: History) => createStore(
243 | reducer,
244 | preloadedState,
245 | applyMiddleware(epicMiddleware),
246 | );
247 | ```
248 |
249 | ---
250 |
251 | Next we set some required variables and create our **Express**-application
252 | ```typescript
253 | const defaultPort = 8080;
254 | const port = normalizePort(process.env.PORT || defaultPort);
255 | const app = express();
256 | ```
257 | where we use our previously created `normalizePort` to normalize the (*possibly*) given `PORT` environment variable.
258 |
259 | ---
260 |
261 | Next up we set up **Express** to serve our static assets
262 | ```typescript
263 | app.use('/js', express.static(path.join('js'), { redirect: false }));
264 | app.use('/styles', express.static(path.join('styles'), { redirect: false }));
265 | ```
266 | with [`use`](https://expressjs.com/en/4x/api.html#app.use) you can set a middleware-function to a specific path, in this case [`express.static`](https://expressjs.com/en/4x/api.html#express.static), which will serve static files from a relative path.
267 | > [`path.join`](https://nodejs.org/api/path.html#path_path_join_paths) will create a path using platform-specific separators.
268 |
269 | ---
270 |
271 | Next is the beef of our server application, the actual server-side rendering
272 | ```typescript
273 | app.use((req: express.Request, res: express.Response) => {
274 | const store = createStore(reducer, applyMiddleware(
275 | routerMiddleware(createHistory()),
276 | createEpicMiddleware(epics),
277 | ));
278 | const context: { url?: string } = {};
279 | const html = renderToString(
280 |
281 |
282 |
283 |
284 | ,
285 | );
286 | if (context.url) {
287 | res.redirect(HttpStatus.MOVED_PERMANENTLY, context.url);
288 | } else {
289 | res.send(renderHtml(html, store.getState()));
290 | }
291 | });
292 | ```
293 |
294 | where we use **react-router** to match the current path to our client code, where the [`match`](http://knowbody.github.io/react-router-docs/api/match.html)-function matches the current route without rendering. Afterwards we create a store and render the application as `html` and finally send it to the client.
295 |
296 | ---
297 |
298 | Finally we start the application itself
299 | ```typescript
300 | app.listen(port, () => console.log(`App is listening on ${port}`));
301 | ```
302 |
303 | ---
304 |
305 | Of course we need to again make some changes to our **webpack** configurations, but this time only to the production configuration and then we need to create a configuration for the server code itself.
306 |
307 | For the production configuration we want to remove our `index.html` creation plugin as the server itself serves the index file, so remove the following lines:
308 | ```javascript
309 | var HtmlWebpackPlugin = require('html-webpack-plugin');
310 | // ...
311 | new HtmlWebpackPlugin(),
312 | ```
313 | after which, we can remove the plugin itself, by running
314 | ```
315 | yarn remove html-webpack-plugin
316 | ```
317 |
318 | ---
319 |
320 | For the building of the server side code, we need to create another **webpack** configuration file, this time called `webpack.server.js`, which will have the following content:
321 | ```javascript
322 | var path = require('path');
323 | var webpack = require('webpack');
324 |
325 | module.exports = {
326 | target: 'node',
327 | entry: path.resolve(__dirname, 'src', 'server.tsx'),
328 | output: {
329 | filename: 'server.js',
330 | path: path.resolve(__dirname, 'dist')
331 | },
332 | module: {
333 | rules: [
334 | {
335 | test: /\.tsx?$/,
336 | loader: ['babel-loader', 'awesome-typescript-loader'],
337 | exclude: /node_modules/
338 | }
339 | ]
340 | },
341 | resolve: {
342 | extensions: ['.tsx', '.ts', '.js']
343 | }
344 | };
345 | ```
346 | where we simply define the `entry` to be `server.tsx`, the output folder to be `dist` (*with the output file being `server.js`*) and make it process **TypeScript**.
347 |
348 | Then we also need to update our build scripts to include our server, so we change the old `build`-script into the following three scripts:
349 | ```json
350 | "scripts": {
351 | "build:server": "webpack -d --env=server -p --colors",
352 | "build:client": "webpack -d --env=prod --colors",
353 | "build": "yarn clean && concurrently --kill-others-on-fail -p \"{name}\" -n \"SERVER,CLIENT\" -c \"bgBlue,bgMagenta\" \"yarn build:server\" \"yarn build:client\"",
354 | }
355 | ```
356 | where the `build:client`-script is the same as before, `build:server` calls **webpack** with our new server configuration and `build` runs both of these at the same time using **concurrecntly**.
357 |
358 | ---
359 |
360 | Finally we create the actual `start`-script which will run our application
361 | ```json
362 | "scripts": {
363 | "start": "cd dist && NODE_ENV=production node server.js",
364 | }
365 | ```
366 | which is very simple, just setting the `production`-flag for our `NODE_ENV` and starting the `server` with **node**.
367 |
368 | That's it, you should now have fully working server-side rendered application!
369 |
370 | ### PWA
371 |
372 | PWA stands for [Progressive Web Apps](https://developers.google.com/web/progressive-web-apps/), which are websites that can behave like apps, e.g. work when offline, can be installed on the home screen etc. In order to begin transforming our service into a PWA, we first need to add some values to the `manifest.json`, replace applicable lines with information appropriate to your application:
373 | ```json
374 | {
375 | "name": "HN PWA",
376 | "short_name": "HN PWA",
377 | "description": "An example HN PWA with TypeScript and React",
378 | "lang": "en-US",
379 | "orientation": "portrait-primary",
380 | "start_url": "/",
381 | // ...
382 | }
383 | ```
384 | where `name`, `short_name` and `description` should be self-evident, whereas `lang` defines the language of the application, `orientation` sets the wished [orientation](https://developer.mozilla.org/en-US/docs/Web/Manifest) for your app. `start_url` is one of the most important ones, as it sets the url your app opens into when it is opened from the home screen.
385 |
386 | ### Docker
387 |
388 | If you want to [dockerize](https://www.docker.com/) your application you need to add a `Dockerfile` to your application's root folder (*which is just a file named `Dockerfile`*)
389 | ```
390 | FROM node:4-onbuild
391 |
392 | LABEL maintainer "your.email.here@domain.com"
393 |
394 | COPY dist/ /
395 |
396 | ENV NODE_ENV=production
397 |
398 | EXPOSE 8080
399 |
400 | CMD node /server.js
401 | ```
402 | which tells **Docker** how to build your container (*installation instructions for **Docker** can be found [here](https://docs.docker.com/engine/installation/)*).
403 |
404 | To start your new **Docker**-container you can just run
405 | ```
406 | docker build .
407 | ```
408 | then take the image id given by the build command and use it here
409 | ````
410 | docker run -d -p 8080:8080 IMAGEID
411 | ```
412 |
--------------------------------------------------------------------------------
/docs/REDUCERS.md:
--------------------------------------------------------------------------------
1 | # Reducers
2 |
3 | Now we build our business logic, also known as **reducers**. Stay calm, this will be a bit more complicated than anything before.
4 |
5 | ### IndexReducer
6 |
7 | We begin by creating a file called `IndexReducer.ts` in the `src/modules/index`-folder
8 | > Our **reducers** follow the naming pattern of `[Foldername]Reducer`
9 |
10 | ```typescript
11 | import { Action } from 'redux';
12 | import { Observable } from 'rxjs/Observable';
13 | import 'rxjs/add/operator/delay';
14 | import 'rxjs/add/operator/map';
15 | import 'rxjs/add/operator/mapTo';
16 | import { Epic, combineEpics, ActionsObservable } from 'redux-observable';
17 | import { makeAction, isAction } from '../../redux/guards';
18 | import Todo from '../../common/Todo';
19 |
20 | const testDelay = 1000;
21 |
22 | export class IndexState {
23 | readonly title: string = '';
24 | readonly todos: Todo[] = [];
25 | readonly loading: boolean = false;
26 | }
27 |
28 | export const SET_TITLE = 'boilerplate/Index/SET_TITLE';
29 | export const SAVE_TODO = 'boilerplate/Index/SAVE_TODO';
30 | export const SAVE_TODO_SUCCESS = 'boilerplate/Index/SAVE_TODO_SUCCESS';
31 | export const SET_DONE = 'boilerplate/Index/SET_DONE';
32 | export const SET_DONE_SUCCESS = 'boilerplate/Index/SET_DONE_SUCCESS';
33 |
34 | export const setTitle = makeAction(SET_TITLE)((title: string) => ({ type: SET_TITLE, payload: title }));
35 | export const saveTodo = makeAction(SAVE_TODO)(() => ({ type: SAVE_TODO }));
36 | export const saveTodoSuccess = makeAction(SAVE_TODO_SUCCESS)(() => ({ type: SAVE_TODO_SUCCESS }));
37 | export const setDone = makeAction(SET_DONE)((i: number) => ({ type: SET_DONE, payload: i }));
38 | export const setDoneSuccess = makeAction(SET_DONE_SUCCESS)((i: number) => ({ type: SET_DONE_SUCCESS, payload: i }));
39 |
40 | export const saveTodoEpic: Epic = action$ =>
41 | action$
42 | .ofType(SAVE_TODO)
43 | .delay(testDelay)
44 | .mapTo(saveTodoSuccess());
45 |
46 | export const setDoneEpic: Epic = action$ =>
47 | action$
48 | .ofType(SET_DONE)
49 | .delay(testDelay)
50 | .map(action => isAction(action, setDone) && setDoneSuccess(action.payload));
51 |
52 | export const IndexEpics = combineEpics(saveTodoEpic, setDoneEpic);
53 |
54 | const IndexReducer = (state: IndexState = new IndexState(), action: Action): IndexState => {
55 | if (isAction(action, setTitle)) {
56 | return { ...state, title: action.payload };
57 | } else if (isAction(action, saveTodo)) {
58 | return { ...state, loading: true };
59 | } else if (isAction(action, saveTodoSuccess)) {
60 | return {
61 | ...state,
62 | title: '',
63 | todos: state.todos.concat(new Todo(state.todos.length + 1, state.title)),
64 | loading: false,
65 | };
66 | } else if (isAction(action, setDone)) {
67 | return { ...state, loading: true };
68 | } else if (isAction(action, setDoneSuccess)) {
69 | return {
70 | ...state,
71 | todos: state.todos.map(t => (t.id === action.payload ? t.setDone() : t)),
72 | loading: false,
73 | };
74 | } else {
75 | return state;
76 | }
77 | };
78 |
79 | export default IndexReducer;
80 | ```
81 |
82 | ---
83 |
84 | First we define the **state** for our `IndexReducer`
85 | > **Reducers** usually define their own **state** and we'll show you [later](#connecting) how to connect it to the main **reducer** and **state**
86 |
87 | ```typescript
88 | import Todo from '../../common/Todo';
89 |
90 | export class IndexState {
91 | readonly title: string = '';
92 | readonly todos: Todo[] = [];
93 | readonly loading: boolean = false;
94 | }
95 | ```
96 | which is fairly simple. Here we define the `IndexState` as a class, with the given properties (*make sure you add default values for required properties so you can instantiate it!*), with the `title` for the current `Todo` the user is creating, `todos` for the list of current `Todo`s and `loading` to show the user whether the application is performing an async call or not.
97 |
98 | ---
99 |
100 | Next up we define our [action types](http://redux.js.org/docs/basics/Actions.html)
101 | ```typescript
102 | const SET_TITLE = 'boilerplate/Index/SET_TITLE';
103 | const SAVE_TODO = 'boilerplate/Index/SAVE_TODO';
104 | const SAVE_TODO_SUCCESS = 'boilerplate/Index/SAVE_TODO_SUCCESS';
105 | const SET_DONE = 'boilerplate/Index/SET_DONE';
106 | const SET_DONE_SUCCESS = 'boilerplate/Index/SET_DONE_SUCCESS';
107 | ```
108 | which **redux** recommends to be constant `string`s, but can be of the type `any`. In our case, as we are using **redux-guards** to facilitate the way **TypeScript** works with **redux** we have to make them `string`s.
109 | > Here we follow the [redux-ducks](https://github.com/erikras/ducks-modular-redux) naming pattern of the format `applicationName/ViewName/ACTION_TYPE`
110 |
111 | ---
112 |
113 | Next we define our **action creators** which are functions that return an **action**
114 | ```typescript
115 | import { makeAction } from '../../redux/guards';
116 |
117 | export const setTitle = makeAction(SET_TITLE)((title: string) => ({ type: SET_TITLE, payload: title }));
118 | export const saveTodo = makeAction(SAVE_TODO)(() => ({ type: SAVE_TODO }));
119 | export const saveTodoSuccess = makeAction(SAVE_TODO_SUCCESS)(() => ({ type: SAVE_TODO_SUCCESS }));
120 | export const setDone = makeAction(SET_DONE)((i: number) => ({ type: SET_DONE, payload: i }));
121 | export const setDoneSuccess = makeAction(SET_DONE_SUCCESS)((i: number) => ({ type: SET_DONE_SUCCESS, payload: i }));
122 | ```
123 | of a specific type with a specific `payload` (*which is the way to pass new information to the **reducer***). In this case we use `makeAction` as defined by **redux-guards** to ensure we get proper typings.
124 | > If you want to avoid having to type `type: ACTION_TYPE` you can override the **redux** interface according to [this article](https://medium.com/@danschuman/redux-guards-for-typescript-1b2dc2ed4790), but I personally I dislike overriding libraries so I prefer the duplicity here instead.
125 |
126 | ---
127 |
128 | Next we define our [Epics](https://redux-observable.js.org/docs/basics/Epics.html)
129 | ```typescript
130 | import { Action } from 'redux';
131 | import { Observable } from 'rxjs/Observable';
132 | import 'rxjs/add/operator/delay';
133 | import 'rxjs/add/operator/map';
134 | import 'rxjs/add/operator/mapTo';
135 | import { Epic, combineEpics, ActionsObservable } from 'redux-observable';
136 | import { isAction } from '../../redux/guards';
137 |
138 | const saveTodoEpic: Epic = action$ =>
139 | action$
140 | .ofType(SAVE_TODO)
141 | .delay(testDelay)
142 | .mapTo(saveTodoSuccess());
143 |
144 | const setDoneEpic: Epic = action$ =>
145 | action$
146 | .ofType(SET_DONE)
147 | .delay(testDelay)
148 | .map(action => isAction(action, setDone) && setDoneSuccess(action.payload));
149 |
150 | export const IndexEpics = combineEpics(saveTodoEpic, setDoneEpic);
151 | ```
152 | which are [redux-observable's](https://redux-observable.js.org) way of handling side-effects in **Redux** (*like AJAX calls etc.*). At the end we combine all our **Epics** in this file to a single exportable **Epic** called `IndexEpics` (*so we only need to import one variable when we want access to these later*).
153 | > The importing part may look a little weird, but it's because [RxJS](http://reactivex.io/rxjs/) is a rather large library, we can either import everything using `import * as RxJS from 'rxjs'` or import only the parts we need as shown above, which will allow any proper [minifier](https://developers.google.com/speed/docs/insights/MinifyResources) like [UglifyJS](https://github.com/mishoo/UglifyJS) to include only the needed parts from **RxJS**
154 |
155 | The first line
156 | ```typescript
157 | const saveTodoEpic: Epic = action$ =>
158 | ```
159 | defines an **Epic** which takes in as the first type argument the `type` for the `Actions` the epic takes in (*and returns*), in this case `Action` from **redux**, and as the second argument the type of the **State** it takes in (*which isn't needed this time, so undefined will do*). An **Epic** is a function that takes in a [stream](https://en.wikipedia.org/wiki/Stream_(computing)) (*in this case of the type `ActionsObservable`*) which includes items of the `type` given as the first type argument and returns another stream (*in this case an [`Observable`](http://reactivex.io/documentation/observable.html), which `ActionsObservable` is based on*) which includes items of the same `type` as the input stream.
160 | > In JavaScript the convention is to append a `$` to all variables names that are **streams**, to let the developer know that they are dealing with one
161 |
162 | The second and third line
163 | ```typescript
164 | action$
165 | .ofType(SET_DONE)
166 | ```
167 | utilizes the inbuilt function `ofType(key: string)` of `ActionsObservable`, which basically filters out all **actions** that do not have the `type`-property of the given argument.
168 | > A more verbose, but maybe a simpler to understand version would be to write
169 | ```typescript
170 | action$.filter(action => action.type === SET_DONE)
171 | ```
172 | > If you find yourself needing to understand the types of the components provided by **redux-observable** I suggest reading [this](https://github.com/redux-observable/redux-observable/blob/master/index.d.ts)
173 |
174 | The third and fourth line
175 | ```typescript
176 | .delay(1000)
177 | .mapTo(saveTodoSuccess());
178 | ```
179 | include the actual functionality of our **Epic**. In this case **after** we receive an **action** of the type `SET_DONE` we wait for 1 second (*`delay` takes milliseconds as argument*) and then we return an **action** of the type `SAVE_TODO_SUCCESS` (*in this case using [`mapTo`](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-mapTo) as we just want to return a new **Action***).
180 | > If you wanted to return multiple actions, say `SET_DONE_SUCCESS` and an imaginary `SEND_PUSH_NOTIFICATION` you could do it using [`mergeMap`](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-mergeMap), which is kind of like `flatMap`, like so:
181 | ```typescript
182 | import { Observable } from 'rxjs/Observable';
183 | import 'rxjs/add/observable/from';
184 | import 'rxjs/add/operator/mergeMap';
185 | ...
186 | action$.ofType(SET_DONE)
187 | .mergeMap(action => Observable.from([
188 | setDoneSuccess(action.payload),
189 | // SEND_PUSH_NOTIFICATION,
190 | // OTHER ACTIONS,
191 | ]));
192 | ```
193 |
194 | The other **Epic** is otherwirse similar, but it uses [`map`](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-map)
195 | ```typescript
196 | .delay(1000)
197 | .map(action => isAction(action, setDone) && setDoneSuccess(action.payload));
198 | ```
199 | to return an action of the type `SET_DONE_SUCCESS`, using the payload of the incoming action. We use `isAction` defined by **redux-guards** here to ensure **TypeScript** understands the type of our action, otherwise it would complain that `Action does not have key payload`.
200 |
201 | > If you wanted to do an AJAX call, you would go about it like this:
202 | ```typescript
203 | import { ajax } from 'rxjs/observable/dom/ajax';
204 | ...
205 | action$.ofType(AJAX_CALL).mergeMap(action => {
206 | // For a get JSON call
207 | ajax.getJSON('url', { headers: 'go here' })
208 | .map(response => someAction(response))
209 | .catch(err => errorAction(err));
210 | // For all other calls, just select the correct verb
211 | ajax.post('url', payload, { headers: 'go here' })
212 | .map(response => someAction(response))
213 | .catch(err => errorAction(err));
214 | });
215 | ```
216 | > **Redux-observable** is built upon [RxJS](http://reactivex.io/), the JavaScript implemention of **ReactiveX** and most issues you will run into will be **RxJS** issues
217 |
218 | ---
219 |
220 | Finally we define the rest of our business logic, a.k.a. the **reducer** itself
221 | ```typescript
222 | const IndexReducer = (state: IndexState = new IndexState(), action: Action): IndexState => {
223 | if (isAction(action, setTitle)) {
224 | return { ...state, title: action.payload };
225 | } else if (isAction(action, saveTodo)) {
226 | return { ...state, loading: true };
227 | } else if (isAction(action, saveTodoSuccess)) {
228 | return {
229 | ...state,
230 | title: '',
231 | todos: state.todos.concat(new Todo(state.todos.length + 1, state.title)),
232 | loading: false,
233 | };
234 | } else if (isAction(action, setDone)) {
235 | return { ...state, loading: true };
236 | } else if (isAction(action, setDoneSuccess)) {
237 | return {
238 | ...state,
239 | todos: state.todos.map(t => (t.id === action.payload ? t.setDone() : t)),
240 | loading: false,
241 | };
242 | } else {
243 | return state;
244 | }
245 | };
246 | ```
247 | for which I suggest to break from the **redux-ducks** pattern by using the naming convention of `[Pagename]Reducer`. The important thing to remember with **reducers** is that they have to be [functional](https://en.wikipedia.org/wiki/Functional_programming), a.k.a. they are not allowed to mutate the incoming information or have side-effects.
248 |
249 | On the first line we define the signature of our `IndexReducer`
250 | ```typescript
251 | const IndexReducer = (state: IndexState = new IndexState(), action: Action): IndexState => {
252 | ```
253 | where we define it to take to parameters (*as all **reducers***), our `IndexState` (*with a default for the empty state*) and an action. `IndexReducer` will also return an `IndexState` (*as all **reducers***).
254 |
255 | Next we do the actual logic which all **reducers** are built upon
256 | ```typescript
257 | if (isAction(action, setTitle)) {
258 | return { ...state, title: action.payload };
259 | } else if (isAction(action, saveTodo)) {
260 | return { ...state, loading: true };
261 | } else if (isAction(action, saveTodoSuccess)) {
262 | return {
263 | ...state,
264 | title: '',
265 | todos: state.todos.concat(new Todo(state.todos.length + 1, state.title)),
266 | loading: false,
267 | };
268 | } else if (isAction(action, setDone)) {
269 | return { ...state, loading: true };
270 | } else if (isAction(action, setDoneSuccess)) {
271 | return {
272 | ...state,
273 | todos: state.todos.map(t => (t.id === action.payload ? t.setDone() : t)),
274 | loading: false,
275 | };
276 | } else {
277 | return state;
278 | }
279 | ```
280 | which is usually a [`switch`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/switch)-statement, but due to the way we use **redux-guards** we use an `if-else-if-else`-block instead, where we use `isAction` to check the function's type (*and give **TypeScript** the type information*). In each `block` we do something (*except the `default`-one, where you traditionally just return the incoming **state***) to add value to that **action**, such as setting the `title` to the `payload` in the **action** in case the **action** is a `SET_TITLE`-action. Notice how we are using the [spread syntax](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Spread_operator) to immutably create a new version of the state, thus holding true to the immutability of **reducers**.
281 | > Other options are to use [`Object.assign({}, ...)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) or [Immutable](https://facebook.github.io/immutable-js/)
282 |
283 | ### Connecting the reducer
284 |
285 | Remember our [root-reducer](/REDUX.md#reducer)? Now we connect our `IndexReducer` to it.
286 |
287 | First the **reducer** itself
288 | ```typescript
289 | import { combineReducers } from 'redux';
290 | import IndexReducer from '../modulex/index/IndexReducer';
291 |
292 | const reducer = combineReducers({
293 | index: IndexReducer,
294 | });
295 | ```
296 | where we add the `IndexReducer` under the key `index`, which is very important, as when `combineReducers` combines included **reducers** it will put their specific state under the key given, in the global **state**-object.
297 |
298 | Next we add the `IndexState` to our global `State`-class (*this is just to allow us to define the type and initialize it for tests later on*)
299 | ```typescript
300 | import { IndexState } from '../modules/index/IndexReducer';
301 |
302 | export class State {
303 | readonly index: IndexState = new IndexState();
304 | }
305 | ```
306 | where we define that the global `State`-object has a property `index` of the type `IndexState` (*as our `combineReducer` already says, but we want to be explicit here*).
307 |
308 | Then we want to add our **Epics** into the global `epics` constant
309 | ```typescript
310 | import { combineEpics } from 'redux-observable';
311 | import { IndexEpics } from '../modules/index/IndexReducer';
312 |
313 | export const epics = combineEpics(IndexEpics);
314 | ```
315 | by including it as a parameter to `combineEpics`.
316 |
--------------------------------------------------------------------------------
/docs/REDUX.md:
--------------------------------------------------------------------------------
1 | # Redux
2 |
3 | Next we will setup [redux](http://redux.js.org/) to handle the state for our application (*redux allows us to keep our components pure, helping testing and predictability*).
4 | > You can think of **redux** as an implementation of the [Flux](https://facebook.github.io/flux/) pattern, where the main point is that data flows into a single direction.
5 |
6 | ### Initialize
7 |
8 | 1. This time we will only need to add the necessary dependencies to allow development with **redux**:
9 | ```
10 | yarn add redux redux-observable rxjs react-router-redux@next history
11 | ```
12 | 2. Add the necessary type definitions (*redux, redux-observable and rxjs contain type definitions*):
13 | ```
14 | yarn add -D @types/react-router-redux @types/history
15 | ```
16 | > [Redux-observable](https://redux-observable.js.org/) is our choice for allowing side effects, such as [AJAX](https://developer.mozilla.org/en-US/docs/AJAX/Getting_Started)-calls in **redux**. [RxJS](http://reactivex.io/) is a peer dependency for **redux-observable**. If you want something else you can check the [alternatives](#alternatives). [React-router-redux](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-redux) is used to tie navigation events and browser history into **redux** when using [React Router](https://reacttraining.com/react-router/) (*which well setup later*), and [history](https://github.com/reacttraining/history) is needed to use **react-router-redux**.
17 |
18 | ### Redux guards
19 |
20 | We will begin by creating a file called `guards.js` inside the folder `redux` in `src`. This file will contain some helper functions so that TypeScript will play nicely with **Redux**. The contents of the file are as follows:
21 | ```typescript
22 | import { Action } from 'redux';
23 |
24 | export type IActionType = X & { __actionType: string };
25 |
26 | const _devSet: { [key: string]: any } = {};
27 |
28 | export const makeAction = (type: string, typePrefix = '') => {
29 | // Helpful check against copy-pasting duplicate type keys when creating
30 | // new actions.
31 | if (process.env.NODE_ENV === 'development') {
32 | if (_devSet[type]) {
33 | throw new Error(
34 | 'Attempted creating an action with an existing type key. ' + 'This is almost cetainly an error.',
35 | );
36 | }
37 | _devSet[type] = type;
38 | }
39 | return Z>(fn: X) => {
40 | const returnFn: IActionType = ((...args: any[]) => ({ ...(fn as any)(...args), type })) as any;
41 | returnFn.__actionType = typePrefix + type;
42 | return returnFn;
43 | };
44 | };
45 |
46 | export const isAction = (
47 | action: Action,
48 | actionCreator: IActionType<(...args: any[]) => T>,
49 | ): action is T & Action => {
50 | return action.type === actionCreator.__actionType;
51 | };
52 | ```
53 | [Actions](http://redux.js.org/docs/basics/Actions.html) are the only way to send new content to the **redux**-state, and are usually in the form of an object with the properties `type` (*a unique string*) and an optional `payload` (*something to pass to the reducer*). However as **redux** has defined the `type` key to be of the type `any`, we lose type safety and that is why we alias the actual string to `__actionType`, which allows **TypeScript** to infer the type of an action implicitly, which is where `makeAction` comes in. The `_devSet` variable and the things related to it inside `makeAction` are for development, to ensure we don't create duplicate actions. The `isAction`-function is a [type guard](https://www.typescriptlang.org/docs/handbook/advanced-types.html) which allows us to use the action creators (*more about them in [reducers](./REDUCERS.md)*) own return type as the actual type for the action, giving us implicit, but safe, typings. You can read more about [redux guards](https://github.com/quicksnap/redux-guards) [here](https://medium.com/@danschuman/redux-guards-for-typescript-1b2dc2ed4790). Don't worry if this seems too complex as it uses a lot of advanced features of **TypeScript** to work.
54 |
55 | ### Reducer
56 |
57 | Now we will define our root-reducer in a file called `reducer.ts` inside the folder `redux`:
58 | ```typescript
59 | import { combineReducers } from 'redux';
60 | import { combineEpics } from 'redux-observable';
61 | import { routerReducer, RouterState } from 'react-router-redux';
62 |
63 | const reducer = combineReducers({
64 | router: routerReducer,
65 | });
66 |
67 | export class State {
68 | readonly router: RouterState = null;
69 | }
70 |
71 | export const epics = combineEpics(
72 | );
73 |
74 | export default reducer;
75 | ```
76 | This file will allow us to export all of the following:
77 | - Our root reducer (*all specific reducers will be combined into this one, as according to [redux documentation](http://redux.js.org/docs/basics/Reducers.html#handling-actions), allowing our reducers to only handle a slice of the entire `state`*) made with [combineReducers](http://redux.js.org/docs/api/combineReducers.html)
78 | - The class of our entire `state` (*defined as a class to allow initialization in for example tests*)
79 | - Our combined [epics](https://redux-observable.js.org/docs/basics/Epics.html) (*more about epics later*) made with [combineEpics](https://redux-observable.js.org/docs/api/combineEpics.html)
80 |
81 | ### Store
82 |
83 | Now we will define our store creator. Having it as a separate function helps us in doing [server-side rendering](https://github.com/reactjs/redux/blob/master/docs/recipes/ServerRendering.md) but if you don't want to do it you can define this function later. The store creator goes in a file called `store.ts` inside the `redux`-folder:
84 | ```typescript
85 | import { createStore, applyMiddleware } from 'redux';
86 | import { createEpicMiddleware } from 'redux-observable';
87 | import { History } from 'history';
88 | import { routerMiddleware } from 'react-router-redux';
89 | import reducer, { epics, State } from './reducer';
90 |
91 | const epicMiddleware = createEpicMiddleware(epics);
92 |
93 | const configureStore = (history: History) => createStore(
94 | reducer,
95 | applyMiddleware(routerMiddleware(history), epicMiddleware),
96 | );
97 |
98 | export default configureStore;
99 | ```
100 |
101 | ---
102 |
103 | On the fifth line we create a [middleware](http://redux.js.org/docs/advanced/Middleware.html) for our **store** to handle our epics, using [createEpicMiddleware](https://redux-observable.js.org/docs/api/createEpicMiddleware.html) (*and here you see why we combined all our epics into one*):
104 | ```typescript
105 | import { createEpicMiddleware } from 'redux-observable';
106 | import { epics } from './reducer';
107 | const epicMiddleware = createEpicMiddleware(epics);
108 | ```
109 |
110 | ---
111 |
112 | And then on the seventh line we define our store creator method (*which is exported on the 14th line*):
113 | ```typescript
114 | import { createStore, applyMiddleware, Store } from 'redux';
115 | import { History } from 'history';
116 | import { routerMiddleware } from 'react-router-redux';
117 | import reducer, { State } from './reducer';
118 | const configureStore = (history: History) => createStore(
119 | reducer,
120 | applyMiddleware(routerMiddleware(history), epicMiddleware),
121 | );
122 | export default configureStore;
123 | ```
124 |
125 | [createStore](http://redux.js.org/docs/api/createStore.html) is the function that creates a **Store** for **redux** and as it's first argument it takes the root-reducer and as the second one all the applicable middleware (*combined with [applyMiddleware](http://redux.js.org/docs/api/applyMiddleware.html)*), in this case our **epicMiddleware** and **routerMiddleware**. The function `configureStore` takes a `History` as an argument, to allow us to call it with different types of histories.
126 |
127 | ---
128 |
129 | Now we have everything set up to start doing the beef of the application, a.k.a. the views!
130 |
131 | ### Alternatives
132 |
133 | If **redux** doesn't float your boat, you can always try [MobX](https://github.com/mobxjs/mobx), but **redux** is maybe the more used one at this point.
134 |
135 | For **redux-observable** you have multiple alternatives, namely:
136 | - [redux-loop](https://github.com/redux-loop/redux-loop), which is inspired by [elm](http://elm-lang.org/)'s effect system
137 | - [redux-thunk](https://github.com/gaearon/redux-thunk)
138 | - [redux-saga](https://github.com/redux-saga/redux-saga)
139 |
--------------------------------------------------------------------------------
/docs/STRUCTURE.md:
--------------------------------------------------------------------------------
1 | # Structure
2 |
3 | All source code in our application will live in a folder called `src`.
4 | `src`-folder will mainly contain other folders (which we will go through in a moment) and the main entry files for both server-side rendering and the application itself.
5 |
6 | Always remember though that this structure is not set in stone and you can modify it as much as you want to, this has mainly proven a good structure in previous projects.
7 |
8 | The full architecture will look something like this:
9 | ```
10 | src/
11 | -- common/
12 | -- components/
13 | -- modules/
14 | -- redux/
15 | -- styles/
16 | -- index.tsx
17 | -- server.tsx
18 | package.json
19 | // And all the other configuration files
20 | ```
21 | ## Common
22 |
23 | In this folder we will have common files needed by modules and components alike, such as classes to define information needed in multiple places. We dive more deeply into it [here](/docs/COMMON.md).
24 |
25 | ## Components
26 |
27 | In this folder we will have common components, such as buttons, links and inputs. A deeper dive can be found [here](/docs/COMPONENTS.md).
28 |
29 | ## Modules
30 |
31 | In this folder we will have all modules, which can basically be thought of as pages (or parts of pages) and their functionalities. This will consist of [views](/docs/VIEWS.md), [containers](/docs/CONTAINERS.md) and [reducers](/docs/REDUCERS.md).
32 |
33 | ## Redux
34 |
35 | In this folder we will have common redux files, such as a store, utilities etc. A deeper dive can be found [here](/docs/REDUX.md).
36 |
37 | ## Styles
38 |
39 | In this folder we will include all the stylesheets for the application. A deeper dive can be found [here](/docs/STYLES.md).
40 |
41 | ## Tests
42 |
43 | Tests for each file will be contained in a folder called `__specs__` in the same directory as the file itself. You can read more about testing [here](/docs/TESTS.md)
44 |
--------------------------------------------------------------------------------
/docs/STYLES.md:
--------------------------------------------------------------------------------
1 | # Styles
2 |
3 | Next up we are going to add styles to our not-so-fancy-yet application. We are going to use [Sass](http://sass-lang.com/) as it's perhaps the most widely used **CSS preprocessor** (*and you should use one, as it helps you with managing your styles*).
4 |
5 | ### Initialize
6 |
7 | First we will again add some dependencies to our project
8 | ```
9 | yarn add -D node-sass sass-lint
10 | ```
11 | to actually build **Sass** with [node-sass](https://github.com/sass/node-sass) and then lint it with [sass-lint](https://www.npmjs.com/package/sass-lint).
12 |
13 | Next we'll create a `.sass-lint.yml` file to define the conventions for our **Sass**-files, you can check the available [rules here](https://github.com/sasstools/sass-lint/tree/master/docs/rules), but this is what I use
14 | ```yaml
15 | options:
16 | merge-default-rules: false
17 | rules:
18 | bem-depth:
19 | - 4
20 | - max-depth: 4
21 | class-name-format:
22 | - allow-leading-underscore: false
23 | - convention: hyphenatedbem
24 | declarations-before-nesting: true
25 | extends-before-declarations: true
26 | ```
27 | from which the first command `merge-default-rules` indicates that I do not want to use the default rules as a basis, the second defines that for [BEM naming](http://getbem.com/naming/) the maximum depth is four, the third that I don't allow class names that start with an underscore and that they must follow `hyphenatedbem`-convention, the fourth that I want style declarations to be before nested selectors and the last that possible [`@extend`](http://sass-lang.com/guide) must be before style declarations.
28 |
29 | ### Styles.scss
30 |
31 | All of our style-sheets will live inside a folder `src/styles` and the first will be the "main"-stylesheet, called `styles.scss` (*`scss` is the **Sass** file extension*)
32 | ```scss
33 | @import 'variables.scss';
34 | @import 'index.scss';
35 | @import 'button.scss';
36 | @import 'todocomponent.scss';
37 | @import 'loader.scss';
38 |
39 | body {
40 | background-color: $tertiary-color;
41 | font-family: 'Roboto', sans-serif;
42 | }
43 | ```
44 | and at this point it only [imports](http://sass-lang.com/guide) our other (*soon-to-be-written stylesheets*) so that the compiler will know to bundle them all and sets two styles on our `body`, a `background-color` using the variable `$tertiary-color` (*more about variables in a bit*) and a `font-family` of `'Roboto'`, with a backup font of `sans-serif`. But wait, what is this `'Roboto'` you might ask? It is the main font of [Google's Material Design](https://material.io/) used in Android etc. and a very nice simple font. Now the way to be able to use it in our styles is to add the following line to the `head` element of our `index.html`
45 | ```html
46 |
47 | ```
48 | which will include the font from Google's CDN.
49 |
50 | ### Variables
51 |
52 | Next we will define those things we call `variables` in **Sass** in their own file called `variables.scss`
53 | ```scss
54 | // Colors
55 | $primary-color: #343488;
56 | $secondary-color: #5656AA;
57 | $tertiary-color: #F0F0FF;
58 | $modal-background: rgba(100, 100, 100, .8);
59 | ```
60 | where the underscore in the beginning of the file name is just a convention to differentiate it from regular style files. Inside it we define four different colors (*using comments to define the variable "blocks" of colors, sizes etc. is just another convention*), a primary color, secondary color, tertiary color and a color for the background of a modal. All variables in **Sass** must begin with the dollar `$` sign.
61 |
62 | ### Index
63 |
64 | Next up is the styles for the `Index`-page, inside a file called `index.scss`
65 | ```scss
66 | @import 'variables.scss';
67 |
68 | .index {
69 | align-items: center;
70 | display: flex;
71 | flex-direction: column;
72 | justify-content: space-around;
73 |
74 | &__header {
75 | color: $primary-color;
76 | }
77 |
78 | &__form {
79 | align-items: inherit;
80 | display: flex;
81 | flex-direction: inherit;
82 | justify-content: inherit;
83 |
84 | &__label {
85 | margin-bottom: 5px;
86 | }
87 |
88 | &__input {
89 | background-color: inherit;
90 | border: 0;
91 | border-bottom: 1px solid $primary-color;
92 | margin-bottom: 10px;
93 | text-align: center;
94 | width: 250px;
95 |
96 | &:focus {
97 | border-bottom: 2px solid $secondary-color;
98 | outline: none;
99 | }
100 | }
101 | }
102 |
103 | &__todo-container {
104 | display: flex;
105 | flex-direction: inherit;
106 | justify-content: flex-start;
107 | }
108 | }
109 | ```
110 | where we introduce **nesting**. I will just explain the simplest use case so you understand what is happening
111 | ```scss
112 | @import 'variables.scss';
113 | .index {
114 | align-items: center;
115 | display: flex;
116 | flex-direction: column;
117 | justify-content: space-around;
118 |
119 | &__header {
120 | color: $primary-color;
121 | }
122 | }
123 | ```
124 | where we see that first we have defined a block for the class `index`, which styles the [flexbox](https://developer.mozilla.org/en/docs/Web/CSS/flex) properties for it. Inside we have another block, which starts with `&`, which is a special character in **Sass** as it translates the `&` ampersand to the parent block's selector. So the above would output as CSS something like this:
125 | ```css
126 | .index {
127 | align-items: center;
128 | display: flex;
129 | flex-direction: column;
130 | justify-content: space-around;
131 | }
132 | .index__header {
133 | color: #343488;
134 | }
135 | ```
136 | > This shows us one the uses of **BEM** as it ties very nicely with the nesting in **Sass**. You can read more about the reasoning behind **BEM** [here](http://getbem.com/faq/#why-bem), but the main point is that **BEM** is as modular and independent as most JavaScript modules, while keeping it similar for every developer.
137 |
138 | ### Button
139 |
140 | In a file called `button.scss` we are going to write our styles for the `Button`-component
141 | ```scss
142 | @import 'variables.scss';
143 |
144 | .btn {
145 | background-color: $tertiary-color;
146 | border: 1px solid $primary-color;
147 | border-radius: 50%;
148 | cursor: pointer;
149 | }
150 | ```
151 | which are very simple, like the component itself.
152 |
153 | ### TodoComponent
154 |
155 | The styles for our `TodoComponent` will be set in a file called `todocomponent.scss`
156 | ```scss
157 | @import 'variables.scss';
158 |
159 | .todo {
160 | display: flex;
161 | flex-direction: row;
162 | margin-bottom: 10px;
163 |
164 | &__checkbox {
165 | cursor: pointer;
166 | }
167 |
168 | &__number {
169 | margin-left: 5px;
170 | }
171 |
172 | &__title {
173 | margin-left: 10px;
174 | }
175 | }
176 | ```
177 | which is a rather simple style as well, just a little **flexbox** in there.
178 |
179 | ### Loader
180 |
181 | Now this is something a bit more interesting, we are going to make our `Loader`-component finally come to life, by creating the styles for it inside `loader.scss``
182 | ```scss
183 | @import 'variables.scss';
184 |
185 | .loader {
186 | animation: .8s fadein .2s linear forwards;
187 | background: $modal-background;
188 | height: 200vh;
189 | left: -50vw;
190 | opacity: 0;
191 | position: fixed;
192 | top: -50vh;
193 | width: 200vw;
194 | z-index: 1000;
195 |
196 | &__spinner:before {
197 | animation: spinner .6s linear infinite;
198 | border: 2px solid #cccccc;
199 | border-radius: 50%;
200 | border-top-color: #333333;
201 | box-sizing: border-box;
202 | content: '';
203 | height: 120px;
204 | left: 50%;
205 | margin-left: -60px;
206 | margin-top: -60px;
207 | position: fixed;
208 | top: 50%;
209 | width: 120px;
210 | }
211 | }
212 |
213 | @keyframes spinner {
214 | to {
215 | transform: rotate(360deg);
216 | }
217 | }
218 |
219 | @keyframes fadein {
220 | from {
221 | opacity: 0;
222 | }
223 | to {
224 | opacity: 1;
225 | }
226 | }
227 | ```
228 | and this might use some explaining. There are two major points of interest here, the first being [CSS animations](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations) and the second being [the `:before` selector](https://developer.mozilla.org/en/docs/Web/CSS/::before).
229 |
230 | ---
231 |
232 | CSS animations are built by setting a value for `animation` inside the CSS selector
233 | ```css
234 | .selector {
235 | animation: 1s name 2s linear forwards;
236 | }
237 | ```
238 | where the first variable (*optional*) is the delay for starting the animation, second is the name of the animation (*more about that in a bit*), third is the length of the animation, fourth is an [animation timing function](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function) and the last one is [animation fill mode](https://developer.mozilla.org/en/docs/Web/CSS/animation-fill-mode).
239 |
240 | Now the animation name has to match a [`@keyframes`](https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes) selector
241 | ```css
242 | @keyframes name {
243 | from {
244 | opacity: 0;
245 | }
246 | 50% {
247 | opacity: 0.5;
248 | }
249 | to {
250 | opacity: 1;
251 | }
252 | }
253 | ```
254 | where you define the style for the object being styled at different points of the animation (*any CSS style is valid here*). You can either specify those styles with percentages (*such as the 50% here*) or as the `from` and `to` selectors (*`from` for the first frame and `to` for the last one*) or mix them up.
255 |
256 | ---
257 |
258 | The `:before` selector on the other hand is used to create a child (*it must have something set to it's `content` property to show*) for the element, that is used to create some kind of styling otherwise impossible to create, for example in our case the actual spinning element.
259 |
260 | ### Scripts
261 |
262 | Now that we have our styles set up, we need to include them in our application and to do that, we are going to update our **webpack** configurations and update our `index.tsx`-file a bit, beginning with the `index.tsx`-change, which is to add the following line as the last `import` statement:
263 | ```typescript
264 | import './styles/styles.scss';
265 | ```
266 | which is because **webpack** only bundles files related to the `entry`-file (*our `index.tsx`*).
267 |
268 | ---
269 |
270 | To use **webpack** with **Sass** we need to first add a couple of new development dependencies, so go ahead and:
271 | ```
272 | yarn add -D style-loader css-loader sass-loader extract-text-webpack-plugin
273 | ```
274 | where [`sass-loader`](https://webpack.js.org/loaders/sass-loader/#src/components/Sidebar/Sidebar.jsx) compiles our **Sass** to **CSS**, [`css-loader`](https://webpack.js.org/loaders/css-loader/#src/components/Sidebar/Sidebar.jsx) allows **webpack** to process **CSS**, [`style-loader`](https://webpack.js.org/loaders/style-loader/#src/components/Sidebar/Sidebar.jsx) injects said **CSS** straight into the HTML (*faster for development*) and [`extract-text-webpack-plugin`](https://webpack.js.org/plugins/extract-text-webpack-plugin/#src/components/Sidebar/Sidebar.jsx) extracts our **CSS** into a single file when doing production builds.
275 |
276 | For our development configuration we only need to add the following new rule to `webpack.dev.js`:
277 | ```javascript
278 | // ...
279 | module: {
280 | rules: [
281 | {
282 | test: /\.scss$/,
283 | use: ['style-loader', 'css-loader', 'sass-loader']
284 | }
285 | ]
286 | }
287 | ```
288 | where we tell **webpack** to process our **Sass** through the described loaders (*from right to left*).
289 |
290 | For our production build we need to do a few more changes into `webpack.prod.js`:
291 | ```javascript
292 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
293 | // ...
294 | module : {
295 | rules: [
296 | {
297 | test: /\.scss$/,
298 | use: ExtractTextPlugin.extract(['css-loader', 'sass-loader'])
299 | }
300 | ]
301 | },
302 | // ...
303 | plugins: [
304 | new ExtractTextPlugin(path.join('styles.css'))
305 | ]
306 | ```
307 | where at first we use `extract-text-webpack-plugin` to extract all processed **Sass** and then in `plugins` we tell it to save them in a file called `styles.css`.
308 |
309 | ### Alternatives
310 |
311 | - [Less](http://lesscss.org/), another CSS preprocessor
312 | - [Stylus](http://stylus-lang.com/), the "third" CSS preprocessor (*Sass and Less are older*)
313 | - [PostCSS](http://postcss.org/), a CSS postprocessor
314 |
--------------------------------------------------------------------------------
/docs/TESTING.md:
--------------------------------------------------------------------------------
1 | # Testing
2 |
3 | Even though this part was left as the last one, it is one of the most important parts of a software project and this will only be one way of doing tests.
4 |
5 | ### Initialize
6 |
7 | For our testing framework we are going to use [Jest](https://facebook.github.io/jest/), with [Enzyme](https://github.com/airbnb/enzyme) for [BDD testing](https://en.wikipedia.org/wiki/Behavior-driven_development), [enzyme-to-json](https://github.com/adriantoine/enzyme-to-json) for [snapshot testing](https://facebook.github.io/jest/docs/snapshot-testing.html), [ts-jest](https://github.com/kulshekhar/ts-jest) so **Jest** plays nice with **TypeScript** and [react-test-renderer](https://www.npmjs.com/package/react-test-renderer) for rendering **React**-components. Because of the new adapters in **Enzyme** we also need to add the appropriate [adapter](https://github.com/airbnb/enzyme/tree/master/packages/enzyme-adapter-react-16#upgrading-from-enzyme-2x-or-react--16) (in our case for **React** version 16) and finally we also add a little tool called [concurrently](https://github.com/kimmobrunfeldt/concurrently) to allow the simultaneous running of multiple **NPM** or **Yarn** scripts at the same time
8 | ```
9 | yarn add -D jest ts-jest enzyme enzyme-to-json react-test-renderer enzyme-adapter-react-16 concurrently
10 | ```
11 |
12 | ### setup.js
13 |
14 | As **Enzyme** requires the adapter for **React** and on top of that error messages related to HTTP-calls can sometimes be incomprehensible, we need to create a small setup file to fix these things, so create a file called `setup.js` in `src/jest`-folder and fill it as follows:
15 | ```javascript
16 | // Import adapter for enzyme
17 | var enzyme = require('enzyme');
18 | var Adapter = require('enzyme-adapter-react-16');
19 | enzyme.configure({ adapter: new Adapter() })
20 |
21 | // Log all jsDomErrors when using jsdom testEnvironment
22 | window._virtualConsole && window._virtualConsole.on('jsdomError', function (error) {
23 | console.error('jsDomError', error.stack, error.detail);
24 | });
25 | ```
26 |
27 | After that we need to register it for **Jest** so open up your `package.json` and add the following:
28 | ```json
29 | "jest": {
30 | // Other content in the "jest"-object
31 | "setupTestFrameworkScriptFile": "/src/jest/setup.js"
32 | }
33 | ```
34 |
35 | ### Linting
36 |
37 | Let's start with the easiest part, linting our codebase. For the **TypeScript** code we already have our linting script setup under `lint:ts`.
38 |
39 | For our **Sass** files it will be simply
40 | ```json
41 | "scripts": {
42 | "lint:sass": "sass-lint src/**/*.scss -v --max-warnings 1",
43 | }
44 | ```
45 | by running **sass-lint** on the files mathing our **glob pattern**, adding the `v`-flag for verbose output and `max-warnings` to only allow for one warning (*you can omit it if you dare*).
46 |
47 | ### TS-Jest and enzyme-to-json
48 |
49 | We will also need to make some setting for **Jest** in our `package.json` to use **ts-jest** and **enzyme-to-json** with our code
50 | ```json
51 | "jest": {
52 | "snapshotSerializers": ["/node_modules/enzyme-to-json/serializer"],
53 | "transform": {
54 | ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js"
55 | },
56 | "testRegex": "(/__specs__/.*|\\.(spec))\\.(ts|tsx)$",
57 | "moduleFileExtensions": ["ts", "tsx", "js"]
58 | }
59 | ```
60 | where the first setting `snapshotSerializers` sets **Jest** to use **enzyme-to-json** for serializing, `testRegex` makes it so that **Jest** looks for tests in folders named `__specs__` with the extension `spec.ts` or `spec.tsx` and the rest are there so that it uses **TS-Jest** to build the code.
61 |
62 | ### Button
63 |
64 | We will start with one of the simple components, in this case the `Button`. With all our tests we will follow the convention of having the tests in a folder next to the testable code called `__specs__` named `[TestableCode].spec.ts` or `*.tsx` if it includes **JSX** like this
65 | ```
66 | src/
67 | -- components/
68 | -- -- __specs__/
69 | -- -- -- Button.spec.tsx
70 | -- -- Button.tsx
71 | ```
72 |
73 | Now the actual content of `Button.spec.tsx` will look like this
74 | ```typescript
75 | import * as React from 'react';
76 | import { shallow } from 'enzyme';
77 | import Button from '../Button';
78 |
79 | describe('Button', () => {
80 | const click = jest.fn();
81 | const wrapper = shallow();
82 |
83 | it('should render correctly', () => (
84 | expect(wrapper).toMatchSnapshot()
85 | ));
86 |
87 | it('should call the correct function on click', () => {
88 | const testTimes = 5;
89 | for (let i = 0; i < testTimes; i++) {
90 | wrapper.find('.btn').simulate('click');
91 | }
92 | expect(click).toHaveBeenCalledTimes(testTimes);
93 | });
94 | });
95 | ```
96 |
97 | ---
98 |
99 | Going through it line by line, the first line of importance is creating a `describe`-block
100 | ```typescript
101 | describe('Button', () => {});
102 | ```
103 | which **Jest** uses to group tests together (*in this case all tests related to 'Button'*).
104 |
105 | ---
106 |
107 | After that we create a [mocked function](https://facebook.github.io/jest/docs/mock-functions.html) that will allow us to test how many times the function has been called, what values it was called with etc.
108 | ```typescript
109 | const click = jest.fn();
110 | ```
111 |
112 | ---
113 |
114 | Next we create a [shallow render](https://github.com/airbnb/enzyme/blob/master/docs/api/shallow.md) of the `Button`
115 | ```typescript
116 | import * as React from 'react';
117 | import { shallow } from 'enzyme';
118 | import Button from '../Button';
119 | // ...
120 | const button = shallow();
121 | ```
122 | which will allow us to do snapshot testing and to simulate user interaction.
123 |
124 | ---
125 |
126 | First we test our `Button`'s snapshot (*to ensure we don't accidentally change the way it looks like*)
127 | ```typescript
128 | it('should render correctly', () => (
129 | expect(wrapper).toMatchSnapshot()
130 | ));
131 | ```
132 | which will create a snapshot-file inside a folder `__snapshots__` which you should include in your version control, as it will compare the render to the file if it exists.
133 | > `it`-clauses are **Jest**'s tests and `expect` defines something to test
134 |
135 | ---
136 |
137 | Finally we simulate user interaction and ensure it has the correct result
138 | ```typescript
139 | it('should call the correct function on click', () => {
140 | const testTimes = 5;
141 | for (let i = 0; i < testTimes; i++) {
142 | wrapper.find('.btn').simulate('click');
143 | }
144 | expect(click).toHaveBeenCalledTimes(testTimes);
145 | });
146 | ```
147 | where we first define the number of times we want to simulate a click, then using `find` (*which takes a CSS-selector to find the correct HTML-element*) we simulate a `click`-event and test that the mock function was called the correct amount of times.
148 |
149 | ### Loader
150 |
151 | Next up we create a simple snapshot test for our `Loader`-component
152 | ```typescript
153 | import * as React from 'react';
154 | import { shallow } from 'enzyme';
155 | import Loader from '../Loader';
156 |
157 | describe('Loader', () => (
158 | it('should render correctly', () => (
159 | expect(shallow()).toMatchSnapshot()
160 | ))
161 | ));
162 | ```
163 | where everything is very similar to the `Button`-tests.
164 |
165 | ### TodoComponent
166 |
167 | Then for our last component `TodoComponent` we do, once again, similar tests
168 | ```typescript
169 | import * as React from 'react';
170 | import { shallow } from 'enzyme';
171 | import TodoComponent from '../TodoComponent';
172 | import Todo from '../../common/Todo';
173 |
174 | describe('TodoComponent', () => {
175 | const testTodo1 = new Todo(1, 'Title');
176 | const testTodo2 = new Todo(0, 'Testing', true);
177 | const setDone = jest.fn();
178 | const wrapper1 = shallow();
179 | const wrapper2 = shallow();
180 |
181 | it('should render correctly', () => {
182 | expect(wrapper1).toMatchSnapshot();
183 | expect(wrapper2).toMatchSnapshot();
184 | });
185 |
186 | it('should call setDone correctly', () => {
187 | wrapper1.find('.todo__checkbox').simulate('change');
188 | expect(setDone).toBeCalledWith(testTodo1.id);
189 | expect(setDone).toHaveBeenCalledTimes(1);
190 | wrapper2.find('.todo__checkbox').simulate('change');
191 | expect(setDone).toBeCalledWith(testTodo2.id);
192 | expect(setDone).toHaveBeenCalledTimes(1);
193 | });
194 | });
195 | ```
196 | where the only difference is that we create two renders and as the `setDone` function shouldn't be called when the **Todo** is already done we ensure that it behaves as such.
197 |
198 | ### PageNotFound
199 |
200 | For `PageNotFound` we only need to make a simple snapshot-test
201 | ```typescript
202 | import * as React from 'react';
203 | import { shallow } from 'enzyme';
204 | import PageNotFound from '../PageNotFound';
205 |
206 | describe('PageNotFound', () => (
207 | it('should render correctly', () => (
208 | expect(shallow()).toMatchSnapshot()
209 | ))
210 | ));
211 | ```
212 |
213 | ### IndexView
214 |
215 | Next up we create tests for our `IndexView`
216 | ```typescript
217 | import * as React from 'react';
218 | import { shallow } from 'enzyme';
219 | import IndexView from '../IndexView';
220 | import Todo from '../../../common/Todo';
221 |
222 | describe('IndexView', () => {
223 | const testTodo1 = new Todo(0, 'title');
224 | const testTodo2 = new Todo(1, 'testing', true);
225 | const testTitle = 'A title';
226 | const testSetTitle = jest.fn();
227 | const testSaveTodo = jest.fn();
228 | const testSetDone = jest.fn();
229 | const wrapperMinimalProps = shallow((
230 |
241 | ));
242 | const wrapperMaximumProps = shallow((
243 |
254 | ));
255 |
256 | it('should render with correct props', () => {
257 | expect(wrapperMinimalProps).toMatchSnapshot();
258 | expect(wrapperMaximumProps).toMatchSnapshot();
259 | });
260 |
261 | it('should call the correct functions when typing to input field', () => {
262 | const testValue = 'A_TEST_VALUE';
263 | wrapperMinimalProps.find('[type="text"]').simulate('change', { target: { value: testValue }});
264 | expect(testSetTitle).toBeCalledWith(testValue);
265 | });
266 | });
267 | ```
268 | which is a bit more complex, but in essence built on the same things our previous tests were.
269 |
270 | ---
271 |
272 | The biggest difference here is that for the simulation function
273 | ```typescript
274 | it('should call the correct functions when typing to input field', () => {
275 | const testValue = 'A_TEST_VALUE';
276 | wrapperMinimalProps.find('[type="text"]').simulate('change', { target: { value: testValue }});
277 | expect(testSetTitle).toBeCalledWith(testValue);
278 | });
279 | ```
280 | where we give the `change` function a value to send with the event and test that it goes to the correct function.
281 |
282 | ### IndexReducer
283 |
284 | For the `IndexReducer` we are not going to use **enzyme**, but rather normal **Jest** functions
285 | ```typescript
286 | import { ActionsObservable } from 'redux-observable';
287 | import Todo from '../../../common/Todo';
288 | import IndexReducer, {
289 | SET_TITLE,
290 | setTitle,
291 | SAVE_TODO,
292 | saveTodo,
293 | saveTodoEpic,
294 | SAVE_TODO_SUCCESS,
295 | saveTodoSuccess,
296 | SET_DONE,
297 | setDone,
298 | setDoneEpic,
299 | SET_DONE_SUCCESS,
300 | setDoneSuccess,
301 | IndexState,
302 | } from '../IndexReducer';
303 |
304 | describe('IndexReducer', () => {
305 | it('should set the correct title as payload on setTitle', () => {
306 | const payload = 'THIS_IS_A_TEST_TITLE';
307 | const setTitleAction = setTitle(payload);
308 | expect(setTitleAction).toEqual({ type: SET_TITLE, payload });
309 | const newState: IndexState = IndexReducer(undefined, setTitleAction);
310 | expect(newState.title).toEqual(payload);
311 | });
312 |
313 | it('should set the correct values on saveTodo', () => {
314 | const saveTodoAction = saveTodo();
315 | expect(saveTodoAction).toEqual({ type: SAVE_TODO });
316 | const newState: IndexState = IndexReducer(undefined, saveTodoAction);
317 | expect(newState.loading).toBeTruthy();
318 | });
319 |
320 | it('should trigger the correct action on saveTodoEpic', async () =>
321 | await saveTodoEpic(ActionsObservable.of(saveTodo()), undefined, undefined).forEach(actionReceived =>
322 | expect(actionReceived).toEqual({ type: SAVE_TODO_SUCCESS }),
323 | ));
324 |
325 | it('should set the correct values on saveTodoSuccess', () => {
326 | /* tslint:disable:no-magic-numbers */
327 | const testT = new Todo(1, 'Doing', true);
328 | const initialState: IndexState = { title: 'TEST', todos: [testT], loading: true };
329 | const saveTodoSuccessAction = saveTodoSuccess();
330 | expect(saveTodoSuccessAction).toEqual({ type: SAVE_TODO_SUCCESS });
331 | const newState: IndexState = IndexReducer(initialState, saveTodoSuccessAction);
332 | expect(newState.title).toEqual('');
333 | expect(newState.todos.length).toEqual(2);
334 | expect(newState.todos[1].done).toBeFalsy();
335 | expect(newState.todos[1].id).toEqual(2);
336 | expect(newState.todos[1].title).toEqual(initialState.title);
337 | expect(newState.loading).toBeFalsy();
338 | /* tslint:enable:no-magic-numbers */
339 | });
340 |
341 | it('should set the correct values on setDone', () => {
342 | const setDoneAction = setDone(0);
343 | expect(setDoneAction).toEqual({ type: SET_DONE, payload: 0 });
344 | const newState: IndexState = IndexReducer(undefined, setDoneAction);
345 | expect(newState.loading).toBeTruthy();
346 | });
347 |
348 | it('should trigger the correct action on setDoneEpic', async () =>
349 | await setDoneEpic(ActionsObservable.of(setDone(0)), undefined, undefined).forEach(actionReceived =>
350 | expect(actionReceived).toEqual({ type: SET_DONE_SUCCESS, payload: 0 }),
351 | ));
352 |
353 | it('should set the correct values on setDoneSuccess', () => {
354 | const initialState: IndexState = { title: '', todos: [new Todo(0, '')], loading: true };
355 | const setDoneSuccessAction = setDoneSuccess(0);
356 | expect(setDoneSuccessAction).toEqual({ type: SET_DONE_SUCCESS, payload: 0 });
357 | const newState: IndexState = IndexReducer(initialState, setDoneSuccessAction);
358 | expect(newState.loading).toBeFalsy();
359 | expect(newState.todos[0].done).toBeTruthy();
360 | expect(newState.todos[0].id).toEqual(initialState.todos[0].id);
361 | expect(newState.todos[0].title).toEqual(initialState.todos[0].title);
362 | });
363 | });
364 | ```
365 | where we test each of our **action creator**-functions and **epics** (*we validate our `reducer` itself via the **action creators***).
366 | > I also highly recommend always explicitly setting the type of values in your tests, as accidentally changing the values in your code will result in a compiler error then.
367 |
368 | ---
369 |
370 | For the first **action creator** `setTitle` we do it as follows
371 | ```typescript
372 | import IndexReducer, { SET_TITLE, setTitle } from '../IndexReducer';
373 | // ...
374 | it('should set the correct title as payload on setTitle', () => {
375 | const payload = 'THIS_IS_A_TEST_TITLE';
376 | const setTitleAction = setTitle(payload);
377 | expect(setTitleAction).toEqual({ type: SET_TITLE, payload });
378 | const newState = IndexReducer(undefined, setTitleAction);
379 | expect(newState.title).toEqual(payload);
380 | });
381 | ```
382 | where we first check that it has the correct return values and types and then test that running it through our `IndexReducer` has the desired effects. The same is done for `saveTodo`, `saveTodoSuccess`, `setDone` and `setDoneSuccess`.
383 |
384 | ---
385 |
386 | Possibly the most important part of our testing is testing our **epics**, for example in the case of our `saveTodoEpic`
387 | ```typescript
388 | import { ActionsObservable } from 'redux-observable';
389 | import { SAVE_TODO_SUCCESS, saveTodo } from '../IndexReducer';
390 | // ...
391 | it('should trigger the correct action on saveTodoEpic', async () => (
392 | await saveTodoEpic(ActionsObservable.of(saveTodo()), undefined, undefined)
393 | .forEach(actionReceived => expect(actionReceived).toEqual({ type: SAVE_TODO_SUCCESS }));
394 | ));
395 | ```
396 | where we have to use an [`async`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) function, as **Observables** are not synchronous. We use `ActionsObservable.of` to create an **Observable** out of our **action creator** and give our **epic** an `undefined` as the second argument (*which, if you remember is defined as a type of `undefined` in `IndexReducer`*) and as the third one, as we don't use that feature. After that we [`subscribe`](http://reactivex.io/documentation/operators/subscribe.html) to our new **Observable** returned by `saveTodoEpic` and check that the action received matches what we expect.
397 |
398 | ---
399 |
400 | In our case we don't have any AJAX calls, but most likely you will require them, so to test them you would do something like this
401 | ```typescript
402 | import * as nock from 'nock';
403 | import { ActionsObservable } from 'redux-observable';
404 | import { State } '../../../redux/reducer';
405 | import IndexReducer, {
406 | fetchTodo,
407 | fetchTodoSuccess,
408 | fetchTodoFail,
409 | fetchTodoEpic,
410 | } from '../IndexReducer';
411 |
412 | describe('fetchTodoEpic', () => {
413 | afterEach(() => {
414 | nock.cleanAll();
415 | });
416 | it('should return the correct effects', async () => {
417 | const payload = 'Todo';
418 | nock('path/to/endpoint')
419 | .get('/lastparam')
420 | .reply(200, payload, { 'Content-Type': 'application/json' });
421 | return await fetchTagsEpic(ActionsObservable.of(fetchTodo()), { getState: () => new State(), dispatch: () => {} }, undefined)
422 | .forEach(actionReceived => expect(actionReceived).toEqual(fetchTodoSuccess(payload)));
423 | });
424 | it('should fail correctly', async () => {
425 | const payload = 'ERROR';
426 | nock('path/to/endpoint')
427 | .get('/lastparam')
428 | .replyWithError(payload);
429 | return await fetchTagsEpic(ActionsObservable.of(fetchTodo()), { getState: () => new State(), dispatch: () => {} }, undefined)
430 | .forEach(actionReceived => expect(actionReceived).toEqual(fetchTodoFail(payload)));
431 | });
432 | });
433 | ```
434 | where we use [nock](https://github.com/node-nock/nock) to mock the API responses.
435 |
436 | ### AppView
437 |
438 | We also add simple snapshot tests for our `AppView`
439 | ```typescript
440 | import * as React from 'react';
441 | import { shallow } from 'enzyme';
442 | import createHistory from 'history/createBrowserHistory';
443 | import AppView from '../AppView';
444 |
445 | describe('AppView', () => {
446 | const index = (
447 |
452 | );
453 | const notFound = (
454 |
459 | );
460 |
461 | it('should render correctly', () => {
462 | expect(shallow(index)).toMatchSnapshot();
463 | expect(shallow(notFound)).toMatchSnapshot();
464 | });
465 | });
466 | ```
467 |
468 | ### Scripts
469 |
470 | Finally we also want to run our tests! So back into our `package.json`
471 | ```json
472 | "scripts": {
473 | "test": "concurrently --kill-others-on-fail -p \"{name}\" -n \"SASS-LINT,TS-LINT,JEST\" -c \"bgBlue,bgMagenta,bgCyan\" \"yarn lint:sass\" \"yarn lint:ts\" \"jest\"",
474 | "test:watch": "jest --watch",
475 | "test:ci": "yarn run lint:sass && yarn run lint:ts && jest --runInBand --forceExit",
476 | }
477 | ```
478 | where the first command `test` will run our `lint`-scripts and **Jest** with its default configuration (*`--kill-others-on-fail` will kill all three running processes if one test process fails*). `test:watch` will run **Jest** in watch mode, so when you're working on your tests, it will only run the ones your changes affect, saving time. The last one `test:ci` adds a couple of flags to the **Jest** command, `--runInBand` which will run all tests in a single process (*easier to spot errors*) and `--forceExit` to ensure **Jest** will shut down after tests (*[Travis](https://travis-ci.org) can freeze without this sometimes*).
479 |
480 | ### Alternatives
481 |
482 | - Possibly the best known alternative to **Jest** is [Mocha](https://mochajs.org/)
483 | - [Jasmine](https://jasmine.github.io/) is one of the tools often used to extend **Mocha**
484 | - [Chai](http://chaijs.com/) is another one of them
485 | - And so is [Sinon](http://sinonjs.org/)
486 |
--------------------------------------------------------------------------------
/docs/VIEWS.md:
--------------------------------------------------------------------------------
1 | # Views
2 |
3 | Now we are ready to start working towards the beef of the application: different pages (or views).
4 |
5 | ### IndexView
6 |
7 | We will begin by creating a file called `IndexView.tsx` (*remember that 'x' in the end of the file type means that it contains [jsx](https://facebook.github.io/react/docs/jsx-in-depth.html)*) inside a folder called `index` inside the `components`-folder:
8 | > All of our pages will be inside folders named after the page, in this case **Index** and the view will be named `[Pagename]View.tsx`
9 |
10 | ```typescript
11 | import * as React from 'react';
12 | import Todo from '../../common/Todo';
13 | import TodoComponent from '../../components/TodoComponent';
14 | import Button from '../../components/Button';
15 | import Loader from '../../components/Loader';
16 |
17 | interface IIndexState {
18 | title: string;
19 | todos: Todo[];
20 | loading: boolean;
21 | }
22 |
23 | interface IIndexDispatch {
24 | setTitle(n: string): void;
25 | saveTodo(): void;
26 | setDone(i: number): void;
27 | }
28 |
29 | type IIndexProps = IIndexState & IIndexDispatch;
30 |
31 | const IndexView: React.StatelessComponent = ({ title, todos, loading, setTitle, saveTodo, setDone }) => (
32 |
33 | {loading && }
34 |
Todo app
35 |
47 |
48 |
49 | {todos.map(t => )}
50 |
51 |
52 | );
53 |
54 | export default IndexView;
55 | ```
56 |
57 | ---
58 |
59 | The first [interface](https://www.typescriptlang.org/docs/handbook/interfaces.html) we declare, called `IIndexState`
60 | ```typescript
61 | import Todo from '../../common/Todo';
62 | interface IIndexState {
63 | title: string;
64 | todos: Todo[];
65 | loading: boolean;
66 | }
67 | ```
68 | is an `interface` that holds the "state"-values for our `View`, i.e. the stuff that defines what is shown to the user, in this case a title (*the title of the current `Todo` being created*), a list of `Todo`s (*the current `Todo`s*) and a boolean that indicates whether or not some kind of long-taking call is currently running.
69 |
70 | ---
71 |
72 | The second `interface` we declare, called `IIndexDispatch`
73 | ```typescript
74 | interface IIndexDispatch {
75 | setTitle(n: string): void;
76 | saveTodo(): void;
77 | setDone(i: number): void;
78 | }
79 | ```
80 | is an `interface` that holds all the "dispatch"-functions for our `View`, i.e. all the functionality that our users can trigger, in this case a function (`setTitle(n: string)`) to change the current `title` (*that takes a string as an argument*), a function (`saveTodo()`) to save the new `Todo` and a function (`setDone(i: number)`) to set an existing `Todo` as done (*that takes the number of the `Todo` as argument*).
81 | > Having [`void`](https://www.typescriptlang.org/docs/handbook/basic-types.html) as a return type allows us to not care about the return type (*which we don't need here*) without having an implicit any
82 |
83 | ---
84 |
85 | Together `IIndexState` and `IIndexDispatch` form the type `IIndexProps`
86 | ```typescript
87 | type IIndexProps = IIndexState & IIndexDispatch;
88 | ```
89 | which is the definition of all the [props](https://facebook.github.io/react/docs/components-and-props.html) our `IndexView` will need to function.
90 | > The `IIndexState & IIndexDispatch` part defines that the type called `IIndexProps` is an [intersection](https://www.typescriptlang.org/docs/handbook/advanced-types.html) of those two interfaces, meaning that to fulfill the type, the object needs to have all values present in both of the aforementioned interfaces.
91 |
92 | ---
93 |
94 | And now we get to the beef of it all, starting with declaring the actual `IndexView`
95 | ```typescript
96 | import * as React from 'react';
97 | const IndexView: React.StatelessComponent = ({ title, todos, loading, setTitle, saveTodo, setDone }) => (
98 | );
99 | ```
100 | in which we declare a [constant](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const) called `IndexView` which is of the type [`React.StatelessComponent`](https://hackernoon.com/react-stateless-functional-components-nine-wins-you-might-have-overlooked-997b0d933dbc). `React.StatelessComponent` is a type of **React Component** that does not have an internal state, only **props** and can be declared as a function that returns `jsx`, where the type of **props** received is put inside the angle brackets (*where `T` is now*).
101 | > These kinds of declarations are called [type arguments or generics](https://www.typescriptlang.org/docs/handbook/generics.html), which allow you to define a function without knowing beforehand what the type of the argument or return value is.
102 |
103 | After declaring the **const** we define `IndexView` to be a function, that fulfills the [render](https://facebook.github.io/react/docs/react-api.html) function, i.e. takes in an object containing the declared properties (*using [object destructuring](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) so we don't have to write `something.title`, instead of `title`*) and returns `jsx` encapsulated in the brackets.
104 |
105 | ---
106 |
107 | For the actual content inside the brackets you can think of it as "HTML on steroids", beginning, as is semantically correct with a [`main`](https://developer.mozilla.org/en/docs/Web/HTML/Element/main) tag
108 | ```typescript
109 |
110 |
111 | ```
112 | which will hold all the content of our `IndexView` (*and here we begin the introduction of [BEM](http://getbem.com/naming/)-naming*).
113 |
114 | ---
115 |
116 | Next is our first tag that is different with different props
117 | ```typescript
118 | import Loader from '../../components/Loader';
119 | // ...
120 |
121 | {loading && }
122 |
123 | ```
124 | which means, that depending if the **prop** `loading` is true (*using [logical operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators)-shorthand that returns the right hand-side if the both sides evaluate as true*), we want to **render** the `Loader`-component we defined earlier in the [Components](/COMPONENTS.md) section.
125 |
126 | ---
127 |
128 | Next up we define the title and a form to add a new `Todo`
129 | ```typescript
130 |
131 | // ...
132 |
133 |
Todo app
134 |
146 |
147 | ```
148 | where we first define the title (*in this case a static string `Todo app`*). Next we define a form to create new `Todos` which introduces us to a lot of nice features of **React**, but first we do a little *"hack"* as we want to enable the user to press the `enter`-key when submitting a new `Todo` but we don't want to send the form to a new page, we set the value of `onSubmit` (*the handler for a form's submit method in React*) as `e => e.preventDefault()` where `e` is the event received and it's function `.preventDefault()` will (*as its name implies*) prevent the default functionality (*in this case send the form data to the current url, causing a reload*).
149 |
150 | Next we define a label for the `input`-field utilizing **React**'s `htmlFor`-value which is an alias for the [for](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label)-attribute, except it uses the `name`-field instead of `id` for matching. Then we define the actual `input`-field for inputting a `Todo`, which is a simple text `input`, but we also make it [autofocus](https://developer.mozilla.org/en/docs/Web/HTML/Element/input) (*if you open the page, you can start typing into it directly*) and make it a [controlled component](https://facebook.github.io/react/docs/forms.html#controlled-components), meaning that whenever the value of `title` changes, the `input` will also update. We also add the `onChange`-listener to actually set the new `title` the user has typed in. Finally we create a simple `Button` (*we defined earlier in [components](/COMPONENTS.md#button)*) to submit the form.
151 |
152 | ---
153 |
154 | Finally we want to of course show all the `Todo`s created
155 | ```typescript
156 |
157 | // ...
158 |
159 | {todos.map(t => )}
160 |
161 |
162 | ```
163 | where we first encapsulate it into a semantic tag called [`section`](https://developer.mozilla.org/en/docs/Web/HTML/Element/section). Then we want to create a new `TodoComponent` (*as we defined in [components](/COMPONENTS.md#todocomponent)*) for every `Todo` in our current state, which can be achieved by a simple [`map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) (*I highly recommend getting to know all the major Array-functions*). Here you want to also remember to add a property for our `TodoComponent` it isn't expecting to receive: [`key`](https://facebook.github.io/react/docs/lists-and-keys.html) which **React** uses to distinguish between tags in a list (*it has to be unique inside the list*).
164 |
165 | ### Alternatives
166 |
167 | - If you want something a bit simpler you can try [Vue.js](https://vuejs.org/)
168 | - Or the classic [jQuery](https://jquery.com/)
169 |
--------------------------------------------------------------------------------
/docs/WEBPACK.md:
--------------------------------------------------------------------------------
1 | # webpack
2 |
3 | Now as we have everything necessary for our application, it's time to get it working!
4 |
5 | ### Initialize
6 |
7 | We'll begin by installing a "couple" of new dependencies
8 | ```
9 | yarn add -D webpack webpack-dev-server source-map-loader react-hot-loader copy-webpack-plugin babel babel-cli babel-core babel-loader babel-preset-env awesome-typescript-loader html-webpack-plugin
10 | ```
11 | from which [webpack](https://webpack.js.org/) is a very powerful tool for bundling and configuration, [webpack-dev-server](https://webpack.js.org/configuration/dev-server/#src/components/Sidebar/Sidebar.jsx) is a lightweight server to host client code, [source-map-loader](https://webpack.js.org/loaders/source-map-loader/#src/components/Sidebar/Sidebar.jsx) to allow errors and other messages to be pointed to the right source code lines while developing, [react-hot-loader](https://github.com/gaearon/react-hot-loader) to allow for [Hot Module Replcement (HMR)](https://webpack.js.org/concepts/hot-module-replacement/), [copy-webpack-plugin](https://webpack.js.org/plugins/copy-webpack-plugin/#src/components/Sidebar/Sidebar.jsx) to copy our `index.html` over to **webpack**'s process, [babel](https://babeljs.io) and it's plugins to speed up our process and [awesome-typescript-loader](https://github.com/s-panferov/awesome-typescript-loader) to use **webpack** with **TypeScript**. Finally we also add [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) to generate an `index.html` for us in the build phase.
12 |
13 | ---
14 |
15 | First we need a couple of configuration files for **webpack** to get started. (*There are a few ways to have multiple different configurations for **webpack** simultaneously and the way I do it here is just one of them.*) Begin by creating a file in your root folder called `webpack.config.js`, which will look like this:
16 | ```javascript
17 | module.exports = function(env) {
18 | return require('./webpack.' + env + '.js');
19 | }
20 | ```
21 | This file is simply a way to call one configuration file from our script and provide it with an environment variable that will call the corresponding configuration.
22 |
23 | ---
24 |
25 | To actually use **Babel** we need to give it a simple configuration inside our root folder within a file called `.babelrc`:
26 | ```json
27 | {
28 | "presets": ["babel-preset-env"],
29 | "plugins": ["react-hot-loader/babel"]
30 | }
31 | ```
32 | where the `presets` gives us automatic compilation down to ES5 via [`babel-preset-env`](https://github.com/babel/babel/tree/master/packages/babel-preset-env) and the `plugins` allows **Babel** to work together with our **Hot Module Replacement**.
33 |
34 | ### Development
35 |
36 | For our development configuration we will need another file in our root folder, called `webpack.dev.js` so create the file and type in the following:
37 | ```javascript
38 | var path = require('path');
39 | var webpack = require('webpack');
40 | var CopyWebpackPlugin = require('copy-webpack-plugin');
41 |
42 | module.exports = {
43 | context: __dirname,
44 | entry: [
45 | 'react-hot-loader/patch',
46 | 'webpack-dev-server/client?http://localhost:9966',
47 | 'webpack/hot/only-dev-server',
48 | path.resolve(__dirname, 'src', 'index.tsx')
49 | ],
50 | output: {
51 | filename: 'bundle.js',
52 | path: path.resolve(__dirname, 'dist', 'js'),
53 | publicPath: '/'
54 | },
55 | devtool: 'source-map',
56 | devServer: {
57 | hot: true,
58 | contentBase: path.resolve(__dirname, 'dist'),
59 | publicPath: '/',
60 | historyApiFallback: true
61 | },
62 | module: {
63 | rules: [
64 | {
65 | enforce: 'pre',
66 | test: /\.js$/,
67 | loader: 'source-map-loader'
68 | },
69 | {
70 | test: /\.tsx?$/,
71 | use: ['react-hot-loader/webpack', 'awesome-typescript-loader'],
72 | exclude: /node_modules/
73 | }
74 | ]
75 | },
76 | resolve: {
77 | extensions: ['.tsx', '.ts', '.js']
78 | },
79 | plugins: [
80 | new CopyWebpackPlugin([{ from: path.resolve(__dirname, 'index.html') }]),
81 | new webpack.EnvironmentPlugin({ 'NODE_ENV': 'development' }),
82 | new webpack.HotModuleReplacementPlugin(),
83 | new webpack.NamedModulesPlugin(),
84 | new webpack.NoEmitOnErrorsPlugin()
85 | ]
86 | };
87 | ```
88 |
89 | Now there is a lot to digest, but we'll go through the sections one by one.
90 |
91 | The first section after `module.exports` (*which just tells JavaScript that when this file is called, we want the following lines to be called*), `context`, simply gives our configuration a location context to work in, in this case `__dirname`, which is a variable **Node.JS** gives us to point to our current location.
92 |
93 | The second section gets a little more interesting, `entry`, which contains a few lines of strings. The first tells **webpack** to use the `patch`-mode from `react-hot-loader`, meaning that we do not have to refresh the whole page when something changes in our code, only the affected parts will be updated. The second line `webpack-dev-server/client?http://localhost:9966` tells webpack where we want our `webpack-dev-server` to run (*in this case inside port `9966`*). The third line `webpack/hot/only-dev-server` tells `webpack-dev-server` that we want to do **HMR**. Finally the fourth line `path.resolve(__dirname, 'src', 'index.tsx')` is the most important one, as it tells *webpack* where our application actually starts from, a.k.a. the *entry* file.
94 |
95 | The third section, `output` tells **webpack** where we want our compiled and bundled code to come out to. The first line `filename` tells **webpack** to store our bundled file as `bundle.js`. The second line configures the location for our `bundle.js` under `path`, in this case a folder `js` inside `dist`. Finally the [`publicPath`](https://webpack.js.org/guides/public-path/) variable sets the base path for all assets (such as images and stylesheets) for your application, in this case the same as the context folder.
96 |
97 | The fourth section [`devtool`](https://webpack.js.org/configuration/devtool/#src/components/Sidebar/Sidebar.jsx) is a simple section, which sets if and how source maps are generated, in our case, using `source-map-loader`.
98 |
99 | The fifth section [`devServer`](https://webpack.js.org/configuration/dev-server/) allows configuration for the development server. `hot` defines whether we want **HMR** or not (in our case yes), `contentBase` tells the server where to serve content from, in this case the `dist`-folder. `publicPath` defines where our files are accessible from the browser, in this case from the root. `historyApiFallback` will allow the server to serve `index.html` even if any other path is requested, as is the case when using **react-router**.
100 |
101 | The sixth section [`module`](https://webpack.js.org/configuration/module/) defines how different types of files are processed. In our case we have two different `rules`, one for **JavaScript** files and one for **TypeScript** files. The one for **TypeScript** files consists of a `test`, to see whether we want to use this rule or not, a `use`, meaning which loaders to use and an `exclude`, to determine excluded files. The **JavaScript** `rule` is `enforce`d as `pre`, meaning it'll come into affect for files coming out of other rules (*a.k.a. our compiled **TypeScript** files*) and will be passed through to `source-map-loader`.
102 |
103 | The seventh section [`resolve`](https://webpack.js.org/configuration/resolve/) allows us to define `extensions` that will be resolved automatically. This means that in our source code when we `import` a file that ends in `.tsx`, `.ts` or `.js` we don't need to write the file extension type.
104 |
105 | The final section [`plugins`](https://webpack.js.org/configuration/plugins/) allows for customization of the **webpack** build process. The line `new CopyWebpackPlugin([{ from: path.resolve(__dirname, 'index.html') }])` copies our `index.html` to the same folder as our `bundle.js`. `new webpack.EnvironmentPlugin({ 'NODE_ENV': 'development' })` sets the [`NODE_ENV`](https://dzone.com/articles/what-you-should-know-about-node-env) environment variable to `development` so our code is not minimized. [`new webpack.HotModuleReplacementPlugin()`](https://webpack.js.org/plugins/hot-module-replacement-plugin/) enables **HMR**. [`new webpack.NamedModulesPlugin()`](https://webpack.js.org/plugins/named-modules-plugin/) shows the relative path of modules when using **HMR** and [`new webpack.NoEmitOnErrorsPlugin()`](https://webpack.js.org/plugins/no-emit-on-errors-plugin/) will ensure no errors are emitted when calling non-existent assets in development.
106 |
107 | ---
108 |
109 | Now that we have our configuration file for the development process, we need to write a script for it, so head on over to your `package.json`-file and add the following lines to `scripts`:
110 | ```json
111 | // ...
112 | "scripts": {
113 | "clean": "rm -rf dist",
114 | "develop": "yarn clean && webpack-dev-server -d --env=dev --colors --port 9966",
115 | }
116 | ```
117 | where `clean` removes our build folder and after that `develop` starts our development server inside port `9966` so go ahead and run it `yarn develop` and open up `http://localhost:9966` inside your browser.
118 |
119 | ### Production
120 |
121 | Next up we want to build our application to be hosted on the Internet, so we begin by creating a configuration file for webpack to use with `production` called `webpack.prod.js` (in our root folder):
122 | ```javascript
123 | var path = require('path');
124 | var webpack = require('webpack');
125 | var HtmlWebpackPlugin = require('html-webpack-plugin');
126 |
127 | module.exports = {
128 | context: __dirname,
129 | entry: path.resolve('src', 'index.tsx'),
130 | output: {
131 | filename: 'bundle.js',
132 | path: path.resolve('dist', 'assets')
133 | },
134 | module: {
135 | rules: [
136 | {
137 | test: /\.tsx?$/,
138 | use: ['babel-loader', 'awesome-typescript-loader'],
139 | exclude: /node_modules/
140 | }
141 | ]
142 | },
143 | resolve: {
144 | extensions: ['.tsx', '.ts', '.js']
145 | },
146 | plugins: [
147 | new HtmlWebpackPlugin(),
148 | new webpack.DefinePlugin({
149 | 'process.env.NODE_ENV': JSON.stringify('production')
150 | })
151 | ]
152 | };
153 | ```
154 |
155 | The first two sections are very similar to our `development`-configuration, as the `context` is the same, and from `entry` we have simply removed the lines related to **HMR**.
156 |
157 | The third section `output` also has the same filename as before, but we have now changed the path where bundled assets should be stored.
158 |
159 | For the fourth section `module` we have removed the `source-map-loader` and replaced the `react-hot-loader` in **TypeScript** files with `babel-loader`.
160 |
161 | The fifth section `resolve` is identical to our development configuration and for the final section `plugins` we have removed most of the plugins and added the `HtmlWebpackPlugin` which will create an `index.html` for us. We have also set the `NODE_ENV` environment variable to `production`.
162 |
163 | ---
164 |
165 | Now that our configuration is complete, we need to make a script to actually build our files, so add the following line to your `package.json` inside the `scripts`-key:
166 | ```json
167 | // ...
168 | "scripts": {
169 | // ...
170 | "build": "yarn clean && webpack -d --env=prod --colors"
171 | }
172 | ```
173 | where we first delete our previous builds and then build again with the environment for `production`.
174 |
175 | ### Alternatives
176 |
177 | - An alternative for **webpack** is [browserify](http://browserify.org/), which I like more (as it is simpler and all configuration is in the scripts), but it does not have all the features I prefer, such as HMR with TypeScript
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | TS-React boilerplate
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "HN PWA",
3 | "short_name": "HN PWA",
4 | "description": "An example HN PWA with TypeScript and React",
5 | "lang": "en-US",
6 | "orientation": "portrait-primary",
7 | "start_url": "/",
8 | "icons": [
9 | {
10 | "src": "/assets/icons/android-chrome-192x192.png",
11 | "sizes": "192x192",
12 | "type": "image/png"
13 | },
14 | {
15 | "src": "/assets/icons/android-chrome-512x512.png",
16 | "sizes": "512x512",
17 | "type": "image/png"
18 | }
19 | ],
20 | "theme_color": "#FF8041",
21 | "background_color": "#FFFFFF",
22 | "display": "standalone"
23 | }
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ts-react-boilerplate",
3 | "version": "0.0.2",
4 | "description": "A very opinionated (React/TypeScript/Redux/etc) frontend boilerplate",
5 | "main": "dist/index.js",
6 | "author": "Lapanti",
7 | "license": "MIT",
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/Lapanti/ts-react-boilerplate.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/Lapanti/ts-react-boilerplate/issues"
14 | },
15 | "homepage": "https://github.com/Lapanti/ts-react-boilerplate#README.md",
16 | "jest": {
17 | "snapshotSerializers": [
18 | "/node_modules/enzyme-to-json/serializer"
19 | ],
20 | "transform": {
21 | ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js"
22 | },
23 | "testRegex": "(/__specs__/.*|\\.(spec))\\.(ts|tsx)$",
24 | "moduleFileExtensions": [
25 | "ts",
26 | "tsx",
27 | "js"
28 | ],
29 | "setupTestFrameworkScriptFile": "/src/jest/setup.js"
30 | },
31 | "scripts": {
32 | "precommit": "lint-staged",
33 | "prepush": "yarn test",
34 | "lint:sass": "sass-lint 'src/**/*.scss' -v --max-warnings 1",
35 | "lint:ts": "tslint --project tsconfig.json",
36 | "test": "concurrently --kill-others-on-fail -p \"{name}\" -n \"SASS-LINT,TS-LINT,JEST\" -c \"bgBlue,bgMagenta,bgCyan\" \"yarn lint:sass\" \"yarn lint:ts\" \"jest\"",
37 | "test:update": "jest --updateSnapshot",
38 | "test:watch": "jest --watch",
39 | "test:ci": "yarn lint:sass && yarn lint:ts && jest --runInBand --forceExit --coverage",
40 | "clean": "rm -rf dist",
41 | "develop": "yarn clean && webpack-dev-server -d --env=dev --colors --port 9966",
42 | "build:server": "webpack -d --env=server -p --colors",
43 | "build:client": "webpack -d --env=prod --colors",
44 | "build": "yarn clean && concurrently --kill-others-on-fail -p \"{name}\" -n \"SERVER,CLIENT\" -c \"bgBlue,bgMagenta\" \"yarn build:server\" \"yarn build:client\"",
45 | "start": "cd dist && NODE_ENV=production node server.js",
46 | "docs:clean": "rm -rf _book",
47 | "docs:prepare": "gitbook install",
48 | "docs:watch": "gitbook serve",
49 | "docs:build": "yarn docs:prepare && gitbook build -g Lapanti/ts-react-boilerplate",
50 | "docs:publish": "yarn docs:clean && yarn docs:build && cp CNAME _book && cd _book && git init && git commit --allow-empty -m \"Update book\" && git checkout -b gh-pages && touch .nojekyll && git add . && git commit -am \"Update book\" && git push git@github.com:Lapanti/ts-react-boilerplate gh-pages --force"
51 | },
52 | "lint-staged": {
53 | "*.{ts,tsx}": [
54 | "prettier --single-quote --print-width 120 --tab-width 4 --trailing-comma all --write",
55 | "git add"
56 | ],
57 | "*.scss": [
58 | "prettier --single-quote --print-width 120 --tab-width 4 --write",
59 | "git add"
60 | ]
61 | },
62 | "dependencies": {
63 | "@types/react-router-redux": "^5.0.11",
64 | "express": "^4.16.2",
65 | "history": "^4.7.2",
66 | "http-status-enum": "^1.0.2",
67 | "react": "^16.2.0",
68 | "react-dom": "^16.2.0",
69 | "react-redux": "^5.0.6",
70 | "react-router-dom": "^4.2.2",
71 | "react-router-redux": "^5.0.0-alpha.9",
72 | "redux": "^3.7.2",
73 | "redux-observable": "^0.17.0",
74 | "rxjs": "^5.5.6"
75 | },
76 | "devDependencies": {
77 | "@types/express": "^4.11.0",
78 | "@types/history": "^4.6.2",
79 | "@types/jest": "^22.0.1",
80 | "@types/nock": "^9.1.1",
81 | "@types/react": "^16.0.34",
82 | "@types/react-dom": "^16.0.3",
83 | "@types/react-hot-loader": "^3.0.5",
84 | "@types/react-redux": "^5.0.14",
85 | "@types/react-router-dom": "^4.2.3",
86 | "awesome-typescript-loader": "^4.0.0",
87 | "babel": "^6.23.0",
88 | "babel-cli": "^6.26.0",
89 | "babel-core": "^6.26.0",
90 | "babel-loader": "^7.1.2",
91 | "babel-preset-env": "^1.6.1",
92 | "concurrently": "^3.5.1",
93 | "copy-webpack-plugin": "^4.3.1",
94 | "coveralls": "^3.0.0",
95 | "css-loader": "^0.28.8",
96 | "enzyme": "^3.3.0",
97 | "enzyme-adapter-react-16": "^1.1.1",
98 | "enzyme-to-json": "^3.3.0",
99 | "extract-text-webpack-plugin": "^3.0.2",
100 | "gitbook-cli": "^2.3.2",
101 | "gitbook-plugin-advanced-emoji": "^0.2.2",
102 | "gitbook-plugin-anker-enable": "^0.0.4",
103 | "gitbook-plugin-edit-link": "^2.0.2",
104 | "gitbook-plugin-github": "^3.0.0",
105 | "gitbook-plugin-prism": "^2.3.0",
106 | "gitbook-plugin-theme-default": "^1.0.7",
107 | "husky": "^0.14.3",
108 | "jest": "^22.0.5",
109 | "lint-staged": "^7.0.0",
110 | "nock": "^9.1.6",
111 | "node-sass": "^4.7.2",
112 | "prettier": "^1.10.2",
113 | "react-hot-loader": "^4.0.0",
114 | "react-test-renderer": "^16.2.0",
115 | "sass-lint": "^1.12.1",
116 | "sass-loader": "^6.0.6",
117 | "source-map-loader": "^0.2.3",
118 | "style-loader": "^0.20.0",
119 | "ts-jest": "^22.0.1",
120 | "tslint": "^5.8.0",
121 | "tslint-config-prettier": "^1.6.0",
122 | "tslint-react": "^3.3.3",
123 | "typescript": "^2.6.2",
124 | "webpack": "^4.0.0",
125 | "webpack-dev-server": "^3.0.0"
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/common/Todo.ts:
--------------------------------------------------------------------------------
1 | export default class Todo {
2 | constructor(id: number, title: string, done?: boolean) {
3 | this.id = id;
4 | this.title = title;
5 | this.done = done || false;
6 | }
7 | readonly id: number;
8 | readonly title: string;
9 | readonly done: boolean;
10 |
11 | setDone = (): Todo => new Todo(this.id, this.title, true);
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export interface IButtonProps {
4 | click(): void;
5 | readonly text: string;
6 | }
7 |
8 | const Button: React.StatelessComponent = ({ click, text }) =>
9 | click()} value={text} />;
10 |
11 | export default Button;
12 |
--------------------------------------------------------------------------------
/src/components/Loader.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | const Loader: React.StatelessComponent = () =>
4 |