tagsSection
React声明函数组件的类型为React.FunctionComponent, 可简写为 React.FC
useState在<>中指定类型
const TagsSection: React.FC = () => {
const [tags, setTags] = useState<string[]>(['衣','食', '住', '行'])
const [selectedTags, setSelectedTags] = useState<string[]>([])
const onCreateTag = () => {
const tag = window.prompt('请输入标签名')
if(tag !== null) {
setTags([...tags, tag])
}
}
const onToggleTags = (tag:string) => {
const index = selectedTags.indexOf(tag)
if (index >= 0) {
setSelectedTags(selectedTags.filter(t => t !== tag))
} else {
setSelectedTags([...selectedTags, tag])
}
}
return (
<Wrapper>
<ol>
{tags.map(tag =>
<li key={tag}
onClick={()=>{onToggleTags(tag)}}
className={selectedTags.indexOf(tag) >= 0 ? 'selected' : ''}>
{tag}
</li>)}
</ol>
<button onClick={onCreateTag}>新增标签</button>
</Wrapper>
)
}
export {TagsSection}
NoteSection
受控组件:state与input组件的value绑定,由onChange事件更新状态的值
const NoteSection = () => {
const [note, setNote] = useState('')
return (
<Wrapper>
<label>
<span>备注</span>
<input type="text"
placeholder="在这里输入备注"
value={note}
onChange={(e)=>{setNote(e.target.value)}}/>
</label>
</Wrapper>
)
}
修改为非受控组件:并不实时更新状态,通过ref在需要的时候获取数据更新状态
const NoteSection = () => {
const [note, setNote] = useState('')
const noteRef = useRef<HTMLInputElement>(null)
const getNote = () => {
if(noteRef.current !== null)
setNote(noteRef.current.value);
}
return (
<Wrapper>
<label>
<span>备注</span>
<input type="text"
placeholder="在这里输入备注"
defaultValue={note}
ref={noteRef}
onBlur={getNote}
/>
</label>
</Wrapper>
)
}
非受控组件相当于vue.js中的.lazy修饰符
<input v-model.lazy="msg">
React onChange的时机
HTML的onchange是在鼠标移走时才触发,但是早于onblur
React的onChange是在输入时就触发
CategorySection
const CategorySection = () => {
const [category, setCategory] = useState('-')
return (
<Wrapper>
<ul>
<li className={category === '-' ? 'selected' : ''}
onClick={()=> setCategory('-')}>支出</li>
<li className={category === '+' ? 'selected' : ''}
onClick={()=> setCategory('+')}>收入</li>
</ul>
</Wrapper>
)
}
优化
const CategorySection = () => {
const categoryMap = {'-': '支出', '+': '收入'}
type Keys = keyof typeof categoryMap
const categoryList: Keys[] = ['-', '+']
const [category, setCategory] = useState('-')
return (
<Wrapper>
<ul>
{categoryList.map(c =>
<li className={category === c ? 'selected' : ''}
onClick={()=> setCategory(c)}
key={c}>
{categoryMap[c]}
</li>)}
</ul>
</Wrapper>
)
}
NumberPad
使用事件委托监听button按钮的点击事件
const NumberPadSection = () => {
const [output, setOutput] = useState('0')
const onButtonClick = (e: React.MouseEvent) => {
console.log((e.target as HTMLButtonElement).textContent);
}
return (
<Wrapper>
<div className="output">{output}</div>
<div className="pad clearfix" onClick={onButtonClick}>
<button>1</button>
<button>2</button>
<button>3</button>
<button>删除</button>
<button>4</button>
<button>5</button>
<button>6</button>
<button>清空</button>
<button>7</button>
<button>8</button>
<button>9</button>
<button className="ok">OK</button>
<button>.</button>
<button className="zero">0</button>
</div>
</Wrapper>
)
}
输入功能的实现
const NumberPadSection = () => {
const [output, _setOutput] = useState('0')
const setOutput = (output: string) => {
if (output.length > 16) {
output = output.slice(0, 16)
} else if (output.length === 0) {
output = '0'
}
_setOutput(output)
}
const onButtonClick = (e: React.MouseEvent) => {
const text = (e.target as HTMLButtonElement).textContent;
if (text === null) return;
if (text === 'OK') {
// TODO
return;
}
if ('0123456789.'.split('').concat(['删除', '清空']).indexOf(text) >= 0) {
setOutput(generateOutput(text, output));
}
};
const generateOutput = (text: string, output = '0') => {
switch (text) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
if (output === '0') {
return text;
} else {
return (output + text);
}
case '.':
if (output.indexOf('.') > 0) {
return output;
}
return (output + '.');
case '删除':
if (output.length === 1) {
return '0';
} else {
return output.slice(0, -1);
}
case '清空':
return '0';
default:
return '';
}
};
export default generateOutput;
收集四个组件的数据
tagsSection
function Money() {
const [selected, setSelected] = useState({
tags: [] as string[],
category: '-' as Category,
note: '',
amount: 0
})
return (
<MyLayout>
<TagsSection value={selected.tags}
onChange={(tags: string[]) => setSelected({...selected, tags: tags})}/>
<NoteSection />
<CategorySection />
<NumberPadSection />
</MyLayout>
);
}
type Props = {
value: string[]
onChange: (tags: string[]) => void
}
const TagsSection: React.FC<Props> = (props) => {
const [tags, setTags] = useState<string[]>(['衣','食', '住', '行'])
const selectedTags = props.value
const onChange = props.onChange
const onCreateTag = () => {
const tag = window.prompt('请输入标签名')
if(tag !== null) {
setTags([...tags, tag])
}
}
const onToggleTags = (tag:string) => {
const index = selectedTags.indexOf(tag)
if (index >= 0) {
onChange(selectedTags.filter(t => t !== tag))
} else {
onChange([...selectedTags, tag])
}
}
return (
<Wrapper>
<ol>
{tags.map(tag => <li key={tag}
onClick={()=>{onToggleTags(tag)}}
className={selectedTags.indexOf(tag) >= 0 ? 'selected' : ''}>{tag}</li>)}
</ol>
<button onClick={onCreateTag}>新增标签</button>
</Wrapper>
)
}
Partial
在typescript中,使用Partial<>语法,使一个类型中的属性都变成可选的
const [selected, setSelected] = useState({
tags: [] as string[],
category: '-' as Category,
note: '',
amount: 0
})
const onChange = (obj: Partial<typeof selected>) => {
setSelected({
...selected,
...obj
})
}