[improvement] Search: tsx (#3028)

This commit is contained in:
neverland 2019-03-21 17:20:06 +08:00 committed by GitHub
parent 60657f800f
commit 7c4908a2e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 186 additions and 106 deletions

View File

@ -1,5 +1,5 @@
{
"printWidth": 80,
"printWidth": 90,
"singleQuote": true,
"trailingComma": "none"
}

View File

@ -1,95 +0,0 @@
import { use } from '../utils';
import Field from '../field';
const [sfc, bem, t] = use('search');
export default sfc({
inheritAttrs: false,
props: {
value: String,
label: String,
showAction: Boolean,
shape: {
type: String,
default: 'square'
},
background: {
type: String,
default: '#ffffff'
}
},
computed: {
listeners() {
return {
...this.$listeners,
input: this.onInput,
keypress: this.onKeypress
};
}
},
methods: {
onInput(value) {
this.$emit('input', value);
},
onKeypress(event) {
// press enter
if (event.keyCode === 13) {
event.preventDefault();
this.$emit('search', this.value);
}
this.$emit('keypress', event);
},
onBack() {
this.$emit('input', '');
this.$emit('cancel');
},
renderLabel() {
return this.slots('label')
? this.slots('label')
: this.label && (<div class={bem('label')}>{this.label}</div>);
}
},
render(h) {
const { showAction } = this;
const props = {
attrs: this.$attrs,
on: this.listeners
};
const scopedSlots = {};
if (this.slots('left-icon')) {
scopedSlots['left-icon'] = () => this.slots('left-icon');
}
return (
<div class={bem({ 'show-action': showAction })} style={{ background: this.background }}>
<div class={bem('content', [this.shape])}>
{this.renderLabel()}
<Field
clearable
type="search"
value={this.value}
border={false}
leftIcon="search"
scopedSlots={scopedSlots}
{...props}
>
</Field>
</div>
{showAction && (
<div class={bem('action')}>
{this.slots('action') || <div onClick={this.onBack}>{t('cancel')}</div>}
</div>
)}
</div>
);
}
});

117
packages/search/index.tsx Normal file
View File

@ -0,0 +1,117 @@
import { use } from '../utils';
import { emit } from '../utils/functional';
import Field from '../field';
// Types
import { CreateElement, RenderContext } from 'vue/types';
import { DefaultSlots, ScopedSlot } from '../utils/use/sfc';
const [sfc, bem, t] = use('search');
export type SearchProps = {
shape: string;
value?: string;
label?: string;
background: string;
showAction?: boolean;
};
export type SearchSlots = DefaultSlots & {
label?: ScopedSlot;
action?: ScopedSlot;
'left-icon'?: ScopedSlot;
};
export type SearchEvents = {
onCancel?(): void;
onInput?(value: string): void;
onSearch?(value: string): void;
onKeypress?(event: KeyboardEvent): void;
};
function Search(
h: CreateElement,
props: SearchProps,
slots: SearchSlots,
ctx: RenderContext<SearchProps>
) {
const Label = () => {
if (!slots.label && !props.label) {
return null;
}
return <div class={bem('label')}>{slots.label ? slots.label() : props.label}</div>;
};
const Action = () => {
if (!props.showAction) {
return null;
}
const onCancel = () => {
emit(ctx, 'input', '');
emit(ctx, 'cancel');
};
return (
<div class={bem('action')}>
{slots.action ? slots.action() : <div onClick={onCancel}>{t('cancel')}</div>}
</div>
);
};
const fieldData = {
attrs: ctx.data.attrs,
on: {
...ctx.listeners,
input(value: string) {
emit(ctx, 'input', value);
},
keypress(event: KeyboardEvent) {
// press enter
if (event.keyCode === 13) {
event.preventDefault();
emit(ctx, 'search', props.value);
}
emit(ctx, 'keypress', event);
}
}
};
return (
<div
class={bem({ 'show-action': props.showAction })}
style={{ background: props.background }}
>
<div class={bem('content', props.shape)}>
{Label()}
<Field
clearable
type="search"
value={props.value}
border={false}
leftIcon="search"
scopedSlots={{ 'left-icon': slots['left-icon'] }}
{...fieldData}
/>
</div>
{Action()}
</div>
);
}
Search.props = {
value: String,
label: String,
showAction: Boolean,
shape: {
type: String,
default: 'square'
},
background: {
type: String,
default: '#fff'
}
};
export default sfc<SearchProps, SearchEvents, SearchSlots>(Search);

View File

@ -3,7 +3,7 @@
exports[`renders demo correctly 1`] = `
<div>
<div>
<div class="van-search" style="background:#ffffff;">
<div class="van-search" style="background:#fff;">
<div class="van-search__content van-search__content--square">
<div class="van-cell van-cell--borderless van-field">
<div class="van-field__left-icon"><i class="van-icon van-icon-search">
@ -17,7 +17,7 @@ exports[`renders demo correctly 1`] = `
</div>
<div>
<form action="/">
<div class="van-search van-search--show-action" style="background:#ffffff;">
<div class="van-search van-search--show-action" style="background:#fff;">
<div class="van-search__content van-search__content--square">
<div class="van-cell van-cell--borderless van-field">
<div class="van-field__left-icon"><i class="van-icon van-icon-search">
@ -34,7 +34,7 @@ exports[`renders demo correctly 1`] = `
</form>
</div>
<div>
<div class="van-search van-search--show-action" style="background:#ffffff;">
<div class="van-search van-search--show-action" style="background:#fff;">
<div class="van-search__content van-search__content--round">
<div class="van-search__label">地址</div>
<div class="van-cell van-cell--borderless van-field">

View File

@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render label slot 1`] = `
<div class="van-search" style="background: rgb(255, 255, 255);">
<div class="van-search__content van-search__content--square">
<div class="van-search__label">Custom Label</div>
<div class="van-cell van-cell--borderless van-field">
<div class="van-field__left-icon"><i class="van-icon van-icon-search">
<!----></i></div>
<div class="van-cell__value van-cell__value--alone">
<div class="van-field__body"><input type="search" class="van-field__control"></div>
</div>
</div>
</div>
</div>
`;

View File

@ -2,34 +2,75 @@ import Search from '..';
import { mount } from '../../../test/utils';
test('listen input event', () => {
const wrapper = mount(Search);
const onInput = jest.fn();
const wrapper = mount(Search, {
context: {
on: {
input: onInput
}
}
});
const input = wrapper.find('input');
input.element.value = '1';
input.trigger('input');
expect(wrapper.emitted('input')[0][0]).toEqual('1');
expect(onInput).toBeCalledWith('1');
});
test('cancel search', () => {
const onInput = jest.fn();
const onCancel = jest.fn();
const wrapper = mount(Search, {
propsData: {
value: 'test',
showAction: true
},
context: {
on: {
input: onInput,
cancel: onCancel
}
}
});
const cancel = wrapper.find('.van-search__action div');
cancel.trigger('click');
expect(wrapper.emitted('input')[0][0]).toEqual('');
expect(wrapper.emitted('cancel')).toBeTruthy();
expect(onInput).toBeCalledWith('');
expect(onCancel).toBeCalled();
});
test('emit a search event', () => {
const wrapper = mount(Search);
const onSearch = jest.fn();
const onKeypress = jest.fn();
const wrapper = mount(Search, {
context: {
on: {
search: onSearch,
keypress: onKeypress
}
}
});
const input = wrapper.find('input');
input.trigger('keypress.enter');
input.trigger('keypress.a');
expect(wrapper.emitted('search')).toBeTruthy();
expect(wrapper.emitted('keypress')).toBeTruthy();
expect(onSearch).toBeCalled();
expect(onKeypress).toBeCalled();
});
test('render label slot', () => {
const wrapper = mount(Search, {
scopedSlots: {
label() {
return 'Custom Label';
}
}
});
expect(wrapper).toMatchSnapshot();
});

View File

@ -6,6 +6,7 @@
"module": "esnext",
"strict": true,
"allowJs": true,
"noEmit": true,
"noImplicitThis": true,
"esModuleInterop": true,
"moduleResolution": "node"