History
Result
Implementation
Radioactive
Architecture
Challenges
Flux
ReactJS
ECMAScript
import chai, { expect } from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import createStore from '../../src/';
chai.use(sinonChai);
describe('Create store', () => {
let mockStore;
let mockHandler;
let mockState;
let mockListener;
beforeEach(() => {
mockHandler = (state, command) => command.type === 'TEST_FLYPRO' ? 'newState' : state;
mockState = () => 'mockState';
mockStore = createStore(mockHandler, mockState);
mockListener = sinon.stub();
});
it('should return public API', () => {
expect(mockStore).to.deep.equal({
send: mockStore.send,
subscribe: mockStore.subscribe,
getState: mockStore.getState,
getListeners: mockStore.getListeners
});
});
it('should always retrieve relevant state', () => {
expect(mockStore.getState()).to.equal(mockState);
});
it('should always retrieve relevant listeners', () => {
expect(mockStore.getListeners()).to.deep.equal([]);
});
it('should add listener when subscribing to store', () => {
mockStore.subscribe(mockListener);
expect(mockStore.getListeners()).to.deep.equal([mockListener]);
});
it('should remove listener when unsubscribing from store', () => {
const mockStoreUnsubscribe = mockStore.subscribe(mockListener);
mockStoreUnsubscribe();
expect(mockStore.getListeners()).to.deep.equal([]);
});
it('should change current state when sending a command', () => {
mockStore.send({ type: 'TEST_FLYPRO' });
expect(mockStore.getState()).to.equal('newState');
});
it('should invoke listener when sending a command', () => {
mockStore.subscribe(mockListener);
mockStore.send({ type: 'TEST_FLYPRO' });
expect(mockListener).calledWith();
});
});
export default function createStore(handler, state) {
const currentHandler = handler;
let currentState = state;
const listeners = [];
function getState() {
return currentState;
}
function getListeners() {
return listeners;
}
function subscribe(listener) {
listeners.push(listener);
return function unsubscribe() {
listeners.splice(listeners.indexOf(listener), 1);
};
}
function send(command) {
currentState = currentHandler(currentState, command);
listeners.slice().forEach(listener => listener());
return command;
}
send({ type: 'INIT_FLYPRO' });
return { send, subscribe, getState, getListeners };
}
import React, { PropTypes } from 'react';
import chai, { expect } from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { mount } from 'enzyme';
import chaiEnzyme from 'chai-enzyme';
import wrap from '../../src/';
chai.use(sinonChai);
chai.use(chaiEnzyme());
describe('Wrap', () => {
let mockStore;
let mockStates;
let mockHandlers;
let MockComponent;
let ExtendedComponent;
let instanceOfExtendedComponent;
const instanceOfExtendedComponentUnsubscribeStub = sinon.stub();
beforeEach(() => {
mockStore = {
getState: () => {},
send: () => {},
subscribe: sinon.stub().returns(instanceOfExtendedComponentUnsubscribeStub)
};
mockStates = () => ({
mockState: 'mockStateValue'
});
mockHandlers = () => ({
mockHandler: 'mockHandlerValue'
});
});
beforeEach(() => {
MockComponent = () => (
<div className="mock-component" />
);
ExtendedComponent = wrap(mockStates, mockHandlers)(<MockComponent store={mockStore} />);
instanceOfExtendedComponent = new ExtendedComponent();
// pass the store to each component explicitly
instanceOfExtendedComponent.props = mount(<MockComponent store={mockStore} />).props();
});
it('should validate prop types', () => {
const propTypes = ExtendedComponent.propTypes();
expect(propTypes).to.deep.equal({
store: PropTypes.object.isRequired
});
});
it('should extend base component with store', () => {
const WrappedComponent = instanceOfExtendedComponent.render();
expect(WrappedComponent.props.store).to.deep.equal(mockStore);
});
it('should extend base component with states', () => {
const WrappedComponent = instanceOfExtendedComponent.render();
expect(WrappedComponent.props.mockState).to.equal('mockStateValue');
});
it('should extend base component with handlers', () => {
const WrappedComponent = instanceOfExtendedComponent.render();
expect(WrappedComponent.props.mockHandler).to.equal('mockHandlerValue');
});
it('should subscribe for store changes on component did mount', () => {
instanceOfExtendedComponent.componentDidMount();
expect(mockStore.subscribe).calledWith(sinon.match.func);
});
it('should unsubscribe for store changes on component will unmount', () => {
instanceOfExtendedComponent.componentDidMount(); // to create unsubscribe handler
instanceOfExtendedComponent.componentWillUnmount();
expect(instanceOfExtendedComponentUnsubscribeStub).calledWith();
});
});
import React, { Component, PropTypes } from 'react';
export default function wrap(states, handlers) {
return function extend(WrappedComponent) {
return class extends Component {
static propTypes() {
return {
store: PropTypes.object.isRequired
};
}
render() {
return (
<WrappedComponent
{...this.props}
{...states(this.props.store.getState(), this.props)}
{...handlers(this.props.store.send, this.props)}
/>
);
}
componentDidMount() {
this.unsubscribe = this.props.store.subscribe(this.forceUpdate.bind(this));
}
componentWillUnmount() {
this.unsubscribe();
}
};
};
}
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
import 'es6-promise';
import * as storage from 'redux-storage';
import createEngine from 'redux-storage-engine-localstorage';
import * as actionTypes from '../actions/actionTypes';
const replacer = (key, value) => {
if (key === '') {
const {
promo: { isHidden },
settings: { showPedigree }
} = value;
return { promo: { isHidden }, settings: { showPedigree } };
}
return value;
};
export default function createLoader() {
const localStorageEngine = createEngine('state', replacer);
const localStorageMiddleware = storage.createMiddleware(
localStorageEngine,
[],
[
actionTypes.HIDE_PROMO,
actionTypes.CHANGE_SETTINGS
]
);
const load = storage.createLoader(localStorageEngine);
return {
load,
localStorageMiddleware
};
}
<?php
include './ReactJS.php';
$rjs = new ReactJS(
file_get_contents('build/react-bundle.js'),
file_get_contents('build/table.js')
);
$data =
array('data' => array(
array(1, 2, 3),
array(4, 5, 6),
array(7, 8, 9)
));
$rjs->setComponent('Table', $data);
?>
<html>
<head>
<title>React page</title>
</head>
<body>
<!-- render server content here -->
<div id="page"><?php echo $rjs->getMarkup(); ?></div>
<!-- load react and app code -->
<script src="react/build/react.min.js"></script>
<script src="build/table.js"></script>
<script><?php echo $rjs->getJS('#page', "GLOB"); ?></script>
</body>
</html>
import React, { PropTypes } from 'react';
const Svg = ({ className = '', size, iconName }) => (
<svg className={`ui-icon ui-icon_size${size} ${className}`}>
<use xlinkHref={`img/icons.svg#icon-${iconName}`} />
</svg>
);
Svg.propTypes = {
iconName: PropTypes.string.isRequired,
size: PropTypes.string,
className: PropTypes.string
};
export default Svg;
const replace = require('replace');
function AssetsVersions(options) {
this.config = options;
}
AssetsVersions.prototype.apply = function resolve(compiler) {
const paths = {
index: this.config.indexPath,
css: this.config.cssPath,
js: this.config.jsPath
};
const patterns = {
cssJs: /(\.css(?!\/)|\.js(?!\/))(\?_t=\d+)?/ig,
img: /(\.png|\.jpg|\.jpeg|\.gif)(\?_t=\d+)?/ig,
replace: `$1?_t=${Math.floor(new Date().getTime() / 1000)}`
};
const files = [
{
path: paths.index,
pattern: patterns.cssJs
},
{
path: paths.css,
pattern: patterns.img
},
{
path: paths.js,
pattern: patterns.img
}
];
compiler.plugin('done', () => {
files.forEach(file => {
if (!file.path) {
return;
}
replace({
regex: file.pattern,
replacement: patterns.replace,
paths: [
file.path
],
recursive: false,
silent: false
});
});
});
};
module.exports = AssetsVersions;