Browse Source

Hello, redux-doctitle!

Tomek Wójcik 1 year ago
commit
64c18f7eba
17 changed files with 560 additions and 0 deletions
  1. 17 0
      .babelrc
  2. 55 0
      .eslintrc.json
  3. 3 0
      .gitignore
  4. 78 0
      Gruntfile.js
  5. 19 0
      LICENSE
  6. 126 0
      README.md
  7. 39 0
      karma.conf.js
  8. 16 0
      package.defs.js
  9. 55 0
      package.json
  10. 8 0
      src/actions.js
  11. 1 0
      src/defs.js
  12. 2 0
      src/index.js
  13. 16 0
      src/middleware.js
  14. 2 0
      tests/__entry__.js
  15. 18 0
      tests/actions.spec.js
  16. 77 0
      tests/middleware.spec.js
  17. 28 0
      webpack.config.js

+ 17 - 0
.babelrc

@@ -0,0 +1,17 @@
+{
+  "presets": [
+    [
+      "@babel/preset-env", {
+        "targets": {
+          "chrome": 60,
+          "edge": 14,
+          "firefox": 54,
+          "ie": 9,
+          "opera": 46,
+          "safari": 9
+        }
+      }
+    ],
+    "@babel/preset-react"
+  ]
+}

+ 55 - 0
.eslintrc.json

@@ -0,0 +1,55 @@
+{
+  "env": {
+    "browser": true,
+    "commonjs": true,
+    "jasmine": true,
+    "es6": true
+  },
+  "extends": "eslint:recommended",
+  "parser": "babel-eslint",
+  "parserOptions": {
+    "sourceType": "module",
+    "ecmaFeatures": {
+      "modules": true
+    }
+  },
+  "plugins": ["jasmine"],
+  "rules": {
+    "no-undef": "error",
+    "quotes": [
+      2, "double", {"avoidEscape": true, "allowTemplateLiterals": true}
+    ],
+    "no-unused-vars": [0],
+    "no-console": [2],
+    "no-empty": ["error", {"allowEmptyCatch": true}],
+    "array-bracket-spacing": ["error", "never"],
+    "block-spacing": ["error", "always"],
+    "brace-style": ["error", "1tbs", {"allowSingleLine": true}],
+    "camelcase": ["error", {"properties": "never"}],
+    "comma-dangle": ["error", "never"],
+    "comma-spacing": ["error", {"before": false, "after": true}],
+    "comma-style": ["error", "last"],
+    "computed-property-spacing": ["error", "never"],
+    "key-spacing": [
+      "error", {"beforeColon": false, "afterColon": true, "mode": "strict"}
+    ],
+    "keyword-spacing": ["error", { "before": true, "after": true }],
+    "linebreak-style": ["error", "unix"],
+    "max-len": ["error", 120],
+    "no-multiple-empty-lines": ["error"],
+    "no-spaced-func": ["error"],
+    "no-trailing-spaces": ["error"],
+    "no-unreachable": [1],
+    "no-whitespace-before-property": ["error"],
+    "object-curly-spacing": ["error", "never"],
+    "one-var-declaration-per-line": ["error", "always"],
+    "one-var": ["error", "never"],
+    "semi-spacing": ["error", {"before": false, "after": true}],
+    "semi": ["error", "always"],
+    "space-before-function-paren": ["error", "always"],
+    "space-before-blocks": ["error", "always"],
+    "space-in-parens": ["error", "never"],
+    "space-infix-ops": ["error"],
+    "unicode-bom": ["error", "never"]
+  }
+}

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+lib/
+node_modules/
+redux-doctitle-*.tgz

+ 78 - 0
Gruntfile.js

@@ -0,0 +1,78 @@
+var path = require("path");
+
+var underscore = require("underscore");
+var webpack = require("webpack");
+
+var defs = require("./package.defs.js");
+
+var devWebpackConfig = require("./webpack.config.js");
+var distWebpackConfig = underscore.extend(
+  underscore.clone(devWebpackConfig),
+  {
+    devtool: false,
+    output: {
+      path: defs.LIB_DIR,
+      filename: defs.FILENAME_DIST,
+      library: defs.LIBRARY_NAME,
+      libraryTarget: "umd",
+      umdNamedDefine: true
+    },
+    plugins: [
+      new webpack.optimize.UglifyJsPlugin({compress: false})
+    ]
+  }
+);
+
+module.exports = function (grunt) {
+  grunt.loadNpmTasks("grunt-contrib-clean");
+  grunt.loadNpmTasks("grunt-karma");
+  grunt.loadNpmTasks("grunt-webpack");
+  grunt.loadNpmTasks("gruntify-eslint");
+
+  grunt.initConfig({
+    clean: {
+      options: {
+        force: true,
+      },
+      all: [defs.LIB_DIR]
+    },
+    eslint: {
+      sources: {
+        src: ["./src/**/*.js"]
+      },
+      tests: {
+        src: ["./tests/**/*.spec.js"]
+      }
+    },
+    karma: {
+      options: {
+        configFile: "karma.conf.js"
+      },
+      dist: {
+        browsers: ["ChromiumHeadless"],
+        reporters: ["progress"],
+        singleRun: true,
+        webpackMiddleware: {
+          noInfo: true,
+          stats: "errors-only"
+        }
+      },
+      dev: {
+        browsers: ["ChromiumHeadless"],
+        mochaReporter: {
+          ignoreSkipped: false
+        },
+        reporters: ["mocha"],
+        singleRun: true
+      }
+    },
+    webpack: {
+      dev: devWebpackConfig,
+      dist: distWebpackConfig
+    }
+  });
+
+  grunt.registerTask("dist", [
+    "clean", "eslint", "karma:dist", "webpack"
+  ]);
+};

+ 19 - 0
LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2018 Tomek Wójcik <tomek@bthlabs.pl>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 126 - 0
README.md

@@ -0,0 +1,126 @@
+# redux-doctitle
+
+Redux middleware for managing document title.
+
+## Installation
+
+redux-doctitle requires **Redux 3.0 or later**.
+
+```
+npm install --save-dev redux-doctitle
+```
+
+This assumes that you’re using [npm](http://npmjs.com/) package manager with a
+module bundler like [Webpack](https://webpack.js.org/) or
+[Browserify](http://browserify.org/).
+
+## Usage
+
+In order to use redux-doctitle in your React components you need to install the
+middleware, first.
+
+```
+import {applyMiddleware, createStore} from "redux";
+import {documentTitleMiddlewareFactory} from "redux-doctitle";
+
+const documentTitleMiddleware = documentTitleMiddlewareFactory(["Example"]);
+const store = createStore(
+  reducer, initialState, applyMiddleware(documentTitleMiddleware)
+);
+```
+
+Once this is done, you can use the provided action to change document title
+from your React components.
+
+```
+import PropTypes from "prop-types";
+import React from "react";
+import {connect} from "react-redux";
+import {bindActionCreators} from "redux";
+import {setDocumentTitle} from "redux-doctitle";
+
+class HelloWorldComponent extends React.Component {
+  componentDidMount () {
+    this.props.actions.setDocumentTitle("Hello, World!");
+  }
+  render () {
+    return <div>Hello, World!</div>;
+  }
+}
+
+HelloWorldComponent.propTypes = {
+  actions: PropTypes.shape({
+    setDocumentTitle: PropTypes.func.isRequired
+  });
+};
+
+const mapStateToProps = (state, props) => {
+  return {};
+};
+
+const mapDispatchToProps = (dispatch, props) => {
+  const actions = bindActionCreators(
+    {
+      setDocumentTitle: setDocumentTitle
+    },
+    dispatch
+  );
+};
+
+const reduxContainer = connect(
+  mapStateToProps, mapDispatchToProps
+)(HelloWorldComponent);
+
+export default reduxContainer;
+```
+
+## API
+
+### `documentMiddlewareFactory(defaultTitleParts = [])`
+
+This is the middleware factory function. The *defaultTitleParts* argument is an
+optional array of title parts which will be appended to custom titles set
+through the `setDocumentTitle` action creator. If it's ommited, nothing will be
+appended.
+
+Returns Redux-compatible middleware function.
+
+### `setDocumentTitle(title = "")`
+
+This is the action creator that allows changing the document title.
+
+Returns Redux action.
+
+## Development
+
+To bootstrap the development environment, clone the repo and run `npm install`
+from the root directory.
+
+The `package.json` file provides the following scripts:
+
+* `build` - builds the library modules (minified and development with source
+  map),
+* `dev` - starts Webpack watcher configured to build development library,
+* `lint` - performs an eslint run over the source code,
+* `test` - performs a single test run,
+* `test:watch` - starts karma with watcher.
+
+**NOTE**: Tests require *Chromium* to be installed and available in the path.
+Consult
+[karma-chrome-launcher](https://github.com/karma-runner/karma-chrome-launcher)
+docs for more info.
+
+## Contributing
+
+If you think you found a bug or want to send a patch, feel free to contact
+me through e-mail.
+
+If you're sending a patch, make sure it passes eslint checks and is tested.
+
+## Author
+
+redux-doctitle is developed by [Tomek Wójcik](https://www.bthlabs.pl/).
+
+## License
+
+redux-doctitle is licensed under the MIT License.

+ 39 - 0
karma.conf.js

@@ -0,0 +1,39 @@
+var path = require("path");
+
+var defs = require("./package.defs.js");
+
+module.exports = function(config) {
+  config.set({
+    basePath: ".",
+    browsers: ["Chromium"],
+    frameworks: ["jasmine"],
+    files: [
+      "tests/__entry__.js"
+    ],
+    mochaReporter: {
+      ignoreSkipped: true
+    },
+    preprocessors: {
+      "tests/__entry__.js": ["webpack", "sourcemap"]
+    },
+    reporters: ["mocha"],
+    singleRun: false,
+    webpack: {
+      devtool: "inline-source-map",
+      module: {
+        loaders: [
+          {
+            test: /\.js?/,
+            include: defs.SRC_DIR,
+            loader: ["babel-loader"]
+          }
+        ]
+      },
+      resolve: {
+        alias: {
+          "src": defs.SRC_DIR
+        }
+      }
+    }
+  });
+};

+ 16 - 0
package.defs.js

@@ -0,0 +1,16 @@
+var path = require("path");
+
+var LIB_DIR = path.resolve(__dirname, "lib");
+var SRC_DIR = path.resolve(__dirname, "src");
+
+var LIBRARY_NAME = "redux-doctitle";
+var FILENAME_DEV = "redux-doctitle.js";
+var FILENAME_DIST = "redux-doctitle.min.js";
+
+module.exports = {
+  LIB_DIR: LIB_DIR,
+  SRC_DIR: SRC_DIR,
+  LIBRARY_NAME: LIBRARY_NAME,
+  FILENAME_DEV: FILENAME_DEV,
+  FILENAME_DIST: FILENAME_DIST
+};

+ 55 - 0
package.json

@@ -0,0 +1,55 @@
+{
+  "name": "redux-doctitle",
+  "version": "1.0.0",
+  "description": "Redux Middleware for managing document title",
+  "main": "./lib/redux-doctitle.js",
+  "scripts": {
+    "build": "./node_modules/.bin/grunt dist",
+    "dev": "./node_modules/.bin/grunt clean && ./node_modules/.bin/webpack --watch",
+    "lint": "./node_modules/.bin/grunt eslint",
+    "test": "./node_modules/.bin/grunt karma:dev",
+    "test:watch": "./node_modules/.bin/karma start"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://git.bthlabs.pl/tomekwojcik/redux-doctitle.git"
+  },
+  "files": [
+    "lib",
+    "src"
+  ],
+  "keywords": [
+    "redux",
+    "middleware",
+    "document",
+    "title"
+  ],
+  "author": "Tomek Wójcik <contact@bthlabs.pl> (https://www.bthlabs.pl/)",
+  "license": "MIT",
+  "devDependencies": {
+    "@babel/core": "^7.0.0-beta.37",
+    "@babel/preset-env": "^7.0.0-beta.37",
+    "@babel/preset-react": "^7.0.0-beta.37",
+    "babel-eslint": "^8.2.1",
+    "babel-loader": "^8.0.0-beta.0",
+    "eslint-plugin-jasmine": "^2.9.1",
+    "grunt": "^1.0.1",
+    "grunt-contrib-clean": "^1.1.0",
+    "grunt-karma": "^2.0.0",
+    "grunt-webpack": "^3.0.2",
+    "gruntify-eslint": "^4.0.0",
+    "jasmine-core": "^2.8.0",
+    "karma": "^2.0.0",
+    "karma-chrome-launcher": "^2.2.0",
+    "karma-jasmine": "^1.1.1",
+    "karma-mocha-reporter": "^2.2.5",
+    "karma-sourcemap-loader": "^0.3.7",
+    "karma-webpack": "^2.0.9",
+    "underscore": "^1.8.3",
+    "webpack": "^3.10.0",
+    "webpack-uglify-js-plugin": "^1.1.9"
+  },
+  "peerDependencies": {
+    "redux": "^3.0.0"
+  }
+}

+ 8 - 0
src/actions.js

@@ -0,0 +1,8 @@
+import {TITLE_ACTIONS_SET} from "./defs";
+
+export const setDocumentTitle = (title = "") => {
+  return {
+    type: TITLE_ACTIONS_SET,
+    title: title
+  };
+};

+ 1 - 0
src/defs.js

@@ -0,0 +1 @@
+export const TITLE_ACTIONS_SET = "BTHLABS/REDUX_DOCTITLE_MW_TITLE_ACTIONS_SET";

+ 2 - 0
src/index.js

@@ -0,0 +1,2 @@
+export {setDocumentTitle} from "./actions";
+export {documentTitleMiddlewareFactory} from "./middleware";

+ 16 - 0
src/middleware.js

@@ -0,0 +1,16 @@
+import {TITLE_ACTIONS_SET} from "./defs";
+
+export const documentTitleMiddlewareFactory = (defaultTitleParts = []) => {
+  return (store, getState) => next => action => {
+    if (action.type == TITLE_ACTIONS_SET) {
+      let titleParts = [].concat(defaultTitleParts);
+      if (action.title) {
+        titleParts.unshift(action.title);
+      }
+
+      window.document.title = titleParts.join(" | ");
+    } else {
+      next(action);
+    }
+  };
+};

+ 2 - 0
tests/__entry__.js

@@ -0,0 +1,2 @@
+let testsContext = require.context(".", true, /\.spec\.js$/);
+testsContext.keys().forEach(testsContext);

+ 18 - 0
tests/actions.spec.js

@@ -0,0 +1,18 @@
+import {TITLE_ACTIONS_SET} from "src/defs";
+import * as doctitleActions from "src/actions";
+
+describe("actions", () => {
+  describe("setDocumentTitle", () => {
+    it("should create the action", () => {
+      let result = doctitleActions.setDocumentTitle("Spam");
+      expect(result.type).toEqual(TITLE_ACTIONS_SET);
+      expect(result.title).toEqual("Spam");
+    });
+
+    it("should create the action without the custom part", () => {
+      let result = doctitleActions.setDocumentTitle();
+      expect(result.type).toEqual(TITLE_ACTIONS_SET);
+      expect(result.title).toEqual("");
+    });
+  });
+});

+ 77 - 0
tests/middleware.spec.js

@@ -0,0 +1,77 @@
+import {TITLE_ACTIONS_SET} from "src/defs";
+
+import * as doctitleMiddleware from "src/middleware";
+
+describe("middleware", () => {
+  describe("documentTitleMiddleware", () => {
+    let origTitle = null;
+    let next = null;
+
+    beforeAll(() => {
+      origTitle = window.document.title;
+    });
+
+    beforeEach(() => {
+      next = jasmine.createSpy("next");
+    });
+
+    afterAll(() => {
+      window.document.title = origTitle;
+    });
+
+    it("should set title to empty if no default parts are specified and custom part is empty", () => {
+      let middleware = doctitleMiddleware.documentTitleMiddlewareFactory()(
+        undefined, undefined
+      );
+
+      let action = {type: TITLE_ACTIONS_SET, title: undefined};
+      middleware(next)(action);
+
+      expect(window.document.title).toEqual("");
+    });
+
+    it("should not set custom document title if it isn't specified", () => {
+      let middleware = doctitleMiddleware.documentTitleMiddlewareFactory(["Test"])(
+        undefined, undefined
+      );
+
+      let action = {type: TITLE_ACTIONS_SET, title: undefined};
+      middleware(next)(action);
+
+      expect(window.document.title).toEqual("Test");
+    });
+
+    it("should set custom document title", () => {
+      let middleware = doctitleMiddleware.documentTitleMiddlewareFactory(["Test"])(
+        undefined, undefined
+      );
+
+      let action = {type: TITLE_ACTIONS_SET, title: "Spam"};
+      middleware(next)(action);
+
+      expect(window.document.title).toEqual("Spam | Test");
+    });
+
+    it("should not call the next middleware if got the setDocumentTitle action", () => {
+      let middleware = doctitleMiddleware.documentTitleMiddlewareFactory(["Test"])(
+        undefined, undefined
+      );
+
+      let action = {type: TITLE_ACTIONS_SET, title: "Spam"};
+      middleware(next)(action);
+
+      expect(next).not.toHaveBeenCalled();
+    });
+
+    it("should call the next middleware if didn't get the setDocumentTitle action", () => {
+      let middleware = doctitleMiddleware.documentTitleMiddlewareFactory(["Test"])(
+        undefined, undefined
+      );
+
+      let action = {type: "SPAM"};
+      middleware(next)(action);
+
+      expect(next).toHaveBeenCalledWith(action);
+    });
+  });
+});

+ 28 - 0
webpack.config.js

@@ -0,0 +1,28 @@
+var path = require("path");
+
+var webpack = require("webpack");
+
+var defs = require("./package.defs.js");
+
+var config = {
+  entry: path.resolve(defs.SRC_DIR, "index.js"),
+  devtool: "source-map",
+  output: {
+    path: defs.LIB_DIR,
+    filename: defs.FILENAME_DEV,
+    library: defs.LIBRARY_NAME,
+    libraryTarget: "umd",
+    umdNamedDefine: true
+  },
+  module: {
+    loaders: [
+      {
+        test: /\.js?/,
+        include: defs.SRC_DIR,
+        loader: ["babel-loader"]
+      }
+    ]
+  }
+};
+
+module.exports = config;