Material UI 如何用 NumberField 组件实现带步进按钮的数字输入?
【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Google's Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui
在 Material UI 项目中需要让用户输入数字、并能通过按钮逐步加/减时,会遇到一个事实:Material UI 本身并不内置数字输入组件。官方文档页 Number Field 给出的方案是:使用由 Base UI 的NumberField组合而成、并按 Material Design(MD2)规范做样式处理的一组组件。因此前提是先安装 Base UI(@base-ui/react)。文档同时说明 Base UI 是 tree-shakeable 的,最终打包产物只会包含你实际用到的组件。
这篇文章的任务就一条:把文档页提供的带步进按钮的数字输入组件复制进你的 React 应用,跑起来并确认效果。文档提供两种视觉形态:Outlined field(步进按钮在输入框右端)和Spinner field(步进按钮在输入框两侧),按你的视觉设计二选一即可。
一、安装依赖
在一个已有的 React 项目中操作。Material UI 的 安装文档 声明react和react-dom是 peer dependencies,要求^17.0.0 || ^18.0.0 || ^19.0.0,即这两个包应先就位。
- 安装 Material UI 及其样式依赖(以 npm 为例,pnpm/yarn 同理替换):
npm install @mui/material @emotion/react @emotion/styled- 安装 Base UI,这一步是数字输入组件的必要前置,文档原文:“you must install Base UI before proceeding”:
npm install @base-ui/react- 文档提供的组件代码里用到了 Material Icons(
KeyboardArrowUp、KeyboardArrowDown、Add、Remove、OpenInFull),来自@mui/icons-material,你的项目也需要能引入这个包。
二、复制 Outlined 版 NumberField 组件(主路径)
文档页给出的官方使用方式是:在 demo 工具栏点Expand code,选择以./components/开头的文件,把代码复制进项目。对应到本仓库,这个组件文件是 components/NumberField.js(同时提供 NumberField.tsx 供 TypeScript 项目使用)。
它的结构是:BaseNumberField.Root用render渲染成 Material UI 的FormControl(variant="outlined"),内部依次是InputLabel、由BaseNumberField.Input渲染的OutlinedInput(步进按钮放在endAdornment里),以及FormHelperText提示文字。文档说明这套组件与 Material UI outlinedTextField使用同一组构建块组件(FormControl、OutlinedInput、InputLabel、FormHelperText)。
组件代码(文档示例,来自仓库,可直接复制到你的components/目录):
import * as React from 'react'; import PropTypes from 'prop-types'; import { NumberField as BaseNumberField } from '@base-ui/react/number-field'; import IconButton from '@mui/material/IconButton'; import FormControl from '@mui/material/FormControl'; import FormHelperText from '@mui/material/FormHelperText'; import OutlinedInput from '@mui/material/OutlinedInput'; import InputAdornment from '@mui/material/InputAdornment'; import InputLabel from '@mui/material/InputLabel'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; /** * This component is a placeholder for FormControl to correctly set the shrink label state on SSR. */ function SSRInitialFilled(_) { return null; } SSRInitialFilled.muiName = 'Input'; function NumberField({ id: idProp, label, error, size = 'medium', ...other }) { let id = React.useId(); if (idProp) { id = idProp; } return ( <BaseNumberField.Root {...other} render={(props, state) => ( <FormControl size={size} ref={props.ref} disabled={state.disabled} required={state.required} error={error} variant="outlined" > {props.children} </FormControl> )} > <SSRInitialFilled {...other} /> <InputLabel htmlFor={id}>{label}</InputLabel> <BaseNumberField.Input id={id} render={(props, state) => ( <OutlinedInput aria-describedby={`${id}-helper-text`} label={label} inputRef={props.ref} value={state.inputValue} onBlur={props.onBlur} onChange={props.onChange} onKeyUp={props.onKeyUp} onKeyDown={props.onKeyDown} onFocus={props.onFocus} slotProps={{ input: props, }} endAdornment={ <InputAdornment position="end" sx={{ flexDirection: 'column', maxHeight: 'unset', alignSelf: 'stretch', borderLeft: '1px solid', borderColor: 'divider', ml: 0, '& button': { py: 0, flex: 1, borderRadius: 0.5, }, }} > <BaseNumberField.Increment render={<IconButton size={size} aria-label="Increase" />} > <KeyboardArrowUpIcon fontSize={size} sx={{ transform: 'translateY(2px)' }} /> </BaseNumberField.Increment> <BaseNumberField.Decrement render={<IconButton size={size} aria-label="Decrease" />} > <KeyboardArrowDownIcon fontSize={size} sx={{ transform: 'translateY(-2px)' }} /> </BaseNumberField.Decrement> </InputAdornment> } sx={{ pr: 0 }} /> )} /> <FormHelperText id={`${id}-helper-text`} sx={{ ml: 0, '&:empty': { mt: 0 } }}> Enter value between 10 and 40 </FormHelperText> </BaseNumberField.Root> ); } NumberField.propTypes = { error: PropTypes.bool, /** * The id of the input element. */ id: PropTypes.string, label: PropTypes.node, size: PropTypes.oneOf(['medium', 'small']), }; export default NumberField;两个需要注意的代码事实(均来自上面的组件源码):
label、size、error、id之外的所有 props(如min、max、defaultValue)都通过{...other}透传给BaseNumberField.Root,取值语义以 Base UI 的 API 为准。FormHelperText里的提示文案Enter value between 10 and 40是硬编码的,如果你用的范围不是 10–40,需要自行修改这行文案。- 组件里有一个名为
SSRInitialFilled的空占位组件,源码注释说明它的作用是让FormControl在 SSR 场景下正确设置 label 的 shrink 状态。如果你的应用有 SSR(如 Next.js),保留它;纯客户端渲染的应用它也只是返回null。
三、在页面中使用并确认效果
组件放好后,按文档 demo 的用法引用即可。下面是文档自带的 FieldDemo.js(文档示例,演示值min={10} max={40}等均为文档原值),import 路径./components/NumberField是相对 demo 文件的位置——只要你在项目里保持“组件放在components子目录”的相对结构,或相应调整这个 import 路径,就能直接运行:
import * as React from 'react'; import Box from '@mui/material/Box'; import NumberField from './components/NumberField'; export default function FieldDemo() { return ( <Box sx={{ display: 'grid', gap: 4 }}> <NumberField label="Number Field" min={10} max={40} /> <NumberField label="Number Field (Small)" size="small" /> <NumberField label="Number Field with Error" min={10} max={40} defaultValue={100} size="small" error /> </Box> ); }运行应用后,确认三个判断点:
- 页面上渲染出 outlined 样式的输入框,右端有一列上/下箭头按钮(
Increment/Decrement,带aria-label="Increase"/aria-label="Decrease")——这正是文档对 number field 的定义:“an input with increment and decrement buttons for capturing numeric input from users”。 - 点击箭头按钮,输入值按步进加/减;也可以在输入框里直接键入数字。
- 文档示例中第三个字段同时传了
min={10} max={40} defaultValue={100} error,用于演示错误态:输入框下方显示 helper text、组件进入 error 样式。
各 props 的作用以组件源码中的propTypes为准:label是标签文案(PropTypes.node),size取'medium' | 'small'(默认medium,demo 中第二个字段用它演示了小尺寸),error是布尔值控制错误样式,id是输入元素的 id。其余 props 透传给 Base UI。
四、可选分支:Spinner 形态
文档对两种形态的区分是:Outlined field 的步进按钮作为 end adornment 放在输入框右端;Spinner field 的步进按钮放在 outlined 输入框两侧,文档认为这种布局 “ideal for touch devices and narrow ranges of values”(适合触屏设备和取值范围较窄的场景)。
如果选择这个分支,组件文件换成 components/NumberSpinner.js(同样有 NumberSpinner.tsx),用法见 SpinnerDemo.js(文档示例):
import * as React from 'react'; import Box from '@mui/material/Box'; import NumberSpinner from './components/NumberSpinner'; export default function SpinnerDemo() { return ( <Box sx={{ display: 'flex', flexDirection: 'column', gap: 4, justifyContent: 'center', }} > <NumberSpinner label="Number Spinner" min={10} max={40} /> <NumberSpinner label="Number Spinner (Small)" size="small" /> <NumberSpinner label="Spinner with Error" min={10} max={40} defaultValue={100} size="small" error /> </Box> ); }Spinner 组件额外包含一个BaseNumberField.ScrubArea(可拖动调节区域)和FormLabel,propTypes中比 Outlined 版多声明了min(PropTypes.number,输入元素的最小值)。
五、边界与延伸阅读
- 这套组件不是
@mui/material的内置导出,而是文档页提供的、基于 Base UI 组合的示例实现;所有 props 的完整参考指向 Base UI 的NumberFieldAPI reference(官方文档页附有链接,此处不重复给出外部地址)。 - Outlined 版的构建块与 Material UI 的 outlined
TextField相同,进一步了解各零件时可查阅 Text Field 组件文档的 Components 部分。 - 仓库中所有 demo 均以
.js/.tsx成对提供(如FieldDemo.js/FieldDemo.tsx),TypeScript 项目直接取.tsx版本即可,两者行为一致。
【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Google's Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考