From 64c18f7ebae46e31fb6079250b0c686976b8aa30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomek=20W=C3=B3jcik?= Date: Thu, 18 Jan 2018 17:53:52 +0100 Subject: [PATCH] Hello, redux-doctitle! --- .babelrc | 17 ++++++ .eslintrc.json | 55 +++++++++++++++++ .gitignore | 3 + Gruntfile.js | 78 ++++++++++++++++++++++++ LICENSE | 19 ++++++ README.md | 126 +++++++++++++++++++++++++++++++++++++++ karma.conf.js | 39 ++++++++++++ package.defs.js | 16 +++++ package.json | 55 +++++++++++++++++ src/actions.js | 8 +++ src/defs.js | 1 + src/index.js | 2 + src/middleware.js | 16 +++++ tests/__entry__.js | 2 + tests/actions.spec.js | 18 ++++++ tests/middleware.spec.js | 77 ++++++++++++++++++++++++ webpack.config.js | 28 +++++++++ 17 files changed, 560 insertions(+) create mode 100644 .babelrc create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 Gruntfile.js create mode 100644 LICENSE create mode 100644 README.md create mode 100644 karma.conf.js create mode 100644 package.defs.js create mode 100644 package.json create mode 100644 src/actions.js create mode 100644 src/defs.js create mode 100644 src/index.js create mode 100644 src/middleware.js create mode 100644 tests/__entry__.js create mode 100644 tests/actions.spec.js create mode 100644 tests/middleware.spec.js create mode 100644 webpack.config.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..eb8e23c --- /dev/null +++ b/.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" + ] +} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..c404adf --- /dev/null +++ b/.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"] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20b379f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +lib/ +node_modules/ +redux-doctitle-*.tgz diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..64d258f --- /dev/null +++ b/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" + ]); +}; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..833a71f --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018 Tomek Wójcik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b53bfdf --- /dev/null +++ b/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
Hello, World!
; + } +} + +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. diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..8096e16 --- /dev/null +++ b/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 + } + } + } + }); +}; diff --git a/package.defs.js b/package.defs.js new file mode 100644 index 0000000..d76fc71 --- /dev/null +++ b/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 +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..f9127fc --- /dev/null +++ b/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 (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" + } +} diff --git a/src/actions.js b/src/actions.js new file mode 100644 index 0000000..1ea7a36 --- /dev/null +++ b/src/actions.js @@ -0,0 +1,8 @@ +import {TITLE_ACTIONS_SET} from "./defs"; + +export const setDocumentTitle = (title = "") => { + return { + type: TITLE_ACTIONS_SET, + title: title + }; +}; diff --git a/src/defs.js b/src/defs.js new file mode 100644 index 0000000..4ca0a5b --- /dev/null +++ b/src/defs.js @@ -0,0 +1 @@ +export const TITLE_ACTIONS_SET = "BTHLABS/REDUX_DOCTITLE_MW_TITLE_ACTIONS_SET"; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..b8b7ceb --- /dev/null +++ b/src/index.js @@ -0,0 +1,2 @@ +export {setDocumentTitle} from "./actions"; +export {documentTitleMiddlewareFactory} from "./middleware"; diff --git a/src/middleware.js b/src/middleware.js new file mode 100644 index 0000000..e3dcf62 --- /dev/null +++ b/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); + } + }; +}; diff --git a/tests/__entry__.js b/tests/__entry__.js new file mode 100644 index 0000000..469faba --- /dev/null +++ b/tests/__entry__.js @@ -0,0 +1,2 @@ +let testsContext = require.context(".", true, /\.spec\.js$/); +testsContext.keys().forEach(testsContext); diff --git a/tests/actions.spec.js b/tests/actions.spec.js new file mode 100644 index 0000000..37615d0 --- /dev/null +++ b/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(""); + }); + }); +}); diff --git a/tests/middleware.spec.js b/tests/middleware.spec.js new file mode 100644 index 0000000..e7eeb6e --- /dev/null +++ b/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); + }); + }); +}); diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..de9fd1f --- /dev/null +++ b/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;