├── .gitignore
├── DOCS.md
├── LICENSE
├── README.md
├── components.json
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
├── App.tsx
├── assets
│ ├── GlobaModeExample.png
│ ├── GlobalIndividiualModeExample.png
│ ├── IndividualModeExample.png
│ ├── InlineModeExample.png
│ ├── collapsed.png
│ └── expanded.png
├── components
│ ├── JsonEditor
│ │ ├── defaultElements
│ │ │ ├── defaultBooleanInput.tsx
│ │ │ ├── defaultDateInput.tsx
│ │ │ ├── defaultNumberInput.tsx
│ │ │ ├── defaultRadioInput.tsx
│ │ │ ├── defaultSelectInput.tsx
│ │ │ ├── defaultTextAreaInput.tsx
│ │ │ ├── defaultTextInput.tsx
│ │ │ └── defaultValueElement.tsx
│ │ ├── inlineElements
│ │ │ ├── inlineCancelButton.tsx
│ │ │ ├── inlineEditButton.tsx
│ │ │ └── resetButton.tsx
│ │ ├── jsonEditor.css
│ │ ├── jsonEditor.tsx
│ │ └── renderElements
│ │ │ ├── renderArray.tsx
│ │ │ ├── renderObject.tsx
│ │ │ └── renderValue.tsx
│ ├── test.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── datePicker.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── popover.tsx
│ │ ├── radio-group.tsx
│ │ ├── select.tsx
│ │ ├── tabs.tsx
│ │ └── textarea.tsx
├── constants
│ └── constants.ts
├── functions
│ └── functions.ts
├── index.css
├── lib
│ └── utils.ts
├── main.tsx
├── temp.ts
├── types
│ └── JsonEditor.types.ts
├── utils
│ └── regexTrie.ts
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/DOCS.md:
--------------------------------------------------------------------------------
1 | # Documentation
2 |
3 | Welcome to the documentation for the React JSON Editor Library. This guide provides detailed information on how to effectively integrate and utilize the library in your React applications. You will find information on installation, configuration, available props, and usage examples to help you get started quickly and easily.
4 |
5 | ## Installation
6 |
7 | To install the library, use npm or yarn:
8 |
9 | ```bash
10 | npm install react-json-editor-alt
11 | ```
12 |
13 | **or**
14 |
15 | ```bash
16 | yarn add react-json-editor-alt
17 | ```
18 |
19 | ## Basic Usage
20 |
21 | Import the `JsonEditor` component into your desired React component file:
22 |
23 | ```jsx
24 | import {JsonEditor} from 'react-json-editor-alt';
25 | ```
26 |
27 | ### Step 3.1: Basic Usage
28 |
29 | ```jsx
30 | import { useState } from 'react';
31 | import {JsonEditor} from 'react-json-editor-alt';
32 |
33 | const App = () => {
34 | const [jsonData, setJsonData] = useState({
35 | name: "John Doe",
36 | age: 30,
37 | active: true
38 | });
39 |
40 | const handleChange = (props) => {
41 | console.log(props.updatedKeys)
42 | };
43 |
44 | return (
45 |
48 |
52 |
53 | );
54 | };
55 |
56 | export default App;
57 | ```
58 |
59 | ### Step 3.2: Advanced Usage
60 |
61 | ```jsx
62 | import { useState } from 'react';
63 | import {JsonEditor} from 'react-json-editor-alt';
64 |
65 | const App = () => {
66 | const [jsonData, setJsonData] = useState({
67 | name: 'John Doe',
68 | id : "DOZJHAH12",
69 | age: 30,
70 | isActive: true,
71 | bio : "Sample bio for john doe",
72 | gender: "male",
73 | contact: {
74 | email : "test@gmail.com",
75 | country : "USA"
76 | }
77 | });
78 |
79 | const handleChange = (props) => {
80 | console.log(props)
81 | };
82 |
83 | const handleSubmit = (props) => {
84 | setJsonData(props.updatedJson);
85 | }
86 |
87 | return (
88 |
91 |
My JSON Editor
92 |
137 |
138 | );
139 | };
140 |
141 | export default App;
142 | ```
143 |
144 | ## JSON Editor Props
145 |
146 | ### `1.json` (object, required)
147 |
148 | The JSON data or JavaScript object to be rendered and edited. The `json` prop must be passed and cannot be `null`, as the editor's primary function is to modify and display this data.
149 |
150 | ```jsx
151 |
159 | ```
160 |
161 | ### `2.onChange` (function, optional)
162 |
163 | The `onChange` callback function is triggered when a user modifies an editable field in the JSON editor.
164 |
165 | ### `3.onSubmit` (function, optional)
166 |
167 | The `onSubmit` Callback function is triggered when the user submits the changes made in the editor.
168 |
169 | #### onChange|onSubmit Props Types
170 |
171 | | Property | Type | Description | |
172 | | ------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | --- |
173 | | `initialJson` | `Record` | The JSON object representing the initial state before any changes were made. |
174 | | `updatedJson` | `Record` | The JSON object reflecting the state after changes have been applied, including all updates. |
175 | | `updatedKeys` | `DiffKeyValues` | An object mapping the keys that were modified, with each key containing its `initial` and `updated` values. |
176 | | `editorMode` | `EditorMode` | Indicates the current editing mode, which can be one of `global`, `individual`, `global-individual`, or `inline`. |
177 | | `submitType` | `Exclude` | Received only in the `onSubmit` callback handler. Specifies the type of submission, which can be `global`, `individual`, or `inline`. | |
178 |
179 | The submitType prop is helpful in cases where you need to perform actions based on the type of submission. For instance, you might want to switch the editor to read-only mode after a global submission, but not when the user submits an individual field.
180 |
181 | ### `4.isExpanded` (boolean, optional)
182 |
183 | Determines whether the editor should be expanded or collapsed by default.
184 |
185 | - `true`: The editor will show all nested keys expanded upon loading.
186 | - `false`: The editor will start in a collapsed state, hiding nested keys until they are explicitly expanded by the user.
187 |
188 | **Default**: `false`
189 |
190 | The optimal approach is to create a state that allows the user to expand or collapse the nested fields in the JSON editor.
191 |
192 | **For example**
193 |
194 | ```jsx
195 | function IsExpandedExample() {
196 | const [isExpanded, setIsExpanded] = useState(false);
197 |
198 | return (
199 | <>
200 |
201 | setIsExpanded(!isExpanded)}>
202 | {isExpanded ? "Collapse" : "Expand"}
203 |
204 |
205 |
209 | >
210 | );
211 | }
212 | ```
213 | **Collapsed Mode**
214 |
215 | 
216 |
217 | **Expanded Mode**
218 | 
219 |
220 | ### `5.className` (string, optional)
221 |
222 | Allows the user to apply a custom CSS class to the editor component for styling purposes.
223 |
224 |
225 | ### `6.styles` (object, optional)
226 |
227 | Accepts an object of inline styles that will be applied directly to the editor component.
228 |
229 | ### `7.editingConfig` (**Important**)
230 |
231 | ---
232 |
233 | The `editingConfig` prop is the core configuration for defining how the JSON Editor behaves in terms of user interaction, field editing, and rendering. This configuration allows you to control which fields are editable, how they are displayed, and which validation rules apply.
234 |
235 | #### Example Usage
236 |
237 | **Inline Mode**
238 | ```jsx
239 |
250 | ```
251 |
252 | **Other Modes**
253 |
254 | ```jsx
255 | const [isEditing, setIsEditing] = useState(false);
256 |
257 |
269 | ```
270 |
271 |
272 | #### `7.1 editingMode` (string, optional)
273 |
274 | Defines how the fields are edited within the JSON Editor. The supported modes are:
275 | - `inline`: Directly edit individual fields by clicking the edit icon next to them.
276 | 
277 |
278 |
279 | - `global`: Enter a global editing state, where all fields are editable simultaneously, with a global submit button.
280 | 
281 |
282 |
283 | - `individual`: All fields are editable simultaneously but each field has its own submit button for individual updates.
284 | - 
285 |
286 |
287 | - `global-individual`: A hybrid mode where both global and individual submissions are allowed.
288 | 
289 |
290 | **Default**: `inline`
291 |
292 | #### `7.2 isEditing` (boolean, optional)
293 |
294 | Determines whether the editor is in reading or editing mode. This prop is essential when using modes other than `inline` to switch between the two states.
295 |
296 | - `true`: The editor is in editing mode, allowing the user to modify the fields.
297 | - `false`: The editor is in reading mode, preventing any changes to the fields.
298 |
299 | **Note**: In `inline` mode, the `isEditing` prop has no effect. It is only applicable in `global`, `individual`, and `global-individual` modes, where it must be used to control the editability of the entire editor or specific fields.
300 |
301 | **Default**: `false`
302 |
303 | #### `7.3 allFieldsEditable` (boolean, optional)
304 |
305 | Determines whether all fields are editable by default.
306 |
307 | - `true`: All fields are editable unless explicitly mentioned in the `nonEditableFields` object.
308 | - `false`: All fields are non-editable unless explicitly mentioned in the `editableFields` object.
309 |
310 | This setting is useful when specifying exceptions. For instance, if most fields should be editable, but a few should not, set this to `true` and specify the non-editable fields and vice versa.
311 |
312 | **Default**: `true`
313 |
314 | #### Use Case:
315 |
316 | ```jsx
317 |
327 | ```
328 | Here, all fields are editable in json except `name` and `contact.email`.
329 |
330 | #### `7.4 editableFields` (object, optional)
331 |
332 | A JSON object specifying which fields are editable and the types or validations they should follow. You can define custom fields with their own input types (`string`, `number`, `textarea`, `boolean`, `radio`, `select`) and validations.
333 |
334 | Each key in the `editableFields` object corresponds to a JSON path, and its value specifies the type of input and validations for that field.
335 |
336 | #### Example Editable Fields Configuration:
337 |
338 | ```jsx
339 | const editableFieldsObject = {
340 | gender: {
341 | type: "radio",
342 | options: [
343 | { key: "male", value: "male" },
344 | { key: "female", value: "female" },
345 | { key: "others", value: "others" },
346 | ],
347 | },
348 | description: {
349 | type: "textArea",
350 | validations: {
351 | minLength: 1,
352 | maxLength: 100,
353 | },
354 | },
355 | "contact.email": {
356 | type: "string",
357 | validations: {
358 | regex: /^[^@]+@[^@]+\.[^@]+$/,
359 | regexValidationMessage: "Please enter a valid email address.",
360 | },
361 | },
362 | "contact.address.country": {
363 | type: "select",
364 | options: [
365 | { key: "India", value: "India" },
366 | { key: "USA", value: "USA" },
367 | ],
368 | },
369 | "education.[].graduationYear": {
370 | type: "number",
371 | validations: {
372 | maxValue: new Date().getFullYear(),
373 | minLength: 4,
374 | maxLength: 4,
375 | validationMessage: `Please enter a valid year. The year can't be greater than ${new Date().getFullYear()}`,
376 | },
377 | },
378 | "hobbies.1": {
379 | type: "string",
380 | validations: {
381 | maxLength: 20,
382 | },
383 | },
384 | }
385 | ```
386 | #### Editable Field Types:
387 |
388 | - `string`: Default field type. Renders a text input.
389 | - `number`: Renders a number input.
390 | - `textArea`: Renders a textarea input with optional length validations.
391 | - `boolean`: Renders a toggle between `true`/`false`.
392 | - `radio`: Renders radio button options.
393 | - `select`: Renders a dropdown with options.
394 |
395 | #### Field Validations:
396 |
397 | - `minLength` / `maxLength`: Set limits for the length of the input value.
398 | - `regex`: Apply a regular expression for validation.
399 | - `validationMessage` / `regexValidationMessage`: Display a custom error message when validation fails.
400 |
401 | Please find more details on validation in the [Validation Section](#validations).
402 |
403 | #### `7.5 nonEditableFields` (object, optional)
404 |
405 | A JSON object that specifies which fields are not editable. This is especially useful when `allFieldsEditable` is set to `true`, allowing you to disable specific fields.
406 |
407 | Each key in `nonEditableFields` corresponds to a JSON path, and its value should be `true` to make the field non-editable.
408 |
409 | #### Example Non-Editable Fields:
410 |
411 | ```jsx
412 | const nonEditableFieldsObject = {
413 | name: true,
414 | "contact.phone": true,
415 | };
416 | ```
417 | In this example `name` and `contact.phone` are not editable.
418 |
419 | Note: If a field is present in both editableFields and nonEditableFields, the non-editable field takes precedence and the field becomes non-editable.
420 |
421 | #### 💡 Special JSON Paths: Array Field Targeting
422 |
423 | When working with arrays in your JSON structure, it can be cumbersome to manually specify validation rules or field types for every element in the array. To simplify this, you can use a special syntax in the `editableFields` and `nonEditableFields` paths by including `[]` in place of array indices.
424 |
425 | #### Usage of `[]` for Array Fields
426 |
427 | If you have an array of objects and want to apply a validation or field type to the same key in every object in the array, you can target all elements at once by using `[]` in the path.
428 |
429 | For example:
430 |
431 | ```json
432 | {
433 | "education": [
434 | { "graduationYear": 2015 },
435 | { "graduationYear": 2018 },
436 | { "graduationYear": 2020 }
437 | ]
438 | }
439 | ```
440 |
441 | You can target the `graduationYear` field across all objects in the education array by using the path `education.[].graduationYear` instead of writing it out for every index (`education.0.graduationYear`, `education.1.graduationYear`, etc.).
442 |
443 | ```jsx
444 | const editableFieldObject = {
445 | "education.[].graduationYear": {
446 | "type": "number",
447 | "validations": {
448 | "minValue": 1900,
449 | "maxValue": new Date().getFullYear(),
450 | "validationMessage": "Please enter a valid graduation year."
451 | }
452 | }
453 | }
454 | ```
455 |
456 | This ensures that the `graduationYear` field in every object within the education array follows the same validation rules and uses the same input type (number).
457 |
458 | #### More Specific Paths Take Precedence
459 | If you want to apply a specific rule or field type to one particular item in the array, you can do so by specifying the index of the element. More specific paths take precedence over general paths using [].
460 |
461 | **For example:**
462 |
463 | ```jsx
464 | {
465 | "education.[].graduationYear": {
466 | "type": "number",
467 | "validations": {
468 | "minValue": 1900,
469 | "maxValue": new Date().getFullYear(),
470 | "validationMessage": "Please enter a valid graduation year."
471 | }
472 | },
473 | "education.1.graduationYear": {
474 | "type": "string",
475 | "validations": {
476 | "maxLength": 4,
477 | "validationMessage": "Graduation year must be a string of 4 characters."
478 | }
479 | }
480 | }
481 | ```
482 |
483 | All elements in the education array will have the graduationYear field as a number with a minimum and maximum value, except for `education[1]`.
484 | For `education[1]`, the graduationYear field will be treated as a string with a maxLength validation of 4 characters.
485 |
486 | This feature helps keep the configuration concise and avoids redundant definitions, while still allowing fine-tuned control over individual array elements when needed.
487 |
488 | #### `7.6 debouncing` (boolean, optional)
489 |
490 | Controls whether input changes are debounced. Debouncing is useful for performance optimization, particularly when handling continuous input changes (such as typing).
491 |
492 | - `true`: Input changes are debounced, reducing the number of times the `onChange` callback is fired.
493 | - `false`: Input changes are processed instantly, and validation messages (if any) are shown immediately.
494 |
495 | **Note**: Setting `debouncing` to true can significantly improve performance when working with large JSON objects. However, for smaller JSONs, the effect may be negligible. However, with debouncing set to true, validation messages will be delayed by 300ms after the user stops typing, as immediate validation is deferred to optimize performance.
496 |
497 | **Default**: `false`
498 |
499 |
500 | #### `7.7 enableTypeBasedRendering` (boolean, optional)
501 |
502 | Enables automatic rendering of input fields based on the type of the JSON value. This means you don't have to explicitly define the field type in the `editableFields` object for basic types like `boolean` and `number`.
503 |
504 | For example:
505 | - A boolean field (`isAdult: true`) will be rendered as a true/false toggle.
506 | - A number field (`age: 23`) will be rendered as a number input.
507 |
508 | If set to `false`, the editor will default to rendering all fields as text inputs unless explicitly defined in `editableFields`.
509 |
510 | **Default**: `true`
511 |
512 | ---
513 |
514 | ### `8.globalSubmitButtonConfigs`
515 |
516 | A configuration Object to customise global submit button in `global` and `global-individual` editing mode.
517 |
518 | #### `8.1 variant` (string, optional)
519 |
520 | Defines the visual style of the button. You can choose from the following options:
521 |
522 | - `"secondary"`: A standard secondary button style.
523 | - `"outline"`: A button with an outlined style.
524 | - `"ghost"`: A transparent button with minimal styling.
525 | - `"link"`: A button that resembles a hyperlink.
526 | - `"destructive"`: A button styled to indicate a destructive action (e.g., delete).
527 |
528 | #### `8.2 className` (string, optional)
529 |
530 | A custom CSS class that can be added to the button for styling purposes. This allows you to apply additional styles or override default styles to match your application's design.
531 |
532 | #### `8.3 buttonText` (string, optional)
533 |
534 | The text that will be displayed on the button. This prop provides a simple way to set the button's label to communicate its action clearly to users.
535 |
536 | #### `8.4 children` (React.ReactNode, optional)
537 |
538 | Enables the inclusion of nested React components or elements within the button. This allows for more complex button content, such as icons or additional text elements, providing greater flexibility in design.
539 |
540 | ### Example
541 |
542 | ```jsx
543 |
551 | ```
552 |
553 | ## Validations
554 |
555 | The `validations` object is applicable to the following field types:
556 |
557 | - `string`
558 | - `number`
559 | - `textarea`
560 |
561 | The validation can be defined using either basic length-based properties or regex-based validation. Here’s how they work:
562 |
563 | #### 1.Basic Length Validation:
564 |
565 | - `minLength`: Specifies the minimum number of characters allowed.
566 | - `maxLength`: Specifies the maximum number of characters allowed.
567 | - `validationMessage`: A custom message to be shown to the user if the validation fails. If no custom message is provided, a default message will be displayed.
568 |
569 | **Example:**
570 |
571 | ```json
572 | "description": {
573 | "type": "textArea",
574 | "validations": {
575 | "minLength": 10,
576 | "maxLength": 100,
577 | "validationMessage": "Description must be between 10 and 100 characters."
578 | }
579 | }
580 | ```
581 |
582 | #### 2.Regex Validation:
583 |
584 | - `regex`: Allows you to define a custom regular expression for validation. This is useful for more complex validations like email formats or phone numbers.
585 | - `regexValidationMessage`: A custom message to be shown to the user if the regex validation fails. Like `validationMessage`, if no custom message is provided, a default message will be shown.
586 |
587 | #### Example:
588 |
589 | ```json
590 | "contact.email": {
591 | "type": "string",
592 | "validations": {
593 | "regex": /^[^@]+@[^@]+\.[^@]+$/,
594 | "regexValidationMessage": "Please enter a valid email address."
595 | }
596 | }
597 | ```
598 |
599 | **Note**: When using regex validation, it can cover most validation needs, so the length properties like minLength or maxLength aren't needed, as regex itself can handle these checks.
600 |
601 | #### 3.Number Field-Specific Validation
602 |
603 | For `number` type fields, additional properties can be used to set minimum and maximum values:
604 |
605 | - `minValue`: Specifies the minimum value allowed for the field.
606 | - `maxValue`: Specifies the maximum value allowed for the field.
607 |
608 | #### Example:
609 |
610 | ```json
611 | "education.[].graduationYear": {
612 | "type": "number",
613 | "validations": {
614 | "minValue": 1900,
615 | "maxValue": new Date().getFullYear(),
616 | "validationMessage": "Please enter a valid graduation year between 1900 and the current year."
617 | }
618 | }
619 | ```
620 | ## Examples
621 | Examples are the most effective way to understand a library’s functionality. Visit [examples](https://himalaya0035.github.io/react-json-editor-alt/) to see what's available. Currently, only a few examples are provided, but we plan to add more soon. In the meantime, please refer to this official documentation. Thank you for your patience!
622 |
623 | ## Community and Support
624 |
625 | We hope this documentation has helped you get the most out of the React JSON Editor. If you have any questions, encounter issues, or want to request features, feel free to open an issue or contribute directly via GitHub. We value your feedback and are always working to improve the library.
626 |
627 | Stay tuned for future updates, and thank you for using the React JSON Editor!
628 |
629 | If you find this library useful, consider giving it a **star** on [GitHub](https://github.com/himalaya0035/react-json-editor-alt).
630 |
631 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Himalaya Gupta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # REACT JSON EDITOR
2 |
3 | The **React JSON Editor** is a flexible and easy-to-use library for rendering and editing JavaScript objects or JSON data in React applications. It lets developers editable and non-editable fields, add validation, and display inputs like select, boolean, radio, textarea, etc., based on field types and editing configuarations ensuring a smooth user experience.
4 |
5 | 
6 | 
7 | 
8 | 
9 | 
10 |
11 |
12 | ## 🌐 Links
13 |
14 | - [📂 **GitHub Repository**](https://github.com/himalaya0035/react-json-editor-alt)
15 | Explore the codebase, raise issues, or contribute to the project.
16 |
17 | - [📄 **Documentation**](https://github.com/himalaya0035/react-json-editor-alt/blob/main/DOCS.md)
18 | Access the complete documentation for installation, usage, and more details.
19 |
20 | - [🖥️ **Examples/Demo**](https://himalaya0035.github.io/react-json-editor-alt/)
21 | Try out the live demo and examples to see the editor in action.
22 |
23 | ## Screenshots
24 | 
25 |
26 | 
27 |
28 | ## Why Use This Library?
29 |
30 | - ✏️ **Dynamic JSON Editing**: Define which fields are editable or non-editable at a granular level.
31 | - ⚙️ **Flexible Field Rendering**: Configure editable fields to be displayed as dropdowns, radio buttons, boolean toggles, datepickers, text areas, and other input types.
32 | - 🔧 **Advanced Validation**: Support for length-based, regex-based, and number-specific validations (min/max values) to ensure data accuracy.
33 | - 🔄 **Multiple Editing Modes**: Seamlessly switch between `inline`, `global`, `individual`, or `global-individual` editing modes, providing a tailored editing experience based on the use case.
34 | - 🧩 **Type-Based Rendering**: Automatically render input types for common JSON values such as booleans and numbers without explicit configuration.
35 | - ⚡ **Performance Optimizations**: Features like debouncing help ensure smooth and responsive interactions, even for large JSON structures.
36 |
37 | Whether you're building complex forms, handling flexible data, or need a customizable JSON editor, this library offers the tools to do it efficiently and easily.
38 |
39 | ## Quick Start Guide
40 |
41 | Get started with the **React JSON Editor** in just a few steps! For detailed documentation, refer to the in depth guide: [DOCS](https://github.com/himalaya0035/react-json-editor-alt/blob/main/DOCS.md).
42 |
43 | ### Step 1: Installation
44 |
45 | To install the library, use npm or yarn:
46 |
47 | ```bash
48 | npm install react-json-editor-alt
49 | ```
50 |
51 | **or**
52 |
53 | ```bash
54 | yarn add react-json-editor-alt
55 | ```
56 |
57 | ### Step 2: Import the Component
58 | Import the `JsonEditor` component into your desired React component file:
59 |
60 | ```jsx
61 | import {JsonEditor} from 'react-json-editor-alt';
62 | ```
63 |
64 | ### Step 3.1: Basic Usage
65 |
66 | ```jsx
67 | import { useState } from 'react';
68 | import {JsonEditor} from 'react-json-editor-alt';
69 |
70 | const App = () => {
71 | const [jsonData, setJsonData] = useState({
72 | name: "John Doe",
73 | age: 30,
74 | active: true
75 | });
76 |
77 | const handleChange = (props) => {
78 | console.log(props.updatedKeys)
79 | };
80 |
81 | return (
82 |
85 |
89 |
90 | );
91 | };
92 |
93 | export default App;
94 | ```
95 |
96 | ### Step 3.2: Advanced Usage
97 |
98 | ```jsx
99 | import { useState } from 'react';
100 | import {JsonEditor} from 'react-json-editor-alt';
101 |
102 | const App = () => {
103 | const [jsonData, setJsonData] = useState({
104 | name: 'John Doe',
105 | id : "DOZJHAH12",
106 | age: 30,
107 | isActive: true,
108 | bio : "Sample bio for john doe",
109 | gender: "male",
110 | contact: {
111 | email : "test@gmail.com",
112 | country : "USA"
113 | }
114 | });
115 |
116 | const handleChange = (props) => {
117 | console.log(props)
118 | };
119 |
120 | const handleSubmit = (props) => {
121 | setJsonData(props.updatedJson);
122 | }
123 |
124 | return (
125 |
128 |
My JSON Editor
129 |
174 |
175 | );
176 | };
177 |
178 | export default App;
179 | ```
180 |
181 | ### Step 4: Customizing the Editor
182 | You can customize the editor by adjusting the `editingConfig` prop. This allows you to define editable and non-editable fields, validation rules, and more. Refer to the [editingConfig prop docs](https://github.com/himalaya0035/react-json-editor-alt/blob/main/DOCS.md#editingConfig) for details.
183 |
184 | ## Examples/Demo
185 |
186 | Examples are the most effective way to understand a library’s functionality. Visit [examples](https://himalaya0035.github.io/react-json-editor-alt/) to see what's available. Currently, only a few examples are provided, but we plan to add more soon. In the meantime, please refer to the [official documentation](https://github.com/himalaya0035/react-json-editor-alt/blob/main/DOCS.md) for examples and usage guidelines to help you get started. Thank you for your patience!
187 |
188 | ## API Reference
189 |
190 | This section provides a brief overview of the props used in the **React JSON Editor**. Each prop's type, purpose, and whether it is required or optional is outlined below.
191 |
192 | ### JsonEditor Props
193 |
194 | | Prop Name | Type | Required | Description |
195 | |-------------------|----------------|----------|-------------------------------------------------------------------------------------------------------|
196 | | `json` | `object` | Yes | The JSON data or JavaScript object to be rendered and edited. |
197 | | `onChange` | `function` | No | Callback function triggered when a user modifies an editable field in the JSON editor. |
198 | | `onSubmit` | `function` | No | Callback function called when the user submits the changes made in the editor. |
199 | | `editingConfig` | `object` | No | Configuration Object that controls editing behavior, specifying editable and non-editable fields along with their validations. |
200 | | `isExpanded` | `boolean` | No | Controls whether the editor is expanded or collapsed. |
201 | | `className` | `string` | No | Custom CSS class for styling the editor component. |
202 | | `styles` | `object` | No | Inline styles to be applied to the editor component. |
203 | | `globalSubmitButtonConfigs` | `object` | No | Configuration Object to customise global submit button in `global` and `global-individual` editing mode. |
204 |
205 | ### onChange|onSubmit Props Types
206 |
207 | | Property | Type | Description ||
208 | |----------------|--------------------|----------|-----------------------------------------------------------------------------------------------------------|
209 | | `initialJson` | `Record` | The JSON object representing the initial state before any changes were made. |
210 | | `updatedJson` | `Record` | The JSON object reflecting the state after changes have been applied, including all updates. |
211 | | `updatedKeys` | `DiffKeyValues` | An object mapping the keys that were modified, with each key containing its `initial` and `updated` values. |
212 | | `editorMode` | `EditorMode` | Indicates the current editing mode, which can be one of `global`, `individual`, `global-individual`, or `inline`. |
213 | | `validations` | `Record` | Available only in the `onChange` callback handler, this object contains all current validation errors in the editor. |
214 | | `submitType` | `Exclude` | Available only in the `onSubmit` callback handler. Specifies the type of submission, which can be `global`, `individual`, or `inline`. | |
215 |
216 |
217 | ### Editing Config Props
218 |
219 | | Prop Name | Type | Required | Description |
220 | |--------------------------|----------------|----------|-------------------------------------------------------------------------------------------------------|
221 | | `isEditing` | `boolean` | No | Determines whether the editor is in editing mode. Not required in `inline` editing mode. |
222 | | `editingMode` | `string` | No | Defines how fields are edited (e.g., `inline`, `global`, `individual`, or `global-individual`). |
223 | | `allFieldsEditable` | `boolean` | No | Indicates whether all fields are editable by default. |
224 | | `editableFields` | `object` | No | Specifies which fields are editable along with their types and validations. |
225 | | `nonEditableFields` | `object` | No | Specifies which fields are non-editable, overriding the editableFields settings. |
226 | | `debouncing` | `boolean` | No | Controls if input changes are debounced for performance optimization. |
227 | | `enableTypeBasedRendering`| `boolean` | No | Enables automatic rendering of input fields based on the type of the JSON value. |
228 |
229 | ### Global Submit Button Configs
230 |
231 | A configuration Object to customise global submit button in `global` and `global-individual` editing mode.
232 |
233 | | Property | Type | Required | Description |
234 | |----------------|-------------------|----------|---------------------------------------------------------------------------------------------------|
235 | | `variant` | `string` | No | Specifies the variant style of the button. Options include `"secondary"`, `"outline"`, `"ghost"`, `"link"`, and `"destructive"`. |
236 | | `className` | `string` | No | Custom CSS class for styling the button. Allows for additional styling options beyond default styles. |
237 | | `buttonText` | `string` | No | Text to be displayed on the button. This provides a straightforward way to set the button label. |
238 | | `children` | `React.ReactNode` | No | Allows for the inclusion of nested React components or elements within the button, enabling complex content. |
239 |
240 | ## Planned Features
241 |
242 | We are currently working on some exciting features for our next release. To make our library even better, we’d love to hear your thoughts! Please specify which features you would like to see implemented next. Your feedback is invaluable in shaping the future of this library.
243 |
244 | Feel free to request features by creating an issue in our [GitHub repository](https://github.com/himalaya0035/react-json-editor-alt/issues) or participating in the discussions.
245 |
246 | **Note**: The React JSON Editor works best in projects where Tailwind CSS is already installed. In projects without Tailwind, there may be instances where global styles of other components could be affected. We are addressing this issue for future releases.
247 |
248 | ## Contributing
249 |
250 | Contributions are welcome! If you encounter any issues or have ideas for improvements, feel free to open an issue or submit a pull request.
251 |
252 | To get started:
253 |
254 | 1. Fork the repository and clone it locally.
255 | 2. Install dependencies: `npm install`
256 | 3. Create a new branch: `git checkout -b feature-name`
257 | 4. Make your changes and commit: `git commit -m 'Add some feature'`
258 | 5. Push to your branch and open a pull request.
259 |
260 | ## License
261 |
262 | This project is licensed under the [MIT License](LICENSE).
263 |
264 | ## Support
265 |
266 | ⭐️ If you find this library useful, consider giving it a star on [GitHub](https://github.com/himalaya0035/react-json-editor-alt).
267 |
268 | ---
269 |
270 | Happy coding! If you have any questions or need further assistance, please don't hesitate to reach out.
271 |
272 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/index.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rje",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@radix-ui/react-icons": "^1.3.0",
14 | "@radix-ui/react-label": "^2.1.0",
15 | "@radix-ui/react-popover": "^1.1.1",
16 | "@radix-ui/react-radio-group": "^1.2.0",
17 | "@radix-ui/react-select": "^2.1.1",
18 | "@radix-ui/react-slot": "^1.1.0",
19 | "@radix-ui/react-tabs": "^1.1.0",
20 | "class-variance-authority": "^0.7.0",
21 | "clsx": "^2.1.1",
22 | "date-fns": "^3.6.0",
23 | "lucide-react": "^0.428.0",
24 | "react": "^18.3.1",
25 | "react-day-picker": "^8.10.1",
26 | "react-dom": "^18.3.1",
27 | "tailwind-merge": "^2.5.2",
28 | "tailwindcss-animate": "^1.0.7"
29 | },
30 | "devDependencies": {
31 | "@eslint/js": "^9.9.0",
32 | "@types/node": "^22.3.0",
33 | "@types/react": "^18.3.3",
34 | "@types/react-dom": "^18.3.0",
35 | "@vitejs/plugin-react": "^4.3.1",
36 | "autoprefixer": "^10.4.20",
37 | "eslint": "^9.9.0",
38 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
39 | "eslint-plugin-react-refresh": "^0.4.9",
40 | "globals": "^15.9.0",
41 | "postcss": "^8.4.41",
42 | "tailwindcss": "^3.4.10",
43 | "typescript": "^5.5.3",
44 | "typescript-eslint": "^8.0.1",
45 | "vite": "^5.4.1"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import JsonEditor from "./components/JsonEditor/jsonEditor";
3 | import {
4 | EditableFielsdObjectType,
5 | NonEditableFieldsObjectType,
6 | onChangePropsType,
7 | OnSubmitPropsType,
8 | } from "./types/JsonEditor.types";
9 | import { indianStatesOptions } from "./temp";
10 | import { Button } from "./components/ui/button";
11 | import { GLOBAL_EDITING_MODE } from "./constants/constants";
12 |
13 | function App() {
14 | const [isEditing, setIsEditing] = useState(false);
15 | const [isExpanded, setIsExpanded] = useState(false);
16 |
17 | const x: Record = {
18 | name: "Himalaya",
19 | surname: true,
20 | age : 23,
21 | testNumberField: 2292,
22 | testNullField: null,
23 | dob: "03/12/2000",
24 | about: "Hello, I am a software developer",
25 | address: {
26 | city: "Bareilly",
27 | state: "Uttar Pradesh",
28 | key: {
29 | _id: "66c3ab28dc5e92c6ea662626",
30 | name: "Allen Beck",
31 | gender: "male",
32 | company: "HAWKSTER",
33 | email: "allenbeck@hawkster.com",
34 | phone: 283839389,
35 | secondKey: {
36 | _id: "66c3ab28dc5e92c6ea662626",
37 | name: "Allen Beck",
38 | gender: "female",
39 | company: "HAWKSTER",
40 | email: "allenbeck@hawkster.com",
41 | phone: "+1 (866) 599-3761",
42 | },
43 | },
44 | },
45 | sampleData: [
46 | {
47 | _id: "66c3ab28dc5e92c6ea662626",
48 | name: "Allen Beck",
49 | gender: "male",
50 | company: "HAWKSTER",
51 | email: "allenbeck@hawkster.com",
52 | phone: "+1 (866) 599-3761",
53 | secondKey: {
54 | _id: "66c3ab28dc5e92c6ea662626",
55 | name: "Allen Beck",
56 | gender: "male",
57 | company: "HAWKSTER",
58 | email: "allenbeck@hawkster.com",
59 | phone: "+1 (866) 599-3761",
60 | thirdKey: {
61 | _id: "66c3ab28dc5e92c6ea662626",
62 | name: "Allen Beck",
63 | gender: "male",
64 | company: "HAWKSTER",
65 | email: "allenbeck@hawkster.com",
66 | phone: "+1 (866) 599-3761",
67 | },
68 | },
69 | },
70 | {
71 | _id: "66c3ab28cdadb9ffd1e92675",
72 | name: "Walters Mullen",
73 | gender: "male",
74 | company: "BOILICON",
75 | email: "waltersmullen@boilicon.com",
76 | phone: "+1 (911) 573-2834",
77 | },
78 | ],
79 | hobbies: ["Movies", "Music"],
80 | };
81 |
82 | const editbaleFieldsObject: EditableFielsdObjectType = {
83 | dob: {
84 | type: "date",
85 | format: "DD/MM/YYYY",
86 | },
87 | age : {
88 | type : "number",
89 | validations : {
90 | minValue:1,
91 | maxValue: 50,
92 | }
93 | },
94 | about: {
95 | type: "textArea",
96 | validations : {
97 | minLength : 1,
98 | }
99 | },
100 | "address.state": {
101 | type: "select",
102 | options: indianStatesOptions,
103 | },
104 | "address.key.phone": {
105 | type: "number"
106 | },
107 | "sampleData.[].email": {
108 | type: "string",
109 | validations: {
110 | regex: /^[^@]+@[^@]+\.[^@]+$/,
111 | regexValidationMessage: "Please enter a valid email address.",
112 | },
113 | },
114 | "address.key.gender": {
115 | type: "radio",
116 | options: [
117 | { key: "male", value: "male" },
118 | { key: "female", value: "female" },
119 | { key: "others", value: "others" },
120 | ],
121 | },
122 | "address.key.secondKey.gender": {
123 | type: "radio",
124 | options: [
125 | { key: "male", value: "male" },
126 | { key: "female", value: "female" },
127 | { key: "others", value: "others" },
128 | ],
129 | },
130 | "sampleData.[].secondKey.gender": {
131 | type: "radio",
132 | options: [
133 | { key: "male", value: "male" },
134 | { key: "female", value: "female" },
135 | { key: "other", value: "other" },
136 | ],
137 | },
138 | "sampleData.[].gender" : {
139 | type : "radio",
140 | options: [
141 | { key: "male", value: "male" },
142 | { key: "female", value: "female" },
143 | { key: "other", value: "other" },
144 | ],
145 | },
146 | "sampleData.[].name" : {
147 | type : "textArea",
148 | validations : {
149 | maxLength : 12
150 | }
151 | },
152 | "hobbies.[]" : {
153 | type: "string",
154 | validations: {
155 | maxLength : 20
156 | }
157 | },
158 | "hobbies.1" : {
159 | type : "textArea",
160 | }
161 | };
162 |
163 | const nonEditbaleFieldObject: NonEditableFieldsObjectType = {};
164 |
165 | const onSubmit = (props : OnSubmitPropsType) => {
166 | console.info(props)
167 | if (props.submitType === GLOBAL_EDITING_MODE){
168 | setIsEditing(false)
169 | }
170 | }
171 |
172 | const onChange = (props : onChangePropsType) => {
173 | console.info(props)
174 | }
175 |
176 | return (
177 |
178 |
Json Editor
179 |
180 | setIsEditing(!isEditing)}>
181 | {isEditing ? "Read Mode" : "Edit Mode"}
182 |
183 | setIsExpanded(!isExpanded)}>
184 | {isExpanded ? "Collapse" : "Expand"}
185 |
186 |
187 |
203 |
204 | );
205 | }
206 |
207 | export default App;
208 |
--------------------------------------------------------------------------------
/src/assets/GlobaModeExample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/himalaya0035/react-json-editor-alt/b4a0267a334ce9e3eb06db3653d848365da5f5b9/src/assets/GlobaModeExample.png
--------------------------------------------------------------------------------
/src/assets/GlobalIndividiualModeExample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/himalaya0035/react-json-editor-alt/b4a0267a334ce9e3eb06db3653d848365da5f5b9/src/assets/GlobalIndividiualModeExample.png
--------------------------------------------------------------------------------
/src/assets/IndividualModeExample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/himalaya0035/react-json-editor-alt/b4a0267a334ce9e3eb06db3653d848365da5f5b9/src/assets/IndividualModeExample.png
--------------------------------------------------------------------------------
/src/assets/InlineModeExample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/himalaya0035/react-json-editor-alt/b4a0267a334ce9e3eb06db3653d848365da5f5b9/src/assets/InlineModeExample.png
--------------------------------------------------------------------------------
/src/assets/collapsed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/himalaya0035/react-json-editor-alt/b4a0267a334ce9e3eb06db3653d848365da5f5b9/src/assets/collapsed.png
--------------------------------------------------------------------------------
/src/assets/expanded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/himalaya0035/react-json-editor-alt/b4a0267a334ce9e3eb06db3653d848365da5f5b9/src/assets/expanded.png
--------------------------------------------------------------------------------
/src/components/JsonEditor/defaultElements/defaultBooleanInput.tsx:
--------------------------------------------------------------------------------
1 | import { Check } from "lucide-react";
2 | import { GLOBAL_EDITING_MODE, INLINE_EDITING_MODE } from "../../../constants/constants";
3 | import { DefaultBooleanElementProps } from "../../../types/JsonEditor.types";
4 | import { Button } from "../../ui/button";
5 | import { Tabs, TabsList, TabsTrigger } from "../../ui/tabs";
6 | import InlineCancelButton from "../inlineElements/inlineCancelButton";
7 | import { useJsonEditorContext } from "../jsonEditor";
8 | import ResetButton from "../inlineElements/resetButton";
9 |
10 | function DefaultBooleanInput({ value, readModeValue, path }: DefaultBooleanElementProps) {
11 | const {
12 | editingMode,
13 | handleOnChange,
14 | handleOnSubmit,
15 | setSelectedFieldsForEditing,
16 | } = useJsonEditorContext();
17 |
18 | const booleanAsString =
19 | value === true ? "true" : value === false ? "false" : "";
20 |
21 | const handleBooleanInputChange = (selectedValue: string) => {
22 | const stringAsBoolean = selectedValue === "true" ? true : false;
23 | handleOnChange(stringAsBoolean, path);
24 | };
25 |
26 | const handleBooleanInputSubmit = () => {
27 | handleOnSubmit(value, path);
28 | if (editingMode === INLINE_EDITING_MODE) {
29 | setSelectedFieldsForEditing((prev) => {
30 | return {
31 | ...prev,
32 | [path]: false,
33 | };
34 | });
35 | }
36 | };
37 |
38 | const disabled = readModeValue === value;
39 |
40 | return (
41 | <>
42 |
46 |
47 | True
48 | False
49 |
50 |
51 | {editingMode !== GLOBAL_EDITING_MODE && (
52 |
60 |
61 |
62 | )}
63 | {editingMode === INLINE_EDITING_MODE && }
64 | {(editingMode !== INLINE_EDITING_MODE && !disabled) && }
65 | >
66 | );
67 | }
68 |
69 | export default DefaultBooleanInput;
70 |
--------------------------------------------------------------------------------
/src/components/JsonEditor/defaultElements/defaultDateInput.tsx:
--------------------------------------------------------------------------------
1 | import { Check } from "lucide-react";
2 | import { convertDateIntoPattern, convertPatternIntoDate } from "../../../functions/functions";
3 | import { DefaultDateElementProps } from "../../../types/JsonEditor.types";
4 | import { Button } from "../../ui/button";
5 | import { DatePicker } from "../../ui/datePicker";
6 | import { useJsonEditorContext } from "../jsonEditor";
7 | import InlineCancelButton from "../inlineElements/inlineCancelButton";
8 | import { GLOBAL_EDITING_MODE, INLINE_EDITING_MODE } from "../../../constants/constants";
9 | import ResetButton from "../inlineElements/resetButton";
10 |
11 | function DefaultDateInput({value,readModeValue,path,format}: DefaultDateElementProps) {
12 | const {
13 | handleOnChange,
14 | handleOnSubmit,
15 | editingMode,
16 | setSelectedFieldsForEditing,
17 | } = useJsonEditorContext();
18 |
19 | const dateValue = convertPatternIntoDate(value,format)
20 | if (!dateValue){
21 | return "Invalid Date"
22 | }
23 |
24 | const handleDateInputChange = (selectedDate : Date | undefined) => {
25 | const dateString = convertDateIntoPattern(selectedDate as Date,format)
26 | if (dateString){
27 | handleOnChange(dateString,path)
28 | }
29 | };
30 |
31 | const handleDateInputSubmit = () => {
32 | handleOnSubmit(value, path);
33 | if (editingMode === INLINE_EDITING_MODE) {
34 | setSelectedFieldsForEditing((prev) => {
35 | return {
36 | ...prev,
37 | [path]: false,
38 | };
39 | });
40 | }
41 | };
42 |
43 | let disabled = readModeValue === value;
44 |
45 | return (
46 | <>
47 |
48 | {editingMode !== GLOBAL_EDITING_MODE && (
49 |
57 |
58 |
59 | )}
60 | {editingMode === INLINE_EDITING_MODE && }
61 | {(editingMode !== INLINE_EDITING_MODE && !disabled) && }
62 | >
63 | );
64 |
65 |
66 | }
67 |
68 | export default DefaultDateInput;
--------------------------------------------------------------------------------
/src/components/JsonEditor/defaultElements/defaultNumberInput.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 | import { DefaultNumberElementProps } from "../../../types/JsonEditor.types";
3 | import { Input } from "../../ui/input";
4 | import { debounce, validateValue } from "../../../functions/functions";
5 | import { DEBOUNCE_DELAY, GLOBAL_EDITING_MODE, INLINE_EDITING_MODE } from "../../../constants/constants";
6 | import { Button } from "../../ui/button";
7 | import { Check } from "lucide-react";
8 | import { useJsonEditorContext } from "../jsonEditor";
9 | import InlineCancelButton from "../inlineElements/inlineCancelButton";
10 | import ResetButton from "../inlineElements/resetButton";
11 |
12 | function DefaultNumberInput({
13 | value,
14 | readModeValue,
15 | path,
16 | fieldValidations
17 | }: DefaultNumberElementProps) {
18 | const [numberInputValue, setNumberInputValue] = useState(value);
19 | const [localValidationError, setLocalValidationError] = useState('')
20 | const {
21 | handleOnChange,
22 | handleOnSubmit,
23 | editingMode,
24 | setSelectedFieldsForEditing,
25 | validations,
26 | setValidations,
27 | debouncing
28 | } = useJsonEditorContext();
29 |
30 | const handleNumberInputChange = (e: React.ChangeEvent) => {
31 | const value = e.target.value;
32 | let result = null;
33 | if (fieldValidations){
34 | result = validateValue(value,fieldValidations)
35 | setLocalValidationError(result || '')
36 | }
37 | setNumberInputValue(value);
38 | debouncedOnChange(value,result || "");
39 | };
40 |
41 | // Memoize debounced onChange with useCallback, recreating when onChange updates.
42 | // This prevents stale closures and ensures the component uses the latest onChange.
43 | const debouncedOnChange = useCallback(
44 | debounce((value: string,validationMessage? : string) => {
45 | const updatedValidations = {
46 | ...validations,
47 | [path] : validationMessage
48 | }
49 | handleOnChange(Number(value), path,updatedValidations);
50 | if (fieldValidations){
51 | setValidations(prev => {
52 | return {
53 | ...prev,
54 | [path] : validationMessage
55 | }
56 | })
57 | }
58 | }, debouncing ? DEBOUNCE_DELAY : 0),
59 | [handleOnChange]
60 | );
61 |
62 | const handleNumberInputSubmit = () => {
63 | handleOnSubmit(Number(numberInputValue), path);
64 | if (editingMode === INLINE_EDITING_MODE) {
65 | setSelectedFieldsForEditing((prev) => {
66 | return {
67 | ...prev,
68 | [path]: false,
69 | };
70 | });
71 | }
72 | };
73 |
74 | const disabled = readModeValue === Number(numberInputValue);
75 | const validationMessage = localValidationError || validations[path]
76 |
77 | return (
78 | <>
79 |
84 | {editingMode !== GLOBAL_EDITING_MODE && !validationMessage && (
85 |
93 |
94 |
95 | )}
96 | {editingMode === INLINE_EDITING_MODE && }
97 | {(editingMode !== INLINE_EDITING_MODE && !disabled) && {
98 | setNumberInputValue(readModeValue as number)
99 | setLocalValidationError("")
100 | }} />}
101 | {validationMessage}
102 | >
103 | );
104 | }
105 |
106 | export default DefaultNumberInput;
107 |
--------------------------------------------------------------------------------
/src/components/JsonEditor/defaultElements/defaultRadioInput.tsx:
--------------------------------------------------------------------------------
1 | import { RadioGroup, RadioGroupItem } from "../../ui/radio-group";
2 | import { Label } from "../../ui/label";
3 | import { DefaultRadioElementProps } from "../../../types/JsonEditor.types";
4 | import { Button } from "../../ui/button";
5 | import { Check } from "lucide-react";
6 | import { useJsonEditorContext } from "../jsonEditor";
7 | import InlineCancelButton from "../inlineElements/inlineCancelButton";
8 | import { GLOBAL_EDITING_MODE, INLINE_EDITING_MODE } from "../../../constants/constants";
9 | import ResetButton from "../inlineElements/resetButton";
10 |
11 | function DefaultRadioInput({
12 | value,
13 | readModeValue,
14 | path,
15 | options,
16 | }: DefaultRadioElementProps) {
17 | const {
18 | handleOnChange,
19 | handleOnSubmit,
20 | editingMode,
21 | setSelectedFieldsForEditing,
22 | } = useJsonEditorContext();
23 |
24 | const handleRadioInputChange = (selectedValue: string) => {
25 | handleOnChange(selectedValue, path);
26 | };
27 |
28 | const handleRadioInputSubmit = () => {
29 | handleOnSubmit(value, path);
30 | if (editingMode === INLINE_EDITING_MODE) {
31 | setSelectedFieldsForEditing((prev) => {
32 | return {
33 | ...prev,
34 | [path]: false,
35 | };
36 | });
37 | }
38 | };
39 |
40 | let disabled = readModeValue === value;
41 |
42 | return (
43 | <>
44 |
49 | {options.map((option) => (
50 |
51 |
52 | {option.value}
53 |
54 | ))}
55 |
56 | {editingMode !== GLOBAL_EDITING_MODE && (
57 |
65 |
66 |
67 | )}
68 | {editingMode === INLINE_EDITING_MODE && }
69 | {(editingMode !== INLINE_EDITING_MODE && !disabled) && }
70 | >
71 | );
72 | }
73 |
74 | export default DefaultRadioInput;
75 |
--------------------------------------------------------------------------------
/src/components/JsonEditor/defaultElements/defaultSelectInput.tsx:
--------------------------------------------------------------------------------
1 | import { Check } from "lucide-react";
2 | import { DefaultSelectElementProps } from "../../../types/JsonEditor.types";
3 | import { Button } from "../../ui/button";
4 | import {
5 | Select,
6 | SelectContent,
7 | SelectItem,
8 | SelectTrigger,
9 | SelectValue,
10 | } from "../../ui/select";
11 | import { useJsonEditorContext } from "../jsonEditor";
12 | import InlineCancelButton from "../inlineElements/inlineCancelButton";
13 | import { GLOBAL_EDITING_MODE, INLINE_EDITING_MODE } from "../../../constants/constants";
14 | import ResetButton from "../inlineElements/resetButton";
15 |
16 | function DefaultSelectInput({
17 | value,
18 | readModeValue,
19 | path,
20 | options,
21 | }: DefaultSelectElementProps) {
22 | const {
23 | handleOnChange,
24 | handleOnSubmit,
25 | editingMode,
26 | setSelectedFieldsForEditing,
27 | } = useJsonEditorContext();
28 |
29 | const handleSelectInputChange = (selectedValue: string) => {
30 | handleOnChange(selectedValue, path);
31 | };
32 |
33 | const handleSelectInputSubmit = () => {
34 | handleOnSubmit(value, path);
35 | if (editingMode === INLINE_EDITING_MODE) {
36 | setSelectedFieldsForEditing((prev) => {
37 | return {
38 | ...prev,
39 | [path]: false,
40 | };
41 | });
42 | }
43 | };
44 |
45 | let disabled = readModeValue === value;
46 |
47 | return (
48 | <>
49 |
50 |
51 |
52 |
53 |
54 | {options?.map((option, index) => {
55 | return (
56 |
57 | {option.value}
58 |
59 | );
60 | })}
61 |
62 |
63 | {editingMode !== GLOBAL_EDITING_MODE && (
64 |
72 |
73 |
74 | )}
75 | {editingMode === INLINE_EDITING_MODE && }
76 | {(editingMode !== INLINE_EDITING_MODE && !disabled) && }
77 | >
78 | );
79 | }
80 |
81 | export default DefaultSelectInput;
82 |
--------------------------------------------------------------------------------
/src/components/JsonEditor/defaultElements/defaultTextAreaInput.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 | import { DefaultTextAreaElementProps } from "../../../types/JsonEditor.types";
3 | import { Textarea } from "../../ui/textarea";
4 | import { debounce, validateValue } from "../../../functions/functions";
5 | import { DEBOUNCE_DELAY, GLOBAL_EDITING_MODE, INLINE_EDITING_MODE } from "../../../constants/constants";
6 | import { Button } from "../../ui/button";
7 | import { Check } from "lucide-react";
8 | import { useJsonEditorContext } from "../jsonEditor";
9 | import InlineCancelButton from "../inlineElements/inlineCancelButton";
10 | import ResetButton from "../inlineElements/resetButton";
11 |
12 | function DefaultTextAreaElement({
13 | value,
14 | readModeValue,
15 | path,
16 | fieldValidations
17 | }: DefaultTextAreaElementProps) {
18 | const [textAreaInputValue, setTextAreaInputValue] = useState(value);
19 | const [localValidationError, setLocalValidationError] = useState('')
20 | const {
21 | handleOnChange,
22 | handleOnSubmit,
23 | editingMode,
24 | setSelectedFieldsForEditing,
25 | validations,
26 | setValidations,
27 | debouncing
28 | } = useJsonEditorContext();
29 |
30 | const handleTextAreaInputChange = (
31 | e: React.ChangeEvent
32 | ) => {
33 | const value = e.target.value;
34 | let result = null;
35 | if (fieldValidations){
36 | result = validateValue(value,fieldValidations)
37 | setLocalValidationError(result || '')
38 | }
39 | setTextAreaInputValue(value);
40 | debouncedOnChange(value,result || "");
41 | };
42 |
43 | // Memoize debounced onChange with useCallback, recreating when onChange updates.
44 | // This prevents stale closures and ensures the component uses the latest onChange.
45 | const debouncedOnChange = useCallback(
46 | debounce((value: string,validationMessage? : string) => {
47 | const updatedValidations = {
48 | ...validations,
49 | [path] : validationMessage
50 | }
51 | handleOnChange(value, path,updatedValidations);
52 | if (fieldValidations){
53 | setValidations(prev => {
54 | return {
55 | ...prev,
56 | [path] : validationMessage
57 | }
58 | })
59 | }
60 | }, debouncing ? DEBOUNCE_DELAY : 0),
61 | [handleOnChange]
62 | );
63 |
64 | const handleTextAreaInputSubmit = () => {
65 | handleOnSubmit(textAreaInputValue, path);
66 | if (editingMode === INLINE_EDITING_MODE) {
67 | setSelectedFieldsForEditing((prev) => {
68 | return {
69 | ...prev,
70 | [path]: false,
71 | };
72 | });
73 | }
74 | };
75 |
76 | const disabled = readModeValue === textAreaInputValue;
77 | const validationMessage = localValidationError || validations[path]
78 |
79 | return (
80 | <>
81 |
85 | {editingMode !== GLOBAL_EDITING_MODE && !validationMessage && (
86 |
94 |
95 |
96 | )}
97 | {editingMode === INLINE_EDITING_MODE && }
98 | {(editingMode !== INLINE_EDITING_MODE && !disabled) && {
99 | setTextAreaInputValue(readModeValue || "" as string)
100 | setLocalValidationError("")
101 | }} />}
102 | {validationMessage}
103 | >
104 | );
105 | }
106 |
107 | export default DefaultTextAreaElement;
108 |
--------------------------------------------------------------------------------
/src/components/JsonEditor/defaultElements/defaultTextInput.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 | import { DefaultTextElementProps } from "../../../types/JsonEditor.types";
3 | import { Input } from "../../ui/input";
4 | import { debounce, validateValue } from "../../../functions/functions";
5 | import { DEBOUNCE_DELAY, GLOBAL_EDITING_MODE, INLINE_EDITING_MODE } from "../../../constants/constants";
6 | import { Check } from "lucide-react";
7 | import { Button } from "../../ui/button";
8 | import { useJsonEditorContext } from "../jsonEditor";
9 | import InlineCancelButton from "../inlineElements/inlineCancelButton";
10 | import ResetButton from "../inlineElements/resetButton";
11 |
12 | function DefaultTextInput({
13 | value,
14 | readModeValue,
15 | path,
16 | fieldValidations
17 | }: DefaultTextElementProps) {
18 | const [textInputValue, setTextInputValue] = useState(value);
19 | const [localValidationError, setLocalValidationError] = useState('')
20 | const {
21 | handleOnChange,
22 | handleOnSubmit,
23 | editingMode,
24 | setSelectedFieldsForEditing,
25 | validations,
26 | setValidations,
27 | debouncing
28 | } = useJsonEditorContext();
29 |
30 | const handleTextInputChange = (e: React.ChangeEvent) => {
31 | const value = e.target.value;
32 | let result = null;
33 | if (fieldValidations){
34 | result = validateValue(value,fieldValidations)
35 | setLocalValidationError(result || '')
36 | }
37 | setTextInputValue(value);
38 | debouncedOnChange(value,result || "");
39 | };
40 |
41 | // Memoize debounced onChange with useCallback, recreating when onChange updates.
42 | // This prevents stale closures and ensures the component uses the latest onChange.
43 | const debouncedOnChange = useCallback(
44 | debounce((value: string,validationMessage? : string) => {
45 | const updatedValidations = {
46 | ...validations,
47 | [path] : validationMessage
48 | }
49 | handleOnChange(value, path,updatedValidations);
50 | if (fieldValidations){
51 | setValidations(prev => {
52 | return {
53 | ...prev,
54 | [path] : validationMessage
55 | }
56 | })
57 | }
58 | }, debouncing ? DEBOUNCE_DELAY : 0),
59 | [handleOnChange]
60 | );
61 |
62 | const handleTextInputSubmit = () => {
63 | handleOnSubmit(textInputValue, path);
64 | if (editingMode === INLINE_EDITING_MODE) {
65 | setSelectedFieldsForEditing((prev) => {
66 | return {
67 | ...prev,
68 | [path]: false,
69 | };
70 | });
71 | }
72 | };
73 |
74 | const disabled = readModeValue === textInputValue;
75 | const validationMessage = localValidationError || validations[path]
76 |
77 | return (
78 | <>
79 |
80 | {editingMode !== GLOBAL_EDITING_MODE && !validationMessage && (
81 |
89 |
90 |
91 | )}
92 | {editingMode === INLINE_EDITING_MODE && }
93 | {(editingMode !== INLINE_EDITING_MODE && !disabled) && {
94 | setTextInputValue(readModeValue || "" as string)
95 | setLocalValidationError("")
96 | }} />}
97 | {validationMessage}
98 | >
99 | );
100 | }
101 |
102 | export default DefaultTextInput;
103 |
--------------------------------------------------------------------------------
/src/components/JsonEditor/defaultElements/defaultValueElement.tsx:
--------------------------------------------------------------------------------
1 | import { DefaultValueElementProps } from "../../../types/JsonEditor.types";
2 | import InlineEditButton from "../inlineElements/inlineEditButton";
3 |
4 | function DefaultValueElement({ value, path, canEditInline}: DefaultValueElementProps) {
5 |
6 | const getFormattedContent = () => {
7 | if (value === undefined) {
8 | return { text: 'Undefined', style: 'text-gray-500' };
9 | }
10 | if (value === null) {
11 | return { text: 'Null', style: 'text-rose-500' };
12 | }
13 | if (value === true) {
14 | return { text: 'True', style: 'text-emerald-500' };
15 | }
16 | if (value === false) {
17 | return { text: 'False', style: 'text-amber-500' };
18 | }
19 | if (value === '') {
20 | return { text: 'Empty', style: 'text-indigo-500' };
21 | }
22 | return { text: value, style : ''};
23 | };
24 |
25 | const { text, style } = getFormattedContent();
26 |
27 | return (
28 | <>
29 | {text}
30 | {canEditInline && }
31 | >
32 | );
33 | }
34 |
35 | export default DefaultValueElement;
36 |
--------------------------------------------------------------------------------
/src/components/JsonEditor/inlineElements/inlineCancelButton.tsx:
--------------------------------------------------------------------------------
1 | import { INLINE_EDITING_MODE } from "../../../constants/constants";
2 | import { Button } from "../../ui/button";
3 | import { useJsonEditorContext } from "../jsonEditor";
4 | import { Cross1Icon } from "@radix-ui/react-icons";
5 |
6 | type InlineCancelButtonProps = {
7 | path: string;
8 | };
9 |
10 | // Implemented dedicated cancel button for each input type to ensure plug-and-play functionality.
11 | // This avoids repetitive function definitions and simplifies adding new input types.
12 | const InlineCancelButton = ({ path }: InlineCancelButtonProps) => {
13 | const {
14 | editingMode,
15 | selectedFieldsForEditing,
16 | setSelectedFieldsForEditing,
17 | handleFieldReset,
18 | } = useJsonEditorContext();
19 |
20 | if (editingMode !== INLINE_EDITING_MODE) {
21 | return null;
22 | }
23 |
24 | const handleInlineCancelButtonClick = () => {
25 | if (selectedFieldsForEditing && setSelectedFieldsForEditing) {
26 | setSelectedFieldsForEditing((prev) => ({
27 | ...prev,
28 | [path]: false,
29 | }));
30 | handleFieldReset(path)
31 | }
32 | };
33 |
34 | return (
35 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default InlineCancelButton;
--------------------------------------------------------------------------------
/src/components/JsonEditor/inlineElements/inlineEditButton.tsx:
--------------------------------------------------------------------------------
1 | import { Edit2Icon } from "lucide-react";
2 | import { Button } from "../../ui/button";
3 | import { useJsonEditorContext } from "../jsonEditor";
4 |
5 | type InlineEditButtonProps = {
6 | path: string;
7 | };
8 |
9 | // Implemented dedicated edit button for each input type to ensure plug-and-play functionality.
10 | // This avoids repetitive function definitions and simplifies adding new input types.
11 | const InlineEditButton = ({ path }: InlineEditButtonProps) => {
12 | const { selectedFieldsForEditing, setSelectedFieldsForEditing } =
13 | useJsonEditorContext();
14 |
15 | const handleInlineEditButtonClick = () => {
16 | if (selectedFieldsForEditing && setSelectedFieldsForEditing) {
17 | setSelectedFieldsForEditing((prev) => ({
18 | ...prev,
19 | [path]: true,
20 | }));
21 | }
22 | };
23 |
24 | return (
25 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default InlineEditButton;
--------------------------------------------------------------------------------
/src/components/JsonEditor/inlineElements/resetButton.tsx:
--------------------------------------------------------------------------------
1 | import { INLINE_EDITING_MODE } from "../../../constants/constants";
2 | import { Button } from "../../ui/button";
3 | import { useJsonEditorContext } from "../jsonEditor";
4 | import { ResetIcon } from "@radix-ui/react-icons";
5 |
6 | type ResetButtonProps = {
7 | path: string;
8 | callBack?: () => void
9 | };
10 |
11 | // Implemented dedicated cancel button for each input type to ensure plug-and-play functionality.
12 | // This avoids repetitive function definitions and simplifies adding new input types.
13 | const ResetButton = ({ path, callBack }: ResetButtonProps) => {
14 | const {
15 | editingMode,
16 | handleFieldReset,
17 | } = useJsonEditorContext();
18 |
19 | if (editingMode === INLINE_EDITING_MODE) {
20 | return null;
21 | }
22 |
23 | const handleResetButtonClick = () => {
24 | handleFieldReset(path)
25 | if (callBack){
26 | callBack()
27 | }
28 | };
29 |
30 | return (
31 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default ResetButton;
--------------------------------------------------------------------------------
/src/components/JsonEditor/jsonEditor.css:
--------------------------------------------------------------------------------
1 | span[data-collapse="true"] ~ .collapsible-identifier {
2 | display: none;
3 | }
4 | span[data-collapse="false"] ~ .collapsible-identifier {
5 | display: block;
6 | border-left: 2px solid #ccc;
7 | margin-left: 5px;
8 | }
9 |
10 | span[data-collapse="true"].collapsible-icon:before {
11 | content: '+';
12 | font-weight: bold;
13 | }
14 |
15 | span[data-collapse="false"].collapsible-icon::before {
16 | content: '-';
17 | font-weight: bold;
18 | }
--------------------------------------------------------------------------------
/src/components/JsonEditor/jsonEditor.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, CSSProperties, ReactNode, useContext, useRef, useState } from "react";
2 | import RenderObject from "./renderElements/renderObject";
3 | import RenderArray from "./renderElements/renderArray";
4 | import RenderValue from "./renderElements/renderValue";
5 | import {
6 | EditingConfig,
7 | GlobalSubmitButtonConfigs,
8 | HandleOnChange,
9 | HandleOnSubmit,
10 | JsonEditorContextType,
11 | JsonEditorProps,
12 | } from "../../types/JsonEditor.types";
13 | import {
14 | deepCopy,
15 | deepEqual,
16 | findJsonDiff,
17 | getValueByPath,
18 | pathToRegex,
19 | updateValueByPath,
20 | } from "../../functions/functions";
21 | import { cn } from "../../lib/utils";
22 | import { Button } from "../ui/button";
23 | import {
24 | ARRAY_PATH_IDENTIFIER,
25 | GLOBAL_EDITING_MODE,
26 | GLOBAL_INDIVIDUAL_EDITING_MODE,
27 | INDIVIDUAL_EDITING_MODE,
28 | INLINE_EDITING_MODE,
29 | } from "../../constants/constants";
30 | import "./jsonEditor.css";
31 | import { RegexTrie } from "../../utils/regexTrie";
32 |
33 | const JsonEditorContext = createContext({} as JsonEditorContextType);
34 | export const useJsonEditorContext = () => useContext(JsonEditorContext);
35 |
36 | function JsonEditor({
37 | json,
38 | className = '',
39 | isExpanded = false,
40 | onSubmit,
41 | onChange,
42 | editingConfig = {} as EditingConfig,
43 | globalSubmitButtonConfigs = {} as GlobalSubmitButtonConfigs,
44 | styles = {} as CSSProperties
45 | }: JsonEditorProps) {
46 | const [jsonState, setJsonState] = useState | null>(json);
47 | const [editJsonState, setEditJsonState] = useState | null>(json);
48 | const jsonRef = useRef(json)
49 | const [selectedFieldsForEditing, setSelectedFieldsForEditing] = useState>({})
50 | const [validations, setValidations] = useState>({})
51 | const {
52 | editingMode = INLINE_EDITING_MODE,
53 | allFieldsEditable = true,
54 | editableFields = {},
55 | nonEditableFields = {},
56 | debouncing = false,
57 | enableTypeBasedRendering = true
58 | } = editingConfig;
59 |
60 | const regexPatternsTrie = useRef(new RegexTrie())
61 | const editableFieldsRef = useRef({})
62 | const nonEditableFieldsRef = useRef({})
63 |
64 | // // update jsonState when json changes
65 | if (!deepEqual(json,jsonRef.current)){ // to prevent infinite rerenders,
66 | // Calculating deepEqual on every render maybe costly, but necessary to update json state on changes.
67 | // In the future, we can offer users an option to enable or disable state updates on json changes.
68 | setJsonState(json)
69 | setEditJsonState(json)
70 | jsonRef.current = json
71 | }
72 |
73 | // only create/update regex paths trie when there is a change in editableFields or nonEditableFields
74 | if (!deepEqual(editableFieldsRef.current,editableFields) || !deepEqual(nonEditableFieldsRef.current,nonEditableFields)){
75 | for (let editableFieldPath in editableFields){
76 | // convert any paths in editableFields that includes [] into regex and store it in trie
77 | // example sample.[].name is stored as regex in trie
78 | // trie is used for efficient retrieval of regex later on in renderValue
79 | if (editableFieldPath.includes(ARRAY_PATH_IDENTIFIER)){
80 | const regex = pathToRegex(editableFieldPath)
81 | regexPatternsTrie.current.insert(editableFieldPath,regex)
82 | }
83 | };
84 | for (let nonEditableFieldPath in nonEditableFields){
85 | if (nonEditableFieldPath.includes(ARRAY_PATH_IDENTIFIER)){
86 | const regex = pathToRegex(nonEditableFieldPath)
87 | regexPatternsTrie.current.insert(nonEditableFieldPath,regex)
88 | }
89 | };
90 | editableFieldsRef.current = deepCopy(editableFields)
91 | nonEditableFieldsRef.current = deepCopy(nonEditableFields)
92 | }
93 |
94 | let isEditing = false;
95 | if ("isEditing" in editingConfig){
96 | isEditing = editingConfig.isEditing || false
97 | }
98 |
99 | const handleOnChange : HandleOnChange = (value,path, updatedValidations = validations) => {
100 | const tempEditJsonState = deepCopy(editJsonState)
101 | updateValueByPath(tempEditJsonState,path,value)
102 | if (onChange && jsonState){
103 | onChange({ // callback function exposed to lib consumer
104 | initialJson : deepCopy(editJsonState),
105 | updatedJson : tempEditJsonState,
106 | updatedKeys: findJsonDiff(jsonState,tempEditJsonState),
107 | editorMode : editingMode,
108 | validations: updatedValidations
109 | })
110 | }
111 | setEditJsonState(tempEditJsonState)
112 | };
113 |
114 | const handleOnSubmit : HandleOnSubmit = (value,path) => {
115 | const tempJsonState = deepCopy(jsonState)
116 | updateValueByPath(tempJsonState,path,value)
117 | if (onSubmit && jsonState){
118 | onSubmit({ // callback function exposed to lib consumer
119 | initialJson : deepCopy(jsonState),
120 | updatedJson : tempJsonState,
121 | updatedKeys: findJsonDiff(jsonState,tempJsonState),
122 | editorMode : editingMode,
123 | submitType : editingMode === GLOBAL_INDIVIDUAL_EDITING_MODE ? INDIVIDUAL_EDITING_MODE : editingMode
124 | })
125 | }
126 | setJsonState(tempJsonState)
127 | }
128 |
129 | const handleGlobalSubmit = () => {
130 | const tempEditJsonState = deepCopy(editJsonState)
131 | if (onSubmit && jsonState){
132 | onSubmit({ // callback function exposed to lib consumer
133 | initialJson : deepCopy(editJsonState),
134 | updatedJson : tempEditJsonState,
135 | updatedKeys: findJsonDiff(jsonState,tempEditJsonState),
136 | editorMode : editingMode,
137 | submitType : editingMode === GLOBAL_INDIVIDUAL_EDITING_MODE ? GLOBAL_EDITING_MODE : editingMode
138 | })
139 | }
140 | setJsonState(tempEditJsonState)
141 | }
142 |
143 | const handleFieldReset = (path: string) => {
144 | if (jsonState){
145 | const fieldOriginalValue = getValueByPath(jsonState,path)
146 | const tempEditJsonState = deepCopy(editJsonState)
147 | updateValueByPath(tempEditJsonState,path,fieldOriginalValue)
148 | setEditJsonState(tempEditJsonState)
149 | // clear out any validation error for that field
150 | setValidations(prev => {
151 | return {
152 | ...prev,
153 | [path] : ""
154 | }
155 | })
156 | }
157 | }
158 |
159 | const renderJson = (
160 | value: any,
161 | path = "",
162 | isRootLevelKey = true
163 | ): ReactNode => {
164 | if (typeof value === "object" && value !== null) {
165 | if (Array.isArray(value)) {
166 | return (
167 |
173 | );
174 | } else {
175 | return (
176 |
182 | );
183 | }
184 | } else {
185 | return (
186 |
190 | );
191 | }
192 | };
193 | const noOfValidationErrors = Object.values(validations).reduce((count, value) => {
194 | return value !== "" ? count + 1 : count;
195 | }, 0);
196 |
197 | return (
198 |
220 |
224 | {renderJson(jsonState)}
225 | {[GLOBAL_EDITING_MODE, GLOBAL_INDIVIDUAL_EDITING_MODE].includes(editingMode)
226 | && isEditing
227 | && noOfValidationErrors === 0
228 | && (
229 |
234 | {globalSubmitButtonConfigs?.buttonText || "Submit"}
235 | {globalSubmitButtonConfigs?.children}
236 |
237 | )}
238 |
239 |
240 | );
241 | }
242 |
243 | export default JsonEditor;
244 |
--------------------------------------------------------------------------------
/src/components/JsonEditor/renderElements/renderArray.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | RenderArrayItemsProp,
3 | RenderArrayProps,
4 | } from "../../../types/JsonEditor.types";
5 | import { useJsonEditorContext } from "../jsonEditor";
6 |
7 | function RenderArray({
8 | arr,
9 | path,
10 | isRootLevelKey,
11 | renderJson,
12 | }: RenderArrayProps) {
13 | const {isExpanded} = useJsonEditorContext();
14 |
15 | const toggleCollapsible = (element : HTMLElement) => {
16 | const isCollapsed = element.getAttribute("data-collapse") === "true";
17 | if (!isCollapsed) {
18 | element.setAttribute("data-collapse", "true");
19 | element.setAttribute("title", "Expand");
20 | } else {
21 | element.setAttribute("data-collapse", "false");
22 | element.setAttribute("title", "Collapse");
23 | }
24 | }
25 |
26 | return (
27 |
34 | {arr.map((val: any, index: number) => {
35 | return (
36 |
37 | {
41 | toggleCollapsible(e.target as HTMLElement)
42 | }}
43 | tabIndex={0}
44 | onKeyDown={(e) => {
45 | if (e.key === "Enter"){
46 | toggleCollapsible(e.target as HTMLElement)
47 | }
48 | }}
49 | className="cursor-pointer float-left min-w-2.5 px-2 rounded-sm mr-2 text-[0.9em] text-center bg-gray-200"
50 | >
51 | {index}:
52 | {" "}
53 | : {" "}
54 | {renderJson(val, `${path}.${index}`, true)}
55 |
56 | );
57 | })}
58 |
59 | );
60 | }
61 |
62 | export default RenderArray;
63 |
64 | function RenderArrayItems({ val, children }: RenderArrayItemsProp) {
65 | if (typeof val === "object" && val !== null) {
66 | return (
67 |
68 |
69 | {Array.isArray(val) ? "[" : "{"}
70 |
71 | {Object.keys(val).length + " props "}
72 |
73 |
74 | {children}
75 | {Array.isArray(val) ? "]," : "},"}
76 |
77 | );
78 | } else {
79 | return {children} ;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/JsonEditor/renderElements/renderObject.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | RenderObjectKeysProps,
3 | RenderObjectProps,
4 | } from "../../../types/JsonEditor.types";
5 | import { useJsonEditorContext } from "../jsonEditor";
6 |
7 | // recursively renders a object, here object is neither an array nor null
8 | function RenderObject({
9 | obj,
10 | path,
11 | renderJson,
12 | isRootLevelKey,
13 | searchText,
14 | }: RenderObjectProps) {
15 | return (
16 |
23 | {Object.entries(obj).map(([key, val], index) => {
24 | return (
25 |
31 | {renderJson(val, path ? `${path}.${key}` : key, false)}
32 |
33 | );
34 | })}
35 |
36 | );
37 | }
38 |
39 | export default RenderObject;
40 |
41 | function RenderObjectKeys({ keyName, val, children }: RenderObjectKeysProps) {
42 | const {isExpanded} = useJsonEditorContext();
43 |
44 | const toggleCollapsible = (element : HTMLElement) => {
45 | const isCollapsed = element.getAttribute("data-collapse") === "true";
46 | if (!isCollapsed) {
47 | element.setAttribute("data-collapse", "true");
48 | element.setAttribute("title", "Expand");
49 | } else {
50 | element.setAttribute("data-collapse", "false");
51 | element.setAttribute("title", "Collapse");
52 | }
53 | }
54 |
55 | if (typeof val === "object" && val !== null) {
56 | return (
57 |
58 | {
63 | if (e.key === "Enter"){
64 | toggleCollapsible(e.target as HTMLElement)
65 | }
66 | }}
67 | className="cursor-pointer inline-block w-5 h-5 leading-5 text-center mr-1.5 bg-gray-200 rounded-full transition-transform duration-300 collapsible-icon"
68 | onClick={(e) => {
69 | toggleCollapsible(e.target as HTMLElement)
70 | }}
71 | >
72 |
73 | {keyName}:{Array.isArray(val) ? " [ " : " { "}
74 |
75 | {Object.keys(val).length + " props "}
76 |
77 |
78 | {children}
79 | {Array.isArray(val) ? "]" : "}"}
80 |
81 | );
82 | } else {
83 | return (
84 |
85 | {keyName}:
86 | {children}
87 |
88 | );
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/components/JsonEditor/renderElements/renderValue.tsx:
--------------------------------------------------------------------------------
1 | import DefaultTextInput from "../defaultElements/defaultTextInput";
2 | import DefaultNumberInput from "../defaultElements/defaultNumberInput";
3 | import DefaultDateInput from "../defaultElements/defaultDateInput";
4 | import DefaultSelectInput from "../defaultElements/defaultSelectInput";
5 | import DefaultRadioInput from "../defaultElements/defaultRadioInput";
6 | import DefaultTextAreaElement from "../defaultElements/defaultTextAreaInput";
7 | import DefaultBooleanInput from "../defaultElements/defaultBooleanInput";
8 | import { containsArrayIndex, getValueByPath, testPathAgainstRegex } from "../../../functions/functions";
9 | import { FieldsType, RenderValueProps } from "../../../types/JsonEditor.types";
10 | import DefaultValueElement from "../defaultElements/defaultValueElement";
11 | import { useJsonEditorContext } from "../jsonEditor";
12 | import { INLINE_EDITING_MODE } from "../../../constants/constants";
13 |
14 | function RenderValue({
15 | value,
16 | path
17 | }: RenderValueProps) {
18 |
19 | const {
20 | isEditing,
21 | editingMode,
22 | editJsonState,
23 | editableFields,
24 | nonEditableFields,
25 | allFieldsEditable,
26 | selectedFieldsForEditing,
27 | regexPatternsTrie,
28 | enableTypeBasedRendering
29 | } = useJsonEditorContext();
30 |
31 | let resolvedPath = null
32 | if (containsArrayIndex(path) && !editableFields[path]){
33 | // resolving paths like sample.1.name to sample.[].name
34 | resolvedPath = testPathAgainstRegex(regexPatternsTrie.current,path)
35 | }
36 | if (!resolvedPath){
37 | resolvedPath = path
38 | }
39 |
40 | const isFieldPresentInEditabeLookup =
41 | editableFields && editableFields.hasOwnProperty(resolvedPath);
42 | const isFieldPresentInNonEditableLookup =
43 | nonEditableFields && nonEditableFields.hasOwnProperty(resolvedPath);
44 |
45 | // render a editable input field when:
46 | // the editor is in editing mode and,
47 | // either all fields are editable or the field is in the editableFields lookup and,
48 | // the field is not present in the nonEditableFields lookup
49 | // editingMode is not inline
50 | const canEditField =
51 | isEditing &&
52 | (allFieldsEditable || isFieldPresentInEditabeLookup) &&
53 | !isFieldPresentInNonEditableLookup &&
54 | editingMode !== INLINE_EDITING_MODE;
55 |
56 | // render a editable input field when:
57 | // the editingMode is inline and,
58 | // and the field is requested by user to be editable and,
59 | // the field is not present in the nonEditableFields lookup.
60 | const canEditInlineField =
61 | editingMode === INLINE_EDITING_MODE &&
62 | selectedFieldsForEditing[path] &&
63 | !isFieldPresentInNonEditableLookup;
64 |
65 | if (canEditField || canEditInlineField){
66 | let editableValue = getValueByPath(editJsonState, path);
67 | if (editableValue === null || editableValue === undefined){
68 | editableValue = ''
69 | }
70 | if (value === null || value == undefined){
71 | value = ''
72 | }
73 | if (
74 | isFieldPresentInEditabeLookup &&
75 | editableFields[resolvedPath] !== true
76 | ) {
77 | const editableField = editableFields[resolvedPath] as FieldsType;
78 | switch (editableField.type) {
79 | case "string": {
80 | const fieldValidations = editableField?.validations
81 | return (
82 |
88 | );
89 | }
90 | case "number": {
91 | const fieldValidations = editableField?.validations
92 | return (
93 |
99 | );
100 | }
101 | case "select": {
102 | return (
103 |
109 | );
110 | }
111 | case "date": {
112 | return (
113 |
119 | );
120 | }
121 | case "radio": {
122 | return (
123 |
129 | );
130 | }
131 | case "textArea": {
132 | const fieldValidations = editableField?.validations
133 | return (
134 |
140 | );
141 | }
142 | case "boolean": {
143 | return (
144 |
149 | );
150 | }
151 | default: {
152 | return (
153 |
158 | );
159 | }
160 | }
161 | } else {
162 | if (enableTypeBasedRendering){
163 | if (typeof value === "number"){
164 | return (
165 |
170 | )
171 | }else if (typeof value === "boolean"){
172 | return (
173 |
178 | )
179 | }
180 | }
181 | return (
182 |
187 | );
188 | }
189 | }
190 |
191 | const canEditInline =
192 | editingMode === INLINE_EDITING_MODE &&
193 | (allFieldsEditable || isFieldPresentInEditabeLookup) &&
194 | !isFieldPresentInNonEditableLookup;
195 | return (
196 |
201 | )
202 | }
203 |
204 | export default RenderValue;
205 |
--------------------------------------------------------------------------------
/src/components/test.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/himalaya0035/react-json-editor-alt/b4a0267a334ce9e3eb06db3653d848365da5f5b9/src/components/test.tsx
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "../../lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-7 w-7 rounded-[50%]",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/src/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
3 | import { DayPicker } from "react-day-picker"
4 |
5 | import { cn } from "../../lib/utils"
6 | import { buttonVariants } from "../ui/button"
7 |
8 | export type CalendarProps = React.ComponentProps
9 |
10 | function Calendar({
11 | className,
12 | classNames,
13 | showOutsideDays = true,
14 | ...props
15 | }: CalendarProps) {
16 | return (
17 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
41 | : "[&:has([aria-selected])]:rounded-md"
42 | ),
43 | day: cn(
44 | buttonVariants({ variant: "ghost" }),
45 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100"
46 | ),
47 | day_range_start: "day-range-start",
48 | day_range_end: "day-range-end",
49 | day_selected:
50 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
51 | day_today: "bg-accent text-accent-foreground",
52 | day_outside:
53 | "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
54 | day_disabled: "text-muted-foreground opacity-50",
55 | day_range_middle:
56 | "aria-selected:bg-accent aria-selected:text-accent-foreground",
57 | day_hidden: "invisible",
58 | ...classNames,
59 | }}
60 | components={{
61 | IconLeft: () => ,
62 | IconRight: () => ,
63 | }}
64 | {...props}
65 | />
66 | )
67 | }
68 | Calendar.displayName = "Calendar"
69 |
70 | export { Calendar }
71 |
--------------------------------------------------------------------------------
/src/components/ui/datePicker.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { CalendarIcon } from "@radix-ui/react-icons"
3 | import { format } from "date-fns"
4 |
5 | import { cn } from "../../lib/utils"
6 | import { Button } from "../ui/button"
7 | import { Calendar } from "../ui/calendar"
8 | import {
9 | Popover,
10 | PopoverContent,
11 | PopoverTrigger,
12 | } from "../ui/popover"
13 |
14 | type DatePickerProps = {
15 | dateValue : Date | undefined,
16 | onChange : (date : Date | undefined) => void
17 | }
18 |
19 | export function DatePicker({dateValue,onChange} : DatePickerProps) {
20 | const [date, setDate] = React.useState(dateValue)
21 |
22 | if (dateValue !== date){
23 | setDate(dateValue)
24 | }
25 |
26 | const handleDatePickerChange = (selectedDate : Date | undefined) => {
27 | onChange(selectedDate)
28 | setDate(selectedDate)
29 | }
30 |
31 | return (
32 |
33 |
34 |
41 |
42 | {date ? format(date, "PPP") : Pick a date }
43 |
44 |
45 |
46 |
52 |
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "../../lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "../../lib/utils"
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9 | )
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ))
22 | Label.displayName = LabelPrimitive.Root.displayName
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as PopoverPrimitive from "@radix-ui/react-popover"
3 |
4 | import { cn } from "../../lib/utils"
5 |
6 | const Popover = PopoverPrimitive.Root
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger
9 |
10 | const PopoverAnchor = PopoverPrimitive.Anchor
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
32 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { CheckIcon } from "@radix-ui/react-icons"
3 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
4 |
5 | import { cn } from "../../lib/utils"
6 |
7 | const RadioGroup = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => {
11 | return (
12 |
17 | )
18 | })
19 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
20 |
21 | const RadioGroupItem = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => {
25 | return (
26 |
34 |
35 |
36 |
37 |
38 | )
39 | })
40 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
41 |
42 | export { RadioGroup, RadioGroupItem }
43 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import {
3 | CaretSortIcon,
4 | CheckIcon,
5 | ChevronDownIcon,
6 | ChevronUpIcon,
7 | } from "@radix-ui/react-icons"
8 | import * as SelectPrimitive from "@radix-ui/react-select"
9 |
10 | import { cn } from "../../lib/utils"
11 |
12 | const Select = SelectPrimitive.Root
13 |
14 | const SelectGroup = SelectPrimitive.Group
15 |
16 | const SelectValue = SelectPrimitive.Value
17 |
18 | const SelectTrigger = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, children, ...props }, ref) => (
22 | span]:line-clamp-1",
26 | className
27 | )}
28 | {...props}
29 | >
30 | {children}
31 |
32 |
33 |
34 |
35 | ))
36 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
37 |
38 | const SelectScrollUpButton = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
50 |
51 |
52 | ))
53 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
54 |
55 | const SelectScrollDownButton = React.forwardRef<
56 | React.ElementRef,
57 | React.ComponentPropsWithoutRef
58 | >(({ className, ...props }, ref) => (
59 |
67 |
68 |
69 | ))
70 | SelectScrollDownButton.displayName =
71 | SelectPrimitive.ScrollDownButton.displayName
72 |
73 | const SelectContent = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, children, position = "popper", ...props }, ref) => (
77 |
78 |
89 |
90 |
97 | {children}
98 |
99 |
100 |
101 |
102 | ))
103 | SelectContent.displayName = SelectPrimitive.Content.displayName
104 |
105 | const SelectLabel = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SelectLabel.displayName = SelectPrimitive.Label.displayName
116 |
117 | const SelectItem = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, children, ...props }, ref) => (
121 |
129 |
130 |
131 |
132 |
133 |
134 | {children}
135 |
136 | ))
137 | SelectItem.displayName = SelectPrimitive.Item.displayName
138 |
139 | const SelectSeparator = React.forwardRef<
140 | React.ElementRef,
141 | React.ComponentPropsWithoutRef
142 | >(({ className, ...props }, ref) => (
143 |
148 | ))
149 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
150 |
151 | export {
152 | Select,
153 | SelectGroup,
154 | SelectValue,
155 | SelectTrigger,
156 | SelectContent,
157 | SelectLabel,
158 | SelectItem,
159 | SelectSeparator,
160 | SelectScrollUpButton,
161 | SelectScrollDownButton,
162 | }
163 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TabsPrimitive from "@radix-ui/react-tabs"
3 |
4 | import { cn } from "../../lib/utils"
5 |
6 | const Tabs = TabsPrimitive.Root
7 |
8 | const TabsList = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | TabsList.displayName = TabsPrimitive.List.displayName
22 |
23 | const TabsTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
35 | ))
36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
37 |
38 | const TabsContent = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
50 | ))
51 | TabsContent.displayName = TabsPrimitive.Content.displayName
52 |
53 | export { Tabs, TabsList, TabsTrigger, TabsContent }
54 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "../../lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/src/constants/constants.ts:
--------------------------------------------------------------------------------
1 | export const DEBOUNCE_DELAY = 300 // 300ms seems the most optimal
2 | export const INLINE_EDITING_MODE = 'inline'
3 | export const GLOBAL_INDIVIDUAL_EDITING_MODE = "global-individual"
4 | export const GLOBAL_EDITING_MODE = "global"
5 | export const INDIVIDUAL_EDITING_MODE = "individual"
6 | export const ARRAY_PATH_IDENTIFIER = '[]'
--------------------------------------------------------------------------------
/src/functions/functions.ts:
--------------------------------------------------------------------------------
1 | import { RegexTrie } from "../utils/regexTrie";
2 | import { DiffKeyValues, isNumberFieldValidations, NumberFieldValidations, TextFieldValidations } from "../types/JsonEditor.types";
3 |
4 | export const deepEqual = (obj1 : any, obj2 : any) => {
5 | if (obj1 === obj2) return true;
6 |
7 | if (typeof obj1 !== "object" || typeof obj2 !== "object") return false;
8 |
9 | const keys1 = Object.keys(obj1);
10 | const keys2 = Object.keys(obj2);
11 |
12 | if (keys1.length !== keys2.length) return false;
13 |
14 | for (let key of keys1) {
15 | if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
16 | return false;
17 | }
18 | }
19 |
20 | return true;
21 | };
22 |
23 | export const deepCopy = (obj:any) => JSON.parse(JSON.stringify(obj))
24 |
25 | export const removeArrayIndexFromPropertyPath = (path: string): string => {
26 | const segments = path.split(".");
27 | const simplifiedSegments = segments.map((segment) => {
28 | return isNaN(Number(segment)) ? segment : "";
29 | });
30 | const simplifiedPath = simplifiedSegments.join(".").replace(/\.\./g, ".");
31 | return simplifiedPath;
32 | };
33 |
34 |
35 | export const convertPatternIntoDate = (dateString: string, format: string): Date | null => {
36 | const formatPatterns: { [key: string]: RegExp } = {
37 | 'DD/MM/YYYY': /^(\d{2})\/(\d{2})\/(\d{4})$/,
38 | 'YYYY/MM/DD': /^(\d{4})\/(\d{2})\/(\d{2})$/,
39 | 'MM/DD/YYYY': /^(\d{2})\/(\d{2})\/(\d{4})$/,
40 | 'DD-MM-YYYY': /^(\d{2})-(\d{2})-(\d{4})$/,
41 | 'YYYY-MM-DD': /^(\d{4})-(\d{2})-(\d{2})$/,
42 | 'MM-DD-YYYY': /^(\d{2})-(\d{2})-(\d{4})$/
43 | };
44 |
45 | const pattern = formatPatterns[format];
46 | if (!pattern) {
47 | console.warn('Unsupported format. Returning null.');
48 | return null;
49 | }
50 |
51 | const match = dateString.match(pattern);
52 | if (!match) {
53 | console.warn('Invalid date string for the provided format. Returning null.');
54 | return null;
55 | }
56 |
57 | const [, part1, part2, part3] = match;
58 |
59 | let year: number, month: number, day: number;
60 |
61 | switch (format) {
62 | case 'DD/MM/YYYY':
63 | case 'DD-MM-YYYY':
64 | day = parseInt(part1, 10);
65 | month = parseInt(part2, 10) - 1; // Months are zero-indexed
66 | year = parseInt(part3, 10);
67 | break;
68 | case 'YYYY/MM/DD':
69 | case 'YYYY-MM-DD':
70 | year = parseInt(part1, 10);
71 | month = parseInt(part2, 10) - 1; // Months are zero-indexed
72 | day = parseInt(part3, 10);
73 | break;
74 | case 'MM/DD/YYYY':
75 | case 'MM-DD-YYYY':
76 | month = parseInt(part1, 10) - 1; // Months are zero-indexed
77 | day = parseInt(part2, 10);
78 | year = parseInt(part3, 10);
79 | break;
80 | default:
81 | console.warn('Unsupported format. Returning null.');
82 | return null;
83 | }
84 |
85 | return new Date(year, month, day);
86 | }
87 |
88 | export const convertDateIntoPattern = (date: Date, format: string): string | null => {
89 | const padZero = (num: number): string => num.toString().padStart(2, '0');
90 |
91 | const day = padZero(date.getDate());
92 | const month = padZero(date.getMonth() + 1);
93 | const year = date.getFullYear();
94 |
95 | switch (format) {
96 | case 'DD/MM/YYYY':
97 | return `${day}/${month}/${year}`;
98 | case 'YYYY/MM/DD':
99 | return `${year}/${month}/${day}`;
100 | case 'MM/DD/YYYY':
101 | return `${month}/${day}/${year}`;
102 | case 'DD-MM-YYYY':
103 | return `${day}-${month}-${year}`;
104 | case 'YYYY-MM-DD':
105 | return `${year}-${month}-${day}`;
106 | case 'MM-DD-YYYY':
107 | return `${month}-${day}-${year}`;
108 | default:
109 | console.warn('Unsupported format. Returning null');
110 | return null;
111 | }
112 | };
113 |
114 |
115 | export const updateValueByPath = (obj: any, path: string, value: any): void => {
116 | const keys: string[] = path.split(".");
117 |
118 | let current: any = obj;
119 | for (let i = 0; i < keys.length - 1; i++) {
120 | let key: string | number = keys[i];
121 | if (Array.isArray(current) && !isNaN(Number(key))) {
122 | key = parseInt(key, 10);
123 | }
124 | if (current[key] === undefined) {
125 | return;
126 | }
127 | current = current[key];
128 | }
129 | if (current) {
130 | current[keys[keys.length - 1]] = value;
131 | }
132 | };
133 |
134 | export const getValueByPath = (obj: any, path: string): any => {
135 | const keys: string[] = path.split(".");
136 |
137 | let current: any = obj;
138 | for (let key of keys) {
139 | if (Array.isArray(current) && !isNaN(Number(key))) {
140 | current = current[parseInt(key, 10)];
141 | } else {
142 | current = current[key];
143 | }
144 | if (current === undefined) {
145 | return undefined;
146 | }
147 | }
148 |
149 | return current;
150 | };
151 |
152 | export const findJsonDiff = (
153 | obj1: Record,
154 | obj2: Record,
155 | path: string = '',
156 | diffKeyValues: DiffKeyValues = {}
157 | ): DiffKeyValues => {
158 | const keys1 = Object.keys(obj1);
159 | const keys2 = Object.keys(obj2);
160 | const allKeys = Array.from(new Set([...keys1, ...keys2]));
161 |
162 | allKeys.forEach((key) => {
163 | const newPath = path ? `${path}.${key}` : key;
164 |
165 | if (obj1[key] && typeof obj1[key] === 'object' && obj2[key] && typeof obj2[key] === 'object') {
166 | findJsonDiff(obj1[key], obj2[key], newPath, diffKeyValues);
167 | } else if (obj1[key] !== obj2[key]) {
168 | diffKeyValues[newPath] = { initial: obj1[key], updated: obj2[key] };
169 | }
170 | });
171 |
172 | return diffKeyValues;
173 | }
174 |
175 | export const debounce = (func: (...args: any[]) => void, delay: number) => {
176 | let timeout: NodeJS.Timeout;
177 | return (...args: any[]) => {
178 | clearTimeout(timeout);
179 | timeout = setTimeout(() => {
180 | func(...args);
181 | }, delay);
182 | };
183 | };
184 |
185 | export const validateValue = (value: string, validations: TextFieldValidations | NumberFieldValidations): string | null => {
186 | if (validations.minLength && value.length < validations.minLength) {
187 | return validations.validationMessage || `Value must be at least ${validations.minLength} characters long.`;
188 | }
189 |
190 | if (validations.maxLength && value.length > validations.maxLength) {
191 | return validations.validationMessage || `Value must be no more than ${validations.maxLength} characters long.`;
192 | }
193 |
194 | if (isNumberFieldValidations(validations)){
195 | if (validations.maxValue && Number(value) > validations.maxValue) {
196 | return validations.validationMessage || `Value must be less than ${validations.maxValue}.`;
197 | }
198 |
199 | if (validations.minValue && Number(value) < validations.minValue) {
200 | return validations.validationMessage || `Value must be greater than ${validations.minValue}.`;
201 | }
202 | }
203 |
204 | if (validations.regex) {
205 | const regex = new RegExp(validations.regex);
206 | if (!regex.test(value)) {
207 | return validations.regexValidationMessage || "Value does not match the required pattern.";
208 | }
209 | }
210 |
211 | return null;
212 | };
213 |
214 | export const pathToRegex = (path: string): RegExp => {
215 | const regexString = path
216 | .replace(/\[\]/g, '\\d+')
217 | .replace(/\./g, '\\.')
218 | .replace(/\*/g, '.*');
219 | return new RegExp(`^${regexString}$`);
220 | };
221 |
222 | // Function to test a path against stored regex patterns (in trie) and return the matching path
223 | export const testPathAgainstRegex = (trie: RegexTrie, newPath: string): string | null => {
224 | const { regexPatterns, paths } = trie.findMatchingRegex(newPath);
225 | for (let i = 0; i < regexPatterns.length; i++) {
226 | if (regexPatterns[i].test(newPath)) {
227 | return paths[i]; // Return the matching original path
228 | }
229 | }
230 | return null;
231 | };
232 |
233 | export const containsArrayIndex = (path: string): boolean => {
234 | const segments = path.split('.');
235 | return segments.some(segment => !isNaN(Number(segment)));
236 | };
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 222.2 84% 4.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 222.2 84% 4.9%;
13 | --primary: 222.2 47.4% 11.2%;
14 | --primary-foreground: 210 40% 98%;
15 | --secondary: 210 40% 96.1%;
16 | --secondary-foreground: 222.2 47.4% 11.2%;
17 | --muted: 210 40% 96.1%;
18 | --muted-foreground: 215.4 16.3% 46.9%;
19 | --accent: 210 40% 96.1%;
20 | --accent-foreground: 222.2 47.4% 11.2%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 210 40% 98%;
23 | --border: 214.3 31.8% 91.4%;
24 | --input: 214.3 31.8% 91.4%;
25 | --ring: 222.2 84% 4.9%;
26 | --radius: 0.5rem;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 | }
33 |
34 | .dark {
35 | --background: 222.2 84% 4.9%;
36 | --foreground: 210 40% 98%;
37 | --card: 222.2 84% 4.9%;
38 | --card-foreground: 210 40% 98%;
39 | --popover: 222.2 84% 4.9%;
40 | --popover-foreground: 210 40% 98%;
41 | --primary: 210 40% 98%;
42 | --primary-foreground: 222.2 47.4% 11.2%;
43 | --secondary: 217.2 32.6% 17.5%;
44 | --secondary-foreground: 210 40% 98%;
45 | --muted: 217.2 32.6% 17.5%;
46 | --muted-foreground: 215 20.2% 65.1%;
47 | --accent: 217.2 32.6% 17.5%;
48 | --accent-foreground: 210 40% 98%;
49 | --destructive: 0 62.8% 30.6%;
50 | --destructive-foreground: 210 40% 98%;
51 | --border: 217.2 32.6% 17.5%;
52 | --input: 217.2 32.6% 17.5%;
53 | --ring: 212.7 26.8% 83.9%;
54 | --chart-1: 220 70% 50%;
55 | --chart-2: 160 60% 45%;
56 | --chart-3: 30 80% 55%;
57 | --chart-4: 280 65% 60%;
58 | --chart-5: 340 75% 55%;
59 | }
60 | }
61 |
62 | @layer base {
63 | * {
64 | @apply border-border;
65 | }
66 | body {
67 | @apply bg-background text-foreground;
68 | }
69 | }
70 |
71 | *{
72 | box-sizing: border-box;
73 | }
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './index.css'
5 |
6 | createRoot(document.getElementById('root')!).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/src/temp.ts:
--------------------------------------------------------------------------------
1 | export const indianStatesOptions = [
2 | { key: "Andhra Pradesh", value: "Andhra Pradesh" },
3 | { key: "Arunachal Pradesh", value: "Arunachal Pradesh" },
4 | { key: "Assam", value: "Assam" },
5 | { key: "Bihar", value: "Bihar" },
6 | { key: "Chhattisgarh", value: "Chhattisgarh" },
7 | { key: "Goa", value: "Goa" },
8 | { key: "Gujarat", value: "Gujarat" },
9 | { key: "Haryana", value: "Haryana" },
10 | { key: "Himachal Pradesh", value: "Himachal Pradesh" },
11 | { key: "Jharkhand", value: "Jharkhand" },
12 | { key: "Karnataka", value: "Karnataka" },
13 | { key: "Kerala", value: "Kerala" },
14 | { key: "Madhya Pradesh", value: "Madhya Pradesh" },
15 | { key: "Maharashtra", value: "Maharashtra" },
16 | { key: "Manipur", value: "Manipur" },
17 | { key: "Meghalaya", value: "Meghalaya" },
18 | { key: "Mizoram", value: "Mizoram" },
19 | { key: "Nagaland", value: "Nagaland" },
20 | { key: "Odisha", value: "Odisha" },
21 | { key: "Punjab", value: "Punjab" },
22 | { key: "Rajasthan", value: "Rajasthan" },
23 | { key: "Sikkim", value: "Sikkim" },
24 | { key: "Tamil Nadu", value: "Tamil Nadu" },
25 | { key: "Telangana", value: "Telangana" },
26 | { key: "Tripura", value: "Tripura" },
27 | { key: "Uttar Pradesh", value: "Uttar Pradesh" },
28 | { key: "Uttarakhand", value: "Uttarakhand" },
29 | { key: "West Bengal", value: "West Bengal" },
30 | ];
31 |
--------------------------------------------------------------------------------
/src/types/JsonEditor.types.ts:
--------------------------------------------------------------------------------
1 | import { CSSProperties } from "react";
2 | import {
3 | GLOBAL_EDITING_MODE,
4 | GLOBAL_INDIVIDUAL_EDITING_MODE,
5 | INDIVIDUAL_EDITING_MODE,
6 | INLINE_EDITING_MODE,
7 | } from "../constants/constants";
8 | import { RegexTrie } from "../utils/regexTrie";
9 |
10 | type LengthValidations = {
11 | minLength?: number;
12 | maxLength?: number;
13 | validationMessage? : string;
14 | regex?: never;
15 | regexValidationMessage?: never;
16 | };
17 |
18 | type RegexValidations = {
19 | regex: RegExp;
20 | regexValidationMessage?: string;
21 | minLength?: never;
22 | maxLength?: never;
23 | validationMessage? : never;
24 | };
25 |
26 | export type Validations = LengthValidations | RegexValidations;
27 | export type TextFieldValidations = Validations
28 | export type NumberFieldValidations = Validations & {
29 | minValue? : number,
30 | maxValue? : number
31 | };
32 |
33 | export function isNumberFieldValidations(validations: any): validations is NumberFieldValidations {
34 | return (
35 | typeof validations.minValue === 'number' ||
36 | typeof validations.maxValue === 'number'
37 | );
38 | }
39 |
40 | type NumberField = {
41 | type: "number";
42 | validations?: NumberFieldValidations
43 | };
44 |
45 | type StringField = {
46 | type: "string";
47 | validations?: TextFieldValidations;
48 | };
49 |
50 | type TextAreaField = {
51 | type: "textArea";
52 | validations?: TextFieldValidations;
53 | };
54 |
55 | type SelectField = {
56 | type: "select";
57 | options: Array<{ key: string; value: string }>;
58 | };
59 |
60 | type RadioField = {
61 | type: "radio";
62 | options: Array<{ key: string; value: string }>;
63 | };
64 |
65 | // type RootField = {
66 | // type: "root/*" | "root/**";
67 | // };
68 |
69 | type BooleanTypeField = {
70 | type: "boolean";
71 | };
72 |
73 | type DateFormat =
74 | | "DD/MM/YYYY"
75 | | "YYYY/MM/DD"
76 | | "MM/DD/YYYY"
77 | | "DD-MM-YYYY"
78 | | "YYYY-MM-DD"
79 | | "MM-DD-YYYY";
80 |
81 | type DateField = {
82 | type: "date";
83 | format: F;
84 | minDate?: F extends "DD/MM/YYYY" | "YYYY/MM/DD" | "MM/DD/YYYY"
85 | ? `${number}/${number}/${number}`
86 | : F extends "DD-MM-YYYY" | "YYYY-MM-DD" | "MM-DD-YYYY"
87 | ? `${number}-${number}-${number}`
88 | : never;
89 | maxDate?: F extends "DD/MM/YYYY" | "YYYY/MM/DD" | "MM/DD/YYYY"
90 | ? `${number}/${number}/${number}`
91 | : F extends "DD-MM-YYYY" | "YYYY-MM-DD" | "MM-DD-YYYY"
92 | ? `${number}-${number}-${number}`
93 | : never;
94 | };
95 |
96 | export type FieldsType = | StringField
97 | | NumberField
98 | | SelectField
99 | | RadioField
100 | | TextAreaField
101 | | DateField
102 | // | RootField
103 | | BooleanTypeField;
104 |
105 | export type EditableFielsdObjectType = {
106 | [path: string]:
107 | | true
108 | | FieldsType
109 | };
110 |
111 | export type NonEditableFieldsObjectType = {
112 | [path: string]: true
113 | // | RootField;
114 | };
115 |
116 | export type DiffKeyValues = {
117 | [key: string]: { initial: string; updated: string };
118 | }
119 |
120 | type EditorMode = 'global' | 'individual' | 'global-individual' | 'inline'
121 |
122 | // Type definition for callback functions exposed to the library consumer.
123 | // The consumer receives initialJson, finalJson, and updatedKeys on events like onChange and onSubmit.
124 | export type onChangePropsType = {
125 | initialJson : Record,
126 | updatedJson : Record,
127 | updatedKeys : DiffKeyValues,
128 | editorMode: EditorMode,
129 | validations: Record
130 | }
131 |
132 | export type OnSubmitPropsType = {
133 | submitType : Exclude
134 | } & Omit
135 |
136 | export type JsonEditorContextType = {
137 | jsonState: Record | null,
138 | editJsonState: Record | null,
139 | isEditing: boolean;
140 | editingMode?: EditorMode;
141 | allFieldsEditable: boolean;
142 | editableFields: EditableFielsdObjectType;
143 | nonEditableFields: NonEditableFieldsObjectType;
144 | isExpanded : boolean,
145 | handleOnChange: HandleOnChange;
146 | handleOnSubmit: HandleOnSubmit;
147 | selectedFieldsForEditing: Record;
148 | setSelectedFieldsForEditing: React.Dispatch>>;
149 | validations: Record;
150 | setValidations: React.Dispatch>>;
151 | debouncing: boolean;
152 | regexPatternsTrie: React.MutableRefObject;
153 | handleFieldReset: (path: string) => void;
154 | enableTypeBasedRendering: boolean;
155 | }
156 | type InlineEditingConfig = {
157 | editingMode?: typeof INLINE_EDITING_MODE;
158 | allFieldsEditable?: boolean;
159 | editableFields?: EditableFielsdObjectType;
160 | nonEditableFields?: NonEditableFieldsObjectType;
161 | debouncing? : boolean;
162 | enableTypeBasedRendering? : boolean;
163 | }
164 |
165 | type StandardEditingConfig = {
166 | isEditing?: boolean;
167 | editingMode?: typeof GLOBAL_EDITING_MODE | typeof INDIVIDUAL_EDITING_MODE | typeof GLOBAL_INDIVIDUAL_EDITING_MODE;
168 | allFieldsEditable?: boolean;
169 | editableFields?: EditableFielsdObjectType;
170 | nonEditableFields?: NonEditableFieldsObjectType;
171 | debouncing? : boolean;
172 | enableTypeBasedRendering? : boolean;
173 | }
174 |
175 | export type EditingConfig = StandardEditingConfig | InlineEditingConfig;
176 |
177 | export type GlobalSubmitButtonConfigs = {
178 | variant?: "secondary" | "outline" | 'ghost' | "link" | 'destructive';
179 | className?: string;
180 | buttonText?: string;
181 | children?: React.ReactNode;
182 | }
183 |
184 | export type JsonEditorProps = {
185 | json: Record;
186 | className?: string;
187 | isExpanded? : boolean;
188 | onSubmit? : (props : OnSubmitPropsType) => void;
189 | onChange? : (props : onChangePropsType) => void;
190 | editingConfig?: EditingConfig;
191 | globalSubmitButtonConfigs? : GlobalSubmitButtonConfigs,
192 | styles?: CSSProperties
193 | };
194 |
195 | export type RenderJsonFunctionType = (
196 | val: any,
197 | path: string,
198 | isRootLevelKey: boolean
199 | ) => React.ReactNode;
200 |
201 | export type RenderArrayProps = {
202 | arr: [] | any;
203 | path: string;
204 | isRootLevelKey: boolean;
205 | renderJson: RenderJsonFunctionType;
206 | };
207 |
208 | export type RenderArrayItemsProp = {
209 | val: any;
210 | children: React.ReactNode;
211 | };
212 |
213 | export type RenderObjectProps = {
214 | obj: any;
215 | path: string;
216 | renderJson: RenderJsonFunctionType;
217 | isRootLevelKey: boolean;
218 | searchText?: string;
219 | };
220 |
221 | export type RenderObjectKeysProps = {
222 | keyName: string;
223 | val: any;
224 | children: React.ReactNode;
225 | searchText?: string;
226 | };
227 |
228 | export type HandleOnChange = (value : string | number | boolean,path : string, validations?: Record) => void
229 | export type HandleOnSubmit = (value : string | number | boolean,path : string) => void
230 |
231 | export type RenderValueProps = {
232 | value: string | number | boolean | undefined | null;
233 | path: string;
234 | };
235 |
236 | export type DefaultValueElementProps = {
237 | path: string;
238 | value: string | number | boolean | null | undefined;
239 | canEditInline?:boolean
240 | }
241 |
242 | export type DefaultInputField = {
243 | path: string;
244 | value: string;
245 | readModeValue ?: string,
246 | fieldValidations?: TextFieldValidations | NumberFieldValidations
247 | };
248 |
249 | export type DefaultSelectElementProps = {
250 | options: Array<{ key: string; value: string }>;
251 | } & DefaultInputField;
252 |
253 | export type DefaultTextElementProps = {} & DefaultInputField;
254 |
255 | export type DefaultNumberElementProps = Omit & {
256 | value: number; // Override value to be of type number,
257 | readModeValue? : number
258 | };
259 |
260 | export type DefaultTextAreaElementProps = {} & DefaultInputField;
261 |
262 | export type DefaultBooleanElementProps = Omit & {
263 | value: boolean; // Override value to be of type boolean,
264 | readModeValue? : boolean
265 | };
266 |
267 | export type DefaultRadioElementProps = {
268 | options: Array<{ key: string; value: string }>;
269 | } & DefaultInputField
270 |
271 | export type DefaultDateElementProps = {
272 | format: DateFormat;
273 | minDate?: F extends "DD/MM/YYYY" | "YYYY/MM/DD" | "MM/DD/YYYY"
274 | ? `${number}/${number}/${number}`
275 | : F extends "DD-MM-YYYY" | "YYYY-MM-DD" | "MM-DD-YYYY"
276 | ? `${number}-${number}-${number}`
277 | : never;
278 | maxDate?: F extends "DD/MM/YYYY" | "YYYY/MM/DD" | "MM/DD/YYYY"
279 | ? `${number}/${number}/${number}`
280 | : F extends "DD-MM-YYYY" | "YYYY-MM-DD" | "MM-DD-YYYY"
281 | ? `${number}-${number}-${number}`
282 | : never;
283 | } & DefaultInputField;
284 |
--------------------------------------------------------------------------------
/src/utils/regexTrie.ts:
--------------------------------------------------------------------------------
1 | class TrieNode {
2 | children: Record;
3 | regexPatterns: RegExp[];
4 | paths: string[];
5 |
6 | constructor() {
7 | this.children = {};
8 | this.regexPatterns = [];
9 | this.paths = [];
10 | }
11 | }
12 |
13 | export class RegexTrie {
14 | root: TrieNode;
15 |
16 | constructor() {
17 | this.root = new TrieNode();
18 | }
19 |
20 | // Insert paths into the trie with regex patterns
21 | insert(path: string, regex: RegExp): void {
22 | let node: TrieNode = this.root;
23 | const parts: string[] = path.split(".");
24 |
25 | for (const part of parts) {
26 | if (!node.children[part]) {
27 | node.children[part] = new TrieNode();
28 | }
29 | node = node.children[part];
30 | }
31 |
32 | node.regexPatterns.push(regex);
33 | node.paths.push(path);
34 | }
35 |
36 | // Function to traverse trie and gather potential regex patterns for matching
37 | findMatchingRegex(newPath: string): {
38 | regexPatterns: RegExp[];
39 | paths: string[];
40 | } {
41 | const queue: [TrieNode, number][] = [[this.root, 0]]; // BFS queue with root node and current part index
42 | const parts: string[] = newPath.split(".");
43 | const matchingPatterns = {
44 | regexPatterns: [] as RegExp[],
45 | paths: [] as string[],
46 | };
47 |
48 | while (queue.length > 0) {
49 | const [node, index] = queue.shift()!;
50 |
51 | // If we've matched all parts of the path, gather regex patterns
52 | if (index === parts.length) {
53 | matchingPatterns.regexPatterns.push(...node.regexPatterns);
54 | matchingPatterns.paths.push(...node.paths);
55 | continue;
56 | }
57 |
58 | // Add exact matches or wildcard handling
59 | const part: string = parts[index];
60 | for (const key in node.children) {
61 | if (key === "[]" || key === part) {
62 | queue.push([node.children[key], index + 1]);
63 | }
64 | }
65 | }
66 | return matchingPatterns;
67 | }
68 | }
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | prefix: "",
11 | theme: {
12 | container: {
13 | center: true,
14 | padding: "2rem",
15 | screens: {
16 | "2xl": "1400px",
17 | },
18 | },
19 | extend: {
20 | colors: {
21 | border: "hsl(var(--border))",
22 | input: "hsl(var(--input))",
23 | ring: "hsl(var(--ring))",
24 | background: "hsl(var(--background))",
25 | foreground: "hsl(var(--foreground))",
26 | primary: {
27 | DEFAULT: "hsl(var(--primary))",
28 | foreground: "hsl(var(--primary-foreground))",
29 | },
30 | secondary: {
31 | DEFAULT: "hsl(var(--secondary))",
32 | foreground: "hsl(var(--secondary-foreground))",
33 | },
34 | destructive: {
35 | DEFAULT: "hsl(var(--destructive))",
36 | foreground: "hsl(var(--destructive-foreground))",
37 | },
38 | muted: {
39 | DEFAULT: "hsl(var(--muted))",
40 | foreground: "hsl(var(--muted-foreground))",
41 | },
42 | accent: {
43 | DEFAULT: "hsl(var(--accent))",
44 | foreground: "hsl(var(--accent-foreground))",
45 | },
46 | popover: {
47 | DEFAULT: "hsl(var(--popover))",
48 | foreground: "hsl(var(--popover-foreground))",
49 | },
50 | card: {
51 | DEFAULT: "hsl(var(--card))",
52 | foreground: "hsl(var(--card-foreground))",
53 | },
54 | },
55 | borderRadius: {
56 | lg: "var(--radius)",
57 | md: "calc(var(--radius) - 2px)",
58 | sm: "calc(var(--radius) - 4px)",
59 | },
60 | keyframes: {
61 | "accordion-down": {
62 | from: { height: "0" },
63 | to: { height: "var(--radix-accordion-content-height)" },
64 | },
65 | "accordion-up": {
66 | from: { height: "var(--radix-accordion-content-height)" },
67 | to: { height: "0" },
68 | },
69 | },
70 | animation: {
71 | "accordion-down": "accordion-down 0.2s ease-out",
72 | "accordion-up": "accordion-up 0.2s ease-out",
73 | },
74 | },
75 | },
76 | plugins: [require("tailwindcss-animate")],
77 | }
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 |
25 | "baseUrl": ".",
26 | "paths": {
27 | "@/*": [
28 | "./src/*"
29 | ]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ],
7 | "compilerOptions": {
8 | "baseUrl": ".",
9 | "paths": {
10 | "@/*": ["./src/*"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "lib": ["ES2023"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "isolatedModules": true,
12 | "moduleDetection": "force",
13 | "noEmit": true,
14 |
15 | /* Linting */
16 | "strict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": ["vite.config.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import react from "@vitejs/plugin-react"
2 | import { defineConfig } from "vite"
3 |
4 | export default defineConfig({
5 | plugins: [react()],
6 | })
7 |
--------------------------------------------------------------------------------