├── .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 | [![Greenkeeper badge](https://badges.greenkeeper.io/Lapanti/ts-react-boilerplate.svg)](https://greenkeeper.io/) 4 | [![Build Status](https://img.shields.io/travis/Lapanti/ts-react-boilerplate/master.svg?style=flat-square)](https://travis-ci.org/Lapanti/ts-react-boilerplate) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) [![Dependency Status](https://david-dm.org/lapanti/ts-react-boilerplate.svg?style=flat-square)](https://david-dm.org/lapanti/ts-react-boilerplate) [![DevDependency Status](https://img.shields.io/david/dev/lapanti/ts-react-boilerplate.svg?style=flat-square)](https://david-dm.org/lapanti/ts-react-boilerplate?type=dev) [![Coverage Status](https://img.shields.io/coveralls/Lapanti/ts-react-boilerplate/master.svg?style=flat-square)](https://coveralls.io/github/Lapanti/ts-react-boilerplate?branch=master) [![Code Climate](https://img.shields.io/codeclimate/issues/github/Lapanti/ts-react-boilerplate.svg?style=flat-square)](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 |
115 | !todo.done && setDone(todo.id)} 120 | /> 121 | {todo.id}: 122 | {todo.title} 123 |
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(