update(components): Add support to create new items inside EditableList

- Adds logic to allow simple item creation
- Adds new onItemCreated callback
- Updates specs
This commit is contained in:
Juan Tejada 2015-12-07 12:38:38 -08:00
parent 68a0a81f77
commit 31796e396d
3 changed files with 87 additions and 8 deletions

View file

@ -38,6 +38,37 @@ describe('EditableList', ()=> {
}); });
}); });
describe('_onCreateInputKeyDown', ()=> {
it('calls onItemCreated', ()=> {
const onItemCreated = jasmine.createSpy('onItemCreated');
const list = makeList(['1', '2'], {initialState: {creatingItem: true}, onItemCreated});
const createItem = findRenderedDOMComponentWithClass(list, 'create-item');
const input = findRenderedDOMComponentWithTag(createItem, 'input');
findDOMNode(input).value = 'New Item';
Simulate.keyDown(input, {key: 'Enter'});
expect(onItemCreated).toHaveBeenCalledWith('New Item');
});
});
describe('_onCreateItem', ()=> {
it('should call prop callback when provided', ()=> {
const onCreateItem = jasmine.createSpy('onCreateItem');
const list = makeList(['1', '2'], {onCreateItem});
list._onCreateItem();
expect(onCreateItem).toHaveBeenCalled();
});
it('should set state for creating item when no callback provided', ()=> {
const list = makeList(['1', '2']);
spyOn(list, 'setState');
list._onCreateItem();
expect(list.setState).toHaveBeenCalledWith({creatingItem: true});
});
});
describe('_renderItem', ()=> { describe('_renderItem', ()=> {
const makeItem = (item, idx, state = {}, handlers = {})=> { const makeItem = (item, idx, state = {}, handlers = {})=> {
const list = makeList(); const list = makeList();
@ -109,18 +140,26 @@ describe('EditableList', ()=> {
const innerList = findDOMNode( const innerList = findDOMNode(
findRenderedDOMComponentWithClass(list, 'items-wrapper') findRenderedDOMComponentWithClass(list, 'items-wrapper')
); );
expect(()=> {
findRenderedDOMComponentWithClass(list, 'create-item');
}).toThrow();
expect(innerList.childNodes.length).toEqual(3); expect(innerList.childNodes.length).toEqual(3);
items.forEach((item, idx)=> expect(innerList.childNodes[idx].textContent).toEqual(item)); items.forEach((item, idx)=> expect(innerList.childNodes[idx].textContent).toEqual(item));
}); });
it('renders create input as an item when creating', ()=> {
const items = ['1', '2', '3'];
const list = makeList(items, {initialState: {creatingItem: true}});
const createItem = findRenderedDOMComponentWithClass(list, 'create-item');
expect(createItem).toBeDefined();
});
it('renders add button', ()=> { it('renders add button', ()=> {
const onCreateItem = jasmine.createSpy('onCreateItem'); const list = makeList();
const list = makeList([], {onCreateItem});
const button = scryRenderedDOMComponentsWithClass(list, 'btn-editable-list')[0]; const button = scryRenderedDOMComponentsWithClass(list, 'btn-editable-list')[0];
Simulate.click(button); expect(findDOMNode(button).textContent).toEqual('+');
expect(onCreateItem).toHaveBeenCalled();
}); });
it('renders delete button', ()=> { it('renders delete button', ()=> {
@ -130,6 +169,7 @@ describe('EditableList', ()=> {
Simulate.click(button); Simulate.click(button);
expect(findDOMNode(button).textContent).toEqual('—');
expect(onDeleteItem).toHaveBeenCalledWith('2', 1); expect(onDeleteItem).toHaveBeenCalledWith('2', 1);
}); });
}); });

View file

@ -16,15 +16,16 @@ class EditableList extends Component {
onDeleteItem: PropTypes.func, onDeleteItem: PropTypes.func,
onItemEdited: PropTypes.func, onItemEdited: PropTypes.func,
onItemSelected: PropTypes.func, onItemSelected: PropTypes.func,
onItemCreated: PropTypes.func,
initialState: PropTypes.object, initialState: PropTypes.object,
} }
static defaultProps = { static defaultProps = {
children: [], children: [],
onCreateItem: ()=> {},
onDeleteItem: ()=> {}, onDeleteItem: ()=> {},
onItemEdited: ()=> {}, onItemEdited: ()=> {},
onItemSelected: ()=> {}, onItemSelected: ()=> {},
onItemCreated: ()=> {},
} }
constructor(props) { constructor(props) {
@ -34,6 +35,7 @@ class EditableList extends Component {
this.state = props.initialState || { this.state = props.initialState || {
editing: null, editing: null,
selected: null, selected: null,
creatingItem: false,
}; };
} }
@ -54,6 +56,13 @@ class EditableList extends Component {
} }
} }
_onCreateInputKeyDown = (event)=> {
if (_.includes(['Enter', 'Return'], event.key)) {
this.setState({creatingItem: false});
this.props.onItemCreated(event.target.value);
}
}
_onItemClick = (event, item, idx)=> { _onItemClick = (event, item, idx)=> {
this._selectItem(item, idx); this._selectItem(item, idx);
} }
@ -83,7 +92,11 @@ class EditableList extends Component {
} }
_onCreateItem = ()=> { _onCreateItem = ()=> {
this.props.onCreateItem(); if (this.props.onCreateItem) {
this.props.onCreateItem();
} else {
this.setState({creatingItem: true});
}
} }
_onDeleteItem = ()=> { _onDeleteItem = ()=> {
@ -137,7 +150,23 @@ class EditableList extends Component {
); );
} }
_renderCreateItem = (onCreateInputKeyDown = this._onCreateInputKeyDown)=> {
return (
<div className="create-item">
<input
autoFocus
type="text"
onKeyDown={onCreateInputKeyDown} />
</div>
);
}
render() { render() {
let items = this._items.map((item, idx)=> this._renderItem(item, idx));
if (this.state.creatingItem === true) {
items = items.concat(this._renderCreateItem());
}
return ( return (
<div className={`nylas-editable-list ${this.props.className}`}> <div className={`nylas-editable-list ${this.props.className}`}>
<div <div
@ -145,7 +174,7 @@ class EditableList extends Component {
tabIndex="1" tabIndex="1"
onBlur={this._onListBlur} onBlur={this._onListBlur}
onKeyDown={this._onListKeyDown}> onKeyDown={this._onListKeyDown}>
{this._items.map((item, idx)=> this._renderItem(item, idx))} {items}
</div> </div>
<div className="buttons-wrapper"> <div className="buttons-wrapper">
<button className="btn btn-small btn-editable-list" onClick={this._onCreateItem}>+</button> <button className="btn btn-small btn-editable-list" onClick={this._onCreateItem}>+</button>

View file

@ -31,6 +31,16 @@
color: @text-color-inverse-very-subtle; color: @text-color-inverse-very-subtle;
} }
} }
.create-item {
padding: @padding-small-vertical @padding-small-horizontal;
border-top: 1px solid @border-color-divider;
input {
border: none;
padding: 0;
font-size: inherit;
}
}
} }
.buttons-wrapper { .buttons-wrapper {