Hello, redux-doctitle!

This commit is contained in:
Tomek Wójcik 2018-01-18 17:53:52 +01:00
commit 64c18f7eba
17 changed files with 560 additions and 0 deletions

17
.babelrc Normal file
View File

@ -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
.eslintrc.json Normal file
View File

@ -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
.gitignore vendored Normal file
View File

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

78
Gruntfile.js Normal file
View File

@ -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
LICENSE Normal file
View File

@ -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
README.md Normal file
View File

@ -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 youre 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
karma.conf.js Normal file
View File

@ -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
package.defs.js Normal file
View File

@ -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
package.json Normal file
View File

@ -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
src/actions.js Normal file
View File

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

1
src/defs.js Normal file
View File

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

2
src/index.js Normal file
View File

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

16
src/middleware.js Normal file
View File

@ -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
tests/__entry__.js Normal file
View File

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

18
tests/actions.spec.js Normal file
View File

@ -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
tests/middleware.spec.js Normal file
View File

@ -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
webpack.config.js Normal file
View File

@ -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;