Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 70 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

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


> A lightweight, easy-to-use React component that makes any text editable with a click!

## ✨ Features
Expand Down Expand Up @@ -39,33 +38,32 @@ import { InputClickEdit } from "@nobrainers/react-click-edit";
function App() {
const [name, setName] = useState("John Doe");

return <InputClickEdit value={name} onInputChange={setName} />;
return <InputClickEdit value={name} onChange={setName} />;
}
```

## 🔧 Props

| Prop | Type | Default | Description |
| -------------------- | ----------------------- | -------- | ------------------------------------------- |
| value | string | "" | Text to display and edit |
| isEditing | boolean | false | Initial editing state |
| inputType | string | "text" | HTML input type (text, number, email, etc.) |
| label | string | "" | Label for the input field |
| className | string | "" | Container class name |
| inputClassName | string | "" | Input field class name |
| editButtonClassName | string | "" | Edit button class name |
| saveButtonClassName | string | "" | Save button class name |
| editWrapperClassName | string | "" | Edit mode wrapper class name |
| saveButtonLabel | React.ReactNode | "Save" | Custom save button label |
| editButtonLabel | React.ReactNode | "Edit" | Custom edit button label |
| showIcons | boolean | false | Toggle button icons visibility |
| iconsOnly | boolean | false | Show only icons without text labels |
| editIcon | React.ElementType | LuPencil | Custom edit icon component |
| saveIcon | React.ElementType | LuCheck | Custom save icon component |
| iconPosition | "left" \| "right" | "left" | Position of icons in buttons |
| onEditButtonClick | () => void | () => {} | Callback when edit button is clicked |
| onInputChange | (value: string) => void | () => {} | Callback when input value changes |
| onSaveButtonClick | () => void | () => {} | Callback when save button is clicked |
| Prop | Type | Required | Default | Description |
| ----------------- | -------------------------------------- | -------- | -------- | ----------------------------------------- |
| value | string | Yes\* | - | Controlled text value to display and edit |
| defaultValue | string | No | - | Initial uncontrolled value |
| type | string | No | "text" | HTML input type attribute |
| onChange | `ChangeEventHandler<HTMLInputElement>` | Yes\* | - | HTML input onChange handler |
| isEditing | boolean | No | false | Control the editing state |
| label | string | No | "" | Label for the input field |
| className | string | No | "" | Container class name |
| editButtonLabel | React.ReactNode | No | "Edit" | Custom edit button label |
| saveButtonLabel | React.ReactNode | No | "Save" | Custom save button label |
| showIcons | boolean | No | false | Toggle button icons visibility |
| iconsOnly | boolean | No | false | Show only icons without text labels |
| editIcon | React.ElementType | No | LuPencil | Custom edit icon component |
| saveIcon | React.ElementType | No | LuCheck | Custom save icon component |
| iconPosition | "left" \| "right" | No | "left" | Position of icons in buttons |
| onEditButtonClick | () => void | No | () => {} | Callback when edit button is clicked |
| onSaveButtonClick | () => void | No | () => {} | Callback when save button is clicked |

\*Either `value` + `onChange` (controlled) or `defaultValue` (uncontrolled) must be provided.

## 💡 Examples

Expand All @@ -74,7 +72,7 @@ function App() {
```tsx
function BasicExample() {
const [name, setName] = useState("John Doe");
return <InputClickEdit value={name} onInputChange={setName} />;
return <InputClickEdit value={name} onChange={setName} />;
}
```

Expand All @@ -83,9 +81,9 @@ function BasicExample() {
```tsx
<InputClickEdit
label="Age"
inputType="number"
type="number"
value="25"
onInputChange={(value) => console.log(value)}
onChange={(value) => console.log(value)}
/>
```

Expand Down Expand Up @@ -133,7 +131,7 @@ function ControlledExample() {
isEditing={isEditing}
onEditButtonClick={() => setIsEditing(true)}
onSaveButtonClick={() => setIsEditing(false)}
onInputChange={setValue}
onChange={setValue}
/>
);
}
Expand All @@ -151,6 +149,51 @@ function ControlledExample() {
/>
```

### React Hook Form Integration

```tsx
import { useForm, Controller } from "react-hook-form";
import { InputClickEdit } from "@nobrainers/react-click-edit";

function FormExample() {
const { control, handleSubmit } = useForm();
const onSubmit = (data) => console.log(data);

return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="editableText"
control={control}
defaultValue="Edit me"
render={({ field }) => (
<InputClickEdit {...field} onChange={field.onChange} />
)}
/>
<button type="submit">Submit</button>
</form>
);
}
```

### Register Example

```tsx
import { useForm } from "react-hook-form";
import { InputClickEdit } from "@nobrainers/react-click-edit";

function RegisterExample() {
const { register, handleSubmit } = useForm();
const onSubmit = (data) => console.log(data);

return (
<form onSubmit={handleSubmit(onSubmit)}>
<InputClickEdit {...register("editableText")} />
<button type="submit">Submit</button>
</form>
);
}
```

## 🎨 Styling

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.
Expand Down
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"jsdom": "^26.0.0",
"lint-staged": "^15.3.0",
"prettier": "^3.4.2",
"react-hook-form": "^7.54.2",
Copy link

@coderabbitai coderabbitai bot Jan 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Move react-hook-form to peerDependencies.

Since react-hook-form is an optional integration feature, it should be a peer dependency rather than a dev dependency. This allows users to choose whether to install it and which version to use.

-    "react-hook-form": "^7.54.2",

Add to peerDependencies:

   "peerDependencies": {
     "react": ">=18.0.0",
-    "react-dom": ">=18.0.0"
+    "react-dom": ">=18.0.0",
+    "react-hook-form": ">=7.0.0"
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"react-hook-form": "^7.54.2",
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0",
"react-hook-form": ">=7.0.0"
},

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I need this as a devDependency because I am using in one test

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matheusrocha89 You're right! Let's keep it in devDependencies for testing, but also add it to peerDependencies since it's an optional integration. This is a common pattern where a package is both a peer and dev dependency:

   "peerDependencies": {
     "react": ">=18.0.0",
-    "react-dom": ">=18.0.0"
+    "react-dom": ">=18.0.0",
+    "react-hook-form": ">=7.0.0"
   },
   "devDependencies": {
     // ... other dev dependencies ...
     "react-hook-form": "^7.54.2",
   }

✏️ Learnings added
Learnt from: matheusrocha89
PR: matheusrocha89/react-click-edit#6
File: package.json:72-72
Timestamp: 2025-01-27T09:09:26.505Z
Learning: When a package is used for optional integration features and also needed for testing, it should be listed in both peerDependencies (with a more relaxed version range) and devDependencies (with a specific version for testing).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

"semantic-release": "^24.2.1",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0",
Expand Down
100 changes: 92 additions & 8 deletions src/InputClickEdit/InputClickEdit.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { vi } from "vitest";
import { useForm } from "react-hook-form";
import { InputClickEdit } from "./InputClickEdit";

describe("InputClickEdit", () => {
Expand Down Expand Up @@ -54,12 +55,16 @@ describe("InputClickEdit", () => {
});

it("should call onInputChange when input value changes", () => {
const onInputChange = vi.fn();
render(<InputClickEdit isEditing onInputChange={onInputChange} />);
fireEvent.change(screen.getByRole("textbox"), {
target: { value: "New Value" },
});
expect(onInputChange).toHaveBeenCalledWith("New Value");
const onChange = vi.fn();
const mockEventChange = { target: { value: "New Value" } };
render(<InputClickEdit isEditing onChange={onChange} />);
fireEvent.change(screen.getByRole("textbox"), mockEventChange);

expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
target: expect.objectContaining({ value: "New Value" }),
})
);
});

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

describe("Input Types", () => {
it("should render different input types", () => {
render(<InputClickEdit isEditing inputType="number" />);
render(<InputClickEdit isEditing type="number" />);
expect(screen.getByRole("spinbutton")).toBeInTheDocument();
});
});
Expand All @@ -145,4 +150,83 @@ describe("InputClickEdit", () => {
expect(screen.getByText("Modify")).toBeInTheDocument();
});
});

describe("Controlled Mode", () => {
it("should update value only when controlled value prop changes", () => {
const { rerender } = render(<InputClickEdit value="Initial" isEditing />);

const input = screen.getByRole("textbox");
fireEvent.change(input, { target: { value: "New Value" } });
expect(input).toHaveValue("Initial"); // Should not change without prop update

rerender(<InputClickEdit value="Updated" isEditing />);
expect(input).toHaveValue("Updated");
});
});

describe("Uncontrolled Mode", () => {
it("should initialize with defaultValue", () => {
render(<InputClickEdit defaultValue="Default" isEditing />);
expect(screen.getByRole("textbox")).toHaveValue("Default");
});

it("should maintain internal state when uncontrolled", () => {
render(<InputClickEdit defaultValue="Initial" isEditing />);
const input = screen.getByRole("textbox");

fireEvent.change(input, { target: { value: "New Value" } });
expect(input).toHaveValue("New Value");
});
});

describe("React Hook Form Compatibility", () => {
const TestForm = () => {
const { register, watch } = useForm({
defaultValues: {
test: "Initial",
},
});
const value = watch("test");

return (
<form>
<InputClickEdit {...register("test")} value={value} isEditing />
</form>
);
};

it("should work with react-hook-form register", async () => {
render(<TestForm />);
const input = screen.getByRole("textbox");

expect(input).toHaveValue("Initial");
fireEvent.input(input, { target: { value: "Updated" } });

await waitFor(() => {
expect(input).toHaveValue("Updated");
});
});
});

describe("Ref Handling", () => {
it("should forward ref to input element", () => {
const ref = vi.fn();
render(<InputClickEdit ref={ref} isEditing />);

expect(ref).toHaveBeenCalledWith(expect.any(HTMLInputElement));
});

it("should maintain ref after toggling edit mode", () => {
const ref = { current: null };
const { rerender } = render(<InputClickEdit ref={ref} />);

fireEvent.click(screen.getByText("Edit"));
expect(ref.current).toBeInstanceOf(HTMLInputElement);

fireEvent.click(screen.getByText("Save"));
rerender(<InputClickEdit ref={ref} />);
fireEvent.click(screen.getByText("Edit"));
expect(ref.current).toBeInstanceOf(HTMLInputElement);
});
});
});
Loading
Loading