Skip to content

Commit 821b9a1

Browse files
Merge pull request #6 from matheusrocha89/uncontrolled-controlled-input
Uncontrolled and controlled input
2 parents c1b4d1d + 0af6e8c commit 821b9a1

File tree

5 files changed

+302
-126
lines changed

5 files changed

+302
-126
lines changed

README.md

Lines changed: 70 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
https://github.com/user-attachments/assets/58c8cca5-878a-4e64-aa06-a8e202318f2a
66

7-
87
> A lightweight, easy-to-use React component that makes any text editable with a click!
98
109
## ✨ Features
@@ -39,33 +38,32 @@ import { InputClickEdit } from "@nobrainers/react-click-edit";
3938
function App() {
4039
const [name, setName] = useState("John Doe");
4140

42-
return <InputClickEdit value={name} onInputChange={setName} />;
41+
return <InputClickEdit value={name} onChange={setName} />;
4342
}
4443
```
4544

4645
## 🔧 Props
4746

48-
| Prop | Type | Default | Description |
49-
| -------------------- | ----------------------- | -------- | ------------------------------------------- |
50-
| value | string | "" | Text to display and edit |
51-
| isEditing | boolean | false | Initial editing state |
52-
| inputType | string | "text" | HTML input type (text, number, email, etc.) |
53-
| label | string | "" | Label for the input field |
54-
| className | string | "" | Container class name |
55-
| inputClassName | string | "" | Input field class name |
56-
| editButtonClassName | string | "" | Edit button class name |
57-
| saveButtonClassName | string | "" | Save button class name |
58-
| editWrapperClassName | string | "" | Edit mode wrapper class name |
59-
| saveButtonLabel | React.ReactNode | "Save" | Custom save button label |
60-
| editButtonLabel | React.ReactNode | "Edit" | Custom edit button label |
61-
| showIcons | boolean | false | Toggle button icons visibility |
62-
| iconsOnly | boolean | false | Show only icons without text labels |
63-
| editIcon | React.ElementType | LuPencil | Custom edit icon component |
64-
| saveIcon | React.ElementType | LuCheck | Custom save icon component |
65-
| iconPosition | "left" \| "right" | "left" | Position of icons in buttons |
66-
| onEditButtonClick | () => void | () => {} | Callback when edit button is clicked |
67-
| onInputChange | (value: string) => void | () => {} | Callback when input value changes |
68-
| onSaveButtonClick | () => void | () => {} | Callback when save button is clicked |
47+
| Prop | Type | Required | Default | Description |
48+
| ----------------- | -------------------------------------- | -------- | -------- | ----------------------------------------- |
49+
| value | string | Yes\* | - | Controlled text value to display and edit |
50+
| defaultValue | string | No | - | Initial uncontrolled value |
51+
| type | string | No | "text" | HTML input type attribute |
52+
| onChange | `ChangeEventHandler<HTMLInputElement>` | Yes\* | - | HTML input onChange handler |
53+
| isEditing | boolean | No | false | Control the editing state |
54+
| label | string | No | "" | Label for the input field |
55+
| className | string | No | "" | Container class name |
56+
| editButtonLabel | React.ReactNode | No | "Edit" | Custom edit button label |
57+
| saveButtonLabel | React.ReactNode | No | "Save" | Custom save button label |
58+
| showIcons | boolean | No | false | Toggle button icons visibility |
59+
| iconsOnly | boolean | No | false | Show only icons without text labels |
60+
| editIcon | React.ElementType | No | LuPencil | Custom edit icon component |
61+
| saveIcon | React.ElementType | No | LuCheck | Custom save icon component |
62+
| iconPosition | "left" \| "right" | No | "left" | Position of icons in buttons |
63+
| onEditButtonClick | () => void | No | () => {} | Callback when edit button is clicked |
64+
| onSaveButtonClick | () => void | No | () => {} | Callback when save button is clicked |
65+
66+
\*Either `value` + `onChange` (controlled) or `defaultValue` (uncontrolled) must be provided.
6967

7068
## 💡 Examples
7169

@@ -74,7 +72,7 @@ function App() {
7472
```tsx
7573
function BasicExample() {
7674
const [name, setName] = useState("John Doe");
77-
return <InputClickEdit value={name} onInputChange={setName} />;
75+
return <InputClickEdit value={name} onChange={setName} />;
7876
}
7977
```
8078

@@ -83,9 +81,9 @@ function BasicExample() {
8381
```tsx
8482
<InputClickEdit
8583
label="Age"
86-
inputType="number"
84+
type="number"
8785
value="25"
88-
onInputChange={(value) => console.log(value)}
86+
onChange={(value) => console.log(value)}
8987
/>
9088
```
9189

@@ -133,7 +131,7 @@ function ControlledExample() {
133131
isEditing={isEditing}
134132
onEditButtonClick={() => setIsEditing(true)}
135133
onSaveButtonClick={() => setIsEditing(false)}
136-
onInputChange={setValue}
134+
onChange={setValue}
137135
/>
138136
);
139137
}
@@ -151,6 +149,51 @@ function ControlledExample() {
151149
/>
152150
```
153151

152+
### React Hook Form Integration
153+
154+
```tsx
155+
import { useForm, Controller } from "react-hook-form";
156+
import { InputClickEdit } from "@nobrainers/react-click-edit";
157+
158+
function FormExample() {
159+
const { control, handleSubmit } = useForm();
160+
const onSubmit = (data) => console.log(data);
161+
162+
return (
163+
<form onSubmit={handleSubmit(onSubmit)}>
164+
<Controller
165+
name="editableText"
166+
control={control}
167+
defaultValue="Edit me"
168+
render={({ field }) => (
169+
<InputClickEdit {...field} onChange={field.onChange} />
170+
)}
171+
/>
172+
<button type="submit">Submit</button>
173+
</form>
174+
);
175+
}
176+
```
177+
178+
### Register Example
179+
180+
```tsx
181+
import { useForm } from "react-hook-form";
182+
import { InputClickEdit } from "@nobrainers/react-click-edit";
183+
184+
function RegisterExample() {
185+
const { register, handleSubmit } = useForm();
186+
const onSubmit = (data) => console.log(data);
187+
188+
return (
189+
<form onSubmit={handleSubmit(onSubmit)}>
190+
<InputClickEdit {...register("editableText")} />
191+
<button type="submit">Submit</button>
192+
</form>
193+
);
194+
}
195+
```
196+
154197
## 🎨 Styling
155198

156199
The component comes with minimal default styling through its base CSS file. You can override these styles or add additional styling using CSS classes. All main elements accept custom class names through props.

package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"jsdom": "^26.0.0",
7070
"lint-staged": "^15.3.0",
7171
"prettier": "^3.4.2",
72+
"react-hook-form": "^7.54.2",
7273
"semantic-release": "^24.2.1",
7374
"typescript": "^5.7.3",
7475
"typescript-eslint": "^8.20.0",

src/InputClickEdit/InputClickEdit.test.tsx

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { render, screen, fireEvent } from "@testing-library/react";
1+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
22
import { vi } from "vitest";
3+
import { useForm } from "react-hook-form";
34
import { InputClickEdit } from "./InputClickEdit";
45

56
describe("InputClickEdit", () => {
@@ -54,12 +55,16 @@ describe("InputClickEdit", () => {
5455
});
5556

5657
it("should call onInputChange when input value changes", () => {
57-
const onInputChange = vi.fn();
58-
render(<InputClickEdit isEditing onInputChange={onInputChange} />);
59-
fireEvent.change(screen.getByRole("textbox"), {
60-
target: { value: "New Value" },
61-
});
62-
expect(onInputChange).toHaveBeenCalledWith("New Value");
58+
const onChange = vi.fn();
59+
const mockEventChange = { target: { value: "New Value" } };
60+
render(<InputClickEdit isEditing onChange={onChange} />);
61+
fireEvent.change(screen.getByRole("textbox"), mockEventChange);
62+
63+
expect(onChange).toHaveBeenCalledWith(
64+
expect.objectContaining({
65+
target: expect.objectContaining({ value: "New Value" }),
66+
})
67+
);
6368
});
6469

6570
it("should call onSaveButtonClick when save button is clicked", () => {
@@ -132,7 +137,7 @@ describe("InputClickEdit", () => {
132137

133138
describe("Input Types", () => {
134139
it("should render different input types", () => {
135-
render(<InputClickEdit isEditing inputType="number" />);
140+
render(<InputClickEdit isEditing type="number" />);
136141
expect(screen.getByRole("spinbutton")).toBeInTheDocument();
137142
});
138143
});
@@ -145,4 +150,83 @@ describe("InputClickEdit", () => {
145150
expect(screen.getByText("Modify")).toBeInTheDocument();
146151
});
147152
});
153+
154+
describe("Controlled Mode", () => {
155+
it("should update value only when controlled value prop changes", () => {
156+
const { rerender } = render(<InputClickEdit value="Initial" isEditing />);
157+
158+
const input = screen.getByRole("textbox");
159+
fireEvent.change(input, { target: { value: "New Value" } });
160+
expect(input).toHaveValue("Initial"); // Should not change without prop update
161+
162+
rerender(<InputClickEdit value="Updated" isEditing />);
163+
expect(input).toHaveValue("Updated");
164+
});
165+
});
166+
167+
describe("Uncontrolled Mode", () => {
168+
it("should initialize with defaultValue", () => {
169+
render(<InputClickEdit defaultValue="Default" isEditing />);
170+
expect(screen.getByRole("textbox")).toHaveValue("Default");
171+
});
172+
173+
it("should maintain internal state when uncontrolled", () => {
174+
render(<InputClickEdit defaultValue="Initial" isEditing />);
175+
const input = screen.getByRole("textbox");
176+
177+
fireEvent.change(input, { target: { value: "New Value" } });
178+
expect(input).toHaveValue("New Value");
179+
});
180+
});
181+
182+
describe("React Hook Form Compatibility", () => {
183+
const TestForm = () => {
184+
const { register, watch } = useForm({
185+
defaultValues: {
186+
test: "Initial",
187+
},
188+
});
189+
const value = watch("test");
190+
191+
return (
192+
<form>
193+
<InputClickEdit {...register("test")} value={value} isEditing />
194+
</form>
195+
);
196+
};
197+
198+
it("should work with react-hook-form register", async () => {
199+
render(<TestForm />);
200+
const input = screen.getByRole("textbox");
201+
202+
expect(input).toHaveValue("Initial");
203+
fireEvent.input(input, { target: { value: "Updated" } });
204+
205+
await waitFor(() => {
206+
expect(input).toHaveValue("Updated");
207+
});
208+
});
209+
});
210+
211+
describe("Ref Handling", () => {
212+
it("should forward ref to input element", () => {
213+
const ref = vi.fn();
214+
render(<InputClickEdit ref={ref} isEditing />);
215+
216+
expect(ref).toHaveBeenCalledWith(expect.any(HTMLInputElement));
217+
});
218+
219+
it("should maintain ref after toggling edit mode", () => {
220+
const ref = { current: null };
221+
const { rerender } = render(<InputClickEdit ref={ref} />);
222+
223+
fireEvent.click(screen.getByText("Edit"));
224+
expect(ref.current).toBeInstanceOf(HTMLInputElement);
225+
226+
fireEvent.click(screen.getByText("Save"));
227+
rerender(<InputClickEdit ref={ref} />);
228+
fireEvent.click(screen.getByText("Edit"));
229+
expect(ref.current).toBeInstanceOf(HTMLInputElement);
230+
});
231+
});
148232
});

0 commit comments

Comments
 (0)