npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-icecream-form

v1.1.0

Published

Integration for react-icecream & formstate-x.

Downloads

89

Readme

react-icecream-form

集成 formstate-x & 部分交互逻辑的表单

react-icecream-form 在 react-icecream 的基础上:

  1. 提供 Form (& ModalFormDrawerForm) 的封装后版本,内置了表单提交的校验逻辑
  2. 提供 FormItem 的封装后版本,可以省略校验状态信息或 state 实例
  3. 提供各输入组件(TextInputSelect 等)的封装后版本,接受直接传入 FieldState 实例
  4. 提供了用于构造表单状态的 hook useFormstateX

如 3 中提到的,react-icecream-form 模块对所有 react-icecream 内置的输入组件进行了适配,使其支持直接传入 FieldState 实例,而不是通过 value / onChange 来绑定输入;除此之外,适配后的输入组件可以被 react-icecream-form(如 2 中所述)提供的 FormItem 感知到,因此在搭配使用 FormItem 与输入组件时(二者一一对应),可以省略将 FieldState 实例手动传递给 FormItem 的行为;即可以写为:

import { FormItem, TextInput } from 'react-icecream-form'

// 这里 `FormItem` 会自动去使用内容中仅有的那个输入组件(`TextInput`)所对应的 state
<FormItem>
  <TextInput state={state} />
</FormItem>

简单的字段输入场景

import React, { useEffect } from 'react'
import { reaction } from 'mobx'
import { observer } from 'mobx-react'
import { FieldState, Validator } from 'formstate-x'
import { TextInput, useFormstateX } from 'react-icecream-form'

export default observer(() => {
  const nameState = useFormstateX(() => new FieldState('').withValidator(notEmpty), [])

  useEffect(() => reaction(
    () => nameState.error,
    error => error && console.warn('Error:', error)
  ), [nameState])

  return (
    <>
      <p style={{ margin: '0 0 1em' }}>Plz input your name: </p>
      <TextInput state={nameState} />
      {nameState.value && <p>Hello {nameState.value}!</p>}
    </>
  )
})

function notEmpty(v: string) {
  return v.trim() === '' && 'empty input'
}

较复杂表单实现

import React, { useCallback } from 'react'
import { FormState, FieldState, ValueOf } from 'formstate-x'
import { observer } from 'mobx-react'
import { Form, FormItem, useFormstateX, TextInput, PasswordInput, RadioGroup, Radio, Checkbox, MultiSelect, SelectOption } from 'react-icecream-form'

type AccountType = 'default' | 'enterprise'

// 构造表单状态
function createFormState() {
  const accountTypeField = new FieldState<AccountType>('default')
  const usernameField = new FieldState('').withValidator(notEmpty)
  const passwordField = new FieldState('').withValidator(notEmpty)
  const password2Field = new FieldState('').withValidator(
    notEmpty,
    (v: string) => v !== passwordField.value && '密码不一致'
  )
  const productsField = new FieldState<string[]>([]).withValidator(
    products => productsRequired(accountTypeField.value) && products.length === 0 && '不可为空'
  )
  const agreedField = new FieldState(false).withValidator(
    agreed => !agreed && '请勾选'
  )
  return new FormState({
    accountType: accountTypeField,
    username: usernameField,
    password: passwordField,
    password2: password2Field,
    products: productsField,
    agreed: agreedField
  })
}

// 表单中值的类型
type Value = ValueOf<ReturnType<typeof createFormState>>

export default observer(() => {

  const state = useFormstateX(createFormState, [])

  const handleSubmit = useCallback((value: Value) => {
    console.log(`Submit with: ${JSON.stringify(value)}.`)
    return new Promise<void>(resolve => setTimeout(resolve, 2000))
  }, [])

  const handleCancel = useCallback(() => (
    console.log('Cancelled.')
  ), [])

  return (
    <Form
      state={state}
      layout="horizontal"
      labelWidth="4em"
      onSubmit={handleSubmit}
      onCancel={handleCancel}
    >
      <FormItem label="账户类型" labelVerticalAlign="text" tip="企业账号需要额外选择“开通产品”">
        <RadioGroup state={state.$.accountType}>
          <Radio value="default">普通账号</Radio>
          <Radio value="enterprise">企业账号</Radio>
        </RadioGroup>
      </FormItem>
      <FormItem label="用户名" required>
        <TextInput placeholder="请输入用户名" state={state.$.username} />
      </FormItem>
      <FormItem label="密码" required>
        <PasswordInput placeholder="请输入密码" state={state.$.password} />
      </FormItem>
      <FormItem label="确认密码" required tip="两次输入密码应当一致">
        <PasswordInput placeholder="请再次输入密码" state={state.$.password2} />
      </FormItem>
      {productsRequired(state.$.accountType.value) && (
        <FormItem required label="开通产品">
          <MultiSelect
            placeholder="请选择需要开通的产品"
            state={state.$.products}
            collapsed={false}
            style={{ width: '240px' }}
          >
            <SelectOption value="kodo">对象存储 Kodo</SelectOption>
            <SelectOption value="fusion">CDN</SelectOption>
            <SelectOption value="dora">智能多媒体服务 Dora</SelectOption>
            <SelectOption value="qvm">云主机服务 QVM</SelectOption>
            <SelectOption value="pandora">机器数据分析平台 Pandora</SelectOption>
          </MultiSelect>
        </FormItem>
      )}
      <FormItem label="">
        <label style={{ color: '#999999' }}>
          <Checkbox state={state.$.agreed} />&nbsp;
          <small>我已阅读并同意服务协议和隐私政策</small>
        </label>
      </FormItem>
    </Form>
  )
})

function productsRequired(accountType: AccountType) {
  return accountType === 'enterprise'
}

function notEmpty(v: string) {
  return v.trim() === '' && '不可为空'
}

列表输入与校验

import React, { useCallback } from 'react'
import { FormState, ArrayFormState, FieldState, ValueOf } from 'formstate-x'
import { observer } from 'mobx-react'
import { Tooltip } from 'react-icecream'
import { AddThinIcon, CloseThinIcon } from 'react-icecream/icons'
import { Form, FormItem, useFormstateX, TextInput } from 'react-icecream-form'

// 构造单条手机号对应的表单状态
function createNumberState(initialValue: string) {
  return new FieldState(initialValue).withValidator(validateNumber)
}

// 构造手机号列表对应的状态
function createNumbersState() {
  return new ArrayFormState([''], createNumberState).withValidator(
    numbers => numbers.length > 3 && '最多添加 3 个号码'
  )
}

// 构造表单状态
function createFormState() {
  return new FormState({
    numbers: createNumbersState()
  })
}

// 表单中值的类型
type Value = ValueOf<ReturnType<typeof createFormState>>

export default observer(() => {
  const state = useFormstateX(createFormState, [])
  const numbersState = state.$.numbers

  const handleAddNumber = () => numbersState.append('')
  const handleRemoveNumber = (i: number) => numbersState.remove(i)

  const handleSubmit = (value: Value) => {
    console.log(`Submit with: ${JSON.stringify(value)}.`)
    return new Promise<void>(resolve => setTimeout(resolve, 2000))
  }

  const numbersLabel = (
    <span style={{ display: 'flex', alignItems: 'center' }}>
      手机号
      <Tooltip title="点击添加">
        <AddThinIcon
          style={{ marginLeft: '.5em', cursor: 'pointer' }}
          onClick={handleAddNumber}
        />
      </Tooltip>
    </span>
  )

  return (
    <Form layout="horizontal" labelWidth="64px" state={state} onSubmit={handleSubmit}>
      <FormItem state={numbersState} label={numbersLabel}>
        {numbersState.$.map((numberState, i) => (
          <FormItem key={i}>
            <div style={{ display: 'flex', alignItems: 'center' }}>
              <TextInput placeholder="请输入中国大陆手机号" state={numberState} />
              {numbersState.$.length > 1 && (
                <Tooltip title="点击删除">
                  <CloseThinIcon
                    style={{ marginLeft: '.5em', padding: '5px', cursor: 'pointer' }}
                    onClick={() => handleRemoveNumber(i)}
                  />
                </Tooltip>
              )}
            </div>
          </FormItem>
        ))}
      </FormItem>
    </Form>
  )
})

function validateNumber(v: string) {
  if (!v) return '号码不可为空'
  if (!/^1\d{10}$/.test(v)) return '格式不正确'
}

支持新建或编辑的表单组件

import React, { useCallback, useState } from 'react'
import { FormState, FieldState } from 'formstate-x'
import { observer } from 'mobx-react'
import { Button } from 'react-icecream'
import { Form, FormItem, useFormstateX, TextInput } from 'react-icecream-form'

interface User {
  name: string
}

function createUserState(initialValue?: User) {
  return new FormState({
    name: new FieldState(initialValue?.name ?? '')
  })
}

interface UserFormProps {
  user?: User // 当前编辑的用户,如不传则代表新建
  onSubmit: (user: User) => void | Promise<void> // 表单提交的回调
}

// 用于新建或编辑用户信息的表单
function UserForm({ user, onSubmit }: UserFormProps) {
  const state = useFormstateX(createUserState, [user])

  return (
    <Form state={state} onSubmit={onSubmit}>
      <FormItem label="姓名">
        <TextInput placeholder="请输入姓名" state={state.$.name} />
      </FormItem>
    </Form>
  )
}

const motoko: User = {
  name: 'Motoko'
}

export default observer(() => {
  const [editing, setEditing] = useState(false)
  const editTarget = editing ? motoko : undefined
  const toggleEditing = useCallback(() => setEditing(v => !v), [])

  const handleSubmit = useCallback((user: User) => {
    const action = editing ? 'Update' : 'Create'
    alert(`${action}: ${JSON.stringify(user)}`)
  }, [editing])

  return (
    <>
      <div style={{ marginBottom: '1em', paddingBottom: '1em', borderBottom: '1px solid #E5E5E5' }}>
        <Button type="primary" onClick={toggleEditing}>
          {editing ? '切换为新建' : `切换为编辑 ${motoko.name}`}
        </Button>
      </div>
      <UserForm
        user={editTarget}
        onSubmit={handleSubmit}
      />
    </>
  )
})

在展示/编辑状态间切换的表单

import { FieldState, FormState } from 'formstate-x'
import React, { useState } from 'react'
import { Button } from 'react-icecream'
import { Form, FormItem, useFormstateX, TextArea } from 'react-icecream-form'

function createState(remark = '') {
  return new FormState({
    remark: new FieldState(remark).withValidator(v => !v && '不可为空')
  })
}

export default function Demo() {
  const remark = '请尽快发货'
  const [editing, setEditing] = useState(false)
  const state = useFormstateX(createState, [remark])

  const remarkEditingView = (
    <>
      <TextArea style={{ marginBottom: '12px' }} state={state.$.remark} />
      <Button onClick={() => setEditing(false)}>取消</Button>
    </>
  )

  const remarkDisplayView = (
    <div style={{ display: 'flex', alignItems: 'center' }}>
      {remark}&nbsp;
      <Button type="link" onClick={() => setEditing(true)}>编辑</Button>
    </div>
  )

  return (
    <Form
      layout="horizontal"
      labelWidth="6em"
      labelColor="light"
      state={state}
      footer={null}
    >
      <FormItem label="订单 ID" labelVerticalAlign="text">8901234567</FormItem>
      <FormItem label="订单备注">{editing ? remarkEditingView : remarkDisplayView}</FormItem>
    </Form>
  )
}

自定义输入组件

在项目中,往往会存在自定义的输入组件。如果希望这部分组件也像 react-icecream-form 提供的输入组件一样,被 FormItem 感知到,可以使用 react-icecream-form 提供的组件 InputWrapper 来实现:

import React from 'react'
import { FormState, FieldState } from 'formstate-x'
import { observer } from 'mobx-react'
import { AddThinIcon, RemoveThinIcon } from 'react-icecream/icons'
import { Form, FormItem, useFormstateX, InputGroup, InputGroupItem, InputWrapper } from 'react-icecream-form'

interface CounterProps {
  state: FieldState<number>
}

const Counter = observer(function _Counter({ state }: CounterProps) {

  const increase = () => state.onChange(state.value + 1)
  const decrease = () => state.onChange(state.value - 1)

  return (
    // `InputWrapper` registers Counter's state to outer FormItem
    <InputWrapper state={state}>
      <InputGroup style={{ width: '120px' }}>
        <InputGroupItem>
          <AddThinIcon style={{ height: '100%' }} onClick={increase} />
        </InputGroupItem>
        <InputGroupItem style={{ flex: '1 1 auto', textAlign: 'center' }}>
          {state.value}
        </InputGroupItem>
        <InputGroupItem>
          <RemoveThinIcon style={{ height: '100%' }} onClick={decrease} />
        </InputGroupItem>
      </InputGroup>
    </InputWrapper>
  )
})

export default observer(() => {

  const state = useFormstateX(() => new FormState({
    count: new FieldState(0).withValidator(v => v <= 0 && 'Positive required.')
  }), [])

  return (
    <Form state={state} onSubmit={v => alert(v.count)}>
      {/* So we don't need to pass `state.$.count` to `FormItem` here */}
      <FormItem label="Count">
        <Counter state={state.$.count} />
      </FormItem>
    </Form>
  )
})

注:这往往不是必须的,直接通过 FormItem 的 prop state 指定其对应的 state 实例也是常规的做法。

使用 ModalForm

react-icecream-form 也提供了 ModalForm & DrawerForm 与 formstate-x 的绑定,使用姿势跟 Form 类似:

import React, { useCallback, useState } from 'react'
import { FormState, FieldState, ValueOf } from 'formstate-x'
import { observer } from 'mobx-react'

import { Button } from 'react-icecream'
import { ModalForm, FormItem, useFormstateX, TextInput, PasswordInput } from 'react-icecream-form'

// 构造表单状态
function createFormState() {
  const usernameField = new FieldState('').validators(notEmpty)
  const passwordField = new FieldState('').validators(notEmpty)
  return new FormState({
    username: usernameField,
    password: passwordField
  })
}

// 表单中值的类型
type Value = ValueOf<ReturnType<typeof createFormState>>

export default observer(() => {

  const [visible, setVisible] = useState(false)
  const state = useFormstateX(createFormState, [])

  const handleSubmit = useCallback(async (value: Value) => {
    console.log(`Submit with: ${JSON.stringify(value)}.`)
    await new Promise<void>(resolve => setTimeout(resolve, 2000))
    setVisible(false)
  }, [])

  const handleCancel = useCallback(() => {
    console.log('Cancelled.')
    setVisible(false)
  }, [])

  return (
    <>
      <Button onClick={() => setVisible(true)}>打开弹框</Button>
      <ModalForm
        visible={visible}
        title="示例"
        state={state}
        layout="horizontal"
        labelWidth="4em"
        onSubmit={handleSubmit}
        onCancel={handleCancel}
      >
        <FormItem label="用户名" required>
          <TextInput placeholder="请输入用户名" state={state.$.username} />
        </FormItem>
        <FormItem label="密码" required>
          <PasswordInput placeholder="请输入密码" state={state.$.password} />
        </FormItem>
      </ModalForm>
    </>
  )
})

function notEmpty(v: string) {
  return v.trim() === '' && '不可为空'
}