diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index 0296706f..9fff095a 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -11,6 +11,7 @@ const include = path.resolve(__dirname, '../'); // to "React Create App". This only has babel loader to load JavaScript. module.exports = { + devtool: 'source-map', entry: './stories/index.tsx', output: { filename: include + '/dist/examples/storybook.js' diff --git a/src/core/index.js b/src/core/index.js new file mode 100644 index 00000000..1b70a240 --- /dev/null +++ b/src/core/index.js @@ -0,0 +1,15 @@ +import components from '../components'; +import * as reducer from '../reducers/dataReducer'; +import * as selectors from '../selectors/dataSelectors'; +import * as actions from '../actions'; +import initialState from './initialState'; + +const CorePlugin = { + components, + reducer, + selectors, + actions, + ...initialState, +}; + +export default CorePlugin; diff --git a/src/core/initialState.js b/src/core/initialState.js new file mode 100644 index 00000000..8f329ae0 --- /dev/null +++ b/src/core/initialState.js @@ -0,0 +1,45 @@ +const styleConfig = { + icons: { + TableHeadingCell: { + sortDescendingIcon: '▼', + sortAscendingIcon: '▲' + }, + }, + classNames: { + Cell: 'griddle-cell', + Filter: 'griddle-filter', + Loading: 'griddle-loadingResults', + NextButton: 'griddle-next-button', + NoResults: 'griddle-noResults', + PageDropdown: 'griddle-page-select', + Pagination: 'griddle-pagination', + PreviousButton: 'griddle-previous-button', + Row: 'griddle-row', + RowDefinition: 'griddle-row-definition', + Settings: 'griddle-settings', + SettingsToggle: 'griddle-settings-toggle', + Table: 'griddle-table', + TableBody: 'griddle-table-body', + TableHeading: 'griddle-table-heading', + TableHeadingCell: 'griddle-table-heading-cell', + TableHeadingCellAscending: 'griddle-heading-ascending', + TableHeadingCellDescending: 'griddle-heading-descending', + }, + styles: { + } +}; + +export default { + styleConfig, + + pageProperties: { + currentPage: 1, + pageSize: 10 + }, + enableSettings: true, + textProperties: { + next: 'Next', + previous: 'Previous', + settingsToggle: 'Settings' + }, +}; diff --git a/src/index.js b/src/index.js index a898eead..4b6e0eff 100644 --- a/src/index.js +++ b/src/index.js @@ -1,60 +1,14 @@ import { createStore, combineReducers, bindActionCreators, applyMiddleware, compose } from 'redux'; -import Immutable from 'immutable'; import { createProvider } from 'react-redux'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; import _ from 'lodash'; -import * as dataReducers from './reducers/dataReducer'; -import components from './components'; -import settingsComponentObjects from './settingsComponentObjects'; -import * as selectors from './selectors/dataSelectors'; - -import { buildGriddleReducer, buildGriddleComponents } from './utils/compositionUtils'; -import { getColumnProperties } from './utils/columnUtils'; -import { getRowProperties } from './utils/rowUtils'; -import { setSortProperties } from './utils/sortUtils'; +import corePlugin from './core'; +import init from './utils/initializer'; import { StoreListener } from './utils/listenerUtils'; import * as actions from './actions'; -const defaultEvents = { - ...actions, - onFilter: actions.setFilter, - setSortProperties -}; - - -const defaultStyleConfig = { - icons: { - TableHeadingCell: { - sortDescendingIcon: '▼', - sortAscendingIcon: '▲' - }, - }, - classNames: { - Cell: 'griddle-cell', - Filter: 'griddle-filter', - Loading: 'griddle-loadingResults', - NextButton: 'griddle-next-button', - NoResults: 'griddle-noResults', - PageDropdown: 'griddle-page-select', - Pagination: 'griddle-pagination', - PreviousButton: 'griddle-previous-button', - Row: 'griddle-row', - RowDefinition: 'griddle-row-definition', - Settings: 'griddle-settings', - SettingsToggle: 'griddle-settings-toggle', - Table: 'griddle-table', - TableBody: 'griddle-table-body', - TableHeading: 'griddle-table-heading', - TableHeadingCell: 'griddle-table-heading-cell', - TableHeadingCellAscending: 'griddle-heading-ascending', - TableHeadingCellDescending: 'griddle-heading-descending', - }, - styles: { - } -}; - class Griddle extends Component { static childContextTypes = { components: PropTypes.object.isRequired, @@ -69,86 +23,23 @@ class Griddle extends Component { super(props); const { - plugins=[], - data, - children:rowPropertiesComponent, - events={}, - sortProperties={}, - styleConfig={}, - pageProperties:importedPageProperties, - components:userComponents, - renderProperties:userRenderProperties={}, - settingsComponentObjects:userSettingsComponentObjects, + core = corePlugin, storeKey = Griddle.storeKey || 'store', - reduxMiddleware = [], - listeners = {}, - ...userInitialState } = props; - const rowProperties = getRowProperties(rowPropertiesComponent); - const columnProperties = getColumnProperties(rowPropertiesComponent); - - //Combine / compose the reducers to make a single, unified reducer - const reducers = buildGriddleReducer([dataReducers, ...plugins.map(p => p.reducer)]); - - //Combine / Compose the components to make a single component for each component type - this.components = buildGriddleComponents([components, ...plugins.map(p => p.components), userComponents]); - - this.settingsComponentObjects = Object.assign({}, settingsComponentObjects, ...plugins.map(p => p.settingsComponentObjects), userSettingsComponentObjects); - - this.events = Object.assign({}, events, ...plugins.map(p => p.events)); - - this.selectors = plugins.reduce((combined, plugin) => ({ ...combined, ...plugin.selectors }), {...selectors}); - - const mergedStyleConfig = _.merge({}, defaultStyleConfig, ...plugins.map(p => p.styleConfig), styleConfig); - - const pageProperties = Object.assign({}, { - currentPage: 1, - pageSize: 10 - }, - importedPageProperties, - ); - - //TODO: This should also look at the default and plugin initial state objects - const renderProperties = Object.assign({ - rowProperties, - columnProperties - }, ...plugins.map(p => p.renderProperties), userRenderProperties); - - // TODO: Make this its own method - const initialState = _.merge( - { - enableSettings: true, - textProperties: { - next: 'Next', - previous: 'Previous', - settingsToggle: 'Settings' - }, - }, - ...plugins.map(p => p.initialState), - userInitialState, - { - data, - pageProperties, - renderProperties, - sortProperties, - styleConfig: mergedStyleConfig, - } - ); + const { initialState, reducers, reduxMiddleware } = init.call(this, core); const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose this.store = createStore( reducers, initialState, composeEnhancers( - applyMiddleware(..._.compact(_.flatten(plugins.map(p => p.reduxMiddleware))), ...reduxMiddleware) + applyMiddleware(...reduxMiddleware) ) ); this.provider = createProvider(storeKey); - const sanitizedListeners = _.pickBy(listeners, (value, key) => typeof value === "function"); - this.listeners = plugins.reduce((combined, plugin) => ({...combined, ..._.pickBy(plugin.listeners, (value, key) => typeof value === "function")}), {...sanitizedListeners}); this.storeListener = new StoreListener(this.store); _.forIn(this.listeners, (listener, name) => { this.storeListener.addListener(listener, name, {events: this.events, selectors: this.selectors}); @@ -188,6 +79,10 @@ class Griddle extends Component { } render() { + if (!this.components.Layout) { + return null; + } + return ( diff --git a/src/module.d.ts b/src/module.d.ts index b25f4af4..0fb8bb09 100644 --- a/src/module.d.ts +++ b/src/module.d.ts @@ -383,7 +383,9 @@ interface GriddleExtensibility { interface GriddleInitialState { enableSettings?: boolean; + pageProperties?: GriddlePageProperties; sortMethod?: (data: any[], column: string, sortAscending?: boolean) => number; + sortProperties?: GriddleSortKey[]; textProperties?: { next?: string, previous?: string, @@ -399,10 +401,9 @@ export interface GriddlePlugin extends GriddleExtensibility { } export interface GriddleProps extends GriddlePlugin, GriddleInitialState { + core?: GriddlePlugin; plugins?: GriddlePlugin[]; data?: T[]; - sortProperties?: GriddleSortKey[]; - pageProperties?: GriddlePageProperties; storeKey?: string; } @@ -441,6 +442,8 @@ export namespace utils { } export namespace plugins { + var CorePlugin : GriddlePlugin; + var LegacyStylePlugin : GriddlePlugin; var LocalPlugin : GriddlePlugin; diff --git a/src/module.js b/src/module.js index a5d36a0c..1a1c04f8 100644 --- a/src/module.js +++ b/src/module.js @@ -7,11 +7,13 @@ import * as selectors from './selectors/dataSelectors'; import settingsComponentObjects from './settingsComponentObjects'; import utils from './utils'; +import CorePlugin from './core'; import LegacyStylePlugin from './plugins/legacyStyle'; import LocalPlugin from './plugins/local'; import PositionPlugin from './plugins/position'; const plugins = { + CorePlugin, LegacyStylePlugin, LocalPlugin, PositionPlugin, diff --git a/src/utils/__tests__/compositionUtilsTest.js b/src/utils/__tests__/compositionUtilsTest.js index c163212d..40b777e2 100644 --- a/src/utils/__tests__/compositionUtilsTest.js +++ b/src/utils/__tests__/compositionUtilsTest.js @@ -11,7 +11,6 @@ import { removeHooksFromObject, isKeyGriddleHook, buildGriddleReducer, - buildGriddleReducerObject, getAfterHooksFromObject, getBeforeHooksFromObject, removeKeyNamePartFromObject, @@ -326,7 +325,7 @@ test('builds griddle reducer', test => { } } - const griddleReducer = buildGriddleReducerObject([reducer1, reducer2, reducer3]); + const griddleReducer = buildGriddleReducer([reducer1, reducer2, reducer3]); test.deepEqual(Object.keys(griddleReducer), ['REDUCE_THING', 'REDUCE_OTHER']); test.deepEqual(griddleReducer.REDUCE_THING({ number: 5}), { number: -45 }); @@ -357,6 +356,7 @@ test('builds griddle reducer with BEFORE_REDUCE and AFTER_REDUCE', (t) => { const griddleReducer = buildGriddleReducer([reducer1, reducer2]); const output = griddleReducer({number: 5}, { type: 'REDUCE_THING'}); + t.deepEqual(Object.keys(griddleReducer), ['AFTER_REDUCE', 'REDUCE_THING', 'BEFORE_REDUCE']); t.deepEqual(output, { number: 55 }); }); @@ -376,9 +376,25 @@ test('builds griddle reducer without BEFORE / AFTER if they dont exist', (t) => const griddleReducer = buildGriddleReducer([reducer1, reducer2]); const output = griddleReducer({number: 5}, { type: 'REDUCE_THING'}); + t.deepEqual(Object.keys(griddleReducer), ['REDUCE_THING']); t.deepEqual(output, { number: 15 }); }); +test('builds griddle reducer that calls GRIDDLE_INITIALIZED for missing action type, if it exists', (assert) => { + const initReducer = { GRIDDLE_INITIALIZED: () => ({ init: true }) }; + const griddleReducer = buildGriddleReducer([initReducer]); + const output = griddleReducer({}, { type: 'MISSING' }); + + assert.deepEqual(output, { init: true }); +}); + +test('builds griddle reducer that does noop for missing action type, if GRIDDLE_INITIALIZED is also missing', (assert) => { + const griddleReducer = buildGriddleReducer([]); + const output = griddleReducer({}, { type: 'MISSING' }); + + assert.deepEqual(output, {}); +}); + test('combineAndEnhanceComponents', test => { const initial = { one: (someNumber) => (someNumber + 5)} const enhancing = { oneEnhancer: originalMethod => (someNumber) => originalMethod(someNumber * 5)}; diff --git a/src/utils/__tests__/initilizerTests.js b/src/utils/__tests__/initilizerTests.js new file mode 100644 index 00000000..31522b38 --- /dev/null +++ b/src/utils/__tests__/initilizerTests.js @@ -0,0 +1,409 @@ +import test from 'ava'; +import _ from 'lodash'; + +import init from '../initializer'; + +import { getColumnProperties } from '../columnUtils'; +import { getRowProperties } from '../rowUtils'; + +const expectedDefaultInitialState = { + data: [], + renderProperties: { + rowProperties: null, + columnProperties: {}, + }, + styleConfig: {}, +}; + +test('init succeeds given null defaults and empty props', (assert) => { + const ctx = { props: {} }; + const defaults = null; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.deepEqual(res.initialState, expectedDefaultInitialState); + + assert.is(typeof res.reducers, 'function'); + assert.deepEqual(res.reducers({}, { type: 'REDUCE' }), {}); + + assert.deepEqual(res.reduxMiddleware, []); + + assert.deepEqual(ctx.components, {}); + assert.deepEqual(ctx.settingsComponentObjects, {}); + assert.deepEqual(ctx.events, {}); + assert.deepEqual(ctx.selectors, {}); + assert.deepEqual(ctx.listeners, {}); +}); + +test('init succeeds given empty defaults and props', (assert) => { + const ctx = { props: {} }; + const defaults = {}; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.deepEqual(res.initialState, expectedDefaultInitialState); + + assert.is(typeof res.reducers, 'function'); + assert.deepEqual(res.reducers({}, { type: 'REDUCE' }), {}); + + assert.deepEqual(res.reduxMiddleware, []); + + assert.deepEqual(ctx.components, {}); + assert.deepEqual(ctx.settingsComponentObjects, {}); + assert.deepEqual(ctx.events, {}); + assert.deepEqual(ctx.selectors, {}); + assert.deepEqual(ctx.listeners, {}); +}); + +test('init returns defaults given minimum props', (assert) => { + const ctx = { props: { data: [] } }; + const defaults = { + reducers: { REDUCE: () => ({ reduced: true }) }, + components: { Layout: () => null }, + settingsComponentObjects: { mySettings: { order: 10 } }, + selectors: { aSelector: () => null }, + styleConfig: { classNames: {} }, + pageProperties: { pageSize: 100 }, + init: true, + }; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.deepEqual(res.initialState, { + ...expectedDefaultInitialState, + + init: true, + data: ctx.props.data, + pageProperties: defaults.pageProperties, + styleConfig: defaults.styleConfig, + }); + + assert.is(typeof res.reducers, 'function'); + assert.deepEqual(Object.keys(res.reducers), Object.keys(defaults.reducers)); + assert.deepEqual(res.reducers({}, { type: 'REDUCE' }), { reduced: true }); + + assert.deepEqual(res.reduxMiddleware, []); + + assert.deepEqual(ctx.components, defaults.components); + assert.deepEqual(ctx.settingsComponentObjects, defaults.settingsComponentObjects); + assert.deepEqual(ctx.events, {}); + assert.deepEqual(ctx.selectors, defaults.selectors); + assert.deepEqual(ctx.listeners, {}); +}); + +test('init returns expected initialState.data given props.data', (assert) => { + const ctx = { + props: { + data: [{ foo: 'bar' }], + }, + }; + const defaults = {}; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.deepEqual(res.initialState.data, ctx.props.data); +}); + +test('init returns expected initialState.pageProperties given props (user)', (assert) => { + const ctx = { + props: { + pageProperties: { user: true }, + }, + }; + const defaults = { + pageProperties: { + defaults: true, + user: false, + }, + }; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.deepEqual(res.initialState.pageProperties, { + defaults: true, + user: true, + }); +}); + +test('init returns expected initialState.renderProperties given props (children, plugins, user)', (assert) => { + const ctx = { + props: { + children: { + props: { + children: [{ props: { id: 'foo', order: 1 } }], + } + }, + plugins: [ + { renderProperties: { plugin: 0, user: false } }, + { renderProperties: { plugin: 1 } }, + ], + renderProperties: { user: true }, + }, + }; + const defaults = {}; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.deepEqual(res.initialState.renderProperties, { + rowProperties: getRowProperties(ctx.props.children), + columnProperties: getColumnProperties(ctx.props.children), + plugin: 1, + user: true, + }); +}); + +test('init returns expected initialState.sortProperties given props (user)', (assert) => { + const ctx = { + props: { + sortProperties: { user: true }, + }, + }; + const defaults = {}; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.deepEqual(res.initialState.sortProperties, { + user: true, + }); +}); + +test('init returns merged initialState.styleConfig given props (plugins, user)', (assert) => { + const ctx = { + props: { + plugins: [ + { styleConfig: { styles: { plugin: 0, user: false } } }, + { styleConfig: { styles: { plugin: 1, defaults: false } } }, + ], + styleConfig: { + styles: { user: true }, + }, + }, + }; + const defaults = { + styleConfig: { + classNames: { defaults: true }, + styles: { defaults: true, plugin: false, user: false }, + }, + }; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.deepEqual(res.initialState.styleConfig, { + classNames: { defaults: true }, + styles: { + defaults: false, + plugin: 1, + user: true, + }, + }); +}); + +test('init returns expected extra initialState given props (plugins, user)', (assert) => { + const ctx = { + props: { + plugins: [ + { initialState: { plugin: 0, user: false } }, + { initialState: { plugin: 1 } }, + ], + user: true, + }, + }; + const defaults = { + defaults: true, + user: false, + plugin: false, + }; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.deepEqual(res.initialState, { + ...expectedDefaultInitialState, + + defaults: true, + user: true, + plugin: 1, + }); +}); + +test('init returns composed reducer given plugins', (assert) => { + const ctx = { + props: { + plugins: [ + { reducer: { PLUGIN: () => ({ plugin: 0 }) } }, + { reducer: { PLUGIN: () => ({ plugin: 1 }) } }, + ], + }, + }; + const defaults = { + reducers: { + DEFAULTS: () => ({ defaults: true }), + PLUGIN: () => ({ plugin: false }), + }, + }; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.is(typeof res.reducers, 'function'); + assert.deepEqual(Object.keys(res.reducers), ['DEFAULTS', 'PLUGIN']); + assert.deepEqual(res.reducers({}, { type: 'DEFAULTS' }), { defaults: true }); + assert.deepEqual(res.reducers({}, { type: 'PLUGIN' }), { plugin: 1 }); +}); + +test('init returns flattened/compacted reduxMiddleware given plugins', (assert) => { + const mw = _.range(0, 4).map(i => () => i); + const ctx = { + props: { + plugins: [ + {}, + { reduxMiddleware: [mw[0]] }, + {}, + { reduxMiddleware: [null, mw[1], undefined, mw[2], null] }, + {}, + ], + reduxMiddleware: [null, mw[3], undefined], + }, + }; + const defaults = {}; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.deepEqual(res.reduxMiddleware, mw); +}); + +test('init sets context.components as expected given plugins', (assert) => { + const ctx = { + props: { + plugins: [ + { components: { Plugin: 0, User: false } }, + { components: { Plugin: 1 } }, + ], + components: { User: true }, + }, + }; + const defaults = { + components: { + Defaults: true, + Plugin: false, + }, + }; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.deepEqual(ctx.components, { + Defaults: true, + Plugin: 1, + User: true, + }); +}); + +test('init sets context.settingsComponentObjects as expected given plugins', (assert) => { + const ctx = { + props: { + plugins: [ + { settingsComponentObjects: { Plugin: 0, User: false } }, + { settingsComponentObjects: { Plugin: 1 } }, + ], + settingsComponentObjects: { User: true }, + }, + }; + const defaults = { + settingsComponentObjects: { + Defaults: true, + Plugin: false, + }, + }; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.deepEqual(ctx.settingsComponentObjects, { + Defaults: true, + Plugin: 1, + User: true, + }); +}); + +test('init sets context.events as expected given plugins', (assert) => { + const ctx = { + props: { + plugins: [ + { events: { Plugin: 0, User: false } }, + { events: { Plugin: 1 } }, + ], + events: { User: true, User2: true }, + }, + }; + const defaults = {}; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.deepEqual(ctx.events, { + Plugin: 1, + User: false, // TODO: bug that plugins overwrite user events? + User2: true, + }); +}); + +test('init sets context.selectors as expected given plugins', (assert) => { + const ctx = { + props: { + plugins: [ + { selectors: { Plugin: 0 } }, + { selectors: { Plugin: 1 } }, + ], + }, + }; + const defaults = { + selectors: { + Defaults: true, + Plugin: false, + }, + }; + + const res = init.call(ctx, defaults); + assert.truthy(res); + + assert.deepEqual(ctx.selectors, { + Defaults: true, + Plugin: 1, + }); +}); + + +test('init sets context.listeners as expected given props (plugins, user)', (assert) => { + const ctx = { + props: { + plugins: [ + { listeners: { plugin: () => 0, user: () => false } }, + { listeners: { plugin: () => 1 } }, + ], + listeners: { + user: () => true, + user2: () => true, + }, + }, + }; + const defaults = {}; + + const res = init.call(ctx, defaults); + assert.truthy(res); + assert.truthy(res); + + assert.false('defaults' in ctx.listeners); + assert.deepEqual(ctx.listeners.plugin(), 1); + assert.deepEqual(ctx.listeners.user(), false); // TODO: bug that plugins overwrite user listeners? + assert.deepEqual(ctx.listeners.user2(), true); +}); diff --git a/src/utils/compositionUtils.js b/src/utils/compositionUtils.js index 78b68177..ed94f9a2 100644 --- a/src/utils/compositionUtils.js +++ b/src/utils/compositionUtils.js @@ -171,9 +171,10 @@ export function composeReducerObjects(reducerObjects) { /** Builds a new reducer that composes hooks and extends standard reducers between reducerObjects * @param {Object } reducers - An array of reducerObjects + * Note: this used to be exported, but the same properties are available from buildGriddleReducer. * TODO: This method should be broken down a bit -- it's doing too much currently */ -export function buildGriddleReducerObject(reducerObjects) { +function buildGriddleReducerObject(reducerObjects) { let reducerMethodsWithoutHooks = []; let beforeHooks = []; let afterHooks = []; @@ -227,7 +228,7 @@ export function callReducerWithBeforeAfterPipe(reducerObject, state, action) { const call = (action.type && reducerObject[action.type] && reducerObject[action.type] - ) || reducerObject.GRIDDLE_INITIALIZED; + ) || reducerObject.GRIDDLE_INITIALIZED || noop; const partialCall = (partialAction => partialState => call(partialState, partialAction))(action); @@ -241,7 +242,9 @@ export function callReducerWithBeforeAfterPipe(reducerObject, state, action) { */ export function buildGriddleReducer(reducerObjects) { const reducerObject = buildGriddleReducerObject(reducerObjects); - return (state, action) => callReducerWithBeforeAfterPipe(reducerObject, state, action); + const reducer = (state, action) => callReducerWithBeforeAfterPipe(reducerObject, state, action); + Object.assign(reducer, reducerObject); + return reducer; } /** Gets all reducers by a specific wordEnding diff --git a/src/utils/initializer.js b/src/utils/initializer.js new file mode 100644 index 00000000..193c7e86 --- /dev/null +++ b/src/utils/initializer.js @@ -0,0 +1,91 @@ +import _ from 'lodash'; +import { buildGriddleReducer, buildGriddleComponents } from './compositionUtils'; +import { getColumnProperties } from './columnUtils'; +import { getRowProperties } from './rowUtils'; + +module.exports = function initializer(defaults) { + if (!this) throw new Error('this missing!'); + + const { + reducers: dataReducers, + components, + settingsComponentObjects, + selectors, + styleConfig: defaultStyleConfig, + ...defaultInitialState + } = defaults || {}; + + const { + plugins = [], + data = [], + children: rowPropertiesComponent, + events: userEvents = {}, + styleConfig: userStyleConfig = {}, + components: userComponents, + renderProperties: userRenderProperties = {}, + settingsComponentObjects: userSettingsComponentObjects, + reduxMiddleware = [], + listeners = {}, + ...userInitialState + } = this.props; + + const rowProperties = getRowProperties(rowPropertiesComponent); + const columnProperties = getColumnProperties(rowPropertiesComponent); + + // Combine / compose the reducers to make a single, unified reducer + const reducers = buildGriddleReducer([dataReducers, ...plugins.map(p => p.reducer)]); + + // Combine / Compose the components to make a single component for each component type + this.components = buildGriddleComponents([ + components, + ...plugins.map(p => p.components), + userComponents, + ]); + + this.settingsComponentObjects = Object.assign( + { ...settingsComponentObjects }, + ...plugins.map(p => p.settingsComponentObjects), + userSettingsComponentObjects); + + this.events = Object.assign({}, userEvents, ...plugins.map(p => p.events)); + + this.selectors = plugins.reduce( + (combined, plugin) => ({ ...combined, ...plugin.selectors }), + { ...selectors }); + + const styleConfig = _.merge( + { ...defaultStyleConfig }, + ...plugins.map(p => p.styleConfig), + userStyleConfig); + + + // TODO: This should also look at the default and plugin initial state objects + const renderProperties = Object.assign({ + rowProperties, + columnProperties + }, ...plugins.map(p => p.renderProperties), userRenderProperties); + + // TODO: Make this its own method + const initialState = _.merge( + defaultInitialState, + ...plugins.map(p => p.initialState), + userInitialState, + { + data, + renderProperties, + styleConfig, + } + ); + + const sanitizedListeners = _.pickBy(listeners, value => typeof value === 'function'); + this.listeners = plugins.reduce((combined, plugin) => ({ ...combined, ..._.pickBy(plugin.listeners, value => typeof value === 'function') }), sanitizedListeners); + + return { + initialState, + reducers, + reduxMiddleware: _.compact([ + ..._.flatten(plugins.map(p => p.reduxMiddleware)), + ...reduxMiddleware + ]), + }; +}; diff --git a/stories/index.tsx b/stories/index.tsx index 58be9ea2..46531895 100644 --- a/stories/index.tsx +++ b/stories/index.tsx @@ -97,16 +97,27 @@ storiesOf('Griddle main', module) }) .add('with local, delayed data', () => { class DeferredGriddle extends React.Component, { data?: FakeData[] }> { + private timeout; + constructor(props) { super(props); this.state = {}; + } + + componentDidMount() { this.resetData(); } + componentWillUnmount() { + this.timeout && clearTimeout(this.timeout); + } + resetData = () => { this.setState({ data: null }); - setTimeout(() => { + this.timeout && clearTimeout(this.timeout); + + this.timeout = setTimeout(() => { this.setState({ data: this.props.data }); }, 2000); } @@ -1627,6 +1638,24 @@ storiesOf('Settings', module) ); }) +storiesOf('core', module) + .add('Can replace core', () => { + const core = { + components: { + Layout: () =>

Core Replaced!

, + }, + }; + + return ( + + ); + }) + .add('Can handle null core', () => { + return ( + + ); + }) + storiesOf('TypeScript', module) .add('GriddleComponent accepts expected types', () => { class Custom extends React.Component<{ value }> {