Skip to content

Commit

Permalink
Merge pull request #113 from creative-commoners/pulls/master/reactify…
Browse files Browse the repository at this point in the history
…-tags

Update module to use react-select
  • Loading branch information
robbieaverill authored Jul 17, 2018
2 parents bf6664e + 2067a3b commit c18690b
Show file tree
Hide file tree
Showing 20 changed files with 8,914 additions and 55 deletions.
3 changes: 2 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[{*.yml,package.json}]
[{*.yml,package.json,*.js,*.scss}]
indent_size = 2
indent_style = space

# The indent size used in the package.json file cannot be changed:
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@silverstripe/eslint-config/.eslintrc');
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
composer.lock
vendor
framework
node_modules/
/**/*.js.map
/**/*.css.map
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ dist: trusty
env:
global:
- COMPOSER_ROOT_VERSION=2.0.x-dev
- TRAVIS_NODE_VERSION="6"

matrix:
include:
Expand All @@ -14,6 +15,8 @@ matrix:
env: DB=PGSQL PHPUNIT_TEST=1
- php: 7.1
env: DB=MYSQL PHPUNIT_COVERAGE_TEST=1
- php: 7.1
env: DB=MYSQL NPM_TEST=1

before_script:
- phpenv rehash
Expand All @@ -24,10 +27,17 @@ before_script:
- if [[ $DB == PGSQL ]]; then composer require --prefer-dist --no-update silverstripe/postgresql:2.0.x-dev; fi
- composer install --prefer-dist --no-interaction --no-progress --no-suggest --optimize-autoloader --verbose --profile

# Install NPM dependencies
- if [[ $NPM_TEST ]]; then nvm install $TRAVIS_NODE_VERSION && nvm use $TRAVIS_NODE_VERSION && npm install -g yarn && yarn install --network-concurrency 1 && (cd vendor/silverstripe/admin && yarn install --network-concurrency 1) && yarn run build; fi

script:
- if [[ $PHPUNIT_TEST ]]; then vendor/bin/phpunit; fi
- if [[ $PHPUNIT_COVERAGE_TEST ]]; then phpdbg -qrr vendor/bin/phpunit --coverage-clover=coverage.xml; fi
- if [[ $PHPCS_TEST ]]; then vendor/bin/phpcs --standard=vendor/silverstripe/framework/phpcs.xml.dist src/ tests/; fi
- if [[ $NPM_TEST ]]; then git diff-files --quiet -w --relative=client; fi
- if [[ $NPM_TEST ]]; then git diff --name-status --relative=client; fi
- if [[ $NPM_TEST ]]; then yarn run test; fi
- if [[ $NPM_TEST ]]; then yarn run lint; fi

after_success:
- if [[ $PHPUNIT_COVERAGE_TEST ]]; then bash <(curl -s https://codecov.io/bash) -f coverage.xml; fi
1 change: 1 addition & 0 deletions client/dist/js/bundle.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions client/dist/styles/bundle.css

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

6 changes: 6 additions & 0 deletions client/src/boot/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* global window */
import registerComponents from 'boot/registerComponents';

window.document.addEventListener('DOMContentLoaded', () => {
registerComponents();
});
8 changes: 8 additions & 0 deletions client/src/boot/registerComponents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Injector from 'lib/Injector';
import TagField from 'components/TagField';

export default () => {
Injector.component.registerMany({
TagField,
});
};
2 changes: 2 additions & 0 deletions client/src/bundles/bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require('legacy/entwine/TagField.js');
require('boot');
107 changes: 107 additions & 0 deletions client/src/components/TagField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React, { Component, PropTypes } from 'react';
import Select from 'react-select';
import fetch from 'isomorphic-fetch';
import url from 'url';

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

this.state = {
value: props.value,
};

this.onChange = this.onChange.bind(this);
this.getOptions = this.getOptions.bind(this);
}

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

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

getOptions(input) {
const { lazyLoad, options, optionUrl, labelKey, valueKey } = this.props;

if (!lazyLoad) {
return Promise.resolve({ options });
}

if (!input) {
return Promise.resolve({ options: [] });
}

const fetchURL = url.parse(optionUrl, true);
fetchURL.query.term = input;

return fetch(url.format(fetchURL), { credentials: 'same-origin' })
.then((response) => response.json())
.then((json) => ({
options: json.items.map(item => ({
[labelKey]: item.id,
[valueKey]: item.text,
}))
}));
}

render() {
const {
lazyLoad,
options,
creatable,
...passThroughAttributes
} = this.props;

const optionAttributes = lazyLoad
? { loadOptions: this.getOptions }
: { options };

let SelectComponent = Select;
if (lazyLoad && creatable) {
SelectComponent = Select.AsyncCreatable;
} else if (lazyLoad) {
SelectComponent = Select.Async;
} else if (creatable) {
SelectComponent = Select.Creatable;
}

passThroughAttributes.value = this.state.value;

return (
<SelectComponent
{...passThroughAttributes}
onChange={this.onChange}
inputProps={{ className: 'no-change-track' }}
{...optionAttributes}
/>
);
}
}

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,
disabled: PropTypes.bool,
options: PropTypes.arrayOf(PropTypes.object),
optionUrl: PropTypes.string,
value: PropTypes.any,
onChange: PropTypes.func,
onBlur: PropTypes.func,
};

TagField.defaultProps = {
labelKey: 'Title',
valueKey: 'Value',
disabled: false
};

export default TagField;
5 changes: 5 additions & 0 deletions client/src/components/TagField.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import "~react-select/dist/react-select.css";

.ss-tag-field .Select--multi .Select-value {
margin-top: 3px;
}
81 changes: 81 additions & 0 deletions client/src/components/tests/TagField-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-disable import/no-extraneous-dependencies */
/* global jest, describe, beforeEach, it, expect, setTimeout, document */

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 Select from 'react-select';
import fetch from 'isomorphic-fetch';

Enzyme.configure({ adapter: new Adapter() });

describe('TagField', () => {
let props;

beforeEach(() => {
props = {
name: 'Test',
labelKey: 'label',
valueKey: 'value',
lazyLoad: false,
creatable: false,
multi: true,
};
});

describe('should render a Select component with type', () => {
it('Select', () => {
const wrapper = shallow(
<TagField {...props} />
);
expect(wrapper.find(Select).length).toBe(1);
});
it('Select.Creatable with creatable option', () => {
props.creatable = true;
const wrapper = shallow(
<TagField {...props} />
);
expect(wrapper.find(Select.Creatable).length).toBe(1);
});
it('Select.Async with lazyLoad option', () => {
props.lazyLoad = true;
const wrapper = shallow(
<TagField {...props} />
);
expect(wrapper.find(Select.Async).length).toBe(1);
});
it('Select.AsyncCreatable with both creatable and lazyLoad options', () => {
props.creatable = true;
props.lazyLoad = true;
const wrapper = shallow(
<TagField {...props} />
);
expect(wrapper.find(Select.AsyncCreatable).length).toBe(1);
});
});

describe('with lazyLoad on and given a URL', () => {
let wrapper;

beforeEach(() => {
props.lazyLoad = true;
props.optionUrl = 'localhost/some-fetch-url';

wrapper = shallow(
<TagField {...props} />
);

fetch.mockImplementation(() => Promise.resolve({
json: () => ({}),
}));
});

it('should fetch the URL for results', () => {
wrapper.instance().getOptions('a');
expect(fetch).toBeCalledWith('localhost/some-fetch-url?term=a', expect.anything());
});
});
});
33 changes: 33 additions & 0 deletions client/src/legacy/entwine/TagField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* global window */
import React from 'react';
import ReactDOM from 'react-dom';
import { loadComponent } from 'lib/Injector';

window.jQuery.entwine('ss', ($) => {
$('.js-injector-boot .ss-tag-field').entwine({
onmatch() {
const cmsContent = this.closest('.cms-content').attr('id');
const context = (cmsContent)
? { context: cmsContent }
: {};
const TagField = loadComponent('TagField', context);
const dataSchema = {
...this.data('schema'),
onBlur: () => {
this.parents('.cms-edit-form:first').trigger('change');
}
};

ReactDOM.render(
<TagField
{...dataSchema}
/>,
this[0]
);
},

onunmatch() {
ReactDOM.unmountComponentAtNode(this[0]);
}
});
});
5 changes: 5 additions & 0 deletions client/src/styles/bundle.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Import core variables
@import "variables";

// Components
@import '../components/TagField';
5 changes: 2 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "silverstripe/tagfield",
"description": "Tag field for Silverstripe",
"description": "Tag field for SilverStripe",
"license": "BSD-3-Clause",
"type": "silverstripe-vendormodule",
"keywords": [
Expand Down Expand Up @@ -37,8 +37,7 @@
"dev-master": "2.x-dev"
},
"expose": [
"css",
"js"
"client/dist"
]
},
"minimum-stability": "dev",
Expand Down
Loading

0 comments on commit c18690b

Please sign in to comment.