Skip to content

Commit 663c60e

Browse files
committed
feat(component): Field builder component using grid layout
1 parent f7112d8 commit 663c60e

File tree

5 files changed

+560
-0
lines changed

5 files changed

+560
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
section: Component groups
3+
subsection: Helpers
4+
id: Field Builder
5+
source: react
6+
propComponents: ['FieldBuilder']
7+
---
8+
9+
import { FunctionComponent, useState } from 'react';
10+
import { FieldBuilder } from '@patternfly/react-component-groups/dist/dynamic/FieldBuilder';
11+
import { MinusCircleIcon } from '@patternfly/react-icons';
12+
13+
14+
## Examples
15+
16+
### Basic Field Builder
17+
18+
This is a basic field builder!
19+
20+
```js file="./FieldBuilderExample.tsx"
21+
22+
```
23+
24+
### Field Builder Select
25+
26+
This is a field builder with Select components!
27+
28+
```js file="./FieldBuilderSelectExample.tsx"
29+
30+
```
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React, { useState } from 'react';
2+
import {
3+
Form,
4+
TextInput,
5+
} from '@patternfly/react-core';
6+
import { FieldBuilder } from '@patternfly/react-component-groups/dist/dynamic/FieldBuilder';
7+
8+
interface Contact {
9+
name: string;
10+
email: string;
11+
}
12+
13+
export const FieldBuilderExample: React.FunctionComponent = () => {
14+
const [ contacts, setContacts ] = useState<Contact[]>([
15+
{ name: '', email: '' }
16+
]);
17+
18+
// Handle adding a new contact row
19+
const handleAddContact = () => {
20+
setContacts([ ...contacts, { name: '', email: '' } ]);
21+
};
22+
23+
// Handle removing a contact row
24+
const handleRemoveContact = (index: number) => {
25+
setContacts(contacts.filter((_, i) => i !== index));
26+
};
27+
28+
// Handle updating contact data
29+
const handleContactChange = (index: number, field: keyof Contact, value: string) => {
30+
const updatedContacts = [ ...contacts ];
31+
updatedContacts[index] = { ...updatedContacts[index], [field]: value };
32+
setContacts(updatedContacts);
33+
};
34+
35+
return (
36+
<Form>
37+
<FieldBuilder
38+
label=""
39+
labelInfo=""
40+
isRequired
41+
firstColumnLabel="Name"
42+
secondColumnLabel="Email"
43+
rowCount={contacts.length}
44+
onAddRow={handleAddContact}
45+
onRemoveRow={handleRemoveContact}
46+
addButtonProps={{
47+
children: 'Add team member'
48+
}}
49+
>
50+
{({ rowIndex, focusRef, firstColumnAriaLabelledBy, secondColumnAriaLabelledBy }) => [
51+
<TextInput
52+
key="name"
53+
ref={focusRef}
54+
type="text"
55+
value={contacts[rowIndex]?.name || ''}
56+
placeholder="Enter full name"
57+
onChange={(_event, value) => handleContactChange(rowIndex, 'name', value)}
58+
aria-labelledby={firstColumnAriaLabelledBy}
59+
isRequired
60+
/>,
61+
<TextInput
62+
key="email"
63+
type="email"
64+
value={contacts[rowIndex]?.email || ''}
65+
placeholder="[email protected]"
66+
onChange={(_event, value) => handleContactChange(rowIndex, 'email', value)}
67+
aria-labelledby={secondColumnAriaLabelledBy}
68+
isRequired
69+
/>
70+
]}
71+
</FieldBuilder>
72+
</Form>
73+
);
74+
};
75+
76+
export default FieldBuilderExample;
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React, { useState } from 'react';
2+
import {
3+
Form,
4+
FormSelect,
5+
FormSelectOption,
6+
} from '@patternfly/react-core';
7+
import { FieldBuilder } from '@patternfly/react-component-groups/dist/dynamic/FieldBuilder';
8+
9+
interface TeamMember {
10+
department: string;
11+
role: string;
12+
}
13+
14+
export const FieldBuilderSelectExample: React.FunctionComponent = () => {
15+
const [ teamMembers, setTeamMembers ] = useState<TeamMember[]>([
16+
{ department: '', role: '' }
17+
]);
18+
19+
// Handle adding a new team member row
20+
const handleAddTeamMember = () => {
21+
setTeamMembers([ ...teamMembers, { department: '', role: '' } ]);
22+
};
23+
24+
// Handle removing a team member row
25+
const handleRemoveTeamMember = (index: number) => {
26+
setTeamMembers(teamMembers.filter((_, i) => i !== index));
27+
};
28+
29+
// Handle updating team member data
30+
const handleTeamMemberChange = (index: number, field: keyof TeamMember, value: string) => {
31+
const updatedTeamMembers = [ ...teamMembers ];
32+
updatedTeamMembers[index] = { ...updatedTeamMembers[index], [field]: value };
33+
setTeamMembers(updatedTeamMembers);
34+
};
35+
36+
// Create a ref callback that works with FormSelect
37+
const createFormSelectRef = (focusRef: (element: HTMLElement | null) => void) =>
38+
(instance: React.ComponentRef<typeof FormSelect> | HTMLElement | null) => {
39+
if (instance) {
40+
// Get the underlying DOM element from the FormSelect instance
41+
const domElement = (instance as any)?.ref?.current || instance;
42+
if (domElement instanceof HTMLElement) {
43+
focusRef(domElement);
44+
}
45+
}
46+
};
47+
48+
const departmentOptions = [
49+
{ label: 'Choose a department', value: '', disabled: true },
50+
{ label: 'Engineering', value: 'engineering' },
51+
{ label: 'Marketing', value: 'marketing' },
52+
{ label: 'Sales', value: 'sales' },
53+
{ label: 'Human Resources', value: 'hr' },
54+
{ label: 'Finance', value: 'finance' }
55+
];
56+
57+
const roleOptions = [
58+
{ label: 'Choose a role', value: '', disabled: true },
59+
{ label: 'Manager', value: 'manager' },
60+
{ label: 'Senior', value: 'senior' },
61+
{ label: 'Junior', value: 'junior' },
62+
{ label: 'Intern', value: 'intern' },
63+
{ label: 'Contractor', value: 'contractor' }
64+
];
65+
66+
return (
67+
<Form>
68+
<FieldBuilder
69+
label=""
70+
labelInfo=""
71+
isRequired
72+
firstColumnLabel="Department"
73+
secondColumnLabel="Role"
74+
rowCount={teamMembers.length}
75+
onAddRow={handleAddTeamMember}
76+
onRemoveRow={handleRemoveTeamMember}
77+
rowGroupLabelPrefix="Team member"
78+
addButtonProps={{
79+
children: 'Add team member'
80+
}}
81+
>
82+
{({ rowIndex, focusRef, firstColumnAriaLabelledBy, secondColumnAriaLabelledBy }) => [
83+
<FormSelect
84+
key="department"
85+
ref={createFormSelectRef(focusRef)}
86+
value={teamMembers[rowIndex]?.department || ''}
87+
onChange={(event, value) => handleTeamMemberChange(rowIndex, 'department', value)}
88+
aria-labelledby={firstColumnAriaLabelledBy}
89+
isRequired
90+
>
91+
{departmentOptions.map((option, optionIndex) => (
92+
<FormSelectOption
93+
key={optionIndex}
94+
value={option.value}
95+
label={option.label}
96+
isDisabled={option.disabled}
97+
/>
98+
))}
99+
</FormSelect>,
100+
<FormSelect
101+
key="role"
102+
value={teamMembers[rowIndex]?.role || ''}
103+
onChange={(event, value) => handleTeamMemberChange(rowIndex, 'role', value)}
104+
aria-labelledby={secondColumnAriaLabelledBy}
105+
isRequired
106+
>
107+
{roleOptions.map((option, optionIndex) => (
108+
<FormSelectOption
109+
key={optionIndex}
110+
value={option.value}
111+
label={option.label}
112+
isDisabled={option.disabled}
113+
/>
114+
))}
115+
</FormSelect>
116+
]}
117+
</FieldBuilder>
118+
</Form>
119+
);
120+
};
121+
122+
export default FieldBuilderSelectExample;

0 commit comments

Comments
 (0)