commit 64c18f7ebae46e31fb6079250b0c686976b8aa30 Author: Tomek Wójcik Date: Thu Jan 18 17:53:52 2018 +0100 Hello, redux-doctitle! 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;