Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setup TagField to work within AssetAdmin (Fixes #107) #120

Merged
merged 6 commits into from
Jul 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
6
4 changes: 4 additions & 0 deletions .ss-storybook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
src: 'client/src',
fileMatcher: /(\/bundle\.scss|[A-Za-z]-story\.jsx?)$/,
};
5 changes: 5 additions & 0 deletions _config/admin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
SilverStripe\Admin\LeftAndMain:
extra_requirements_css:
- 'silverstripe/tagfield:client/dist/styles/bundle.css'
extra_requirements_javascript:
- 'silverstripe/tagfield:client/dist/js/bundle.js'
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client/dist/styles/bundle.css

Large diffs are not rendered by default.

101 changes: 77 additions & 24 deletions client/src/components/TagField.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
import React, { Component } from 'react';
import Select from 'react-select';
import fetch from 'isomorphic-fetch';
import fieldHolder from 'components/FieldHolder/FieldHolder';
import url from 'url';
import debounce from 'debounce-promise';
import PropTypes from 'prop-types';


class TagField extends Component {
constructor(props) {
super(props);

this.state = {
value: props.value,
};
if (!this.isControlled()) {
this.state = {
value: props.value,
};
}

this.onChange = this.onChange.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleOnBlur = this.handleOnBlur.bind(this);
this.getOptions = this.getOptions.bind(this);
this.fetchOptions = debounce(this.fetchOptions, 500);
}

onChange(value) {
this.setState({
value
});

if (typeof this.props.onChange === 'function') {
this.props.onChange(value);
}
}

/**
* Get the options that should be shown to the user for this tagfield, optionally filtering by the
* given string input
*
* @param {string} input
* @return {Promise<Array<Object>>|Promise<{options: Array<Object>}>}
*/
getOptions(input) {
const { lazyLoad, options } = this.props;

Expand All @@ -43,6 +43,49 @@ class TagField extends Component {
return this.fetchOptions(input);
}

/**
* Handle a change, either calling the change handler provided (if controlled) or updating
* internal state of this component
*
* @param {string} value
*/
handleChange(value) {
if (this.isControlled()) {
this.props.onChange(value);
return;
}

this.setState({
value
});
}

/**
* Determine if this input should be "controlled" or not. Controlled inputs should rely on their
* value coming from props and a change handler provided to update the state stored elsewhere.
* This is specifically the case for use with `redux-form`.
*
* @return {boolean}
*/
isControlled() {
return typeof this.props.onChange === 'function';
}

/**
* Required to prevent TagField being cleared on blur
*
* @link https://github.com/JedWatson/react-select/issues/805
*/
handleOnBlur() {

}

/**
* Initiate a request to fetch options, optionally using the given string as a filter.
*
* @param {string} input
* @return {Promise<{options: Array<Object>}>}
*/
fetchOptions(input) {
const { optionUrl, labelKey, valueKey } = this.props;
const fetchURL = url.parse(optionUrl, true);
Expand All @@ -52,8 +95,8 @@ class TagField extends Component {
.then((response) => response.json())
.then((json) => ({
options: json.items.map(item => ({
[labelKey]: item.id,
[valueKey]: item.text,
[labelKey]: item.Title,
[valueKey]: item.Value,
}))
}));
}
Expand All @@ -79,12 +122,17 @@ class TagField extends Component {
SelectComponent = Select.Creatable;
}

passThroughAttributes.value = this.state.value;
// Update the value to passthrough with the kept state provided this component is not
// "controlled"
if (!this.isControlled()) {
passThroughAttributes.value = this.state.value;
}

return (
<SelectComponent
{...passThroughAttributes}
onChange={this.onChange}
onChange={this.handleChange}
onBlur={this.handleOnBlur}
inputProps={{ className: 'no-change-track' }}
{...optionAttributes}
/>
Expand All @@ -96,9 +144,9 @@ TagField.propTypes = {
name: PropTypes.string.isRequired,
labelKey: PropTypes.string.isRequired,
valueKey: PropTypes.string.isRequired,
lazyLoad: PropTypes.bool.isRequired,
creatable: PropTypes.bool.isRequired,
multi: PropTypes.bool.isRequired,
lazyLoad: PropTypes.bool,
creatable: PropTypes.bool,
multi: PropTypes.bool,
disabled: PropTypes.bool,
options: PropTypes.arrayOf(PropTypes.object),
optionUrl: PropTypes.string,
Expand All @@ -110,7 +158,12 @@ TagField.propTypes = {
TagField.defaultProps = {
labelKey: 'Title',
valueKey: 'Value',
disabled: false
disabled: false,
lazyLoad: false,
creatable: false,
multi: false,
};

export default TagField;
export { TagField as Component };

export default fieldHolder(TagField);
2 changes: 1 addition & 1 deletion client/src/components/TagField.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@import "~react-select/dist/react-select.css";
@import "~react-select/scss/default";

.ss-tag-field .Select--multi .Select-value {
margin-top: 3px;
Expand Down
36 changes: 36 additions & 0 deletions client/src/components/tests/TagField-story.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { Component as TagField } from '../TagField';

storiesOf('TagField/TagField', module)
.addDecorator(storyFn => (
<div style={{ width: '250px' }} className="ss-tag-field">
{storyFn()}
</div>
))
.add('Simple Example', () => (
<TagField
name="test"
options={[
{ Title: 'One', Value: 1 },
{ Title: 'Two', Value: 2 },
{ Title: 'Three', Value: 3 },
{ Title: 'Four', Value: 4 },
{ Title: 'Five', Value: 5 },
]}
/>
))
.add('Multiple Selection', () => (
<TagField
name="test"
multi
options={[
{ Title: 'One', Value: 1 },
{ Title: 'Two', Value: 2 },
{ Title: 'Three', Value: 3 },
{ Title: 'Four', Value: 4 },
{ Title: 'Five', Value: 5 },
]}
/>
))
;
2 changes: 1 addition & 1 deletion client/src/components/tests/TagField-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ jest.mock('isomorphic-fetch');
import React from 'react';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-15.4';
import TagField from '../TagField';
import { Component as TagField } from '../TagField';
import Select from 'react-select';
import fetch from 'isomorphic-fetch';

Expand Down
3 changes: 2 additions & 1 deletion client/src/legacy/entwine/TagField.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import ReactDOM from 'react-dom';
import { loadComponent } from 'lib/Injector';

window.jQuery.entwine('ss', ($) => {
$('.js-injector-boot .ss-tag-field').entwine({
$('.js-injector-boot .ss-tag-field.entwine').entwine({
onmatch() {
const cmsContent = this.closest('.cms-content').attr('id');
const context = (cmsContent)
Expand All @@ -20,6 +20,7 @@ window.jQuery.entwine('ss', ($) => {

ReactDOM.render(
<TagField
noHolder
{...dataSchema}
/>,
this[0]
Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,15 @@
]
},
"devDependencies": {
"@silverstripe/eslint-config": "^0.0.4",
"@silverstripe/webpack-config": "^0.8.0",
"babel-jest": "^20.0.3",
"@silverstripe/eslint-config": "^0.0.5",
"@silverstripe/webpack-config": "^0.10.0",
"@storybook/react": "^3.2.19",
"babel-jest": "^23.6.0",
"copy-webpack-plugin": "^4.2.0",
"enzyme": "^3.3.0",
"enzyme-adapter-react-15.4": "^1.0.5",
"html-loader": "^0.5.1",
"jest-cli": "^19.0.2",
"jest-cli": "^23.6.0",
"react-addons-test-utils": "15.3.1",
"webpack": "^2"
},
Expand All @@ -77,7 +78,7 @@
"prop-types": "^15.7.2",
"react": "15.3.1",
"react-dom": "15.3.1",
"react-select": "^1.0.0-rc.5",
"react-select": "^1.2.1",
"url": "^0.11.0"
}
}
Loading