Skip to main content

generate-frontend-forms

Guide for creating forms using Sentry's new form system. Use when implementing forms, form fields, validation, or auto-save functionality.

소스 정보

저장소
getsentry/sentry
최근 소스 활동
2026년 10월 8일 12:18
감지된 SKILL.md 언어
영어
스타
45,574
포크
4,930

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
generate-frontend-forms
description
Guide for creating forms using Sentry's new form system. Use when implementing forms, form fields, validation, or auto-save functionality.
# Form System Guide This skill provides patterns for building forms using Sentry's new form system built on TanStack React Form and Zod validation. ## Core Principle - Always use the new form system (`useScrapsForm`, `AutoSaveForm`) for new forms. Never create new forms with the legacy JsonForm or Reflux-based systems. - All forms should be schema based. DO NOT create a form without schema validation. ## Imports All form components are exported from `@sentry/scraps/form`: ```tsx import {z} from 'zod'; import { AutoSaveForm, defaultFormOptions, setFieldErrors, useScrapsForm, } from '@sentry/scraps/form'; ``` > **Important**: DO NOT import from deeper paths, like '@sentry/scraps/form/field'. You can only use what is part of the PUBLIC interface in the index file in @sentry/scraps/form. --- ## Form Hook: `useScrapsForm` The main hook for creating forms with validation and submission handling. ### Basic Usage ```tsx import {z} from 'zod'; import {defaultFormOptions, useScrapsForm} from '@sentry/scraps/form'; const schema = z.object({ email: z.email('Invalid email'), name: z.string().min(2, 'Name must be at least 2 characters'), }); function MyForm() { const form = useScrapsForm({ ...defaultFormOptions, defaultValues: { email: '', name: '', }, validators: { onDynamic: schema, }, onSubmit: ({value, formApi}) => { // Handle submission console.log(value); }, }); return ( <form.AppForm form={form}> <form.AppField name="email"> {field => ( <field.Layout.Stack label="Email" required> <field.Input value={field.state.value} onChange={field.handleChange} /> </field.Layout.Stack> )} </form.AppField> <form.SubmitButton>Submit</form.SubmitButton> </form.AppForm> ); } ``` > **Important**: Always spread `defaultFormOptions` first. It configures validation to run on submit initially, then on every change after the first submission. This is why validators are defined as `onDynamic`, and it's what provides a consistent UX. ### Returned Properties | Property | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------- | | `AppForm` | Root wrapper component (provides form context and renders `<form>` element). Must receive `form={form}` prop. | | `AppField` | Field renderer component | | `FieldGroup` | Section grouping with title | | `SubmitButton` | Pre-wired submit button | | `Subscribe` | Subscribe to form state changes | | `reset()` | Reset form to default values | | `handleSubmit()` | Manually trigger submission | --- ## Field Components All fields are accessed via the `field` render prop and follow consistent patterns. ### Input Field (Text) ```tsx <form.AppField name="firstName"> {field => ( <field.Layout.Stack label="First Name" required> <field.Input value={field.state.value} onChange={field.handleChange} placeholder="Enter your name" /> </field.Layout.Stack> )} </form.AppField> ``` ### Number Field ```tsx <form.AppField name="age"> {field => ( <field.Layout.Stack label="Age" required> <field.Number value={field.state.value} onChange={field.handleChange} min={0} max={120} step={1} /> </field.Layout.Stack> )} </form.AppField> ``` ### Select Field (Single) ```tsx <form.AppField name="country"> {field => ( <field.Layout.Stack label="Country"> <field.Select value={field.state.value} onChange={field.handleChange} options={[ {value: 'us', label: 'United States'}, {value: 'uk', label: 'United Kingdom'}, ]} /> </field.Layout.Stack> )} </form.AppField> ``` ### Select Field (Multiple) ```tsx <form.AppField name="tags"> {field => ( <field.Layout.Stack label="Tags"> <field.Select multiple value={field.state.value} onChange={field.handleChange} options={[ {value: 'bug', label: 'Bug'}, {value: 'feature', label: 'Feature'}, ]} clearable /> </field.Layout.Stack> )} </form.AppField> ``` ### Switch Field (Boolean) ```tsx <form.AppField name="notifications"> {field => ( <field.Layout.Stack label="Enable notifications"> <field.Switch checked={field.state.value} onChange={field.handleChange} /> </field.Layout.Stack> )} </form.AppField> ``` ### TextArea Field ```tsx <form.AppField name="bio"> {field => ( <field.Layout.Stack label="Bio"> <field.TextArea value={field.state.value} onChange={field.handleChange} rows={4} placeholder="Tell us about yourself" /> </field.Layout.Stack> )} </form.AppField> ``` ### Range Field (Slider) ```tsx <form.AppField name="volume"> {field => ( <field.Layout.Stack label="Volume"> <field.Range value={field.state.value} onChange={field.handleChange} min={0} max={100} step={10} /> </field.Layout.Stack> )} </form.AppField> ``` ### Radio Field Radio fields use a composable API with `Radio.Group` and `Radio.Item`. `Radio.Group` provides group context that changes how the label is rendered for proper accessibility semantics. > **Important**: The layout (and its label) **must** be rendered _inside_ `Radio.Group`. The group context is provided by `Radio.Group`, so placing the layout outside will result in incorrect accessibility semantics. ```tsx <form.AppField name="priority"> {field => ( <field.Radio.Group value={field.state.value} onChange={field.handleChange}> <field.Layout.Stack label="Priority"> <field.Radio.Item value="low">Low</field.Radio.Item> <field.Radio.Item value="medium">Medium</field.Radio.Item> <field.Radio.Item value="high" description="Urgent issues"> High </field.Radio.Item> </field.Layout.Stack> </field.Radio.Group> )} </form.AppField> ``` For horizontal arrangement of radio items, use a `Flex` or `Stack` wrapper inside the layout: ```tsx import {Flex} from '@sentry/scraps/layout'; <field.Radio.Group value={field.state.value} onChange={field.handleChange}> <field.Layout.Row label="Priority"> <Flex gap="lg"> <field.Radio.Item value="low">Low</field.Radio.Item> <field.Radio.Item value="high">High</field.Radio.Item> </Flex> </field.Layout.Row> </field.Radio.Group>; ``` ### Custom Fields with BaseField For one-off fields that don't have a built-in component (e.g. a color picker, or any custom input), use `field.Base`. It provides a render prop with all the necessary accessibility and form integration props (`ref`, `disabled`, `aria-invalid`, `aria-describedby`, `onBlur`, `name`, `id`) that you spread onto your native element. ```tsx <form.AppField name="color"> {field => ( <field.Layout.Row label="Brand Color"> <field.Base<HTMLInputElement>> {(baseProps, {indicator}) => ( <Flex flexGrow={1}> <input {...baseProps} type="color" value={field.state.value} onChange={e => field.handleChange(e.target.value)} /> {indicator} </Flex> )} </field.Base> </field.Layout.Row> )} </form.AppField> ``` The render prop receives two arguments: 1. **`baseProps`** — accessibility and form integration props (`ref`, `disabled`, `aria-invalid`, `aria-describedby`, `onBlur`, `name`, `id`) to spread onto your element 2. **`{indicator}`** — the auto-save status indicator (spinner/checkmark) as a React node, which you can place wherever makes sense in your custom layout The element type is inferred from the passed `ref`, so if you don't pass one, you have to manually annotate it with `<field.Base<HTMLInputElement>>`. `field.Base` automatically handles: - Merging refs (for scroll-to-hash and external ref forwarding) - Disabling the field when auto-save is pending - Setting `aria-invalid` based on validation state - Linking to hint text via `aria-describedby` Use `field.Base` instead of building custom wrappers that duplicate this logic. It works with any native HTML element or third-party component that accepts standard props. --- ## Layouts Two layout options are available for positioning labels and fields. ### Stack Layout (Vertical) Label above, field below. Best for forms with longer labels or mobile layouts. ```tsx <field.Layout.Stack label="Email Address" hintText="We'll never share your email" required > <field.Input value={field.state.value} onChange={field.handleChange} /> </field.Layout.Stack> ``` ### Row Layout (Horizontal) Label on left (~50%), field on right. Compact layout for settings pages. ```tsx <field.Layout.Row label="Email Address" hintText="We'll never share your email" required> <field.Input value={field.state.value} onChange={field.handleChange} /> </field.Layout.Row> ``` ### Compact Variant Both Stack and Row layouts support a `variant="compact"` prop. In compact mode, the hint text appears as a tooltip on the label instead of being displayed below. This saves vertical space while still providing the hint information. ```tsx // Default: hint text appears below the label <field.Layout.Row label="Email" hintText="We'll never share your email"> <field.Input ... /> </field.Layout.Row> // Compact: hint text appears in tooltip when hovering the label <field.Layout.Row label="Email" hintText="We'll never share your email" variant="compact"> <field.Input ... /> </field.Layout.Row> // Also works with Stack layout <field.Layout.Stack label="Email" hintText="We'll never share your email" variant="compact"> <field.Input ... /> </field.Layout.Stack> ``` **When to Use Compact**: - Settings pages with many fields where vertical space is limited - Forms where hint text is supplementary, not essential - Dashboards or panels with constrained height ### Custom Layouts You are allowed to create new layouts if necessary, or not use any layouts at all. Without a layout, you _should_ render `field.meta.Label` and optionally `field.meta.HintText` for a11y. ```tsx <form.AppField name="firstName"> {field => ( <Flex gap="md"> <field.Meta.Label required>First Name:</field.Meta.Label> <field.Input value={field.state.value ?? ''} onChange={field.handleChange} /> </Flex> )} </form.AppField> ``` ### Layout Props | Prop | Type | Description |
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기