diff --git a/.babelrc b/.babelrc index 30ec41d..7b5ba33 100644 --- a/.babelrc +++ b/.babelrc @@ -1,29 +1,43 @@ { "env": { - // Compatibility Profile. ES5 output and CommonJS module format. - "commonjs": { + // Compatibility Profile. + // ES5 output and CommonJS module format. + "es5_cjs": { "presets": [ - ["env", { - "useBuiltIns": false - }], - "react" + "@babel/preset-env", + "@babel/preset-react" ] }, - // Future Profile. ES6 output and module syntax. + // Future Profile. + // ES6 output with no module transformation (ES Modules syntax). "es": { "presets": [ - ["env", { - "useBuiltIns": false, + ["@babel/preset-env", { "modules": false, "targets": { "node": "6.5" } }], - "react" + "@babel/preset-react" + ] + }, + // Bundled Profile. + // ES5 output and UMD module format. + "umd": { + "presets": [ + ["@babel/preset-env", { + "modules": false, + }], + "@babel/preset-react" ] }, + // Jest Profile. + // To be used by jest tests. "test": { - "presets": ["env", "react"] + "presets": [ + "@babel/preset-env", + "@babel/preset-react" + ] } } } diff --git a/.gitignore b/.gitignore index 8c00fd0..8539b49 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,30 @@ -node_modules -.DS_Store -dist/ -typings/ -*.orig +# Common aux folders +.awcache/ +.vscode/ .idea/ -*/src/**/*.js.map -yarn.lock -*.log +.cache/ + +# Dependencies & Build +node_modules/ +build/ lib/ +dist/ es/ + +# Aux files +*.cer +*.log +*/src/**/*.orig +*/src/**/*.js.map + +# Win +desktop.ini + +# MacOs +.DS_Store + +# Yarn +yarn.lock + +# NPM package-lock.json diff --git a/.vscode/launch.json b/.vscode/launch.json index 4974b0e..0ba6254 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,17 +1,48 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Jest single run", - "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", - "args": [ - "--verbose", - "-i" - ], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen" - }, - ] -} + +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Jest single run", + "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", + "args": [ + "--verbose", + "-i", + "--no-cache" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + { + "type": "node", + "request": "launch", + "name": "Jest watch run", + "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", + "args": [ + "--verbose", + "-i", + "--no-cache", + "--watchAll" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + { + "type": "node", + "request": "launch", + "name": "Jest current file", + "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", + "args": [ + "${fileBasename}", + "--verbose", + "-i", + "--no-cache", + "--watchAll" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ] +} diff --git a/config/test/polyfills.js b/config/test/polyfills.js new file mode 100644 index 0000000..0d9f7ef --- /dev/null +++ b/config/test/polyfills.js @@ -0,0 +1,2 @@ +// Polyfill requestAnimationFrame required by React >=16.0.0 +require('raf/polyfill'); diff --git a/test/jestsetup.js b/config/test/setupJest.js similarity index 100% rename from test/jestsetup.js rename to config/test/setupJest.js diff --git a/config/webpack/helpers.js b/config/webpack/helpers.js new file mode 100644 index 0000000..f122eed --- /dev/null +++ b/config/webpack/helpers.js @@ -0,0 +1,25 @@ +const path = require('path'); + +// String helpers +const capitalizeString = s => s.charAt(0).toUpperCase() + s.slice(1); +const camelCaseString = dashedName => dashedName.split("-").map( + (s, i) => i > 0 ? capitalizeString(s) : s +).join(""); + +// Name helpers +const packageName = process.env.npm_package_name; +const packageNameCamelCase = camelCaseString(packageName); +const version = JSON.stringify(process.env.npm_package_version).replace(/"/g, ''); +const getBundleFileName = min => `${packageName}-${version}${min ? ".min" : ''}.js`; + +// Path helpers +const rootPath = path.resolve(__dirname, "../.."); +const resolveFromRootPath = (...args) => path.join(rootPath, ...args); + +// Export constants +exports.srcPath = resolveFromRootPath("src"); +exports.buildPath = resolveFromRootPath("build",); +exports.distPath = resolveFromRootPath("build", "dist"); +exports.version = version; +exports.packageNameCamelCase = packageNameCamelCase; +exports.getBundleFileName = getBundleFileName; diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js new file mode 100644 index 0000000..07e04db --- /dev/null +++ b/config/webpack/webpack.common.js @@ -0,0 +1,30 @@ +const helpers = require('./helpers'); + +module.exports = (env, argv) => { + const minimizeBundle = Boolean(argv['optimize-minimize']); + + return { + entry: ['./src/index.js'], + output: { + path: helpers.distPath, + filename: helpers.getBundleFileName(minimizeBundle), + library: helpers.packageNameCamelCase, + libraryTarget: 'umd', + }, + externals: { + react: 'react', + }, + optimization: { + minimize: minimizeBundle, + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader', + }, + ], + }, + }; +}; diff --git a/config/webpack/webpack.dev.js b/config/webpack/webpack.dev.js new file mode 100644 index 0000000..864751c --- /dev/null +++ b/config/webpack/webpack.dev.js @@ -0,0 +1,7 @@ +const merge = require('webpack-merge'); +const commonConfig = require('./webpack.common.js'); + +module.exports = (env, argv) => merge(commonConfig(env, argv), { + mode: 'development', + devtool: 'inline-source-map', +}); diff --git a/config/webpack/webpack.prod.js b/config/webpack/webpack.prod.js new file mode 100644 index 0000000..52f6ed1 --- /dev/null +++ b/config/webpack/webpack.prod.js @@ -0,0 +1,17 @@ +const merge = require('webpack-merge'); +const commonConfig = require('./webpack.common.js'); +const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; +const CompressionPlugin = require('compression-webpack-plugin'); + +module.exports = (env, argv) => merge(commonConfig(env, argv), { + mode: 'production', + devtool: 'none', + plugins: [ + new BundleAnalyzerPlugin({ + analyzerMode: "static", + openAnalyzer: false, + reportFilename: "report/report.html", + }), + new CompressionPlugin(), + ], +}); diff --git a/examples/00-example-basic/.babelrc b/examples/00-example-basic/.babelrc new file mode 100644 index 0000000..ae31d35 --- /dev/null +++ b/examples/00-example-basic/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": [ + "@babel/preset-react", + [ + "@babel/preset-env", + { + "useBuiltIns": "entry" + } + ] + ] +} diff --git a/examples/00-example-basic/package.json b/examples/00-example-basic/package.json new file mode 100644 index 0000000..a6c9c3c --- /dev/null +++ b/examples/00-example-basic/package.json @@ -0,0 +1,34 @@ +{ + "name": "webpack-by-example", + "version": "1.0.0", + "description": "In this sample we are going to setup a web project that can be easily managed\r by webpack.", + "main": "index.js", + "scripts": { + "start": "webpack-dev-server --mode development --open", + "build": "rimraf dist && webpack --mode development", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@babel/cli": "^7.2.3", + "@babel/core": "^7.4.0", + "@babel/preset-env": "^7.4.0", + "@babel/preset-react": "^7.0.0", + "babel-loader": "^8.0.5", + "css-loader": "^2.1.1", + "html-webpack-plugin": "^3.2.0", + "mini-css-extract-plugin": "^0.5.0", + "rimraf": "^2.6.3", + "style-loader": "^0.23.1", + "webpack": "^4.29.6", + "webpack-cli": "^3.3.0", + "webpack-dev-server": "^3.2.1" + }, + "dependencies": { + "react": "^16.8.4", + "react-dom": "^16.8.4", + "react-loader-spinner": "^2.3.0", + "react-promise-tracker": "file:../../build" + } +} diff --git a/examples/00-example-basic/src/api/fetch.js b/examples/00-example-basic/src/api/fetch.js new file mode 100644 index 0000000..93dcfb0 --- /dev/null +++ b/examples/00-example-basic/src/api/fetch.js @@ -0,0 +1,12 @@ +export const fetchWithDelay = (url) => { + const promise = new Promise((resolve, reject) => { + setTimeout(() => { + resolve(fetch(url, { + method: 'GET', + }) + .then((response) => response.json())); + }, 3000) + }); + + return promise; +} \ No newline at end of file diff --git a/examples/00-example-basic/src/api/postAPI.js b/examples/00-example-basic/src/api/postAPI.js new file mode 100644 index 0000000..3831918 --- /dev/null +++ b/examples/00-example-basic/src/api/postAPI.js @@ -0,0 +1,9 @@ +import { fetchWithDelay } from './fetch'; +const url = 'https://jsonplaceholder.typicode.com/posts'; + +const fetchPosts = () => fetchWithDelay(url) + .then((posts) => posts.slice(0, 10)); + +export const postAPI = { + fetchPosts, +}; \ No newline at end of file diff --git a/examples/00-example-basic/src/api/userAPI.js b/examples/00-example-basic/src/api/userAPI.js new file mode 100644 index 0000000..17274bd --- /dev/null +++ b/examples/00-example-basic/src/api/userAPI.js @@ -0,0 +1,8 @@ +import { fetchWithDelay } from './fetch'; +const url = 'https://jsonplaceholder.typicode.com/users'; + +const fetchUsers = () => fetchWithDelay(url); + +export const userAPI = { + fetchUsers, +}; \ No newline at end of file diff --git a/examples/00-example-basic/src/app.css b/examples/00-example-basic/src/app.css new file mode 100644 index 0000000..db650a3 --- /dev/null +++ b/examples/00-example-basic/src/app.css @@ -0,0 +1,11 @@ +.tables { + display: flex; + flex-direction: row; + flex-wrap: nowrap; +} + +.tables > div { + flex-basis: 50%; + margin-left: 1rem; + margin-right: 1rem; +} \ No newline at end of file diff --git a/examples/00-example-basic/src/app.js b/examples/00-example-basic/src/app.js new file mode 100644 index 0000000..85a42cd --- /dev/null +++ b/examples/00-example-basic/src/app.js @@ -0,0 +1,59 @@ +import React, { Component } from 'react'; +import { trackPromise } from 'react-promise-tracker'; +import { userAPI } from './api/userAPI'; +import { postAPI } from './api/postAPI'; +import { UserTable, PostTable, LoadButton } from './components'; +import './app.css'; + +export class App extends Component { + constructor() { + super(); + + this.state = { + users: [], + posts: [], + }; + + this.onLoadTables = this.onLoadTables.bind(this); + } + + onLoadTables() { + this.setState({ + users: [], + posts: [], + }); + + trackPromise( + userAPI.fetchUsers() + .then((users) => { + this.setState({ + users, + }) + }) + ); + + trackPromise( + postAPI.fetchPosts() + .then((posts) => { + this.setState({ + posts, + }) + }) + ); + } + + render() { + return ( +
+ +
+ + +
+
+ ); + } +} diff --git a/examples/00-example-basic/src/common/components/spinner/index.js b/examples/00-example-basic/src/common/components/spinner/index.js new file mode 100644 index 0000000..dc50628 --- /dev/null +++ b/examples/00-example-basic/src/common/components/spinner/index.js @@ -0,0 +1 @@ +export * from './spinner'; \ No newline at end of file diff --git a/examples/00-example-basic/src/common/components/spinner/spinner.css b/examples/00-example-basic/src/common/components/spinner/spinner.css new file mode 100644 index 0000000..ebdff53 --- /dev/null +++ b/examples/00-example-basic/src/common/components/spinner/spinner.css @@ -0,0 +1,10 @@ +.spinner { + width: 100%; + height: 100%; +} + +.spinner > div { + display: flex; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/examples/00-example-basic/src/common/components/spinner/spinner.js b/examples/00-example-basic/src/common/components/spinner/spinner.js new file mode 100644 index 0000000..65ac951 --- /dev/null +++ b/examples/00-example-basic/src/common/components/spinner/spinner.js @@ -0,0 +1,17 @@ +import React from "react"; +import { usePromiseTracker } from "react-promise-tracker"; +import Loader from "react-loader-spinner"; +import "./spinner.css"; + +export const Spinner = (props) => { + const { promiseInProgress } = usePromiseTracker(); + + return ( + promiseInProgress && ( +
+ +
+ ) + ); +}; + diff --git a/examples/00-example-basic/src/common/components/table/index.js b/examples/00-example-basic/src/common/components/table/index.js new file mode 100644 index 0000000..759aa4e --- /dev/null +++ b/examples/00-example-basic/src/common/components/table/index.js @@ -0,0 +1 @@ +export * from './table' \ No newline at end of file diff --git a/examples/00-example-basic/src/common/components/table/table.css b/examples/00-example-basic/src/common/components/table/table.css new file mode 100644 index 0000000..738f080 --- /dev/null +++ b/examples/00-example-basic/src/common/components/table/table.css @@ -0,0 +1,27 @@ +.title { + color: #000; +} + +.table { + width: 100%; + max-width: 100%; + margin-bottom: 1rem; + background-color: transparent; + border-collapse: collapse; +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #248f4f; +} + +.table td, .table th { + text-align: inherit; + padding: .75rem; + vertical-align: top; + border-top: 1px solid #248f4f; +} + +.table tbody tr:nth-of-type(odd) { + background-color: rgba(43, 173, 96, .05); +} \ No newline at end of file diff --git a/examples/00-example-basic/src/common/components/table/table.js b/examples/00-example-basic/src/common/components/table/table.js new file mode 100644 index 0000000..de9e681 --- /dev/null +++ b/examples/00-example-basic/src/common/components/table/table.js @@ -0,0 +1,16 @@ +import React from 'react'; +import './table.css'; + +export const Table = (props) => ( +
+

{props.title}

+ + + {props.headerRender()} + + + {props.items.map(props.rowRender)} + +
+
+); diff --git a/examples/00-example-basic/src/components/index.js b/examples/00-example-basic/src/components/index.js new file mode 100644 index 0000000..6d29848 --- /dev/null +++ b/examples/00-example-basic/src/components/index.js @@ -0,0 +1,3 @@ +export * from './userTable'; +export * from './postTable'; +export * from './loadButton'; \ No newline at end of file diff --git a/examples/00-example-basic/src/components/loadButton/index.js b/examples/00-example-basic/src/components/loadButton/index.js new file mode 100644 index 0000000..0319437 --- /dev/null +++ b/examples/00-example-basic/src/components/loadButton/index.js @@ -0,0 +1 @@ +export * from './loadButton'; \ No newline at end of file diff --git a/examples/00-example-basic/src/components/loadButton/loadButton.css b/examples/00-example-basic/src/components/loadButton/loadButton.css new file mode 100644 index 0000000..39cbd1c --- /dev/null +++ b/examples/00-example-basic/src/components/loadButton/loadButton.css @@ -0,0 +1,26 @@ +.load-button { + cursor: pointer; + margin: 1rem; + color: #fff; + text-shadow: 1px 1px 0 #888; + background-color: #2BAD60; + border-color: #14522d; + display: inline-block; + font-weight: 400; + text-align: center; + white-space: nowrap; + vertical-align: middle; + border: 1px solid transparent; + padding: .375rem .75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: .25rem; + outline: none; +} + + + +.load-button:hover { + background-color: #248f4f; + border-color: #1e7b43; +} \ No newline at end of file diff --git a/examples/00-example-basic/src/components/loadButton/loadButton.js b/examples/00-example-basic/src/components/loadButton/loadButton.js new file mode 100644 index 0000000..f0cc6c0 --- /dev/null +++ b/examples/00-example-basic/src/components/loadButton/loadButton.js @@ -0,0 +1,11 @@ +import React from 'react'; +import './loadButton.css'; + +export const LoadButton = (props) => ( + +); \ No newline at end of file diff --git a/examples/00-example-basic/src/components/postTable/header.js b/examples/00-example-basic/src/components/postTable/header.js new file mode 100644 index 0000000..cd85184 --- /dev/null +++ b/examples/00-example-basic/src/components/postTable/header.js @@ -0,0 +1,9 @@ +import React from 'react'; + +export const Header = (props) => ( + + Id + Title + Body + +); \ No newline at end of file diff --git a/examples/00-example-basic/src/components/postTable/index.js b/examples/00-example-basic/src/components/postTable/index.js new file mode 100644 index 0000000..9834d57 --- /dev/null +++ b/examples/00-example-basic/src/components/postTable/index.js @@ -0,0 +1 @@ +export * from './table'; \ No newline at end of file diff --git a/examples/00-example-basic/src/components/postTable/row.js b/examples/00-example-basic/src/components/postTable/row.js new file mode 100644 index 0000000..b455df2 --- /dev/null +++ b/examples/00-example-basic/src/components/postTable/row.js @@ -0,0 +1,15 @@ +import React from 'react'; + +export const Row = (post) => ( + + + {post.id} + + + {post.title} + + + {post.body} + + +); \ No newline at end of file diff --git a/examples/00-example-basic/src/components/postTable/table.js b/examples/00-example-basic/src/components/postTable/table.js new file mode 100644 index 0000000..9ec572e --- /dev/null +++ b/examples/00-example-basic/src/components/postTable/table.js @@ -0,0 +1,13 @@ +import React, { Component } from 'react'; +import { Table } from '../../common/components/table'; +import { Header } from './header'; +import { Row } from './row'; + +export const PostTable = (props) => ( + +); diff --git a/examples/00-example-basic/src/components/userTable/header.js b/examples/00-example-basic/src/components/userTable/header.js new file mode 100644 index 0000000..0a5ad22 --- /dev/null +++ b/examples/00-example-basic/src/components/userTable/header.js @@ -0,0 +1,9 @@ +import React from 'react'; + +export const Header = (props) => ( + + + + + +); \ No newline at end of file diff --git a/examples/00-example-basic/src/components/userTable/index.js b/examples/00-example-basic/src/components/userTable/index.js new file mode 100644 index 0000000..9834d57 --- /dev/null +++ b/examples/00-example-basic/src/components/userTable/index.js @@ -0,0 +1 @@ +export * from './table'; \ No newline at end of file diff --git a/examples/00-example-basic/src/components/userTable/row.js b/examples/00-example-basic/src/components/userTable/row.js new file mode 100644 index 0000000..c76e3e1 --- /dev/null +++ b/examples/00-example-basic/src/components/userTable/row.js @@ -0,0 +1,15 @@ +import React from 'react'; + +export const Row = (user) => ( + + + + + +); \ No newline at end of file diff --git a/examples/00-example-basic/src/components/userTable/table.js b/examples/00-example-basic/src/components/userTable/table.js new file mode 100644 index 0000000..3cacf77 --- /dev/null +++ b/examples/00-example-basic/src/components/userTable/table.js @@ -0,0 +1,13 @@ +import React, { Component } from 'react'; +import { Table } from '../../common/components/table'; +import { Header } from './header'; +import { Row } from './row'; + +export const UserTable = (props) => ( +
IdNameEmail
+ {user.id} + + {user.name} + + {user.email} +
+); \ No newline at end of file diff --git a/examples/00-example-basic/src/index.html b/examples/00-example-basic/src/index.html new file mode 100644 index 0000000..2b604d2 --- /dev/null +++ b/examples/00-example-basic/src/index.html @@ -0,0 +1,12 @@ + + + + + + + React Promise tracker sample app + + +
+ + diff --git a/examples/00-example-basic/src/index.jsx b/examples/00-example-basic/src/index.jsx new file mode 100644 index 0000000..cd51e59 --- /dev/null +++ b/examples/00-example-basic/src/index.jsx @@ -0,0 +1,11 @@ +import React, { Component } from 'react'; +import { render } from 'react-dom'; +import { App } from './app'; +import { Spinner } from './common/components/spinner'; + +render( +
+ + +
, + document.getElementById('root')); diff --git a/examples/00-example-basic/webpack.config.js b/examples/00-example-basic/webpack.config.js new file mode 100644 index 0000000..4715f2c --- /dev/null +++ b/examples/00-example-basic/webpack.config.js @@ -0,0 +1,62 @@ +var HtmlWebpackPlugin = require("html-webpack-plugin"); +var webpack = require("webpack"); +var MiniCssExtractPlugin = require("mini-css-extract-plugin"); + +var path = require("path"); +var basePath = __dirname; + +module.exports = { + context: path.join(basePath, "src"), + resolve: { + extensions: [".js", ".jsx"], + alias: { + react: path.resolve('./node_modules/react'), + 'react-dom': path.resolve('./node_modules/react-dom') + }, + }, + entry: { + app: "./index.jsx", + vendor: ["react", "react-dom"], + }, + output: { + filename: "[name].[chunkhash].js" + }, + optimization: { + splitChunks: { + cacheGroups: { + vendor: { + chunks: "initial", + name: "vendor", + test: "vendor", + enforce: true + } + } + } + }, + devtool: 'inline-source-map', + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + loader: "babel-loader" + }, + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, "css-loader"] + } + ] + }, + plugins: [ + //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: "index.html", //Name of file in ./dist/ + template: "index.html", //Name of template in ./src + hash: true + }), + new MiniCssExtractPlugin({ + filename: "[name].css", + chunkFilename: "[id].css" + }) + ] +}; diff --git a/examples/01-example-areas/.babelrc b/examples/01-example-areas/.babelrc new file mode 100644 index 0000000..ae31d35 --- /dev/null +++ b/examples/01-example-areas/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": [ + "@babel/preset-react", + [ + "@babel/preset-env", + { + "useBuiltIns": "entry" + } + ] + ] +} diff --git a/examples/01-example-areas/package.json b/examples/01-example-areas/package.json new file mode 100644 index 0000000..a6c9c3c --- /dev/null +++ b/examples/01-example-areas/package.json @@ -0,0 +1,34 @@ +{ + "name": "webpack-by-example", + "version": "1.0.0", + "description": "In this sample we are going to setup a web project that can be easily managed\r by webpack.", + "main": "index.js", + "scripts": { + "start": "webpack-dev-server --mode development --open", + "build": "rimraf dist && webpack --mode development", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@babel/cli": "^7.2.3", + "@babel/core": "^7.4.0", + "@babel/preset-env": "^7.4.0", + "@babel/preset-react": "^7.0.0", + "babel-loader": "^8.0.5", + "css-loader": "^2.1.1", + "html-webpack-plugin": "^3.2.0", + "mini-css-extract-plugin": "^0.5.0", + "rimraf": "^2.6.3", + "style-loader": "^0.23.1", + "webpack": "^4.29.6", + "webpack-cli": "^3.3.0", + "webpack-dev-server": "^3.2.1" + }, + "dependencies": { + "react": "^16.8.4", + "react-dom": "^16.8.4", + "react-loader-spinner": "^2.3.0", + "react-promise-tracker": "file:../../build" + } +} diff --git a/examples/01-example-areas/src/api/fetch.js b/examples/01-example-areas/src/api/fetch.js new file mode 100644 index 0000000..93dcfb0 --- /dev/null +++ b/examples/01-example-areas/src/api/fetch.js @@ -0,0 +1,12 @@ +export const fetchWithDelay = (url) => { + const promise = new Promise((resolve, reject) => { + setTimeout(() => { + resolve(fetch(url, { + method: 'GET', + }) + .then((response) => response.json())); + }, 3000) + }); + + return promise; +} \ No newline at end of file diff --git a/examples/01-example-areas/src/api/postAPI.js b/examples/01-example-areas/src/api/postAPI.js new file mode 100644 index 0000000..3831918 --- /dev/null +++ b/examples/01-example-areas/src/api/postAPI.js @@ -0,0 +1,9 @@ +import { fetchWithDelay } from './fetch'; +const url = 'https://jsonplaceholder.typicode.com/posts'; + +const fetchPosts = () => fetchWithDelay(url) + .then((posts) => posts.slice(0, 10)); + +export const postAPI = { + fetchPosts, +}; \ No newline at end of file diff --git a/examples/01-example-areas/src/api/userAPI.js b/examples/01-example-areas/src/api/userAPI.js new file mode 100644 index 0000000..17274bd --- /dev/null +++ b/examples/01-example-areas/src/api/userAPI.js @@ -0,0 +1,8 @@ +import { fetchWithDelay } from './fetch'; +const url = 'https://jsonplaceholder.typicode.com/users'; + +const fetchUsers = () => fetchWithDelay(url); + +export const userAPI = { + fetchUsers, +}; \ No newline at end of file diff --git a/examples/01-example-areas/src/app.css b/examples/01-example-areas/src/app.css new file mode 100644 index 0000000..f54583b --- /dev/null +++ b/examples/01-example-areas/src/app.css @@ -0,0 +1,11 @@ +.tables, .load-buttons { + display: flex; + flex-direction: row; + flex-wrap: nowrap; +} + +.tables > div, .load-buttons > button { + flex-basis: 50%; + margin-left: 1rem; + margin-right: 1rem; +} diff --git a/examples/01-example-areas/src/app.js b/examples/01-example-areas/src/app.js new file mode 100644 index 0000000..63d8504 --- /dev/null +++ b/examples/01-example-areas/src/app.js @@ -0,0 +1,74 @@ +import React, { Component } from 'react'; +import { trackPromise } from 'react-promise-tracker'; +import { userAPI } from './api/userAPI'; +import { postAPI } from './api/postAPI'; +import { UserTable, PostTable, LoadButton } from './components'; +import { areas } from './common/constants/areas'; +import './app.css'; + +export class App extends Component { + constructor() { + super(); + + this.state = { + users: [], + posts: [], + }; + + this.onLoadUsers = this.onLoadUsers.bind(this); + this.onLoadPosts = this.onLoadPosts.bind(this); + } + + onLoadUsers() { + this.setState({ + users: [], + }); + + trackPromise( + userAPI.fetchUsers() + .then((users) => { + this.setState({ + users, + }) + }), + areas.user, + ); + } + + onLoadPosts() { + this.setState({ + posts: [], + }); + + trackPromise( + postAPI.fetchPosts() + .then((posts) => { + this.setState({ + posts, + }) + }), + areas.post, + ); + } + + render() { + return ( +
+
+ + +
+
+ + +
+
+ ); + } +} diff --git a/examples/01-example-areas/src/common/components/spinner/index.js b/examples/01-example-areas/src/common/components/spinner/index.js new file mode 100644 index 0000000..dc50628 --- /dev/null +++ b/examples/01-example-areas/src/common/components/spinner/index.js @@ -0,0 +1 @@ +export * from './spinner'; \ No newline at end of file diff --git a/examples/01-example-areas/src/common/components/spinner/spinner.css b/examples/01-example-areas/src/common/components/spinner/spinner.css new file mode 100644 index 0000000..0396535 --- /dev/null +++ b/examples/01-example-areas/src/common/components/spinner/spinner.css @@ -0,0 +1,10 @@ +.spinner { + width: 100%; + height: 100%; +} + +.spinner > div { + display: flex; + justify-content: center; + align-items: center; +} diff --git a/examples/01-example-areas/src/common/components/spinner/spinner.js b/examples/01-example-areas/src/common/components/spinner/spinner.js new file mode 100644 index 0000000..8aaccad --- /dev/null +++ b/examples/01-example-areas/src/common/components/spinner/spinner.js @@ -0,0 +1,17 @@ +import React from "react"; +import { usePromiseTracker } from "react-promise-tracker"; +import Loader from "react-loader-spinner"; +import "./spinner.css"; + +export const Spinner = (props) => { + const { promiseInProgress } = usePromiseTracker({area: props.area, delay: 0}); + + return ( + promiseInProgress && ( +
+ +
+ ) + ); +}; + diff --git a/examples/01-example-areas/src/common/components/table/index.js b/examples/01-example-areas/src/common/components/table/index.js new file mode 100644 index 0000000..759aa4e --- /dev/null +++ b/examples/01-example-areas/src/common/components/table/index.js @@ -0,0 +1 @@ +export * from './table' \ No newline at end of file diff --git a/examples/01-example-areas/src/common/components/table/table.css b/examples/01-example-areas/src/common/components/table/table.css new file mode 100644 index 0000000..bcbfb4f --- /dev/null +++ b/examples/01-example-areas/src/common/components/table/table.css @@ -0,0 +1,27 @@ +.title { + color: #000; +} + +.table { + width: 100%; + max-width: 100%; + margin-bottom: 1rem; + background-color: transparent; + border-collapse: collapse; +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #248f4f; +} + +.table td, .table th { + text-align: inherit; + padding: .75rem; + vertical-align: top; + border-top: 1px solid #248f4f; +} + +.table tbody tr:nth-of-type(odd) { + background-color: rgba(43, 173, 96, .05); +} diff --git a/examples/01-example-areas/src/common/components/table/table.js b/examples/01-example-areas/src/common/components/table/table.js new file mode 100644 index 0000000..de9e681 --- /dev/null +++ b/examples/01-example-areas/src/common/components/table/table.js @@ -0,0 +1,16 @@ +import React from 'react'; +import './table.css'; + +export const Table = (props) => ( +
+

{props.title}

+
+ + {props.headerRender()} + + + {props.items.map(props.rowRender)} + +
+ +); diff --git a/examples/01-example-areas/src/common/constants/areas.js b/examples/01-example-areas/src/common/constants/areas.js new file mode 100644 index 0000000..8b706a7 --- /dev/null +++ b/examples/01-example-areas/src/common/constants/areas.js @@ -0,0 +1,4 @@ +export const areas = { + user: 'user-area', + post: 'post-area', +}; diff --git a/examples/01-example-areas/src/components/index.js b/examples/01-example-areas/src/components/index.js new file mode 100644 index 0000000..6d29848 --- /dev/null +++ b/examples/01-example-areas/src/components/index.js @@ -0,0 +1,3 @@ +export * from './userTable'; +export * from './postTable'; +export * from './loadButton'; \ No newline at end of file diff --git a/examples/01-example-areas/src/components/loadButton/index.js b/examples/01-example-areas/src/components/loadButton/index.js new file mode 100644 index 0000000..0319437 --- /dev/null +++ b/examples/01-example-areas/src/components/loadButton/index.js @@ -0,0 +1 @@ +export * from './loadButton'; \ No newline at end of file diff --git a/examples/01-example-areas/src/components/loadButton/loadButton.css b/examples/01-example-areas/src/components/loadButton/loadButton.css new file mode 100644 index 0000000..39cbd1c --- /dev/null +++ b/examples/01-example-areas/src/components/loadButton/loadButton.css @@ -0,0 +1,26 @@ +.load-button { + cursor: pointer; + margin: 1rem; + color: #fff; + text-shadow: 1px 1px 0 #888; + background-color: #2BAD60; + border-color: #14522d; + display: inline-block; + font-weight: 400; + text-align: center; + white-space: nowrap; + vertical-align: middle; + border: 1px solid transparent; + padding: .375rem .75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: .25rem; + outline: none; +} + + + +.load-button:hover { + background-color: #248f4f; + border-color: #1e7b43; +} \ No newline at end of file diff --git a/examples/01-example-areas/src/components/loadButton/loadButton.js b/examples/01-example-areas/src/components/loadButton/loadButton.js new file mode 100644 index 0000000..f0cc6c0 --- /dev/null +++ b/examples/01-example-areas/src/components/loadButton/loadButton.js @@ -0,0 +1,11 @@ +import React from 'react'; +import './loadButton.css'; + +export const LoadButton = (props) => ( + +); \ No newline at end of file diff --git a/examples/01-example-areas/src/components/postTable/header.js b/examples/01-example-areas/src/components/postTable/header.js new file mode 100644 index 0000000..cd85184 --- /dev/null +++ b/examples/01-example-areas/src/components/postTable/header.js @@ -0,0 +1,9 @@ +import React from 'react'; + +export const Header = (props) => ( + + Id + Title + Body + +); \ No newline at end of file diff --git a/examples/01-example-areas/src/components/postTable/index.js b/examples/01-example-areas/src/components/postTable/index.js new file mode 100644 index 0000000..9834d57 --- /dev/null +++ b/examples/01-example-areas/src/components/postTable/index.js @@ -0,0 +1 @@ +export * from './table'; \ No newline at end of file diff --git a/examples/01-example-areas/src/components/postTable/row.js b/examples/01-example-areas/src/components/postTable/row.js new file mode 100644 index 0000000..b455df2 --- /dev/null +++ b/examples/01-example-areas/src/components/postTable/row.js @@ -0,0 +1,15 @@ +import React from 'react'; + +export const Row = (post) => ( + + + {post.id} + + + {post.title} + + + {post.body} + + +); \ No newline at end of file diff --git a/examples/01-example-areas/src/components/postTable/table.js b/examples/01-example-areas/src/components/postTable/table.js new file mode 100644 index 0000000..2aaec43 --- /dev/null +++ b/examples/01-example-areas/src/components/postTable/table.js @@ -0,0 +1,20 @@ +import React, { Component } from 'react'; +import { Spinner } from '../../common/components/spinner'; +import { areas } from '../../common/constants/areas'; +import { Table } from '../../common/components/table'; +import { Header } from './header'; +import { Row } from './row'; + +export const PostTable = (props) => ( +
+ + + +); diff --git a/examples/01-example-areas/src/components/userTable/header.js b/examples/01-example-areas/src/components/userTable/header.js new file mode 100644 index 0000000..0a5ad22 --- /dev/null +++ b/examples/01-example-areas/src/components/userTable/header.js @@ -0,0 +1,9 @@ +import React from 'react'; + +export const Header = (props) => ( + + + + + +); \ No newline at end of file diff --git a/examples/01-example-areas/src/components/userTable/index.js b/examples/01-example-areas/src/components/userTable/index.js new file mode 100644 index 0000000..9834d57 --- /dev/null +++ b/examples/01-example-areas/src/components/userTable/index.js @@ -0,0 +1 @@ +export * from './table'; \ No newline at end of file diff --git a/examples/01-example-areas/src/components/userTable/row.js b/examples/01-example-areas/src/components/userTable/row.js new file mode 100644 index 0000000..c76e3e1 --- /dev/null +++ b/examples/01-example-areas/src/components/userTable/row.js @@ -0,0 +1,15 @@ +import React from 'react'; + +export const Row = (user) => ( + + + + + +); \ No newline at end of file diff --git a/examples/01-example-areas/src/components/userTable/table.js b/examples/01-example-areas/src/components/userTable/table.js new file mode 100644 index 0000000..c45fc60 --- /dev/null +++ b/examples/01-example-areas/src/components/userTable/table.js @@ -0,0 +1,21 @@ +import React, { Component } from 'react'; +import { Spinner } from '../../common/components/spinner'; +import { areas } from '../../common/constants/areas'; +import { Table } from '../../common/components/table'; +import { Header } from './header'; +import { Row } from './row'; + +export const UserTable = (props) => ( +
+
IdNameEmail
+ {user.id} + + {user.name} + + {user.email} +
+ + + +); diff --git a/examples/01-example-areas/src/index.html b/examples/01-example-areas/src/index.html new file mode 100644 index 0000000..2b604d2 --- /dev/null +++ b/examples/01-example-areas/src/index.html @@ -0,0 +1,12 @@ + + + + + + + React Promise tracker sample app + + +
+ + diff --git a/examples/01-example-areas/src/index.jsx b/examples/01-example-areas/src/index.jsx new file mode 100644 index 0000000..cd51e59 --- /dev/null +++ b/examples/01-example-areas/src/index.jsx @@ -0,0 +1,11 @@ +import React, { Component } from 'react'; +import { render } from 'react-dom'; +import { App } from './app'; +import { Spinner } from './common/components/spinner'; + +render( +
+ + +
, + document.getElementById('root')); diff --git a/examples/01-example-areas/webpack.config.js b/examples/01-example-areas/webpack.config.js new file mode 100644 index 0000000..4715f2c --- /dev/null +++ b/examples/01-example-areas/webpack.config.js @@ -0,0 +1,62 @@ +var HtmlWebpackPlugin = require("html-webpack-plugin"); +var webpack = require("webpack"); +var MiniCssExtractPlugin = require("mini-css-extract-plugin"); + +var path = require("path"); +var basePath = __dirname; + +module.exports = { + context: path.join(basePath, "src"), + resolve: { + extensions: [".js", ".jsx"], + alias: { + react: path.resolve('./node_modules/react'), + 'react-dom': path.resolve('./node_modules/react-dom') + }, + }, + entry: { + app: "./index.jsx", + vendor: ["react", "react-dom"], + }, + output: { + filename: "[name].[chunkhash].js" + }, + optimization: { + splitChunks: { + cacheGroups: { + vendor: { + chunks: "initial", + name: "vendor", + test: "vendor", + enforce: true + } + } + } + }, + devtool: 'inline-source-map', + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + loader: "babel-loader" + }, + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, "css-loader"] + } + ] + }, + plugins: [ + //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: "index.html", //Name of file in ./dist/ + template: "index.html", //Name of template in ./src + hash: true + }), + new MiniCssExtractPlugin({ + filename: "[name].css", + chunkFilename: "[id].css" + }) + ] +}; diff --git a/examples/02-example-delay/.babelrc b/examples/02-example-delay/.babelrc new file mode 100644 index 0000000..ae31d35 --- /dev/null +++ b/examples/02-example-delay/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": [ + "@babel/preset-react", + [ + "@babel/preset-env", + { + "useBuiltIns": "entry" + } + ] + ] +} diff --git a/examples/02-example-delay/package.json b/examples/02-example-delay/package.json new file mode 100644 index 0000000..a6c9c3c --- /dev/null +++ b/examples/02-example-delay/package.json @@ -0,0 +1,34 @@ +{ + "name": "webpack-by-example", + "version": "1.0.0", + "description": "In this sample we are going to setup a web project that can be easily managed\r by webpack.", + "main": "index.js", + "scripts": { + "start": "webpack-dev-server --mode development --open", + "build": "rimraf dist && webpack --mode development", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@babel/cli": "^7.2.3", + "@babel/core": "^7.4.0", + "@babel/preset-env": "^7.4.0", + "@babel/preset-react": "^7.0.0", + "babel-loader": "^8.0.5", + "css-loader": "^2.1.1", + "html-webpack-plugin": "^3.2.0", + "mini-css-extract-plugin": "^0.5.0", + "rimraf": "^2.6.3", + "style-loader": "^0.23.1", + "webpack": "^4.29.6", + "webpack-cli": "^3.3.0", + "webpack-dev-server": "^3.2.1" + }, + "dependencies": { + "react": "^16.8.4", + "react-dom": "^16.8.4", + "react-loader-spinner": "^2.3.0", + "react-promise-tracker": "file:../../build" + } +} diff --git a/examples/02-example-delay/src/api/fetch.js b/examples/02-example-delay/src/api/fetch.js new file mode 100644 index 0000000..93dcfb0 --- /dev/null +++ b/examples/02-example-delay/src/api/fetch.js @@ -0,0 +1,12 @@ +export const fetchWithDelay = (url) => { + const promise = new Promise((resolve, reject) => { + setTimeout(() => { + resolve(fetch(url, { + method: 'GET', + }) + .then((response) => response.json())); + }, 3000) + }); + + return promise; +} \ No newline at end of file diff --git a/examples/02-example-delay/src/api/postAPI.js b/examples/02-example-delay/src/api/postAPI.js new file mode 100644 index 0000000..3831918 --- /dev/null +++ b/examples/02-example-delay/src/api/postAPI.js @@ -0,0 +1,9 @@ +import { fetchWithDelay } from './fetch'; +const url = 'https://jsonplaceholder.typicode.com/posts'; + +const fetchPosts = () => fetchWithDelay(url) + .then((posts) => posts.slice(0, 10)); + +export const postAPI = { + fetchPosts, +}; \ No newline at end of file diff --git a/examples/02-example-delay/src/api/userAPI.js b/examples/02-example-delay/src/api/userAPI.js new file mode 100644 index 0000000..17274bd --- /dev/null +++ b/examples/02-example-delay/src/api/userAPI.js @@ -0,0 +1,8 @@ +import { fetchWithDelay } from './fetch'; +const url = 'https://jsonplaceholder.typicode.com/users'; + +const fetchUsers = () => fetchWithDelay(url); + +export const userAPI = { + fetchUsers, +}; \ No newline at end of file diff --git a/examples/02-example-delay/src/app.css b/examples/02-example-delay/src/app.css new file mode 100644 index 0000000..db650a3 --- /dev/null +++ b/examples/02-example-delay/src/app.css @@ -0,0 +1,11 @@ +.tables { + display: flex; + flex-direction: row; + flex-wrap: nowrap; +} + +.tables > div { + flex-basis: 50%; + margin-left: 1rem; + margin-right: 1rem; +} \ No newline at end of file diff --git a/examples/02-example-delay/src/app.js b/examples/02-example-delay/src/app.js new file mode 100644 index 0000000..85a42cd --- /dev/null +++ b/examples/02-example-delay/src/app.js @@ -0,0 +1,59 @@ +import React, { Component } from 'react'; +import { trackPromise } from 'react-promise-tracker'; +import { userAPI } from './api/userAPI'; +import { postAPI } from './api/postAPI'; +import { UserTable, PostTable, LoadButton } from './components'; +import './app.css'; + +export class App extends Component { + constructor() { + super(); + + this.state = { + users: [], + posts: [], + }; + + this.onLoadTables = this.onLoadTables.bind(this); + } + + onLoadTables() { + this.setState({ + users: [], + posts: [], + }); + + trackPromise( + userAPI.fetchUsers() + .then((users) => { + this.setState({ + users, + }) + }) + ); + + trackPromise( + postAPI.fetchPosts() + .then((posts) => { + this.setState({ + posts, + }) + }) + ); + } + + render() { + return ( +
+ +
+ + +
+
+ ); + } +} diff --git a/examples/02-example-delay/src/common/components/spinner/index.js b/examples/02-example-delay/src/common/components/spinner/index.js new file mode 100644 index 0000000..dc50628 --- /dev/null +++ b/examples/02-example-delay/src/common/components/spinner/index.js @@ -0,0 +1 @@ +export * from './spinner'; \ No newline at end of file diff --git a/examples/02-example-delay/src/common/components/spinner/spinner.css b/examples/02-example-delay/src/common/components/spinner/spinner.css new file mode 100644 index 0000000..ebdff53 --- /dev/null +++ b/examples/02-example-delay/src/common/components/spinner/spinner.css @@ -0,0 +1,10 @@ +.spinner { + width: 100%; + height: 100%; +} + +.spinner > div { + display: flex; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/examples/02-example-delay/src/common/components/spinner/spinner.js b/examples/02-example-delay/src/common/components/spinner/spinner.js new file mode 100644 index 0000000..d14aeea --- /dev/null +++ b/examples/02-example-delay/src/common/components/spinner/spinner.js @@ -0,0 +1,17 @@ +import React from "react"; +import { usePromiseTracker } from "react-promise-tracker"; +import Loader from "react-loader-spinner"; +import "./spinner.css"; + +export const Spinner = (props) => { + + const { promiseInProgress } = usePromiseTracker({delay: 200}); + + return ( + promiseInProgress && ( +
+ +
+ ) + ); +}; diff --git a/examples/02-example-delay/src/common/components/table/index.js b/examples/02-example-delay/src/common/components/table/index.js new file mode 100644 index 0000000..759aa4e --- /dev/null +++ b/examples/02-example-delay/src/common/components/table/index.js @@ -0,0 +1 @@ +export * from './table' \ No newline at end of file diff --git a/examples/02-example-delay/src/common/components/table/table.css b/examples/02-example-delay/src/common/components/table/table.css new file mode 100644 index 0000000..738f080 --- /dev/null +++ b/examples/02-example-delay/src/common/components/table/table.css @@ -0,0 +1,27 @@ +.title { + color: #000; +} + +.table { + width: 100%; + max-width: 100%; + margin-bottom: 1rem; + background-color: transparent; + border-collapse: collapse; +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #248f4f; +} + +.table td, .table th { + text-align: inherit; + padding: .75rem; + vertical-align: top; + border-top: 1px solid #248f4f; +} + +.table tbody tr:nth-of-type(odd) { + background-color: rgba(43, 173, 96, .05); +} \ No newline at end of file diff --git a/examples/02-example-delay/src/common/components/table/table.js b/examples/02-example-delay/src/common/components/table/table.js new file mode 100644 index 0000000..de9e681 --- /dev/null +++ b/examples/02-example-delay/src/common/components/table/table.js @@ -0,0 +1,16 @@ +import React from 'react'; +import './table.css'; + +export const Table = (props) => ( +
+

{props.title}

+
+ + {props.headerRender()} + + + {props.items.map(props.rowRender)} + +
+
+); diff --git a/examples/02-example-delay/src/components/index.js b/examples/02-example-delay/src/components/index.js new file mode 100644 index 0000000..6d29848 --- /dev/null +++ b/examples/02-example-delay/src/components/index.js @@ -0,0 +1,3 @@ +export * from './userTable'; +export * from './postTable'; +export * from './loadButton'; \ No newline at end of file diff --git a/examples/02-example-delay/src/components/loadButton/index.js b/examples/02-example-delay/src/components/loadButton/index.js new file mode 100644 index 0000000..0319437 --- /dev/null +++ b/examples/02-example-delay/src/components/loadButton/index.js @@ -0,0 +1 @@ +export * from './loadButton'; \ No newline at end of file diff --git a/examples/02-example-delay/src/components/loadButton/loadButton.css b/examples/02-example-delay/src/components/loadButton/loadButton.css new file mode 100644 index 0000000..39cbd1c --- /dev/null +++ b/examples/02-example-delay/src/components/loadButton/loadButton.css @@ -0,0 +1,26 @@ +.load-button { + cursor: pointer; + margin: 1rem; + color: #fff; + text-shadow: 1px 1px 0 #888; + background-color: #2BAD60; + border-color: #14522d; + display: inline-block; + font-weight: 400; + text-align: center; + white-space: nowrap; + vertical-align: middle; + border: 1px solid transparent; + padding: .375rem .75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: .25rem; + outline: none; +} + + + +.load-button:hover { + background-color: #248f4f; + border-color: #1e7b43; +} \ No newline at end of file diff --git a/examples/02-example-delay/src/components/loadButton/loadButton.js b/examples/02-example-delay/src/components/loadButton/loadButton.js new file mode 100644 index 0000000..f0cc6c0 --- /dev/null +++ b/examples/02-example-delay/src/components/loadButton/loadButton.js @@ -0,0 +1,11 @@ +import React from 'react'; +import './loadButton.css'; + +export const LoadButton = (props) => ( + +); \ No newline at end of file diff --git a/examples/02-example-delay/src/components/postTable/header.js b/examples/02-example-delay/src/components/postTable/header.js new file mode 100644 index 0000000..cd85184 --- /dev/null +++ b/examples/02-example-delay/src/components/postTable/header.js @@ -0,0 +1,9 @@ +import React from 'react'; + +export const Header = (props) => ( + + Id + Title + Body + +); \ No newline at end of file diff --git a/examples/02-example-delay/src/components/postTable/index.js b/examples/02-example-delay/src/components/postTable/index.js new file mode 100644 index 0000000..9834d57 --- /dev/null +++ b/examples/02-example-delay/src/components/postTable/index.js @@ -0,0 +1 @@ +export * from './table'; \ No newline at end of file diff --git a/examples/02-example-delay/src/components/postTable/row.js b/examples/02-example-delay/src/components/postTable/row.js new file mode 100644 index 0000000..b455df2 --- /dev/null +++ b/examples/02-example-delay/src/components/postTable/row.js @@ -0,0 +1,15 @@ +import React from 'react'; + +export const Row = (post) => ( + + + {post.id} + + + {post.title} + + + {post.body} + + +); \ No newline at end of file diff --git a/examples/02-example-delay/src/components/postTable/table.js b/examples/02-example-delay/src/components/postTable/table.js new file mode 100644 index 0000000..9ec572e --- /dev/null +++ b/examples/02-example-delay/src/components/postTable/table.js @@ -0,0 +1,13 @@ +import React, { Component } from 'react'; +import { Table } from '../../common/components/table'; +import { Header } from './header'; +import { Row } from './row'; + +export const PostTable = (props) => ( + +); diff --git a/examples/02-example-delay/src/components/userTable/header.js b/examples/02-example-delay/src/components/userTable/header.js new file mode 100644 index 0000000..0a5ad22 --- /dev/null +++ b/examples/02-example-delay/src/components/userTable/header.js @@ -0,0 +1,9 @@ +import React from 'react'; + +export const Header = (props) => ( + + + + + +); \ No newline at end of file diff --git a/examples/02-example-delay/src/components/userTable/index.js b/examples/02-example-delay/src/components/userTable/index.js new file mode 100644 index 0000000..9834d57 --- /dev/null +++ b/examples/02-example-delay/src/components/userTable/index.js @@ -0,0 +1 @@ +export * from './table'; \ No newline at end of file diff --git a/examples/02-example-delay/src/components/userTable/row.js b/examples/02-example-delay/src/components/userTable/row.js new file mode 100644 index 0000000..c76e3e1 --- /dev/null +++ b/examples/02-example-delay/src/components/userTable/row.js @@ -0,0 +1,15 @@ +import React from 'react'; + +export const Row = (user) => ( + + + + + +); \ No newline at end of file diff --git a/examples/02-example-delay/src/components/userTable/table.js b/examples/02-example-delay/src/components/userTable/table.js new file mode 100644 index 0000000..3cacf77 --- /dev/null +++ b/examples/02-example-delay/src/components/userTable/table.js @@ -0,0 +1,13 @@ +import React, { Component } from 'react'; +import { Table } from '../../common/components/table'; +import { Header } from './header'; +import { Row } from './row'; + +export const UserTable = (props) => ( +
IdNameEmail
+ {user.id} + + {user.name} + + {user.email} +
+); \ No newline at end of file diff --git a/examples/02-example-delay/src/index.html b/examples/02-example-delay/src/index.html new file mode 100644 index 0000000..2b604d2 --- /dev/null +++ b/examples/02-example-delay/src/index.html @@ -0,0 +1,12 @@ + + + + + + + React Promise tracker sample app + + +
+ + diff --git a/examples/02-example-delay/src/index.jsx b/examples/02-example-delay/src/index.jsx new file mode 100644 index 0000000..cd51e59 --- /dev/null +++ b/examples/02-example-delay/src/index.jsx @@ -0,0 +1,11 @@ +import React, { Component } from 'react'; +import { render } from 'react-dom'; +import { App } from './app'; +import { Spinner } from './common/components/spinner'; + +render( +
+ + +
, + document.getElementById('root')); diff --git a/examples/02-example-delay/webpack.config.js b/examples/02-example-delay/webpack.config.js new file mode 100644 index 0000000..4715f2c --- /dev/null +++ b/examples/02-example-delay/webpack.config.js @@ -0,0 +1,62 @@ +var HtmlWebpackPlugin = require("html-webpack-plugin"); +var webpack = require("webpack"); +var MiniCssExtractPlugin = require("mini-css-extract-plugin"); + +var path = require("path"); +var basePath = __dirname; + +module.exports = { + context: path.join(basePath, "src"), + resolve: { + extensions: [".js", ".jsx"], + alias: { + react: path.resolve('./node_modules/react'), + 'react-dom': path.resolve('./node_modules/react-dom') + }, + }, + entry: { + app: "./index.jsx", + vendor: ["react", "react-dom"], + }, + output: { + filename: "[name].[chunkhash].js" + }, + optimization: { + splitChunks: { + cacheGroups: { + vendor: { + chunks: "initial", + name: "vendor", + test: "vendor", + enforce: true + } + } + } + }, + devtool: 'inline-source-map', + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + loader: "babel-loader" + }, + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, "css-loader"] + } + ] + }, + plugins: [ + //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: "index.html", //Name of file in ./dist/ + template: "index.html", //Name of template in ./src + hash: true + }), + new MiniCssExtractPlugin({ + filename: "[name].css", + chunkFilename: "[id].css" + }) + ] +}; diff --git a/examples/03-example-hoc/.babelrc b/examples/03-example-hoc/.babelrc new file mode 100644 index 0000000..ae31d35 --- /dev/null +++ b/examples/03-example-hoc/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": [ + "@babel/preset-react", + [ + "@babel/preset-env", + { + "useBuiltIns": "entry" + } + ] + ] +} diff --git a/examples/03-example-hoc/package.json b/examples/03-example-hoc/package.json new file mode 100644 index 0000000..a6c9c3c --- /dev/null +++ b/examples/03-example-hoc/package.json @@ -0,0 +1,34 @@ +{ + "name": "webpack-by-example", + "version": "1.0.0", + "description": "In this sample we are going to setup a web project that can be easily managed\r by webpack.", + "main": "index.js", + "scripts": { + "start": "webpack-dev-server --mode development --open", + "build": "rimraf dist && webpack --mode development", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@babel/cli": "^7.2.3", + "@babel/core": "^7.4.0", + "@babel/preset-env": "^7.4.0", + "@babel/preset-react": "^7.0.0", + "babel-loader": "^8.0.5", + "css-loader": "^2.1.1", + "html-webpack-plugin": "^3.2.0", + "mini-css-extract-plugin": "^0.5.0", + "rimraf": "^2.6.3", + "style-loader": "^0.23.1", + "webpack": "^4.29.6", + "webpack-cli": "^3.3.0", + "webpack-dev-server": "^3.2.1" + }, + "dependencies": { + "react": "^16.8.4", + "react-dom": "^16.8.4", + "react-loader-spinner": "^2.3.0", + "react-promise-tracker": "file:../../build" + } +} diff --git a/examples/03-example-hoc/src/api/fetch.js b/examples/03-example-hoc/src/api/fetch.js new file mode 100644 index 0000000..93dcfb0 --- /dev/null +++ b/examples/03-example-hoc/src/api/fetch.js @@ -0,0 +1,12 @@ +export const fetchWithDelay = (url) => { + const promise = new Promise((resolve, reject) => { + setTimeout(() => { + resolve(fetch(url, { + method: 'GET', + }) + .then((response) => response.json())); + }, 3000) + }); + + return promise; +} \ No newline at end of file diff --git a/examples/03-example-hoc/src/api/postAPI.js b/examples/03-example-hoc/src/api/postAPI.js new file mode 100644 index 0000000..3831918 --- /dev/null +++ b/examples/03-example-hoc/src/api/postAPI.js @@ -0,0 +1,9 @@ +import { fetchWithDelay } from './fetch'; +const url = 'https://jsonplaceholder.typicode.com/posts'; + +const fetchPosts = () => fetchWithDelay(url) + .then((posts) => posts.slice(0, 10)); + +export const postAPI = { + fetchPosts, +}; \ No newline at end of file diff --git a/examples/03-example-hoc/src/api/userAPI.js b/examples/03-example-hoc/src/api/userAPI.js new file mode 100644 index 0000000..17274bd --- /dev/null +++ b/examples/03-example-hoc/src/api/userAPI.js @@ -0,0 +1,8 @@ +import { fetchWithDelay } from './fetch'; +const url = 'https://jsonplaceholder.typicode.com/users'; + +const fetchUsers = () => fetchWithDelay(url); + +export const userAPI = { + fetchUsers, +}; \ No newline at end of file diff --git a/examples/03-example-hoc/src/app.css b/examples/03-example-hoc/src/app.css new file mode 100644 index 0000000..db650a3 --- /dev/null +++ b/examples/03-example-hoc/src/app.css @@ -0,0 +1,11 @@ +.tables { + display: flex; + flex-direction: row; + flex-wrap: nowrap; +} + +.tables > div { + flex-basis: 50%; + margin-left: 1rem; + margin-right: 1rem; +} \ No newline at end of file diff --git a/examples/03-example-hoc/src/app.js b/examples/03-example-hoc/src/app.js new file mode 100644 index 0000000..85a42cd --- /dev/null +++ b/examples/03-example-hoc/src/app.js @@ -0,0 +1,59 @@ +import React, { Component } from 'react'; +import { trackPromise } from 'react-promise-tracker'; +import { userAPI } from './api/userAPI'; +import { postAPI } from './api/postAPI'; +import { UserTable, PostTable, LoadButton } from './components'; +import './app.css'; + +export class App extends Component { + constructor() { + super(); + + this.state = { + users: [], + posts: [], + }; + + this.onLoadTables = this.onLoadTables.bind(this); + } + + onLoadTables() { + this.setState({ + users: [], + posts: [], + }); + + trackPromise( + userAPI.fetchUsers() + .then((users) => { + this.setState({ + users, + }) + }) + ); + + trackPromise( + postAPI.fetchPosts() + .then((posts) => { + this.setState({ + posts, + }) + }) + ); + } + + render() { + return ( +
+ +
+ + +
+
+ ); + } +} diff --git a/examples/03-example-hoc/src/common/components/spinner/index.js b/examples/03-example-hoc/src/common/components/spinner/index.js new file mode 100644 index 0000000..dc50628 --- /dev/null +++ b/examples/03-example-hoc/src/common/components/spinner/index.js @@ -0,0 +1 @@ +export * from './spinner'; \ No newline at end of file diff --git a/examples/03-example-hoc/src/common/components/spinner/spinner.css b/examples/03-example-hoc/src/common/components/spinner/spinner.css new file mode 100644 index 0000000..ebdff53 --- /dev/null +++ b/examples/03-example-hoc/src/common/components/spinner/spinner.css @@ -0,0 +1,10 @@ +.spinner { + width: 100%; + height: 100%; +} + +.spinner > div { + display: flex; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/examples/03-example-hoc/src/common/components/spinner/spinner.js b/examples/03-example-hoc/src/common/components/spinner/spinner.js new file mode 100644 index 0000000..7d84c79 --- /dev/null +++ b/examples/03-example-hoc/src/common/components/spinner/spinner.js @@ -0,0 +1,13 @@ +import React from "react"; +import { promiseTrackerHoc } from "react-promise-tracker"; +import Loader from "react-loader-spinner"; +import "./spinner.css"; + +const SpinnerInner = (props) => + props.promiseInProgress && ( +
+ +
+ ) + +export const Spinner = promiseTrackerHoc(SpinnerInner); diff --git a/examples/03-example-hoc/src/common/components/table/index.js b/examples/03-example-hoc/src/common/components/table/index.js new file mode 100644 index 0000000..759aa4e --- /dev/null +++ b/examples/03-example-hoc/src/common/components/table/index.js @@ -0,0 +1 @@ +export * from './table' \ No newline at end of file diff --git a/examples/03-example-hoc/src/common/components/table/table.css b/examples/03-example-hoc/src/common/components/table/table.css new file mode 100644 index 0000000..738f080 --- /dev/null +++ b/examples/03-example-hoc/src/common/components/table/table.css @@ -0,0 +1,27 @@ +.title { + color: #000; +} + +.table { + width: 100%; + max-width: 100%; + margin-bottom: 1rem; + background-color: transparent; + border-collapse: collapse; +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #248f4f; +} + +.table td, .table th { + text-align: inherit; + padding: .75rem; + vertical-align: top; + border-top: 1px solid #248f4f; +} + +.table tbody tr:nth-of-type(odd) { + background-color: rgba(43, 173, 96, .05); +} \ No newline at end of file diff --git a/examples/03-example-hoc/src/common/components/table/table.js b/examples/03-example-hoc/src/common/components/table/table.js new file mode 100644 index 0000000..de9e681 --- /dev/null +++ b/examples/03-example-hoc/src/common/components/table/table.js @@ -0,0 +1,16 @@ +import React from 'react'; +import './table.css'; + +export const Table = (props) => ( +
+

{props.title}

+
+ + {props.headerRender()} + + + {props.items.map(props.rowRender)} + +
+ +); diff --git a/examples/03-example-hoc/src/components/index.js b/examples/03-example-hoc/src/components/index.js new file mode 100644 index 0000000..6d29848 --- /dev/null +++ b/examples/03-example-hoc/src/components/index.js @@ -0,0 +1,3 @@ +export * from './userTable'; +export * from './postTable'; +export * from './loadButton'; \ No newline at end of file diff --git a/examples/03-example-hoc/src/components/loadButton/index.js b/examples/03-example-hoc/src/components/loadButton/index.js new file mode 100644 index 0000000..0319437 --- /dev/null +++ b/examples/03-example-hoc/src/components/loadButton/index.js @@ -0,0 +1 @@ +export * from './loadButton'; \ No newline at end of file diff --git a/examples/03-example-hoc/src/components/loadButton/loadButton.css b/examples/03-example-hoc/src/components/loadButton/loadButton.css new file mode 100644 index 0000000..39cbd1c --- /dev/null +++ b/examples/03-example-hoc/src/components/loadButton/loadButton.css @@ -0,0 +1,26 @@ +.load-button { + cursor: pointer; + margin: 1rem; + color: #fff; + text-shadow: 1px 1px 0 #888; + background-color: #2BAD60; + border-color: #14522d; + display: inline-block; + font-weight: 400; + text-align: center; + white-space: nowrap; + vertical-align: middle; + border: 1px solid transparent; + padding: .375rem .75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: .25rem; + outline: none; +} + + + +.load-button:hover { + background-color: #248f4f; + border-color: #1e7b43; +} \ No newline at end of file diff --git a/examples/03-example-hoc/src/components/loadButton/loadButton.js b/examples/03-example-hoc/src/components/loadButton/loadButton.js new file mode 100644 index 0000000..f0cc6c0 --- /dev/null +++ b/examples/03-example-hoc/src/components/loadButton/loadButton.js @@ -0,0 +1,11 @@ +import React from 'react'; +import './loadButton.css'; + +export const LoadButton = (props) => ( + +); \ No newline at end of file diff --git a/examples/03-example-hoc/src/components/postTable/header.js b/examples/03-example-hoc/src/components/postTable/header.js new file mode 100644 index 0000000..cd85184 --- /dev/null +++ b/examples/03-example-hoc/src/components/postTable/header.js @@ -0,0 +1,9 @@ +import React from 'react'; + +export const Header = (props) => ( + + Id + Title + Body + +); \ No newline at end of file diff --git a/examples/03-example-hoc/src/components/postTable/index.js b/examples/03-example-hoc/src/components/postTable/index.js new file mode 100644 index 0000000..9834d57 --- /dev/null +++ b/examples/03-example-hoc/src/components/postTable/index.js @@ -0,0 +1 @@ +export * from './table'; \ No newline at end of file diff --git a/examples/03-example-hoc/src/components/postTable/row.js b/examples/03-example-hoc/src/components/postTable/row.js new file mode 100644 index 0000000..b455df2 --- /dev/null +++ b/examples/03-example-hoc/src/components/postTable/row.js @@ -0,0 +1,15 @@ +import React from 'react'; + +export const Row = (post) => ( + + + {post.id} + + + {post.title} + + + {post.body} + + +); \ No newline at end of file diff --git a/examples/03-example-hoc/src/components/postTable/table.js b/examples/03-example-hoc/src/components/postTable/table.js new file mode 100644 index 0000000..9ec572e --- /dev/null +++ b/examples/03-example-hoc/src/components/postTable/table.js @@ -0,0 +1,13 @@ +import React, { Component } from 'react'; +import { Table } from '../../common/components/table'; +import { Header } from './header'; +import { Row } from './row'; + +export const PostTable = (props) => ( + +); diff --git a/examples/03-example-hoc/src/components/userTable/header.js b/examples/03-example-hoc/src/components/userTable/header.js new file mode 100644 index 0000000..0a5ad22 --- /dev/null +++ b/examples/03-example-hoc/src/components/userTable/header.js @@ -0,0 +1,9 @@ +import React from 'react'; + +export const Header = (props) => ( + + + + + +); \ No newline at end of file diff --git a/examples/03-example-hoc/src/components/userTable/index.js b/examples/03-example-hoc/src/components/userTable/index.js new file mode 100644 index 0000000..9834d57 --- /dev/null +++ b/examples/03-example-hoc/src/components/userTable/index.js @@ -0,0 +1 @@ +export * from './table'; \ No newline at end of file diff --git a/examples/03-example-hoc/src/components/userTable/row.js b/examples/03-example-hoc/src/components/userTable/row.js new file mode 100644 index 0000000..c76e3e1 --- /dev/null +++ b/examples/03-example-hoc/src/components/userTable/row.js @@ -0,0 +1,15 @@ +import React from 'react'; + +export const Row = (user) => ( + + + + + +); \ No newline at end of file diff --git a/examples/03-example-hoc/src/components/userTable/table.js b/examples/03-example-hoc/src/components/userTable/table.js new file mode 100644 index 0000000..3cacf77 --- /dev/null +++ b/examples/03-example-hoc/src/components/userTable/table.js @@ -0,0 +1,13 @@ +import React, { Component } from 'react'; +import { Table } from '../../common/components/table'; +import { Header } from './header'; +import { Row } from './row'; + +export const UserTable = (props) => ( +
IdNameEmail
+ {user.id} + + {user.name} + + {user.email} +
+); \ No newline at end of file diff --git a/examples/03-example-hoc/src/index.html b/examples/03-example-hoc/src/index.html new file mode 100644 index 0000000..2b604d2 --- /dev/null +++ b/examples/03-example-hoc/src/index.html @@ -0,0 +1,12 @@ + + + + + + + React Promise tracker sample app + + +
+ + diff --git a/examples/03-example-hoc/src/index.jsx b/examples/03-example-hoc/src/index.jsx new file mode 100644 index 0000000..cd51e59 --- /dev/null +++ b/examples/03-example-hoc/src/index.jsx @@ -0,0 +1,11 @@ +import React, { Component } from 'react'; +import { render } from 'react-dom'; +import { App } from './app'; +import { Spinner } from './common/components/spinner'; + +render( +
+ + +
, + document.getElementById('root')); diff --git a/examples/03-example-hoc/webpack.config.js b/examples/03-example-hoc/webpack.config.js new file mode 100644 index 0000000..4715f2c --- /dev/null +++ b/examples/03-example-hoc/webpack.config.js @@ -0,0 +1,62 @@ +var HtmlWebpackPlugin = require("html-webpack-plugin"); +var webpack = require("webpack"); +var MiniCssExtractPlugin = require("mini-css-extract-plugin"); + +var path = require("path"); +var basePath = __dirname; + +module.exports = { + context: path.join(basePath, "src"), + resolve: { + extensions: [".js", ".jsx"], + alias: { + react: path.resolve('./node_modules/react'), + 'react-dom': path.resolve('./node_modules/react-dom') + }, + }, + entry: { + app: "./index.jsx", + vendor: ["react", "react-dom"], + }, + output: { + filename: "[name].[chunkhash].js" + }, + optimization: { + splitChunks: { + cacheGroups: { + vendor: { + chunks: "initial", + name: "vendor", + test: "vendor", + enforce: true + } + } + } + }, + devtool: 'inline-source-map', + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + loader: "babel-loader" + }, + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, "css-loader"] + } + ] + }, + plugins: [ + //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: "index.html", //Name of file in ./dist/ + template: "index.html", //Name of template in ./src + hash: true + }), + new MiniCssExtractPlugin({ + filename: "[name].css", + chunkFilename: "[id].css" + }) + ] +}; diff --git a/examples/04-initial-load/.babelrc b/examples/04-initial-load/.babelrc new file mode 100644 index 0000000..ae31d35 --- /dev/null +++ b/examples/04-initial-load/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": [ + "@babel/preset-react", + [ + "@babel/preset-env", + { + "useBuiltIns": "entry" + } + ] + ] +} diff --git a/examples/04-initial-load/package.json b/examples/04-initial-load/package.json new file mode 100644 index 0000000..a6c9c3c --- /dev/null +++ b/examples/04-initial-load/package.json @@ -0,0 +1,34 @@ +{ + "name": "webpack-by-example", + "version": "1.0.0", + "description": "In this sample we are going to setup a web project that can be easily managed\r by webpack.", + "main": "index.js", + "scripts": { + "start": "webpack-dev-server --mode development --open", + "build": "rimraf dist && webpack --mode development", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@babel/cli": "^7.2.3", + "@babel/core": "^7.4.0", + "@babel/preset-env": "^7.4.0", + "@babel/preset-react": "^7.0.0", + "babel-loader": "^8.0.5", + "css-loader": "^2.1.1", + "html-webpack-plugin": "^3.2.0", + "mini-css-extract-plugin": "^0.5.0", + "rimraf": "^2.6.3", + "style-loader": "^0.23.1", + "webpack": "^4.29.6", + "webpack-cli": "^3.3.0", + "webpack-dev-server": "^3.2.1" + }, + "dependencies": { + "react": "^16.8.4", + "react-dom": "^16.8.4", + "react-loader-spinner": "^2.3.0", + "react-promise-tracker": "file:../../build" + } +} diff --git a/examples/04-initial-load/src/api/fetch.js b/examples/04-initial-load/src/api/fetch.js new file mode 100644 index 0000000..93dcfb0 --- /dev/null +++ b/examples/04-initial-load/src/api/fetch.js @@ -0,0 +1,12 @@ +export const fetchWithDelay = (url) => { + const promise = new Promise((resolve, reject) => { + setTimeout(() => { + resolve(fetch(url, { + method: 'GET', + }) + .then((response) => response.json())); + }, 3000) + }); + + return promise; +} \ No newline at end of file diff --git a/examples/04-initial-load/src/api/postAPI.js b/examples/04-initial-load/src/api/postAPI.js new file mode 100644 index 0000000..3831918 --- /dev/null +++ b/examples/04-initial-load/src/api/postAPI.js @@ -0,0 +1,9 @@ +import { fetchWithDelay } from './fetch'; +const url = 'https://jsonplaceholder.typicode.com/posts'; + +const fetchPosts = () => fetchWithDelay(url) + .then((posts) => posts.slice(0, 10)); + +export const postAPI = { + fetchPosts, +}; \ No newline at end of file diff --git a/examples/04-initial-load/src/api/userAPI.js b/examples/04-initial-load/src/api/userAPI.js new file mode 100644 index 0000000..17274bd --- /dev/null +++ b/examples/04-initial-load/src/api/userAPI.js @@ -0,0 +1,8 @@ +import { fetchWithDelay } from './fetch'; +const url = 'https://jsonplaceholder.typicode.com/users'; + +const fetchUsers = () => fetchWithDelay(url); + +export const userAPI = { + fetchUsers, +}; \ No newline at end of file diff --git a/examples/04-initial-load/src/app.css b/examples/04-initial-load/src/app.css new file mode 100644 index 0000000..db650a3 --- /dev/null +++ b/examples/04-initial-load/src/app.css @@ -0,0 +1,11 @@ +.tables { + display: flex; + flex-direction: row; + flex-wrap: nowrap; +} + +.tables > div { + flex-basis: 50%; + margin-left: 1rem; + margin-right: 1rem; +} \ No newline at end of file diff --git a/examples/04-initial-load/src/app.js b/examples/04-initial-load/src/app.js new file mode 100644 index 0000000..2fc3a77 --- /dev/null +++ b/examples/04-initial-load/src/app.js @@ -0,0 +1,54 @@ +import React, { Component } from 'react'; +import { trackPromise } from 'react-promise-tracker'; +import { userAPI } from './api/userAPI'; +import { postAPI } from './api/postAPI'; +import { UserTable, PostTable, LoadButton } from './components'; +import './app.css'; + +export class App extends Component { + constructor() { + super(); + + this.state = { + users: [], + posts: [], + }; + + } + + componentDidMount() { + this.setState({ + users: [], + posts: [], + }); + + trackPromise( + userAPI.fetchUsers() + .then((users) => { + this.setState({ + users, + }) + }) + ); + + trackPromise( + postAPI.fetchPosts() + .then((posts) => { + this.setState({ + posts, + }) + }) + ); + } + + render() { + return ( +
+
+ + +
+
+ ); + } +} diff --git a/examples/04-initial-load/src/common/components/spinner/index.js b/examples/04-initial-load/src/common/components/spinner/index.js new file mode 100644 index 0000000..dc50628 --- /dev/null +++ b/examples/04-initial-load/src/common/components/spinner/index.js @@ -0,0 +1 @@ +export * from './spinner'; \ No newline at end of file diff --git a/examples/04-initial-load/src/common/components/spinner/spinner.css b/examples/04-initial-load/src/common/components/spinner/spinner.css new file mode 100644 index 0000000..ebdff53 --- /dev/null +++ b/examples/04-initial-load/src/common/components/spinner/spinner.css @@ -0,0 +1,10 @@ +.spinner { + width: 100%; + height: 100%; +} + +.spinner > div { + display: flex; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/examples/04-initial-load/src/common/components/spinner/spinner.js b/examples/04-initial-load/src/common/components/spinner/spinner.js new file mode 100644 index 0000000..2f5a94b --- /dev/null +++ b/examples/04-initial-load/src/common/components/spinner/spinner.js @@ -0,0 +1,16 @@ +import React from "react"; +import { usePromiseTracker } from "react-promise-tracker"; +import Loader from "react-loader-spinner"; +import "./spinner.css"; + +export const Spinner = (props) => { + const { promiseInProgress } = usePromiseTracker(); + + return ( + promiseInProgress && ( +
+ +
+ ) + ); +}; diff --git a/examples/04-initial-load/src/common/components/table/index.js b/examples/04-initial-load/src/common/components/table/index.js new file mode 100644 index 0000000..759aa4e --- /dev/null +++ b/examples/04-initial-load/src/common/components/table/index.js @@ -0,0 +1 @@ +export * from './table' \ No newline at end of file diff --git a/examples/04-initial-load/src/common/components/table/table.css b/examples/04-initial-load/src/common/components/table/table.css new file mode 100644 index 0000000..738f080 --- /dev/null +++ b/examples/04-initial-load/src/common/components/table/table.css @@ -0,0 +1,27 @@ +.title { + color: #000; +} + +.table { + width: 100%; + max-width: 100%; + margin-bottom: 1rem; + background-color: transparent; + border-collapse: collapse; +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #248f4f; +} + +.table td, .table th { + text-align: inherit; + padding: .75rem; + vertical-align: top; + border-top: 1px solid #248f4f; +} + +.table tbody tr:nth-of-type(odd) { + background-color: rgba(43, 173, 96, .05); +} \ No newline at end of file diff --git a/examples/04-initial-load/src/common/components/table/table.js b/examples/04-initial-load/src/common/components/table/table.js new file mode 100644 index 0000000..de9e681 --- /dev/null +++ b/examples/04-initial-load/src/common/components/table/table.js @@ -0,0 +1,16 @@ +import React from 'react'; +import './table.css'; + +export const Table = (props) => ( +
+

{props.title}

+
+ + {props.headerRender()} + + + {props.items.map(props.rowRender)} + +
+ +); diff --git a/examples/04-initial-load/src/components/index.js b/examples/04-initial-load/src/components/index.js new file mode 100644 index 0000000..6d29848 --- /dev/null +++ b/examples/04-initial-load/src/components/index.js @@ -0,0 +1,3 @@ +export * from './userTable'; +export * from './postTable'; +export * from './loadButton'; \ No newline at end of file diff --git a/examples/04-initial-load/src/components/loadButton/index.js b/examples/04-initial-load/src/components/loadButton/index.js new file mode 100644 index 0000000..0319437 --- /dev/null +++ b/examples/04-initial-load/src/components/loadButton/index.js @@ -0,0 +1 @@ +export * from './loadButton'; \ No newline at end of file diff --git a/examples/04-initial-load/src/components/loadButton/loadButton.css b/examples/04-initial-load/src/components/loadButton/loadButton.css new file mode 100644 index 0000000..39cbd1c --- /dev/null +++ b/examples/04-initial-load/src/components/loadButton/loadButton.css @@ -0,0 +1,26 @@ +.load-button { + cursor: pointer; + margin: 1rem; + color: #fff; + text-shadow: 1px 1px 0 #888; + background-color: #2BAD60; + border-color: #14522d; + display: inline-block; + font-weight: 400; + text-align: center; + white-space: nowrap; + vertical-align: middle; + border: 1px solid transparent; + padding: .375rem .75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: .25rem; + outline: none; +} + + + +.load-button:hover { + background-color: #248f4f; + border-color: #1e7b43; +} \ No newline at end of file diff --git a/examples/04-initial-load/src/components/loadButton/loadButton.js b/examples/04-initial-load/src/components/loadButton/loadButton.js new file mode 100644 index 0000000..f0cc6c0 --- /dev/null +++ b/examples/04-initial-load/src/components/loadButton/loadButton.js @@ -0,0 +1,11 @@ +import React from 'react'; +import './loadButton.css'; + +export const LoadButton = (props) => ( + +); \ No newline at end of file diff --git a/examples/04-initial-load/src/components/postTable/header.js b/examples/04-initial-load/src/components/postTable/header.js new file mode 100644 index 0000000..cd85184 --- /dev/null +++ b/examples/04-initial-load/src/components/postTable/header.js @@ -0,0 +1,9 @@ +import React from 'react'; + +export const Header = (props) => ( + + Id + Title + Body + +); \ No newline at end of file diff --git a/examples/04-initial-load/src/components/postTable/index.js b/examples/04-initial-load/src/components/postTable/index.js new file mode 100644 index 0000000..9834d57 --- /dev/null +++ b/examples/04-initial-load/src/components/postTable/index.js @@ -0,0 +1 @@ +export * from './table'; \ No newline at end of file diff --git a/examples/04-initial-load/src/components/postTable/row.js b/examples/04-initial-load/src/components/postTable/row.js new file mode 100644 index 0000000..b455df2 --- /dev/null +++ b/examples/04-initial-load/src/components/postTable/row.js @@ -0,0 +1,15 @@ +import React from 'react'; + +export const Row = (post) => ( + + + {post.id} + + + {post.title} + + + {post.body} + + +); \ No newline at end of file diff --git a/examples/04-initial-load/src/components/postTable/table.js b/examples/04-initial-load/src/components/postTable/table.js new file mode 100644 index 0000000..9ec572e --- /dev/null +++ b/examples/04-initial-load/src/components/postTable/table.js @@ -0,0 +1,13 @@ +import React, { Component } from 'react'; +import { Table } from '../../common/components/table'; +import { Header } from './header'; +import { Row } from './row'; + +export const PostTable = (props) => ( + +); diff --git a/examples/04-initial-load/src/components/userTable/header.js b/examples/04-initial-load/src/components/userTable/header.js new file mode 100644 index 0000000..0a5ad22 --- /dev/null +++ b/examples/04-initial-load/src/components/userTable/header.js @@ -0,0 +1,9 @@ +import React from 'react'; + +export const Header = (props) => ( + + + + + +); \ No newline at end of file diff --git a/examples/04-initial-load/src/components/userTable/index.js b/examples/04-initial-load/src/components/userTable/index.js new file mode 100644 index 0000000..9834d57 --- /dev/null +++ b/examples/04-initial-load/src/components/userTable/index.js @@ -0,0 +1 @@ +export * from './table'; \ No newline at end of file diff --git a/examples/04-initial-load/src/components/userTable/row.js b/examples/04-initial-load/src/components/userTable/row.js new file mode 100644 index 0000000..c76e3e1 --- /dev/null +++ b/examples/04-initial-load/src/components/userTable/row.js @@ -0,0 +1,15 @@ +import React from 'react'; + +export const Row = (user) => ( + + + + + +); \ No newline at end of file diff --git a/examples/04-initial-load/src/components/userTable/table.js b/examples/04-initial-load/src/components/userTable/table.js new file mode 100644 index 0000000..3cacf77 --- /dev/null +++ b/examples/04-initial-load/src/components/userTable/table.js @@ -0,0 +1,13 @@ +import React, { Component } from 'react'; +import { Table } from '../../common/components/table'; +import { Header } from './header'; +import { Row } from './row'; + +export const UserTable = (props) => ( +
IdNameEmail
+ {user.id} + + {user.name} + + {user.email} +
+); \ No newline at end of file diff --git a/examples/04-initial-load/src/index.html b/examples/04-initial-load/src/index.html new file mode 100644 index 0000000..2b604d2 --- /dev/null +++ b/examples/04-initial-load/src/index.html @@ -0,0 +1,12 @@ + + + + + + + React Promise tracker sample app + + +
+ + diff --git a/examples/04-initial-load/src/index.jsx b/examples/04-initial-load/src/index.jsx new file mode 100644 index 0000000..cd51e59 --- /dev/null +++ b/examples/04-initial-load/src/index.jsx @@ -0,0 +1,11 @@ +import React, { Component } from 'react'; +import { render } from 'react-dom'; +import { App } from './app'; +import { Spinner } from './common/components/spinner'; + +render( +
+ + +
, + document.getElementById('root')); diff --git a/examples/04-initial-load/webpack.config.js b/examples/04-initial-load/webpack.config.js new file mode 100644 index 0000000..4715f2c --- /dev/null +++ b/examples/04-initial-load/webpack.config.js @@ -0,0 +1,62 @@ +var HtmlWebpackPlugin = require("html-webpack-plugin"); +var webpack = require("webpack"); +var MiniCssExtractPlugin = require("mini-css-extract-plugin"); + +var path = require("path"); +var basePath = __dirname; + +module.exports = { + context: path.join(basePath, "src"), + resolve: { + extensions: [".js", ".jsx"], + alias: { + react: path.resolve('./node_modules/react'), + 'react-dom': path.resolve('./node_modules/react-dom') + }, + }, + entry: { + app: "./index.jsx", + vendor: ["react", "react-dom"], + }, + output: { + filename: "[name].[chunkhash].js" + }, + optimization: { + splitChunks: { + cacheGroups: { + vendor: { + chunks: "initial", + name: "vendor", + test: "vendor", + enforce: true + } + } + } + }, + devtool: 'inline-source-map', + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + loader: "babel-loader" + }, + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, "css-loader"] + } + ] + }, + plugins: [ + //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: "index.html", //Name of file in ./dist/ + template: "index.html", //Name of template in ./src + hash: true + }), + new MiniCssExtractPlugin({ + filename: "[name].css", + chunkFilename: "[id].css" + }) + ] +}; diff --git a/examples/05-typescript/.babelrc b/examples/05-typescript/.babelrc new file mode 100644 index 0000000..ae31d35 --- /dev/null +++ b/examples/05-typescript/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": [ + "@babel/preset-react", + [ + "@babel/preset-env", + { + "useBuiltIns": "entry" + } + ] + ] +} diff --git a/examples/05-typescript/Readme.md b/examples/05-typescript/Readme.md new file mode 100644 index 0000000..e1e3f82 --- /dev/null +++ b/examples/05-typescript/Readme.md @@ -0,0 +1,135 @@ +# 01 Hello React + +In this sample we will create our first react component and connect it with the DOM via react-dom. + +We will take a startup point sample _00 Boilerplate_. + +Summary steps: + +- Install react and react-dom libraries. +- Install react and react-dom typescript definitions. +- Update the index.html to create a placeholder for the react components. +- Create a simple react component. +- Wire up this component by using react-dom. + +## Prerequisites + +Install [Node.js and npm](https://nodejs.org/en/) (v8.9.4 or higher) if they are not already installed on your computer. + +> Verify that you are running at least node v8.x.x and npm 5.x.x by running `node -v` and `npm -v` +> in a terminal/console window. Older versions may produce errors. + +## Steps to build it + +- Copy the content of the `00 Boilerplate` folder to an empty folder for the sample. + +- Install the npm packages described in the [./package.json](./package.json) and verify that it works: + +```bash +npm install +``` + +- Install `react` and `react-dom` libraries as project dependencies. + +```bash +npm install react react-dom --save +``` + +- Install also the typescript definitions for `react` and `react-dom` + but as dev dependencies. + +```bash +npm install @types/react @types/react-dom --save-dev +``` + +- Update the [./src/index.html](./src/index.html) to create a placeholder for the react components. + +_[./src/index.html](./src/index.html)_ + +```diff + + + + + + + +
+

Sample app

++
+
+ + + +``` + +- Create a simple react component (let's create it within a new file called `hello.tsx` in `src`folder). + +_[./src/hello.tsx](./src/hello.tsx)_ + +```javascript +import * as React from "react"; + +export const HelloComponent = () => { + return

Hello component !

; +}; +``` + +- Wire up this component by using `react-dom` under [./src/index.tsx](./src/index.tsx) (we have to rename + this file extension from `ts` to `tsx` and replace the content). + +_[./src/index.tsx](./src/index.tsx)_ + +```diff +- document.write('Hello from index.ts!'); + ++ import * as React from 'react'; ++ import * as ReactDOM from 'react-dom'; + ++ import { HelloComponent } from './hello'; + ++ ReactDOM.render( ++ , ++ document.getElementById('root') ++ ); +``` + +- Delete the file _main.ts_ we are not going to need it anymore. + +- Modify the [./webpack.config.js](./webpack.config.js) file and change the entry point from `./index.ts` + to `./index.tsx`. + +_[./webpack.config.js](./webpack.config.js)_ + +```diff +... + +module.exports = { + context: path.join(basePath, 'src'), + resolve: { + extensions: ['.js', '.ts', '.tsx'] + }, + entry: { +- app: './index.ts', ++ app: './index.tsx', + vendorStyles: [ + '../node_modules/bootstrap/dist/css/bootstrap.css', + ], + }, +``` + +- Execute the example: + +```bash +npm start +``` + +# About Basefactor + Lemoncode + +We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. + +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. + +[Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. + +For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend diff --git a/examples/05-typescript/package.json b/examples/05-typescript/package.json new file mode 100644 index 0000000..013068a --- /dev/null +++ b/examples/05-typescript/package.json @@ -0,0 +1,44 @@ +{ + "name": "react-typescript-by-sample", + "version": "1.0.0", + "description": "React Typescript examples", + "main": "index.js", + "scripts": { + "start": "webpack-dev-server --mode development --inline --hot --open", + "build": "webpack --mode development" + }, + "keywords": [ + "react", + "typescript", + "hooks" + ], + "author": "Braulio Diez Botella", + "license": "MIT", + "devDependencies": { + "@babel/cli": "^7.2.3", + "@babel/core": "^7.2.2", + "@babel/polyfill": "^7.2.5", + "@babel/preset-env": "^7.3.1", + "@babel/preset-react": "^7.0.0", + "@types/react": "^16.8.3", + "@types/react-dom": "^16.8.1", + "awesome-typescript-loader": "^5.2.1", + "babel-loader": "^8.0.5", + "css-loader": "^2.1.0", + "file-loader": "^3.0.1", + "html-webpack-plugin": "^3.2.0", + "mini-css-extract-plugin": "^0.5.0", + "style-loader": "^0.23.1", + "typescript": "^3.3.3", + "url-loader": "^1.1.2", + "webpack": "^4.29.3", + "webpack-cli": "^3.2.3", + "webpack-dev-server": "^3.1.14" + }, + "dependencies": { + "react": "^16.8.5", + "react-dom": "^16.8.5", + "react-loader-spinner": "^2.3.0", + "react-promise-tracker": "file:../../build" + } +} diff --git a/examples/05-typescript/src/api/fetch.ts b/examples/05-typescript/src/api/fetch.ts new file mode 100644 index 0000000..1239c07 --- /dev/null +++ b/examples/05-typescript/src/api/fetch.ts @@ -0,0 +1,12 @@ +export const fetchWithDelay = (url) => { + const promise = new Promise((resolve, reject) => { + setTimeout(() => { + resolve(fetch(url, { + method: 'GET', + }) + .then((response) => response.json())); + }, 3000) + }); + + return promise; +} diff --git a/examples/05-typescript/src/api/postAPI.ts b/examples/05-typescript/src/api/postAPI.ts new file mode 100644 index 0000000..cc9f490 --- /dev/null +++ b/examples/05-typescript/src/api/postAPI.ts @@ -0,0 +1,9 @@ +import { fetchWithDelay } from './fetch'; +const url = 'https://jsonplaceholder.typicode.com/posts'; + +const fetchPosts = () => fetchWithDelay(url) + .then((posts : any[]) => posts.slice(0, 10)); + +export const postAPI = { + fetchPosts, +}; diff --git a/examples/05-typescript/src/api/userAPI.ts b/examples/05-typescript/src/api/userAPI.ts new file mode 100644 index 0000000..e4e2929 --- /dev/null +++ b/examples/05-typescript/src/api/userAPI.ts @@ -0,0 +1,8 @@ +import { fetchWithDelay } from './fetch'; +const url = 'https://jsonplaceholder.typicode.com/users'; + +const fetchUsers = () => fetchWithDelay(url); + +export const userAPI = { + fetchUsers, +}; diff --git a/examples/05-typescript/src/app.css b/examples/05-typescript/src/app.css new file mode 100644 index 0000000..0acc593 --- /dev/null +++ b/examples/05-typescript/src/app.css @@ -0,0 +1,11 @@ +.tables { + display: flex; + flex-direction: row; + flex-wrap: nowrap; +} + +.tables > div { + flex-basis: 50%; + margin-left: 1rem; + margin-right: 1rem; +} diff --git a/examples/05-typescript/src/app.tsx b/examples/05-typescript/src/app.tsx new file mode 100644 index 0000000..bc39b48 --- /dev/null +++ b/examples/05-typescript/src/app.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { trackPromise } from 'react-promise-tracker'; +import { userAPI } from './api/userAPI'; +import { postAPI } from './api/postAPI'; +import { UserTable, PostTable, LoadButton } from './components'; +import './app.css'; + +interface State { + users: any[], + posts: any[], +} + +export class App extends React.Component<{}, State> { + constructor(props) { + super(props); + + this.state = { + users: [], + posts: [], + }; + + this.onLoadTables = this.onLoadTables.bind(this); + } + + onLoadTables() { + this.setState({ + users: [], + posts: [], + }); + + trackPromise( + userAPI.fetchUsers() + .then((users : any[]) => { + this.setState({ + users, + }) + }) + ); + + trackPromise( + postAPI.fetchPosts() + .then((posts) => { + this.setState({ + posts, + }) + }) + ); + } + + render() { + return ( +
+ +
+ + +
+
+ ); + } +} diff --git a/examples/05-typescript/src/common/spinner/index.ts b/examples/05-typescript/src/common/spinner/index.ts new file mode 100644 index 0000000..cd17217 --- /dev/null +++ b/examples/05-typescript/src/common/spinner/index.ts @@ -0,0 +1 @@ +export * from './spinner'; diff --git a/examples/05-typescript/src/common/spinner/spinner.css b/examples/05-typescript/src/common/spinner/spinner.css new file mode 100644 index 0000000..0396535 --- /dev/null +++ b/examples/05-typescript/src/common/spinner/spinner.css @@ -0,0 +1,10 @@ +.spinner { + width: 100%; + height: 100%; +} + +.spinner > div { + display: flex; + justify-content: center; + align-items: center; +} diff --git a/examples/05-typescript/src/common/spinner/spinner.tsx b/examples/05-typescript/src/common/spinner/spinner.tsx new file mode 100644 index 0000000..09e5405 --- /dev/null +++ b/examples/05-typescript/src/common/spinner/spinner.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { usePromiseTracker } from 'react-promise-tracker'; +import Loader from 'react-loader-spinner'; +import './spinner.css'; + +export const Spinner: React.FunctionComponent = () => { + const { promiseInProgress } = usePromiseTracker(null); + + return ( + promiseInProgress && ( +
+ +
+ ) + ); +}; diff --git a/examples/05-typescript/src/common/table/index.ts b/examples/05-typescript/src/common/table/index.ts new file mode 100644 index 0000000..302847a --- /dev/null +++ b/examples/05-typescript/src/common/table/index.ts @@ -0,0 +1 @@ +export * from './table' diff --git a/examples/05-typescript/src/common/table/table.css b/examples/05-typescript/src/common/table/table.css new file mode 100644 index 0000000..bcbfb4f --- /dev/null +++ b/examples/05-typescript/src/common/table/table.css @@ -0,0 +1,27 @@ +.title { + color: #000; +} + +.table { + width: 100%; + max-width: 100%; + margin-bottom: 1rem; + background-color: transparent; + border-collapse: collapse; +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #248f4f; +} + +.table td, .table th { + text-align: inherit; + padding: .75rem; + vertical-align: top; + border-top: 1px solid #248f4f; +} + +.table tbody tr:nth-of-type(odd) { + background-color: rgba(43, 173, 96, .05); +} diff --git a/examples/05-typescript/src/common/table/table.tsx b/examples/05-typescript/src/common/table/table.tsx new file mode 100644 index 0000000..3dca7fe --- /dev/null +++ b/examples/05-typescript/src/common/table/table.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import './table.css'; + +export const Table = (props) => ( +
+

{props.title}

+
+ + {props.headerRender()} + + + {props.items.map(props.rowRender)} + +
+ +); diff --git a/examples/05-typescript/src/components/index.ts b/examples/05-typescript/src/components/index.ts new file mode 100644 index 0000000..0d5f110 --- /dev/null +++ b/examples/05-typescript/src/components/index.ts @@ -0,0 +1,3 @@ +export * from './userTable'; +export * from './postTable'; +export * from './loadButton'; diff --git a/examples/05-typescript/src/components/loadButton/index.ts b/examples/05-typescript/src/components/loadButton/index.ts new file mode 100644 index 0000000..2d95cb6 --- /dev/null +++ b/examples/05-typescript/src/components/loadButton/index.ts @@ -0,0 +1 @@ +export * from './loadButton'; diff --git a/examples/05-typescript/src/components/loadButton/loadButton.css b/examples/05-typescript/src/components/loadButton/loadButton.css new file mode 100644 index 0000000..760d45e --- /dev/null +++ b/examples/05-typescript/src/components/loadButton/loadButton.css @@ -0,0 +1,26 @@ +.load-button { + cursor: pointer; + margin: 1rem; + color: #fff; + text-shadow: 1px 1px 0 #888; + background-color: #2BAD60; + border-color: #14522d; + display: inline-block; + font-weight: 400; + text-align: center; + white-space: nowrap; + vertical-align: middle; + border: 1px solid transparent; + padding: .375rem .75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: .25rem; + outline: none; +} + + + +.load-button:hover { + background-color: #248f4f; + border-color: #1e7b43; +} diff --git a/examples/05-typescript/src/components/loadButton/loadButton.tsx b/examples/05-typescript/src/components/loadButton/loadButton.tsx new file mode 100644 index 0000000..6cdcdf1 --- /dev/null +++ b/examples/05-typescript/src/components/loadButton/loadButton.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import './loadButton.css'; + +export const LoadButton = (props) => ( + +); diff --git a/examples/05-typescript/src/components/postTable/header.tsx b/examples/05-typescript/src/components/postTable/header.tsx new file mode 100644 index 0000000..f8feb6c --- /dev/null +++ b/examples/05-typescript/src/components/postTable/header.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; + +export const Header = (props) => ( + + Id + Title + Body + +); diff --git a/examples/05-typescript/src/components/postTable/index.ts b/examples/05-typescript/src/components/postTable/index.ts new file mode 100644 index 0000000..01643f0 --- /dev/null +++ b/examples/05-typescript/src/components/postTable/index.ts @@ -0,0 +1 @@ +export * from './table'; diff --git a/examples/05-typescript/src/components/postTable/row.tsx b/examples/05-typescript/src/components/postTable/row.tsx new file mode 100644 index 0000000..f1ad180 --- /dev/null +++ b/examples/05-typescript/src/components/postTable/row.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; + +export const Row = (post) => ( + + + {post.id} + + + {post.title} + + + {post.body} + + +); diff --git a/examples/05-typescript/src/components/postTable/table.tsx b/examples/05-typescript/src/components/postTable/table.tsx new file mode 100644 index 0000000..eaeae3d --- /dev/null +++ b/examples/05-typescript/src/components/postTable/table.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { Table } from '../../common/table'; +import { Header } from './header'; +import { Row } from './row'; + +export const PostTable = (props) => ( + +); diff --git a/examples/05-typescript/src/components/userTable/header.tsx b/examples/05-typescript/src/components/userTable/header.tsx new file mode 100644 index 0000000..0a75853 --- /dev/null +++ b/examples/05-typescript/src/components/userTable/header.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; + +export const Header = (props) => ( + + + + + +); diff --git a/examples/05-typescript/src/components/userTable/index.ts b/examples/05-typescript/src/components/userTable/index.ts new file mode 100644 index 0000000..01643f0 --- /dev/null +++ b/examples/05-typescript/src/components/userTable/index.ts @@ -0,0 +1 @@ +export * from './table'; diff --git a/examples/05-typescript/src/components/userTable/row.tsx b/examples/05-typescript/src/components/userTable/row.tsx new file mode 100644 index 0000000..377ee31 --- /dev/null +++ b/examples/05-typescript/src/components/userTable/row.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; + +export const Row = (user) => ( + + + + + +); diff --git a/examples/05-typescript/src/components/userTable/table.tsx b/examples/05-typescript/src/components/userTable/table.tsx new file mode 100644 index 0000000..e34d874 --- /dev/null +++ b/examples/05-typescript/src/components/userTable/table.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { Table } from '../../common/table'; +import { Header } from './header'; +import { Row } from './row'; + +export const UserTable = (props) => ( +
IdNameEmail
+ {user.id} + + {user.name} + + {user.email} +
+); diff --git a/examples/05-typescript/src/hello.tsx b/examples/05-typescript/src/hello.tsx new file mode 100644 index 0000000..1d871c9 --- /dev/null +++ b/examples/05-typescript/src/hello.tsx @@ -0,0 +1,5 @@ +import * as React from "react"; + +export const HelloComponent = () => { + return

Hello component !

; +}; diff --git a/examples/05-typescript/src/index.html b/examples/05-typescript/src/index.html new file mode 100644 index 0000000..cef0845 --- /dev/null +++ b/examples/05-typescript/src/index.html @@ -0,0 +1,13 @@ + + + + + + + +
+

Sample app

+
+
+ + diff --git a/examples/05-typescript/src/index.tsx b/examples/05-typescript/src/index.tsx new file mode 100644 index 0000000..615dc99 --- /dev/null +++ b/examples/05-typescript/src/index.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { App } from './app'; +import { Spinner } from './common/spinner'; + +ReactDOM.render( + <> + + + , + document.getElementById('root') +); diff --git a/examples/05-typescript/tsconfig.json b/examples/05-typescript/tsconfig.json new file mode 100644 index 0000000..f90a3f0 --- /dev/null +++ b/examples/05-typescript/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "es6", + "moduleResolution": "node", + "declaration": false, + "noImplicitAny": false, + "jsx": "react", + "sourceMap": true, + "noLib": false, + "suppressImplicitAnyIndexErrors": true + }, + "compileOnSave": false, + "exclude": ["node_modules"] +} diff --git a/examples/05-typescript/webpack.config.js b/examples/05-typescript/webpack.config.js new file mode 100644 index 0000000..b94c61d --- /dev/null +++ b/examples/05-typescript/webpack.config.js @@ -0,0 +1,66 @@ +var HtmlWebpackPlugin = require("html-webpack-plugin"); +var MiniCssExtractPlugin = require("mini-css-extract-plugin"); +var webpack = require("webpack"); +var path = require("path"); + +var basePath = __dirname; + +module.exports = { + context: path.join(basePath, "src"), + resolve: { + extensions: [".js", ".ts", ".tsx"], + alias: { + react: path.resolve('./node_modules/react'), + 'react-dom': path.resolve('./node_modules/react-dom') + } + }, + entry: ["@babel/polyfill", "./index.tsx"], + output: { + path: path.join(basePath, "dist"), + filename: "bundle.js" + }, + devtool: "source-map", + devServer: { + contentBase: "./dist", // Content base + inline: true, // Enable watch and live reload + host: "localhost", + port: 8080, + stats: "errors-only" + }, + module: { + rules: [ + { + test: /\.(ts|tsx)$/, + exclude: /node_modules/, + loader: "awesome-typescript-loader", + options: { + useBabel: true, + babelCore: "@babel/core" // needed for Babel v7 + } + }, + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, "css-loader"] + }, + { + test: /\.(png|jpg|gif|svg)$/, + loader: "file-loader", + options: { + name: "assets/img/[name].[ext]?[hash]" + } + } + ] + }, + plugins: [ + //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: "index.html", //Name of file in ./dist/ + template: "index.html", //Name of template in ./src + hash: true + }), + new MiniCssExtractPlugin({ + filename: "[name].css", + chunkFilename: "[id].css" + }) + ] +}; diff --git a/package.json b/package.json index 5c43c76..cfe69b5 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "react-promise-tracker", - "version": "1.1.1", - "description": "Simple React Promise tracker Hoc, helper to add loading spinner indicators", + "version": "2.0.0", + "description": "Simple React Promise tracker Hook/HOC helper to add loading spinner indicators", "keywords": [ "react", "promise", "tracker", "track", + "hook", "hoc", "higher order component", "spinner", @@ -20,15 +21,18 @@ "author": "Lemoncode", "contributors": [ "Braulio Diez (https://github.com/brauliodiez)", - "Javier Calzado (https://github.com/fjcalzado)", + "Javier Calzado (https://github.com/fjcalzado)", + "Daniel Sanchez (https://github.com/nasdan)", "Alejandro Rosa <> (https://github.com/arp82)" ], "files": [ - "lib", - "es", - "src", "dist", - "index.d.ts" + "es", + "lib", + "index.d.ts", + "LICENSE.txt", + "package.json", + "readme.md" ], "browser": "lib/index.js", "main": "lib/index.js", @@ -40,41 +44,52 @@ "url": "git+https://github.com/Lemoncode/react-promise-tracker.git" }, "scripts": { - "clean": "rimraf lib dist es", - "prepublish": "npm run clean && npm run build", - "build": "npm run clean && npm run build:commonjs && npm run build:umd && npm run build:umd:min && npm run build:es", - "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib --copy-files --ignore spec.js,test.js", - "build:es": "cross-env BABEL_ENV=es babel src --out-dir es --copy-files --ignore spec.js,test.js", - "build:umd": "cross-env BABEL_ENV=commonjs NODE_ENV=development webpack", - "build:umd:min": "cross-env BABEL_ENV=commonjs NODE_ENV=production webpack -p", + "clean": "rimraf build/*", + "build": "npm run clean && npm run build:lib && npm run build:es && npm run build:dist:prod && npm run build:copy", + "build:lib": "cross-env BABEL_ENV=es5_cjs babel src --out-dir build/lib --ignore 'src/**/*.spec.js,src/**/*.test.js'", + "build:es": "cross-env BABEL_ENV=es babel src --out-dir build/es --ignore 'src/**/*.spec.js,src/**/*.test.js'", + "build:dist:prod": "cross-env BABEL_ENV=umd webpack -p --config ./config/webpack/webpack.prod.js", + "build:dist:dev": "cross-env BABEL_ENV=umd webpack --config ./config/webpack/webpack.dev.js", + "build:copy": "copyfiles package.json readme.md LICENSE.txt build && copyfiles \"./src/**/*.d.ts\" -u 1 build", "test": "cross-env NODE_ENV=test jest", - "test:watch": "cross-env NODE_ENV=test jest --watchAll" + "test:watch": "cross-env NODE_ENV=test jest --watchAll", + "release:prepare": "npm run clean && npm run test && npm run build && rimraf build/dist/report", + "release": "npm publish ./build", + "release-beta": "npm run release:prepare && npm publish ./build --tag beta" }, - "dependencies": { - "react": "^16.0.0" + "peerDependencies": { + "react": ">=16.8.0" }, + "dependencies": {}, "devDependencies": { - "babel-cli": "^6.26.0", - "babel-core": "^6.26.0", - "babel-jest": "^21.2.0", - "babel-loader": "^7.1.2", - "babel-preset-env": "^1.6.1", - "babel-preset-react": "^6.24.1", - "cross-env": "^5.1.1", - "enzyme": "^3.2.0", - "enzyme-adapter-react-16": "^1.1.0", - "enzyme-to-json": "^3.2.2", - "jest": "^23.5.0", - "react-dom": "^16.1.1", - "react-test-renderer": "^16.1.1", - "regenerator-runtime": "^0.11.0", - "rimraf": "^2.6.2", - "webpack": "^3.8.1" + "@babel/cli": "^7.2.3", + "@babel/core": "^7.2.2", + "@babel/preset-env": "^7.3.1", + "@babel/preset-react": "^7.0.0", + "@babel/register": "^7.0.0", + "babel-loader": "^8.0.5", + "compression-webpack-plugin": "^2.0.0", + "copyfiles": "^2.1.0", + "cross-env": "^5.2.0", + "enzyme": "^3.9.0", + "enzyme-adapter-react-16": "^1.11.2", + "enzyme-to-json": "^3.3.5", + "jest": "^24.5.0", + "raf": "^3.4.1", + "react": "^16.8.5", + "react-dom": "^16.8.5", + "react-test-renderer": "^16.8.4", + "regenerator-runtime": "^0.13.1", + "rimraf": "^2.6.3", + "webpack": "^4.29.6", + "webpack-bundle-analyzer": "^3.1.0", + "webpack-cli": "^3.3.0", + "webpack-merge": "^4.2.1" }, "jest": { "setupFiles": [ - "./test/shim.js", - "./test/jestsetup.js" + "./config/test/polyfills.js", + "./config/test/setupJest.js" ], "snapshotSerializers": [ "enzyme-to-json/serializer" @@ -82,9 +97,7 @@ "restoreMocks": true, "testPathIgnorePatterns": [ "/node_modules/", - "/dist/", - "/es/", - "/lib/" + "/build/" ] } } diff --git a/readme.md b/readme.md index cb9b92e..a33e3c3 100644 --- a/readme.md +++ b/readme.md @@ -1,14 +1,20 @@ # react-promise-tracker -Simple promise tracker React Hoc. You can see it in action in this [Live Demo](https://stackblitz.com/edit/react-promise-tracker-default-area-sample), and find the basic info to get started in this [post](https://www.basefactor.com/react-how-to-display-a-loading-indicator-on-fetch-calls). +Simple promise tracker React Hoc. You can see it in action in this [Live Demo](https://codesandbox.io/s/wy04jpmly7), and find the basic info to get started in this [post](https://www.basefactor.com/react-how-to-display-a-loading-indicator-on-fetch-calls). # Why do I need this? -Sometimes we need to track blocking promises (e.g. fetch http calls), to choose between displaying a loading spinner or not. +Sometimes we need to track blocking promises (e.g. fetch or axios http calls), and control whether to +display a loading spinner indicator not, you have to take care of scenarios like: + - You could need to track several ajax calls being performed in parallel. + - Some of them you want to be tracked some others to be executed silently in background. + - You may want to have several spinners blocking only certain areas of the screen. + - For high speed connection you may wat to show the loading spinner after an small delay of time + to avoid having a flickering effect in your screen. This library implements: - A simple function that will allow a promise to be tracked. - - An HOC component that will allow us wrap a loading spinner (it will be displayed when the number of tracked request are greater than zero, and hidden when not). + - A Hook + HOC component that will allow us wrap a loading spinner (it will be displayed when the number of tracked request are greater than zero, and hidden when not). # Installation @@ -29,22 +35,23 @@ Whenever you want a promise to be tracked, just wrap it like in the code below: + ); ``` -Then you only need to create a component that will defined a property called _trackedPromiseInProgress_ - -And wrap it around the _promiseTrackerHoc_ +Then you only need to create a spinner component and make use of the _usePromiseTracker_, this +hook will expose a boolean property that will let us decide whether to show or hide the loading +spinner. ## Basic sample: ```diff import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -+ import { promiseTrackerHoc} from 'react-promise-tracker'; ++ import { usePromiseTracker } from "react-promise-tracker"; + +export const LoadingSpinerComponent = (props) => { ++ const { promiseInProgress } = usePromiseTracker(); -const InnerLoadingSpinerComponent = (props) => { return (
{ - (props.trackedPromiseInProgress === true) ? ++ (promiseInProgress === true) ?

Hey I'm a spinner loader wannabe !!!

: null @@ -52,12 +59,6 @@ const InnerLoadingSpinerComponent = (props) => {
) }; - -InnerLoadingSpinerComponent.propTypes = { - trackedPromiseInProgress : PropTypes.bool.isRequired, -}; - -+ export const LoadingSpinnerComponent = promiseTrackerHoc(InnerLoadingSpinerComponent); ``` - To add a cool spinner component you can make use of _react-spinners_: @@ -85,9 +86,26 @@ export const AppComponent = (props) => ( Using react-promise-tracker as is will just display a single spinner in your page, there are cases where you want to display a given spinner only blocking certain area of the screen (e.g.: a product list app with a shopping cart section. We would like to block the ui (show spinner) while is loading the product, but not the rest of the user interface, and the same thing with the shopping cart pop-up section. -![Shopping cart sample](./readme_resources/00-shopping-cart-sample.png) +![Shopping cart sample](/img/started//00-shopping-cart-sample.png) -We could add the `default-area` to show product list spinner: +The _promiseTracker_ hooks exposes a config parameter, here we can define the area that we want to setup +(by default o area). We could just feed the area in the props of the common spinner we have created + +```diff +export const Spinner = (props) => { ++ const { promiseInProgress } = usePromiseTracker({area: props.area}); + + return ( + promiseInProgress && ( +
+ +
+ ) + ); +}; +``` + +We could add the `default-area` to show product list spinner (no params means just default area): ```diff import React from 'react'; @@ -114,7 +132,7 @@ export const ShoppingCartModal = (props) => ( ); ``` -With this approach, we don't need to define different spinners components, it's only one but it will render when we want to track the desired area: +The when we track a given promise we can choose the area that would be impacted. ```diff + import { trackPromise} from 'react-promise-tracker'; @@ -124,13 +142,33 @@ With this approach, we don't need to define different spinners components, it's + ,'shopping-cart-area'); ``` +## Sample with delay: + +You can add as well a delay to display the spinner, When is this useful? if your users are connected on +high speed connections it would be worth to show the spinner right after 500 Ms (checking that the +ajax request hasn't been completed), this will avoid having undesired screen flickering on high speed +connection scenarios. + +```diff +export const Spinner = (props) => { ++ const { promiseInProgress } = usePromiseTracker({delay: 500}); +``` + # Demos -If you want to see it in action: +Full examples: + +- [00 Basic Example](https://codesandbox.io/s/wy04jpmly7): minimum sample to get started. + +- [01 Example Areas](https://codesandbox.io/s/wy04jpmly7): defining more than one spinner to be displayed in separate screen areas. + +- [02 Example Delay](https://codesandbox.io/s/kwrrjjyjm5): displaying the spinner after some miliseconds delay (useful when your users havbe high speed connections). + +- [03 Example Hoc](https://codesandbox.io/s/j2jjrk4ply): using legacy high order component approach (useful if your spinner is a class based component) -- [Default area example](https://stackblitz.com/edit/react-promise-tracker-default-area-sample) +- [04 Initial load](https://codesandbox.io/s/j2jjrk4ply): launching ajax request just on application startup before the spinner is being mounted. -- [Two areas example](https://stackblitz.com/edit/react-promise-tracker-two-areas-sample) +- [05 Typescript](https://codesandbox.io/s/5ww39l90yp): full sample using typescript (using library embedded typings). # About Basefactor + Lemoncode diff --git a/readme_es.md b/readme_es.md index 3090933..1e0dbae 100644 --- a/readme_es.md +++ b/readme_es.md @@ -3,22 +3,23 @@ Componente React Hoc, rastreador de promesas. Puedes verlo en acción: [Demo](https://stackblitz.com/edit/react-promise-tracker-default-area-sample) -# ¿Por qué necesito esto? +## ¿Por qué necesito esto? -Algunas veces necesitas rastrear promesas bloqueantes (ejemplo: fetch http calls), +Algunas veces necesitas rastrear promesas bloqueantes (ejemplo: fetch http calls), para escoger entre mostrar un spinner de cargando... o no. Esta librería implementa: - - Una función simple que te permitirá rastrear una promesa. - - Un componente HOC, que nos permitirá usar un wrapper como spinner de cargando... (se mostrará cuando el número de peticiones rastreadas sea mayor que cero, y estará oculto cuando no). -# Instalación. +- Una función simple que te permitirá rastrear una promesa. +- Un componente HOC, que nos permitirá usar un wrapper como spinner de cargando... (se mostrará cuando el número de peticiones rastreadas sea mayor que cero, y estará oculto cuando no). + +## Instalación ```cmd npm install react-promise-tracker --save ``` -# Uso +## Uso Siempre que quieras rastrear una promesa, simplemente usa el componente como wrapper tal como se muestra en el siguiente código: @@ -35,7 +36,7 @@ Entonces solo necesitas crear el componente que define una propiedad llamada _tr Y envolverlo con el _promiseTrackerHoc_ -## Ejemplo báscio: +## Ejemplo básico ```diff import React, { Component } from 'react'; @@ -67,7 +68,6 @@ InnerLoadingSpinerComponent.propTypes = { - [Demo page](http://www.davidhu.io/react-spinners/) - [Github page](https://github.com/davidhu2000/react-spinners) - - Luego en el punto de entrada de tu apliación (main / app / ...) solo añade este componente loading spinner, para que sea renderizado: ```diff @@ -82,12 +82,12 @@ export const AppComponent = (props) => ( ); ``` -## Ejemplo con áreas: +## Ejemplo con áreas Es posible usar react-promise-tracker como si se mostrara un solo spinner en la página. Hay casos en los que desea mostrar un spinner solo bloqueando cierta área de la pantalla (por ejemplo, una aplicación de lista de productos con una sección de carrito de la compra). Nos gustaría bloquear esa área de la UI (mostrar sólo el spinner) mientras carga el producto, pero no el resto de la interfaz de usuario, y lo mismo con la sección pop-up del carro de compras. -![Shopping cart sample](./readme_resources/00-shopping-cart-sample.png) +![Shopping cart sample](./resources/00-shopping-cart-sample.png) Podemos añadir el área `default-area` para mostrar el spinner de la lista de productos: @@ -126,7 +126,7 @@ Con este enfoque, no necesitamos definir diferentes componentes spinners, con un + ,'shopping-cart-area'); ``` -# Demos +## Demos Si quieres verlo en acción puedes visitar: @@ -134,10 +134,9 @@ Si quieres verlo en acción puedes visitar: - [Ejemplo de dos áreas](https://stackblitz.com/edit/react-promise-tracker-two-areas-sample) - -# Sobre Lemoncode +## Sobre Lemoncode Somos un equipo de una larga experiencia como desarrolladores freelance, establecidos como grupo en 2010. Estamos especializados en tecnologías Front End y .NET. [Click aquí](http://lemoncode.net/services/en/#en-home) para más info sobre nosotros. -Para la audiencia LATAM/Español estamos desarrollando un máster Front End Online, más info: http://lemoncode.net/master-frontend +Para la audiencia LATAM/Español estamos desarrollando un máster Front End Online, más info: [http://lemoncode.net/master-frontend](http://lemoncode.net/master-frontend) diff --git a/readme_resources/00-shopping-cart-sample.png b/resources/00-shopping-cart-sample.png similarity index 100% rename from readme_resources/00-shopping-cart-sample.png rename to resources/00-shopping-cart-sample.png diff --git a/src/__snapshots__/trackerHoc.test.js.snap b/src/__snapshots__/trackerHoc.test.js.snap index bc93922..d1f88b4 100644 --- a/src/__snapshots__/trackerHoc.test.js.snap +++ b/src/__snapshots__/trackerHoc.test.js.snap @@ -1,10 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`trackerHoc should render component with trackedPromiseInProgress equals false and area equals "default-area" when render promiseTrackerHoc 1`] = ` +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false and area equals "default-area" when render promiseTrackerHoc without props 1`] = ` test @@ -13,13 +18,22 @@ exports[`trackerHoc should render component with trackedPromiseInProgress equals `; -exports[`trackerHoc should render component with trackedPromiseInProgress equals false and area equals "testArea" when feeding area equals "testArea" 1`] = ` +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false and area equals "testArea" when feeding area equals "testArea" 1`] = ` test @@ -28,11 +42,41 @@ exports[`trackerHoc should render component with trackedPromiseInProgress equals `; -exports[`trackerHoc should render component with trackedPromiseInProgress equals false when counter is 0 1`] = ` +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false and area equals "testArea" when feeding area equals "testArea" and delay equals 300 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 1`] = ` test @@ -41,14 +85,355 @@ exports[`trackerHoc should render component with trackedPromiseInProgress equals `; -exports[`trackerHoc should render component with trackedPromiseInProgress equals false, area equals "default-area" and customProp equals "test" when feeding customProp equals "test" 1`] = ` +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and delay equals 300 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false and delay equals 300 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false to different area 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false to different area and delay equals 300 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals true to different area 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals true to different area and delay equals 300 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 1 and emit event with progress equals false 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 1 and emit event with progress equals false and delay equals 300 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false, area equals "default-area" and customProp equals "test" when feeding customProp equals "test" 1`] = ` + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 0 and emit event with progress equals true 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 0 and emit event with progress equals true and delay equals 300 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 1 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 1 and delay equals 300 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals false to different area 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals false to different area and delay equals 300 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals true 1`] = ` + + test @@ -57,11 +442,64 @@ exports[`trackerHoc should render component with trackedPromiseInProgress equals `; -exports[`trackerHoc should render component with trackedPromiseInProgress equals true when counter is 1 1`] = ` +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals true and delay equals 300 1`] = ` + + + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals true to different area 1`] = ` + + test + + + +`; + +exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals true to different area and delay equals 300 1`] = ` + + test diff --git a/src/__snapshots__/trackerHook.test.js.snap b/src/__snapshots__/trackerHook.test.js.snap new file mode 100644 index 0000000..7f78a2a --- /dev/null +++ b/src/__snapshots__/trackerHook.test.js.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`trackerHook Initial Status renders without crashing 1`] = ` + + + test + + +`; + +exports[`trackerHook Initial Status should render component with trackedPromiseInProgress equals false and area equals "testArea" when feeding area equals "testArea" and delay equals 300 1`] = ` + + + test + + +`; + +exports[`trackerHook Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 1`] = ` + + + test + + +`; + +exports[`trackerHook Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false to different area 1`] = ` + + + test + + +`; + +exports[`trackerHook Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals true to different area 1`] = ` + + + test + + +`; diff --git a/index.d.ts b/src/index.d.ts similarity index 57% rename from index.d.ts rename to src/index.d.ts index 2b7cd28..927507c 100644 --- a/index.d.ts +++ b/src/index.d.ts @@ -11,21 +11,38 @@ import * as React from "react"; */ export function trackPromise(promise: Promise): Promise; +/** + * Configuration contract: user can setup areas (display more than one spinner) or delay when + * the spinner is shown (this is useful when a user has a fast connection, to avoid unneccessary flickering) + */ + +interface Config { + area: string; + delay: number; +} /** * It wraps a given React component into a new component that adds properties to watch - * pending promises. + * pending promises (HOC). * @param component Input component to be wrapped. * @returns It returns a new component that extends the input one. */ export interface ComponentToWrapProps { - area: string; - trackedPromiseInProgress: boolean; + config: Config; + promiseInProgress: boolean; } export interface TrackerHocProps { - area?: string; + config?: Config; } export function promiseTrackerHoc

(component: React.ComponentType

): React.ComponentType

; + +/** + * React Promise Tracker custom hook, this hook will expose a promiseInProgress boolean flag. + * + * @param configuration (optional can be null). + * @returns promiseInProgressFlag. + */ +export function usePromiseTracker(outerConfig? : Config) : { promiseInProgress : boolean }; diff --git a/src/index.js b/src/index.js index 077eb9e..befeaf0 100644 --- a/src/index.js +++ b/src/index.js @@ -1,2 +1,3 @@ export { trackPromise } from './trackPromise'; export { promiseTrackerHoc } from './trackerHoc'; +export { usePromiseTracker } from './trackerHook'; diff --git a/src/setupConfig.js b/src/setupConfig.js new file mode 100644 index 0000000..9e3bce9 --- /dev/null +++ b/src/setupConfig.js @@ -0,0 +1,9 @@ +import { defaultArea } from "./constants"; + +export const defaultConfig = { area: defaultArea, delay: 0 }; + +// Defensive config setup, fulfill default values +export const setupConfig = (outerConfig) => ({ + area: (!outerConfig || !outerConfig.area) ? defaultArea : outerConfig.area, + delay: (!outerConfig || !outerConfig.delay) ? 0 : outerConfig.delay, +}) diff --git a/src/setupConfig.test.js b/src/setupConfig.test.js new file mode 100644 index 0000000..a967159 --- /dev/null +++ b/src/setupConfig.test.js @@ -0,0 +1,94 @@ +import { defaultArea } from "./constants"; +import {setupConfig} from './setupConfig'; + +describe("setupConfig", () => { + it("should return same config as param if all values are informed", () => { + // Arrange + const userConfig = {area: 'myarea', delay: 200}; + // Act + const result = setupConfig(userConfig); + + // Assert + expect(result.area).toBe('myarea'); + expect(result.delay).toBe(200); + }); + + it("should return default config (default are, 0 delay) if input param is undefined", () => { + // Arrange + const userConfig = void(0); + // Act + const result = setupConfig(userConfig); + + // Assert + expect(result.area).toBe(defaultArea); + expect(result.delay).toBe(0); + }); + + it("should return default config (default area, 0 delay) if input para is null", () => { + // Arrange + const userConfig = null; + // Act + const result = setupConfig(userConfig); + + // Assert + expect(result.area).toBe(defaultArea); + expect(result.delay).toBe(0); + }); + + it("should fullfill default config and area if input param informed but empty object {}", () => { + // Arrange + const userConfig = null; + // Act + const result = setupConfig(userConfig); + + // Assert + expect(result.area).toBe(defaultArea); + expect(result.delay).toBe(0); + }); + + + it("should fullfill defaultArea param if undefined but delay informed", () => { + // Arrange + const userConfig = {area: void(0), delay: 200}; + // Act + const result = setupConfig(userConfig); + + // Assert + expect(result.area).toBe(defaultArea); + expect(result.delay).toBe(200); + }); + + it("should fullfill defaultArea param if null but delay informed", () => { + // Arrange + const userConfig = {area: null, delay: 200}; + // Act + const result = setupConfig(userConfig); + + // Assert + expect(result.area).toBe(defaultArea); + expect(result.delay).toBe(200); + }); + + it("should fullfill delay param (0) if undefined but area informed", () => { + // Arrange + const userConfig = {area: 'myarea', delay: void(0)}; + // Act + const result = setupConfig(userConfig); + + // Assert + expect(result.area).toBe('myarea'); + expect(result.delay).toBe(0); + }); + + it("should fullfill delay param (0) if null but area informed", () => { + // Arrange + const userConfig = {area: 'myarea', delay: null}; + // Act + const result = setupConfig(userConfig); + + // Assert + expect(result.area).toBe('myarea'); + expect(result.delay).toBe(0); + }); + +}); diff --git a/src/trackPromise.js b/src/trackPromise.js index 123195d..a461700 100644 --- a/src/trackPromise.js +++ b/src/trackPromise.js @@ -1,14 +1,14 @@ -import { Emitter } from './tinyEmmiter'; -import { defaultArea } from './constants'; +import { Emitter } from "./tinyEmmiter"; +import { defaultArea } from "./constants"; export const emitter = new Emitter(); -export const promiseCounterUpdateEventId = 'promise-counter-update'; +export const promiseCounterUpdateEventId = "promise-counter-update"; let counter = { - [defaultArea]: 0, + [defaultArea]: 0 }; -export const getCounter = (area) => counter[area]; +export const getCounter = area => counter[area]; export const trackPromise = (promise, area) => { area = area || defaultArea; @@ -23,7 +23,7 @@ export const trackPromise = (promise, area) => { return promise; }; -const incrementCounter = (area) => { +const incrementCounter = area => { if (Boolean(counter[area])) { counter[area]++; } else { @@ -31,15 +31,15 @@ const incrementCounter = (area) => { } }; -const anyPromiseInProgress = (area) => (counter[area] > 0); +const anyPromiseInProgress = area => counter[area] > 0; -const decrementPromiseCounter = (area) => { +const decrementPromiseCounter = area => { decrementCounter(area); - const promiseInProgress = anyPromiseInProgress(); + const promiseInProgress = anyPromiseInProgress(area); emitter.emit(promiseCounterUpdateEventId, promiseInProgress, area); }; -const decrementCounter = (area) => { +const decrementCounter = area => { counter[area]--; }; diff --git a/src/trackPromise.test.js b/src/trackPromise.test.js index d939938..df9ed51 100644 --- a/src/trackPromise.test.js +++ b/src/trackPromise.test.js @@ -1,9 +1,9 @@ -import { trackPromise, emitter } from './trackPromise'; -import { defaultArea } from './constants'; +import { trackPromise, emitter } from "./trackPromise"; +import { defaultArea } from "./constants"; -describe('trackPromise', () => { - describe('using default area', () => { - it('On Initial case, promise fired, promise emitter.emit is called', () => { +describe("trackPromise", () => { + describe("using default area", () => { + it("On Initial case, promise fired, promise emitter.emit is called", () => { // Arrange emitter.emit = jest.fn(); @@ -15,10 +15,14 @@ describe('trackPromise', () => { // Assert expect(emitter.emit).toHaveBeenCalledTimes(1); - expect(emitter.emit).toHaveBeenCalledWith('promise-counter-update', true, defaultArea); + expect(emitter.emit).toHaveBeenCalledWith( + "promise-counter-update", + true, + defaultArea + ); }); - it('Promise tracked, we got resolve, check that emit is called 2 times', (done) => { + it("Promise tracked, we got resolve, check that emit is called 2 times", done => { // Arrange emitter.emit = jest.fn(); @@ -31,14 +35,24 @@ describe('trackPromise', () => { myPromise.then(() => { expect(emitter.emit).toHaveBeenCalledTimes(2); - expect(emitter.emit).toHaveBeenNthCalledWith(1, 'promise-counter-update', true, defaultArea); - - expect(emitter.emit).toHaveBeenNthCalledWith(2, 'promise-counter-update', false, defaultArea); + expect(emitter.emit).toHaveBeenNthCalledWith( + 1, + "promise-counter-update", + true, + defaultArea + ); + + expect(emitter.emit).toHaveBeenNthCalledWith( + 2, + "promise-counter-update", + false, + defaultArea + ); done(); }); }); - it('Promise tracked, we got fail, check that emit is called 2 times', (done) => { + it("Promise tracked, we got fail, check that emit is called 2 times", done => { // Arrange emitter.emit = jest.fn(); @@ -51,16 +65,25 @@ describe('trackPromise', () => { myPromise.catch(() => { expect(emitter.emit).toHaveBeenCalledTimes(2); - expect(emitter.emit).toHaveBeenNthCalledWith(1, 'promise-counter-update', true, defaultArea); - - expect(emitter.emit).toHaveBeenNthCalledWith(2, 'promise-counter-update', false, defaultArea); + expect(emitter.emit).toHaveBeenNthCalledWith( + 1, + "promise-counter-update", + true, + defaultArea + ); + + expect(emitter.emit).toHaveBeenNthCalledWith( + 2, + "promise-counter-update", + false, + defaultArea + ); done(); }); }); - // Pending promise failed - it('Two Promises tracked, we got resolve on both, check that emit is called 4 times', (done) => { + it("Two Promises tracked, we got resolve on both, check that emit is called 4 times", done => { // Arrange emitter.emit = jest.fn(); @@ -76,19 +99,39 @@ describe('trackPromise', () => { Promise.all(promises).then(() => { expect(emitter.emit).toHaveBeenCalledTimes(4); - expect(emitter.emit).toHaveBeenNthCalledWith(1, 'promise-counter-update', true, defaultArea); - - expect(emitter.emit).toHaveBeenNthCalledWith(2, 'promise-counter-update', true, defaultArea); - - expect(emitter.emit).toHaveBeenNthCalledWith(3, 'promise-counter-update', false, defaultArea); - - expect(emitter.emit).toHaveBeenNthCalledWith(4, 'promise-counter-update', false, defaultArea); + expect(emitter.emit).toHaveBeenNthCalledWith( + 1, + "promise-counter-update", + true, + defaultArea + ); + + expect(emitter.emit).toHaveBeenNthCalledWith( + 2, + "promise-counter-update", + true, + defaultArea + ); + + expect(emitter.emit).toHaveBeenNthCalledWith( + 3, + "promise-counter-update", + true, + defaultArea + ); + + expect(emitter.emit).toHaveBeenNthCalledWith( + 4, + "promise-counter-update", + false, + defaultArea + ); done(); }); }); // Promise chaining working properly. - it('Promise returned must handle transparently the result when resolved', (done) => { + it("Promise returned must handle transparently the result when resolved", done => { // Arrange const expectedPromiseResult = "promise result"; const promise = Promise.resolve(expectedPromiseResult); @@ -97,16 +140,15 @@ describe('trackPromise', () => { const trackedPromise = trackPromise(promise); // Assert - trackedPromise.then((trackedPromiseResult) => { + trackedPromise.then(trackedPromiseResult => { expect(trackedPromiseResult).toEqual(expectedPromiseResult); done(); }); }); - }); - describe('using custom area', () => { - it('should call emitter.emit one time when feeding promise and area equals undefined', () => { + describe("using custom area", () => { + it("should call emitter.emit one time when feeding promise and area equals undefined", () => { // Arrange emitter.emit = jest.fn(); @@ -118,10 +160,14 @@ describe('trackPromise', () => { // Assert expect(emitter.emit).toHaveBeenCalledTimes(1); - expect(emitter.emit).toHaveBeenCalledWith('promise-counter-update', true, defaultArea); + expect(emitter.emit).toHaveBeenCalledWith( + "promise-counter-update", + true, + defaultArea + ); }); - it('should call emitter.emit one time when feeding promise and area equals null', () => { + it("should call emitter.emit one time when feeding promise and area equals null", () => { // Arrange emitter.emit = jest.fn(); @@ -133,32 +179,40 @@ describe('trackPromise', () => { // Assert expect(emitter.emit).toHaveBeenCalledTimes(1); - expect(emitter.emit).toHaveBeenCalledWith('promise-counter-update', true, defaultArea); + expect(emitter.emit).toHaveBeenCalledWith( + "promise-counter-update", + true, + defaultArea + ); }); - it('should call emitter.emit one time when feeding promise and area equals testArea', () => { + it("should call emitter.emit one time when feeding promise and area equals testArea", () => { // Arrange emitter.emit = jest.fn(); const myPromise = Promise.resolve(); - const area = 'testArea'; + const area = "testArea"; // Act trackPromise(myPromise, area); // Assert expect(emitter.emit).toHaveBeenCalledTimes(1); - expect(emitter.emit).toHaveBeenCalledWith('promise-counter-update', true, 'testArea'); + expect(emitter.emit).toHaveBeenCalledWith( + "promise-counter-update", + true, + "testArea" + ); }); - it('should call emitter.emit two times when feeding two promises in same area', () => { + it("should call emitter.emit two times when feeding two promises in same area", () => { // Arrange emitter.emit = jest.fn(); const myPromise1 = Promise.resolve(); const myPromise2 = Promise.resolve(); - const area = 'testArea'; + const area = "testArea"; // Act trackPromise(myPromise1, area); @@ -166,19 +220,29 @@ describe('trackPromise', () => { // Assert expect(emitter.emit).toHaveBeenCalledTimes(2); - expect(emitter.emit).toHaveBeenNthCalledWith(1, 'promise-counter-update', true, 'testArea'); - expect(emitter.emit).toHaveBeenNthCalledWith(2, 'promise-counter-update', true, 'testArea'); + expect(emitter.emit).toHaveBeenNthCalledWith( + 1, + "promise-counter-update", + true, + "testArea" + ); + expect(emitter.emit).toHaveBeenNthCalledWith( + 2, + "promise-counter-update", + true, + "testArea" + ); }); - it('should call emitter.emit two times when feeding two promises in different areas', () => { + it("should call emitter.emit two times when feeding two promises in different areas", () => { // Arrange emitter.emit = jest.fn(); const myPromise1 = Promise.resolve(); const myPromise2 = Promise.resolve(); - const area1 = 'testArea1'; - const area2 = 'testArea2'; + const area1 = "testArea1"; + const area2 = "testArea2"; // Act trackPromise(myPromise1, area1); @@ -186,9 +250,18 @@ describe('trackPromise', () => { // Assert expect(emitter.emit).toHaveBeenCalledTimes(2); - expect(emitter.emit).toHaveBeenNthCalledWith(1, 'promise-counter-update', true, 'testArea1'); - expect(emitter.emit).toHaveBeenNthCalledWith(2, 'promise-counter-update', true, 'testArea2'); + expect(emitter.emit).toHaveBeenNthCalledWith( + 1, + "promise-counter-update", + true, + "testArea1" + ); + expect(emitter.emit).toHaveBeenNthCalledWith( + 2, + "promise-counter-update", + true, + "testArea2" + ); }); }); }); - diff --git a/src/trackerHoc.js b/src/trackerHoc.js index dfe0aac..496e56a 100644 --- a/src/trackerHoc.js +++ b/src/trackerHoc.js @@ -1,49 +1,79 @@ -import React, { Component } from 'react' -import { emitter, getCounter, promiseCounterUpdateEventId } from './trackPromise'; -import { defaultArea } from './constants'; - -export const promiseTrackerHoc = (ComponentToWrap) => { - return class promiseTrackerComponent extends Component { - constructor(props) { - super(props); - - this.state = { - trackedPromiseInProgress: false, - area: props.area || defaultArea, - }; - } - - updateProgress(progress, afterUpdateCallback) { - this.setState({ trackedPromiseInProgress: progress }, afterUpdateCallback); - } - - subscribeToCounterUpdate() { - emitter.on(promiseCounterUpdateEventId, (anyPromiseInProgress, area) => { - if (this.state.area === area) { - this.updateProgress(anyPromiseInProgress); - } - }); - } - - componentDidMount() { - this.updateProgress( - Boolean(getCounter(this.state.area) > 0), - this.subscribeToCounterUpdate - ); - } - - componentWillUnmount() { - emitter.off(promiseCounterUpdateEventId); - } - - render() { - return ( - - ) - } - } -} +import React, { Component } from 'react'; +import { + emitter, + getCounter, + promiseCounterUpdateEventId +} from './trackPromise'; +import { setupConfig } from './setupConfig'; + +// Props: +// config: { +// area: // can be null|undefined|'' (will default to DefaultArea) or area name +// delay: // Wait Xms to display the spinner (fast connections scenario avoid blinking) +// default value 0ms +// } +export const promiseTrackerHoc = ComponentToWrap => { + return class promiseTrackerComponent extends Component { + constructor(props) { + super(props); + + this.state = { + promiseInProgress: false, + internalPromiseInProgress: false, + config: setupConfig(props.config) + }; + + this.notifyPromiseInProgress = this.notifyPromiseInProgress.bind(this); + this.updateProgress = this.updateProgress.bind(this); + this.subscribeToCounterUpdate = this.subscribeToCounterUpdate.bind(this); + } + + notifyPromiseInProgress() { + this.state.config.delay === 0 + ? this.setState({ promiseInProgress: true }) + : setTimeout(() => { + this.setState({ promiseInProgress: true }); + }, this.state.config.delay); + } + + updateProgress(progress, afterUpdateCallback) { + this.setState( + { internalPromiseInProgress: progress }, + afterUpdateCallback + ); + + !progress + ? this.setState({ promiseInProgress: false }) + : this.notifyPromiseInProgress(); + } + + subscribeToCounterUpdate() { + emitter.on(promiseCounterUpdateEventId, (anyPromiseInProgress, area) => { + if (this.state.config.area === area) { + this.updateProgress(anyPromiseInProgress); + } + }); + } + + componentDidMount() { + this.updateProgress( + Boolean(getCounter(this.state.config.area) > 0), + this.subscribeToCounterUpdate + ); + } + + componentWillUnmount() { + emitter.off(promiseCounterUpdateEventId); + } + + render() { + return ( + + ); + } + }; +}; diff --git a/src/trackerHoc.test.js b/src/trackerHoc.test.js index 3e25a12..e3dae2a 100644 --- a/src/trackerHoc.test.js +++ b/src/trackerHoc.test.js @@ -1,91 +1,535 @@ -import React from 'react'; -import { promiseTrackerHoc } from './trackerHoc'; -import * as trackPromiseAPI from './trackPromise'; - -describe('trackerHoc', () => { - it('should render component with trackedPromiseInProgress equals false and area equals "default-area" when render promiseTrackerHoc', () => { - // Arrange - const TestSpinnerComponent = (props) => test; - - // Act - const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); - - // Assert - const component = mount( - , - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render component with trackedPromiseInProgress equals false, area equals "default-area" and customProp equals "test" when feeding customProp equals "test"', () => { - // Arrange - const TestSpinnerComponent = (props) => test; - - // Act - const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); - - // Assert - const component = mount( - , - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render component with trackedPromiseInProgress equals false and area equals "testArea" when feeding area equals "testArea"', () => { - // Arrange - const TestSpinnerComponent = (props) => test; - - // Act - const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); - - // Assert - const component = mount( - , - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render component with trackedPromiseInProgress equals false when counter is 0', () => { - // Arrange - const TestSpinnerComponent = (props) => test; - trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); - - // Act - const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); - - // Assert - const component = mount( - , - ); - - expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); - expect(component).toMatchSnapshot(); - }); - - it('should render component with trackedPromiseInProgress equals true when counter is 1', () => { - // Arrange - const TestSpinnerComponent = (props) => test; - trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 1); - - // Act - const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); - - // Assert - const component = mount( - , - ); - - expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); - expect(component).toMatchSnapshot(); - }); -}); +import React from "react"; +import { promiseTrackerHoc } from "./trackerHoc"; +import * as trackPromiseAPI from "./trackPromise"; +import { defaultArea } from "./constants"; + +describe("trackerHoc", () => { + describe("Initial Status", () => { + it('should render component with trackedPromiseInProgress equals false and area equals "default-area" when render promiseTrackerHoc without props', () => { + // Arrange + const TestSpinnerComponent = props => test; + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(component).toMatchSnapshot(); + }); + + it('should render component with trackedPromiseInProgress equals false, area equals "default-area" and customProp equals "test" when feeding customProp equals "test"', () => { + // Arrange + const TestSpinnerComponent = props => test; + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(component).toMatchSnapshot(); + }); + + it('should render component with trackedPromiseInProgress equals false and area equals "testArea" when feeding area equals "testArea"', () => { + // Arrange + const TestSpinnerComponent = props => test; + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount( + + ); + + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals false when counter is 0", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + + const progress = false; + const area = defaultArea; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false to different area", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + + const progress = false; + const area = "otherArea"; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals true when counter is 0 and emit event with progress equals true", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + + const progress = true; + const area = defaultArea; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals true to different area", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + + const progress = true; + const area = "otherArea"; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals true when counter is 1", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 1); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals true", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 1); + + const progress = true; + const area = defaultArea; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals true to different area", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 1); + + const progress = true; + const area = "otherArea"; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals false when counter is 1 and emit event with progress equals false", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 1); + + const progress = false; + const area = defaultArea; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals false to different area", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 1); + + const progress = false; + const area = "otherArea"; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it('should render component with trackedPromiseInProgress equals false and area equals "testArea" when feeding area equals "testArea" and delay equals 300', () => { + // Arrange + const TestSpinnerComponent = props => test; + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount( + + ); + + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals false when counter is 0 and delay equals 300", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false and delay equals 300", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + + const progress = false; + const area = defaultArea; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false to different area and delay equals 300", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + + const progress = false; + const area = "otherArea"; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals true when counter is 0 and emit event with progress equals true and delay equals 300", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + + const progress = true; + const area = defaultArea; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + const setTimeoutStub = jest + .spyOn(window, "setTimeout") + .mockImplementation(callback => callback()); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals true to different area and delay equals 300", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + + const progress = true; + const area = "otherArea"; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals true when counter is 1 and delay equals 300", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 1); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals true and delay equals 300", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 1); + + const progress = true; + const area = defaultArea; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals true to different area and delay equals 300", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 1); + + const progress = true; + const area = "otherArea"; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals false when counter is 1 and emit event with progress equals false and delay equals 300", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 1); + + const progress = false; + const area = defaultArea; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals false to different area and delay equals 300", () => { + // Arrange + const TestSpinnerComponent = props => test; + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 1); + + const progress = false; + const area = "otherArea"; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + + // Assert + const component = mount(); + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + }); + + describe("Handling delay timeouts", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it("should render

NO SPINNER

when counter is 1 but delay is set to 300 (before timing out)", done => { + // Arrange + const TestSpinnerComponent = props => { + return ( +
+ {props.promiseInProgress ?

SPINNER

:

NO SPINNER

} +
+ ); + }; + + const getCounterStub = jest + .spyOn(trackPromiseAPI, "getCounter") + .mockReturnValue(0); + + // Act + const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); + const component = mount(); + + // Check very beginning (no promises going on) NO SPINNER is shown + // TODO: this assert could be skipped (move to another test) + expect(component.text()).toEqual("NO SPINNER"); + expect(getCounterStub).toHaveBeenCalled(); + + // Assert + // This promise will resolved after 1 seconds, by doing this + // we will be able to test 2 scenarios: + // [0] first 200ms spinner won't be shown (text NOSPINNER) + // [1] after 200ms spinner will be shown (text SPINNER) + // [2] after 1000ms spinner will be hidded again (text NOSPINNER) + + const myFakePromise = new Promise(resolve => { + setTimeout(() => { + resolve(true); + }, 1000); + }); + + trackPromiseAPI.trackPromise(myFakePromise); + + jest.advanceTimersByTime(100); + + // [0] first 200ms spinner won't be shown (text NOSPINNER) + expect(component.text()).toEqual("NO SPINNER"); + + jest.advanceTimersByTime(300); + + // Before the promise get's resolved + // [1] after 200ms spinner will be shown (text SPINNER) + expect(component.text()).toEqual("SPINNER"); + + // After the promise get's resolved + jest.runAllTimers(); + + // [2] after 1000ms spinner will be hidded again (text NOSPINNER) + // Wait for fakePromise (simulated ajax call) to be completed + // no spinner should be shown + + myFakePromise.then(() => { + expect(component.text()).toEqual("NO SPINNER"); + done(); + }); + }); + }); +}); diff --git a/src/trackerHook.js b/src/trackerHook.js new file mode 100644 index 0000000..2ff6225 --- /dev/null +++ b/src/trackerHook.js @@ -0,0 +1,80 @@ +import React from "react"; +import { emitter, promiseCounterUpdateEventId, getCounter} from "./trackPromise"; +import { defaultConfig, setupConfig } from './setupConfig'; + + +export const usePromiseTracker = (outerConfig = defaultConfig) => { + // Included in state, it will be evaluated just the first time, + // TODO: discuss if this is a good approach + // We need to apply defensive programming, ensure area and delay default to secure data + // cover cases like not all params informed, set secure defaults + const [config] = React.useState(setupConfig(outerConfig)); + + // Edge case, when we start the application if we are loading just onComponentDidMount + // data, event emitter could have already emitted the event but subscription is not yet + // setup + React.useEffect(() => { + if(config && config.area && getCounter(config.area) > 0) { + setInternalPromiseInProgress(true); + setInternalPromiseInProgress(true); + setPromiseInProgress(true); + } + }, config) + + // Internal will hold the current value + const [ + internalPromiseInProgress, + setInternalPromiseInProgress + ] = React.useState(false); + // Promise in progress is 'public', it can be affected by the _delay_ parameter + // it may not show the current state + const [promiseInProgress, setPromiseInProgress] = React.useState(false); + + // We need to hold a ref to latestInternal, to check the real value on + // callbacks (if not we would get always the same value) + // more info: https://overreacted.io/a-complete-guide-to-useeffect/ + const latestInternalPromiseInProgress = React.useRef( + internalPromiseInProgress + ); + + const notifiyPromiseInProgress = () => { + (!config || !config.delay || config.delay === 0) ? + setPromiseInProgress(true) + : + setTimeout(() => { + // Check here ref to internalPromiseInProgress + if (latestInternalPromiseInProgress.current) { + setPromiseInProgress(true); + } + }, config.delay); + }; + + const updatePromiseTrackerStatus = (anyPromiseInProgress, areaAffected) => { + if (config.area === areaAffected) { + setInternalPromiseInProgress(anyPromiseInProgress); + // Update the ref object as well, we will check it when we need to + // cover the _delay_ case (setTimeout) + latestInternalPromiseInProgress.current = anyPromiseInProgress; + if (!anyPromiseInProgress) { + setPromiseInProgress(false); + } else { + notifiyPromiseInProgress(); + } + } + }; + + React.useEffect(() => { + latestInternalPromiseInProgress.current = internalPromiseInProgress; + emitter.on(promiseCounterUpdateEventId, + (anyPromiseInProgress, areaAffected) => { + updatePromiseTrackerStatus(anyPromiseInProgress, areaAffected); + } + ); + + return () => { + emitter.off(promiseCounterUpdateEventId); + }; + }, []); + + return { promiseInProgress }; +}; diff --git a/src/trackerHook.test.js b/src/trackerHook.test.js new file mode 100644 index 0000000..dd7b73c --- /dev/null +++ b/src/trackerHook.test.js @@ -0,0 +1,255 @@ +import React from "react"; +import { usePromiseTracker } from "./trackerHook"; +import * as trackPromiseAPI from "./trackPromise"; +import { act } from "react-dom/test-utils"; // ES6 + +describe("trackerHook", () => { + describe("Initial Status", () => { + it("renders without crashing", () => { + // Arrange + const TestSpinnerComponent = props => { + const { promiseInProgress } = usePromiseTracker(); + + return test; + }; + + // Act + const component = mount(); + + // Assert + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals false when counter is 0", () => { + // Arrange + const TestSpinnerComponent = props => { + const { promiseInProgress } = usePromiseTracker(); + + return test; + }; + + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + + // Act + const component = mount(); + + // Assert + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false to different area", () => { + // Arrange + const TestSpinnerComponent = props => { + const { promiseInProgress } = usePromiseTracker(); + + return test; + }; + + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + + const progress = false; + const area = "otherArea"; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const component = mount(); + + // Assert + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals true to different area", () => { + // Arrange + const TestSpinnerComponent = props => { + const { promiseInProgress } = usePromiseTracker(); + + return test; + }; + + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + + const progress = true; + const area = "otherArea"; + const emitterStub = jest + .spyOn(trackPromiseAPI.emitter, "on") + .mockImplementation((id, callback) => callback(progress, area)); + + // Act + const component = mount(); + + // Assert + + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it("should render

No Spinner

when counter is 0", () => { + // Arrange + const TestSpinnerComponent = props => { + const { promiseInProgress } = usePromiseTracker(); + + return ( +
+ {promiseInProgress ?

SPINNER

:

NO SPINNER

} +
+ ); + }; + + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + + // Act + const component = mount(); + + // Assert + + expect(component.find("h2")).toHaveLength(1); + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + }); + + it("should render

Spinner

when counter is 1", () => { + // Arrange + const TestSpinnerComponent = props => { + const { promiseInProgress } = usePromiseTracker(); + + return ( +
+ {promiseInProgress ?

SPINNER

:

NO SPINNER

} +
+ ); + }; + + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 1); + + // Act + const component = mount(); + + // Assert + + expect(component.find("h1")).toHaveLength(1); + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + }); + + it('should render component with trackedPromiseInProgress equals false and area equals "testArea" when feeding area equals "testArea" and delay equals 300', () => { + // Arrange + const TestSpinnerComponent = props => { + const { promiseInProgress } = usePromiseTracker({ + area: "testArea", + delay: 300 + }); + + return test; + }; + + // Act + const component = mount(); + + // Assert + + expect(component).toMatchSnapshot(); + }); + }); + + describe("Handling delay timeouts", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it("should render

NO SPINNER

when counter is 1 but delay is set to 200 (before timing out)", done => { + // Arrange + let TestSpinnerComponent = null; + act(() => { + TestSpinnerComponent = props => { + // Do not show spinner in the first 200 milliseconds (delay) + const { promiseInProgress } = usePromiseTracker({ delay: 200 }); + + return ( +
+ {promiseInProgress ?

SPINNER

:

NO SPINNER

} +
+ ); + }; + + trackPromiseAPI.getCounter = jest.fn().mockImplementation(() => 0); + }); + + // Act + let component = null; + + act(() => { + component = mount(); + }); + + // Check very beginning (no promises going on) NO SPINNER is shown + // TODO: this assert could be skipped (move to another test) + expect(component.text()).toEqual("NO SPINNER"); + expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); + + // Assert + // This promise will resolved after 1 seconds, by doing this + // we will be able to test 2 scenarios: + // [0] first 200ms spinner won't be shown (text NOSPINNER) + // [1] after 200ms spinner will be shown (text SPINNER) + // [2] after 1000ms spinner will be hidded again (text NOSPINNER) + let myFakePromise = null; + + act(() => { + myFakePromise = new Promise(resolve => { + setTimeout(() => { + resolve(true); + }, 1000); + }); + }); + + act(() => { + trackPromiseAPI.trackPromise(myFakePromise); + + // Runs all pending timers. whether it's a second from now or a year. + // https://jestjs.io/docs/en/timer-mocks.html + //jest.advanceTimersByTime(300); + }); + + act(() => { + // Runs all pending timers. whether it's a second from now or a year. + // https://jestjs.io/docs/en/timer-mocks.html + jest.advanceTimersByTime(100); + }); + + // [0] first 200ms spinner won't be shown (text NOSPINNER) + expect(component.text()).toEqual("NO SPINNER"); + + act(() => { + // Runs all pending timers. whether it's a second from now or a year. + // https://jestjs.io/docs/en/timer-mocks.html + jest.advanceTimersByTime(300); + }); + + // Before the promise get's resolved + // [1] after 200ms spinner will be shown (text SPINNER) + expect(component.text()).toEqual("SPINNER"); + + // After the promise get's resolved + + act(() => { + jest.runAllTimers(); + }); + + // [2] after 1000ms spinner will be hidded again (text NOSPINNER) + // Wait for fakePromise (simulated ajax call) to be completed + // no spinner should be shown + act(() => { + myFakePromise.then(() => { + expect(component.text()).toEqual("NO SPINNER"); + done(); + }); + }); + }); + }); +}); diff --git a/test/shim.js b/test/shim.js deleted file mode 100644 index bea6cad..0000000 --- a/test/shim.js +++ /dev/null @@ -1,4 +0,0 @@ -// https://github.com/facebook/jest/issues/4545#issuecomment-332762365 -global.requestAnimationFrame = (callback) => { - setTimeout(callback, 0); -}; diff --git a/webpack.config.babel.js b/webpack.config.babel.js deleted file mode 100644 index 2dacb59..0000000 --- a/webpack.config.babel.js +++ /dev/null @@ -1,37 +0,0 @@ -import webpack from 'webpack'; -import path from 'path'; - -const NODE_ENV = process.env.NODE_ENV; - -const version = JSON.stringify(process.env.npm_package_version).replace(/"/g, ''); -const filename = `react-promise-tracker-${version}${NODE_ENV === 'production' ? '.min' : ''}.js`; - -export default { - entry: ['./src/index.js'], - output: { - path: path.join(__dirname, 'dist'), - filename, - library: { - root: 'ReactPromiseTracker', - amd: 'react-promise-tracker', - commonjs: 'react-promise-tracker', - }, - libraryTarget: 'umd', - }, - externals: { - react: { - commonjs: 'react', - commonjs2: 'react', - amd: 'react', - }, - }, - module: { - rules: [ - { - test: /\.js$/, - exclude: /node_modules/, - loader: 'babel-loader', - }, - ], - }, -};