-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #14 from dnlkoch/visible-hoc
VisibleComponent HOC
- Loading branch information
Showing
8 changed files
with
341 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
|
||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters