Skip to content

Commit

Permalink
Merge pull request #14 from dnlkoch/visible-hoc
Browse files Browse the repository at this point in the history
VisibleComponent HOC
  • Loading branch information
dnlkoch committed Sep 6, 2017
2 parents ac6b7f4 + 4983df0 commit 89a6143
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
"semi": ["error", "always"],
"one-var": ["error", "never"],
"no-confusing-arrow": "error",
"no-unused-vars": ["error", {
"ignoreRestSiblings": true
}],
"key-spacing": ["error", {
"beforeColon": false,
"afterColon": true
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"test:watch": "karma start karma.conf.js",
"lint": "eslint --ext js,html,md example-templates/ examples/ src/",
"start": "webpack-dev-server --config webpack.examples.config.js --content-base ./build/ --hot",
"build:examples": "rimraf ./build/examples/* && node tasks/build-examples.js && webpack --config webpack.examples.config.js",
"build:examples": "rimraf ./build/examples/* && node tasks/build-examples.js && webpack -p --config webpack.examples.config.js",
"build:docs": "jsdoc --package ./package.json --readme ./README.md -c .jsdoc",
"build:js": "webpack --config webpack.development.config.js && webpack -p --config webpack.production.config.js",
"build:dist": "BABEL_ENV=build babel src -d dist",
Expand Down
42 changes: 42 additions & 0 deletions src/VisibleComponent/VisibleComponent.example.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { render } from 'react-dom';
import { Button } from 'antd';
import { isVisibleComponent } from './VisibleComponent.jsx'; //@react-geo@

// Enhance (any) Component by wrapping it using isVisibleComponent().
const VisibleButton = isVisibleComponent(Button);

// The activeModules is a whitelist of components (identified by it's names) to
// render.
const activeModules = [{
name: 'visibleButtonName'
}, {
name: 'anotherVisibleButtonName'
}];

render(
<div>
<VisibleButton
name="visibleButtonName"
activeModules={activeModules}
type="primary"
shape="circle"
icon="search"
/>
<VisibleButton
name="notVisibleButtonName"
activeModules={activeModules}
type="primary"
shape="circle"
icon="search"
/>
<VisibleButton
name="anotherVisibleButtonName"
activeModules={activeModules}
type="primary"
shape="circle"
icon="poweroff"
/>
</div>,
document.getElementById('exampleContainer')
);
14 changes: 14 additions & 0 deletions src/VisibleComponent/VisibleComponent.example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
layout: basic.html
title: VisibleComponent HOC example
description: This example shows the usage of the VisibleComponent HOC (High Order Component).
collection: Examples
---

This example shows the usage of the VisibleComponent HOC (High Order Component) to
determine the visibility of a component based on a `activeModules` property. Typically
this property is managed globally by `react-redux` (or similiar).

In the example below you see three components wrapped by the use of
`isVisibleComponent`. As the second one's name isn't listed in the activeModules,
it won't be rendered.
124 changes: 124 additions & 0 deletions src/VisibleComponent/VisibleComponent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React from 'react';
import PropTypes from 'prop-types';

import Logger from '../Util/Logger';

/**
* The HOC factory function.
*
* Wrapped components will be checked against the activeModules array of
* the state: If the wrapped component (identified by it's name) is included
* in the state, it will be rendered, if not, it wont.
*
* @param {Component} WrappedComponent The component to wrap and enhance.
* @param {Object} options The options to apply.
* @return {Component} The wrapped component.
*/
export function isVisibleComponent(WrappedComponent, {
withRef = false
} = {}) {

/**
* The wrapper class for the given component.
*
* @class The VisibleComponent
* @extends React.Component
*/
class VisibleComponent extends React.Component {

/**
* The props.
* @type {Object}
*/
static propTypes = {
activeModules: PropTypes.arrayOf(PropTypes.object)
}

/**
* Create the VisibleComponent.
*
* @constructs VisibleComponent
*/
constructor(props) {
super(props);

/**
* The wrapped instance.
* @type {Element}
*/
this.wrappedInstance = null;
}

/**
* Returns the wrapped instance. Only applicable if withRef is set to true.
*
* @return {Element} The wrappend instance.
*/
getWrappedInstance = () => {
if (withRef) {
return this.wrappedInstance;
} else {
Logger.debug('No wrapped instance referenced, please call the '
+ 'isVisibleComponent with option withRef = true.');
}
}

/**
* Sets the wrapped instance.
*
* @param {Element} instance The instance to set.
*/
setWrappedInstance = (instance) => {
if (withRef) {
this.wrappedInstance = instance;
}
}

/**
* Checks if the current component (identified by it's name) should be
* visible or not.
*
* @param {String} componentName The name of the component.
* @return {Boolean} Whether the component should be visible or not.
*/
isVisibleComponent = (componentName) => {
let activeModules = this.props.activeModules || [];

return activeModules.some(activeModule => {
if (!activeModule.name) {
return false;
} else {
return activeModule.name === componentName;
}
});
}

/**
* The render function.
*/
render() {
// Filter out extra props that are specific to this HOC and shouldn't be
// passed through.
const {
activeModules,
...passThroughProps
} = this.props;

// Check if the current component should be visible or not.
let isVisibleComponent = this.isVisibleComponent(passThroughProps.name);

// Inject props into the wrapped component. These are usually state
// values or instance methods.
return (
isVisibleComponent ?
<WrappedComponent
ref={this.setWrappedInstance}
{...passThroughProps}
/> :
null
);
}
}

return VisibleComponent;
}
127 changes: 127 additions & 0 deletions src/VisibleComponent/VisibleComponent.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*eslint-env mocha*/
import React from 'react';
import expect from 'expect.js';

import { isVisibleComponent } from './VisibleComponent.jsx';
import TestUtils from '../Util/TestUtils';

describe('isVisibleComponent', () => {
let EnhancedComponent;

/* eslint-disable require-jsdoc */
class MockComponent extends React.Component {
render() {
return (
<div>A mock Component</div>
);
}
}
/* eslint-enable require-jsdoc */

beforeEach(() => {
EnhancedComponent = isVisibleComponent(MockComponent, {
withRef: true
});
});

describe('Basics', () => {
it('is defined', () => {
expect(isVisibleComponent).not.to.be(undefined);
});

it('can be rendered', () => {
const wrapper = TestUtils.mountComponent(EnhancedComponent);

expect(wrapper).not.to.be(undefined);
expect(wrapper.first().is(EnhancedComponent)).to.be(true);
});

it('passes through all props except activeModules', () => {
const props = {
someProp: '09',
name: 'shinjiKagawaModule',
activeModules: [{
name: 'shinjiKagawaModule'
}]
};
const expectedProps = {
someProp: '09',
name: 'shinjiKagawaModule'
};
const wrapper = TestUtils.mountComponent(EnhancedComponent, props);
const wrappedInstance = wrapper.instance().getWrappedInstance();

expect(wrappedInstance.props).to.eql(expectedProps);
});

it('saves a reference to the wrapped instance if requested', () => {
const props = {
name: 'shinjiKagawaModule',
activeModules: [{
name: 'shinjiKagawaModule'
}]
};
const wrapper = TestUtils.mountComponent(EnhancedComponent, props);
const wrappedInstance = wrapper.instance().getWrappedInstance();

expect(wrappedInstance).to.be.an(MockComponent);

const EnhancedComponentNoRef = isVisibleComponent(MockComponent, {
withRef: false
});

const wrapperNoRef = TestUtils.mountComponent(EnhancedComponentNoRef, props);
const wrappedInstanceNoRef = wrapperNoRef.instance().getWrappedInstance();

expect(wrappedInstanceNoRef).to.be(undefined);
});

it('shows or hides the wrapped component in relation to it\'s representation in the activeModules prop', () => {
// 1. No name and no activeModules.
let wrapper = TestUtils.mountComponent(EnhancedComponent);
expect(wrapper.find('div').length).to.equal(0);

// 2. Name and no activeModules.
wrapper = TestUtils.mountComponent(EnhancedComponent, {
name: 'shinjiKagawaModule'
});
expect(wrapper.find('div').length).to.equal(0);

// 3. Name and activeModules.
wrapper = TestUtils.mountComponent(EnhancedComponent, {
name: 'shinjiKagawaModule',
activeModules: [{
name: 'shinjiKagawaModule'
}]
});
expect(wrapper.find('div').length).to.equal(1);

// 4. Name and activeModules, but name not in activeModules.
wrapper = TestUtils.mountComponent(EnhancedComponent, {
name: 'someModule',
activeModules: [{
name: 'shinjiKagawaModule'
}]
});
expect(wrapper.find('div').length).to.equal(0);

// 5. No name and activeModules.
wrapper = TestUtils.mountComponent(EnhancedComponent, {
activeModules: [{
name: 'shinjiKagawaModule'
}]
});
expect(wrapper.find('div').length).to.equal(0);

// 6. Name and activeModules, but no name in activeModules
wrapper = TestUtils.mountComponent(EnhancedComponent, {
name: 'shinjiKagawaModule',
activeModules: [{
notName: 'shinjiKagawaModule'
}]
});
expect(wrapper.find('div').length).to.equal(0);
});

});
});
4 changes: 3 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import Toolbar from './Toolbar/Toolbar.jsx';
import SimpleButton from './Button/SimpleButton/SimpleButton.jsx';
import ToggleButton from './Button/ToggleButton/ToggleButton.jsx';
import ToggleGroup from './Button/ToggleGroup/ToggleGroup.jsx';
import {isVisibleComponent} from './VisibleComponent/VisibleComponent.jsx';

export {
SimpleButton,
ToggleButton,
ToggleGroup,
UserChip,
Toolbar
Toolbar,
isVisibleComponent
};
28 changes: 27 additions & 1 deletion webpack.examples.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
const commonConfig = require('./webpack.common.config.js');
const webpack = require('webpack');
const basePath = '/build/examples/';

commonConfig.entry = {
'Button/SimpleButton/SimpleButton': './src/Button/SimpleButton/SimpleButton.example.jsx',
'Button/ToggleButton/ToggleButton': './src/Button/ToggleButton/ToggleButton.example.jsx',
'Button/ToggleGroup/ToggleGroup': './src/Button/ToggleGroup/ToggleGroup.example.jsx',
'Toolbar/Toolbar': './src/Toolbar/Toolbar.example.jsx',
'UserChip/UserChip': './src/UserChip/UserChip.example.jsx'
'UserChip/UserChip': './src/UserChip/UserChip.example.jsx',
'VisibleComponent/VisibleComponent': './src/VisibleComponent/VisibleComponent.example.jsx'
};

commonConfig.output = {
Expand Down Expand Up @@ -53,4 +55,28 @@ commonConfig.module = {
}]
};

commonConfig.devtool = 'cheap-module-source-map';
commonConfig.plugins = [
...commonConfig.plugins || [],
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
new webpack.optimize.UglifyJsPlugin({
sourceMap: 'source-map',
mangle: true,
compress: {
warnings: false,
pure_getters: true,
unsafe: true,
unsafe_comps: true,
screw_ie8: true
},
output: {
comments: false
}
})
];

module.exports = commonConfig;

0 comments on commit 89a6143

Please sign in to comment.