Skip to content

Commit

Permalink
Implement react-select for our select input components (#515)
Browse files Browse the repository at this point in the history
* install react-select

* add utility for retrieving current available modelNames. This is populated via eejs.data from the server.

* add an abstracted base method for generating queryString

* extract model related methods to their own class

* add additional exports

* implement new model-select component. This uses the react-select component

* implement new EventSelect that wraps model-select

* test all the things

* convert datetime select to new component

* export build-options for components
  • Loading branch information
nerrad authored Jun 11, 2018
1 parent e11d47d commit 26c214f
Show file tree
Hide file tree
Showing 45 changed files with 13,345 additions and 5,035 deletions.
8 changes: 4 additions & 4 deletions assets/dist/build-manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"components.js": "ee-components.269a5a9408e9d4ec4a94.dist.js",
"data-stores.js": "ee-data-stores.7cbf939209f1378b61ad.dist.js",
"components.js": "ee-components.6251295f9ed97c5e0d58.dist.js",
"data-stores.js": "ee-data-stores.8be4f554fcdf636c356b.dist.js",
"eejs.js": "ee-eejs.079eec177a6d0cb54b86.dist.js",
"manifest.js": "ee-manifest.3b66c2f77d3b01c1b09a.dist.js",
"vendor.js": "ee-vendor.a7c50608201c56d1424c.dist.js",
"wp-plugins-page.css": "ee-wp-plugins-page.6bf5b0c4985bf9cc730a.dist.css",
"wp-plugins-page.js": "ee-wp-plugins-page.955c372519f507d64cb7.dist.js"
"wp-plugins-page.css": "ee-wp-plugins-page.12612f6ab71570ab1af8.dist.css",
"wp-plugins-page.js": "ee-wp-plugins-page.1700d8fd2144a931cb59.dist.js"
}
4,287 changes: 0 additions & 4,287 deletions assets/dist/ee-components.269a5a9408e9d4ec4a94.dist.js

This file was deleted.

11,675 changes: 11,675 additions & 0 deletions assets/dist/ee-components.6251295f9ed97c5e0d58.dist.js

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions assets/src/components/form/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './select';
72 changes: 72 additions & 0 deletions assets/src/components/form/select/build-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* External dependencies
*/
import { reduce } from 'lodash';

/**
* A default map used for mapping options for select.
* @type {Object}
*/
const DEFAULT_MODEL_OPTIONS_MAP = {
event: {
label: 'EVT_name',
value: 'EVT_ID',
},
datetime: {
label: 'DTT_name',
value: 'DTT_ID',
},
ticket: {
label: 'TKT_name',
value: 'TKT_ID',
},
};

export const OPTION_SELECT_ALL = 'ALL';

/**
* Receives an array of event entities and returns an array of simple objects
* that can be passed along to the options array used for the WordPress
* SelectControl component.
*
* @param { Array } entities
* @param { string } modelName
* @param { string } addAllOptionLabel If present then options array will be
* prepended with an "ALL" option meaning
* that all options are selected.
* @param { Object } map
* @return { Array } Returns an array of simple objects formatted for any
* select control that receives its options in the format of an array of objects
* with label and value keys.
*/
const buildOptions = (
entities,
modelName,
addAllOptionLabel = '',
map = DEFAULT_MODEL_OPTIONS_MAP,
) => {
const MAP_FOR_MODEL = map[ modelName ] ? map[ modelName ] : false;
const generatedOptions = entities && MAP_FOR_MODEL ?
reduce( entities, function( options, entity ) {
if ( entity[ MAP_FOR_MODEL.label ] &&
entity[ MAP_FOR_MODEL.value ] ) {
options.push(
{
label: entity[ MAP_FOR_MODEL.label ],
value: entity[ MAP_FOR_MODEL.value ],
},
);
}
return options;
}, [] ) :
[];
if ( entities && addAllOptionLabel !== '' ) {
generatedOptions.unshift( {
label: addAllOptionLabel,
value: OPTION_SELECT_ALL,
} );
}
return generatedOptions;
};

export default buildOptions;
111 changes: 111 additions & 0 deletions assets/src/components/form/select/default-select-configuration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* External imports
*/
import PropTypes from 'prop-types';
import { __ } from '@eventespresso/i18n';

export const REACT_SELECT_TYPES = {
'aria-describedby': PropTypes.string,
'aria-label': PropTypes.string,
'aria-labelledby': PropTypes.string,
autoFocus: PropTypes.bool,
backspaceRemovesValue: PropTypes.bool,
blurInputOnSelect: PropTypes.bool,
captureMenuScroll: PropTypes.bool,
className: PropTypes.string,
classNamePrefix: PropTypes.string,
closeMenuOnSelect: PropTypes.bool,
components: PropTypes.object,
controlShouldRenderValue: PropTypes.bool,
delimiter: PropTypes.string,
escapeClearsValue: PropTypes.bool,
filterOption: PropTypes.func,
formatGroupLabel: PropTypes.func,
formatOptionLabel: PropTypes.func,
getOptionLabel: PropTypes.func,
getOptionValue: PropTypes.func,
hideSelectedOptions: PropTypes.bool,
id: PropTypes.string,
inputValue: PropTypes.string,
inputId: PropTypes.string,
instanceId: PropTypes.oneOfType( [
PropTypes.number,
PropTypes.string,
] ),
isClearable: PropTypes.bool,
isDisabled: PropTypes.bool,
isLoading: PropTypes.bool,
isOptionDisabled: PropTypes.func,
isOptionSelected: PropTypes.func,
isMulti: PropTypes.bool,
isSearchable: PropTypes.bool,
loadingMessage: PropTypes.func,
minMenuHeight: PropTypes.number,
maxMenuHeight: PropTypes.number,
menuIsOpen: PropTypes.bool,
menuPlacement: PropTypes.oneOf( [
'auto',
'bottom',
'top',
] ),
menuPosition: PropTypes.oneOf( [
'absolute',
'fixed',
] ),
menuPortalTarget: PropTypes.element,
menuShouldBlockScroll: PropTypes.bool,
menuShouldScrollIntoView: PropTypes.bool,
name: PropTypes.string,
noOptionsMessage: PropTypes.func,
onBlur: PropTypes.func,
onChange: PropTypes.func,
onFocus: PropTypes.func,
onInputChange: PropTypes.func,
onKeyDown: PropTypes.func,
onMenuOpen: PropTypes.func,
onMenuClose: PropTypes.func,
onMenuScrollToTop: PropTypes.func,
onMenuScrollToBottom: PropTypes.func,
openMenuOnFocus: PropTypes.bool,
openMenuOnClick: PropTypes.bool,
options: PropTypes.array,
pageSize: PropTypes.number,
placeholder: PropTypes.string,
screenReaderStatus: PropTypes.func,
styles: PropTypes.shape( {
clearIndicator: PropTypes.func,
container: PropTypes.func,
control: PropTypes.func,
dropdownIndicator: PropTypes.func,
group: PropTypes.func,
groupHeading: PropTypes.func,
indicatorsContainer: PropTypes.func,
indicatorSeparator: PropTypes.func,
input: PropTypes.func,
loadingIndicator: PropTypes.func,
loadingMessageCSS: PropTypes.func,
menu: PropTypes.func,
menuList: PropTypes.func,
menuPortal: PropTypes.func,
multiValue: PropTypes.func,
multiValueLabel: PropTypes.func,
multiValueRemove: PropTypes.func,
noOptionsMessageCSS: PropTypes.func,
option: PropTypes.func,
placeholder: PropTypes.func,
singleValue: PropTypes.func,
valueContainer: PropTypes.func,
} ),
tabIndex: PropTypes.string,
tabSelectsValue: PropTypes.bool,
value: PropTypes.oneOfType( [
PropTypes.object,
PropTypes.array,
] ),
};

export const REACT_SELECT_DEFAULTS = {
isClearable: true,
isLoading: true,
placeholder: __( 'Select...', 'event_espresso' ),
};
3 changes: 3 additions & 0 deletions assets/src/components/form/select/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { ModelSelect, default as ModelEnhancedSelect } from './model-select';
export * from './model-selects';
export * from './build-options';
170 changes: 170 additions & 0 deletions assets/src/components/form/select/model-select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* External imports
*/
import Select from 'react-select';
import { Component, Fragment } from '@wordpress/element';
import { isEmpty, uniqueId, find, isUndefined } from 'lodash';
import PropTypes from 'prop-types';

/**
* WP dependencies
*/
import { withSelect } from '@wordpress/data';

/**
* Internal imports
*/
import buildOptions from './build-options';
import { MODEL_NAMES } from '../../../data/model';
import {
REACT_SELECT_DEFAULTS,
REACT_SELECT_TYPES,
} from './default-select-configuration';

/**
* ModelSelect component.
* This is a component that will generate a react-select input for a given
* model and its entities (provided via props).
*
* @see https://deploy-preview-2289--react-select.netlify.com/props#prop-types
* for options that can be passed through via the selectConfiguration prop.
*
* @param { Object } selectConfiguration An object containing options for the
* react-select component.
* @param { Array } modelEntities Array of model entities
* @param { string } modelName The name of the Model the entities
* belong to.
* @param { function } mapOptionsCallback This function will receive by
* default the modelEntities, the modelName (and any custom Map provided) and
* is expected to return an array of options to be used for the react-select
* component.
* @param { Object } optionsEntityMap If provided, it is expected to be a
* map of modelName fields to `label` and `value` keys used by
* `mapOptionsCallback`.
*/
export class ModelSelect extends Component {
static propTypes = {
selectConfiguration: PropTypes.shape( {
...REACT_SELECT_TYPES,
} ),
modelEntities: PropTypes.array,
modelName: PropTypes.oneOf( MODEL_NAMES ),
mapOptionsCallback: PropTypes.func,
optionsEntityMap: PropTypes.object,
queryData: PropTypes.shape( {
limit: PropTypes.number,
orderBy: PropTypes.string,
order: PropTypes.oneOf( [ 'asc', 'desc' ] ),
} ),
getQueryString: PropTypes.func,
selectLabel: PropTypes.string,
addAllOptionLabel: PropTypes.string,
};

static defaultProps = {
selectConfiguration: {
...REACT_SELECT_DEFAULTS,
name: uniqueId( 'model-select-' ),
},
modelEntities: [],
modelName: '',
mapOptionsCallback: buildOptions,
optionsEntityMap: null,
queryData: {
limit: 100,
order: 'desc',
},
selectLabel: '',
addAllOptionLabel: '',
};

static getDerivedStateFromProps( props ) {
const { selectConfiguration } = props;
const options = ModelSelect.getOptions( props );
const updated = {
options,
value: ModelSelect.getOptionObjectForValue(
selectConfiguration.defaultValue, options
),
};
return {
...REACT_SELECT_DEFAULTS,
...selectConfiguration,
...updated,
};
}

static getOptions( props ) {
const {
modelEntities,
modelName,
optionsEntityMap,
mapOptionsCallback,
addAllOptionLabel,
} = props;
if ( ! isEmpty( modelEntities ) ) {
return optionsEntityMap !== null ?
mapOptionsCallback(
modelEntities,
modelName,
addAllOptionLabel,
optionsEntityMap,
) :
mapOptionsCallback(
modelEntities,
modelName,
addAllOptionLabel,
);
}
return [];
}

static getOptionObjectForValue( value, options ) {
if ( ! isEmpty( options ) ) {
const match = find( options, function( option ) {
return option.value === value;
} );
return ! isUndefined( match ) ?
match :
null;
}
return {};
}

getSelectLabel() {
const { selectLabel, selectConfiguration } = this.props;
return selectLabel ?
<label htmlFor={ selectConfiguration.name }>{ selectLabel }</label> :
'';
}

render() {
return (
<Fragment>
{ this.getSelectLabel() }
<Select { ...this.state } />
</Fragment>
);
}
}

/**
* The ModelSelect Component wrapped in the `withSelect` higher order component.
* This subscribes the ModelSelect component to the state maintained via the
* eventespresso/lists store.
*/
export default withSelect( ( select, ownProps ) => {
const { getQueryString, modelName, selectConfiguration } = ownProps;
const queryString = getQueryString( ownProps.queryData );
const { getItems, isRequestingItems } = select( 'eventespresso/lists' );
return {
...ModelSelect.defaultProps,
...ownProps,
modelEntities: getItems( modelName, queryString ),
selectConfiguration: {
...ModelSelect.defaultProps.selectConfiguration,
...selectConfiguration,
isLoading: isRequestingItems( modelName, queryString ),
},
};
} )( ModelSelect );
Loading

0 comments on commit 26c214f

Please sign in to comment.