feat(combobox): Improved change event implementation

Component's state is now updated before triggering the change event. The event handler is executed

only when actual change occurred

Autocomplete and DropDownList are strongly affected by this commit, due to changes in the List

(visible property that should be removed after implementing the Popup), ListItem and SearchBar
This commit is contained in:
Alexander Popov 2016-03-24 18:29:27 +02:00
Родитель c880a88142
Коммит 4c9595c518
7 изменённых файлов: 92 добавлений и 34 удалений

Просмотреть файл

@ -35,7 +35,6 @@ const data = [
{ text: "Malta", value: "Mal" },
{ text: "Moldova", value: "Mol" },
{ text: "Monaco", value: "Mon" },
{ text: "Montenegro", value: "Mon" },
{ text: "Netherlands", value: "Net" },
{ text: "Norway", value: "Nor" },
{ text: "Poland", value: "Pol" },

Просмотреть файл

@ -46,9 +46,12 @@ const defaultProps = {
class ComboBox extends React.Component {
constructor(props) {
super(props);
this._oldText = "";
this._oldValue = "";
this.state = {
dataItem: null,
value: this.props.value || null,
expanded: false,
value: this.props.value || "",
focused: null
};
}
@ -58,17 +61,30 @@ class ComboBox extends React.Component {
if (suggest && data.length) {
this.setState({
expanded: data.length > 0,
word: data[0][textField],
highlight: true,
focused: itemIndex(this.text, data, textField) //get focused item on filtered data
focused: itemIndex(this.text, data, textField) //filtered data focused item
});
}
}
handleChange = () => {
handleChange = (text) => {
if (this._oldText === text || this._oldValue === this.state.value) {
return;
}
this._oldText = this.state.text;
this._oldValue = this.state.value;
this.props.onChange(this.state.value);
}
handleBlur = () => {
if (!this.state.dateItem) {
this.selectFocused();
}
};
handleFilter = (word) => {
const minLength = this.props.minLength;
if (word.length >= minLength) {
@ -77,9 +93,9 @@ class ComboBox extends React.Component {
};
toggle = () => {
//this.setState({
// expanded: !this.state.expanded
// });
this.setState({
expanded: !this.state.expanded //TODO: check data.length
});
};
navigate = (keyCode) => {
@ -103,37 +119,45 @@ class ComboBox extends React.Component {
};
textUpdate = (text) => {
this.text = text;
const index = itemIndex(text, this.props.data, this.props.textField); //unfiltered data focused item
const dataItem = this.props.data[index];
this.text = text;
this.setState({
expanded: index >= 0,
text: text,
word: null,
highlight: false,
focused: itemIndex(text, this.props.data, this.props.textField) //get focused item on unfiltered data
word: dataItem && this.props.suggest ? this.props.data[index][this.props.textField] : null,
highlight: true,
focused: index
});
};
select = (dataItem) => {
select = (dataItem, index) => {
const value = dataItem ? dataItem[this.props.valueField] : this.refs.searchBar._input.value;
const text = dataItem ? dataItem[this.props.textField] : this.refs.searchBar._input.value;
this.setState({
dataItem: dataItem ? dataItem : null,
text: text,
value: value,
word: text,
highlight: false
});
highlight: false,
focused: index,
expanded: false
}, function() {
this.handleChange(value);
}.bind(this));
};
selectFocused = () => {
const focused = this.state.focused;
if (focused !== null) {
this.select(this.props.data[focused]);
this.select(this.props.data[focused], focused);
}
};
render() {
const searchBarProps = {
blur: this.handleBlur,
filter: this.handleFilter,
change: this.textUpdate,
handleChange: this.handleChange,
@ -154,6 +178,9 @@ class ComboBox extends React.Component {
const buttonProps = {
onClick: this.toggle,
onMouseDown: function(event) {
event.preventDefault();
},
className: buttonClasses,
icon: "arrow-s"
};
@ -177,7 +204,8 @@ class ComboBox extends React.Component {
renderer: this.props.itemRenderer,
onClick: this.select,
textField: this.props.textField,
valueField: this.props.valueField
valueField: this.props.valueField,
visible: this.state.expanded
};
return (
<span {...comboBoxProps}>

Просмотреть файл

@ -17,7 +17,12 @@ export default class List extends React.Component {
onClick: PropTypes.func,
selected: PropTypes.number,
textField: PropTypes.string,
valueField: PropTypes.string
value: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]),
valueField: PropTypes.string,
visible: PropTypes.bool
};
constructor(props) {
@ -31,8 +36,8 @@ export default class List extends React.Component {
}
}
clickHandler = (dataItem) => {
this.props.onClick(dataItem);
clickHandler = (dataItem, index) => {
this.props.onClick(dataItem, index);
};
renderItems() {
@ -44,6 +49,7 @@ export default class List extends React.Component {
dataItem={item}
focused={index === focused}
key={util.getter(item, valueField)}
index={index}
onClick={this.clickHandler}
renderer={itemRenderer}
selected={index === selected}
@ -55,7 +61,27 @@ export default class List extends React.Component {
render() {
const style = {
height: 140
height: 140,
display: this.props.visible ? "block" : "none",
height: this.props.height || 200,
overflowY: "scroll" //TODO: remove after popup is added
};
const {
focused,
value,
defaultItem,
textField,
itemRenderer
} = this.props;
const defaultItemProps = {
focused: focused === -1,
selected: value === undefined,
textField: textField,
dataItem: defaultItem,
onClick: this.clickHandler,
renderer: itemRenderer
};
return (

Просмотреть файл

@ -12,6 +12,7 @@ export default class ListItem extends React.Component {
PropTypes.string
]),
focused: PropTypes.bool,
index: PropTypes.number,
onClick: PropTypes.func,
renderer: PropTypes.func,
selected: PropTypes.bool,
@ -37,7 +38,7 @@ export default class ListItem extends React.Component {
};
onClick = () => {
this.props.onClick(this.props.dataItem);
this.props.onClick(this.props.dataItem, this.props.index);
};
render() {

Просмотреть файл

@ -5,6 +5,7 @@ import { caretIndex, indexOfWordAtCaret, caretSelection, textReduced, replaceWor
export default class SearchBar extends React.Component {
static propTypes = {
blur: PropTypes.func,
change: PropTypes.func,
disabled: PropTypes.bool,
filter: PropTypes.func,
@ -18,7 +19,11 @@ export default class SearchBar extends React.Component {
word: PropTypes.string
};
//TODO add defaultProps
static defaultProps = {
blur() {},
handleChange() {}
};
constructor(props) {
super(props);
@ -47,7 +52,8 @@ export default class SearchBar extends React.Component {
} else {
//only when something is chosen from the list
caretSelection(this._input, this._input.value.length);
this.props.handleChange(this._input.value);
// console.log("call handleChange from SearchBar.componentDidUpdate")
// this.props.handleChange(this._input.value);
}
} else {
//in every other case
@ -64,11 +70,8 @@ export default class SearchBar extends React.Component {
};
onBlur = () => {
this.props.blur();
window.document.removeEventListener("selectionchange", this.onSelectionChange);
//only when NOT chosen from list
if (!this.props.word || (this.props.word && this.props.highlight)) {
this.props.handleChange(this._input.value);
}
};
onKeyDown = (event) => {

Просмотреть файл

@ -50,10 +50,10 @@ describe('List', () => {
const items = result.find(ListItem);
items.at(0).shallow().simulate('click');
expect(spy).toHaveBeenCalledWith({ text: 'foo', value: 1 });
expect(spy).toHaveBeenCalledWith({ text: 'foo', value: 1 }, 0);
items.at(1).shallow().simulate('click');
expect(spy).toHaveBeenCalledWith({ text: 'bar', value: 2 });
expect(spy).toHaveBeenCalledWith({ text: 'bar', value: 2 }, 1);
});
it('should fire onClick (array of strings)', () => {
@ -62,9 +62,9 @@ describe('List', () => {
const items = result.find(ListItem);
items.at(0).shallow().simulate('click');
expect(spy).toHaveBeenCalledWith("foo");
expect(spy).toHaveBeenCalledWith("foo", 0);
items.at(1).shallow().simulate('click');
expect(spy).toHaveBeenCalledWith("bar");
expect(spy).toHaveBeenCalledWith("bar", 1);
});
});

Просмотреть файл

@ -39,8 +39,9 @@ describe('ListItem', () => {
it('should pass dataItem to the click handler', () => {
const spy = jasmine.createSpy('spy');
result = shallow(<ListItem dataItem={dataItem} onClick={spy} />);
const index = 1;
result = shallow(<ListItem dataItem={dataItem} onClick={spy} index={index}/>);
result.simulate('click');
expect(spy).toHaveBeenCalledWith({ text: 'foo', value: 1 });
expect(spy).toHaveBeenCalledWith({ text: 'foo', value: 1 }, index);
});
});