Current section

17 Versions

Jump to

Compare versions

8 files changed
+329 additions
-13 deletions
  @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5 5 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 6 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 7
8 + ## [0.9.8] - 2026-02-01
9 +
10 + ### Added
11 + - Support for custom schema extensions (`x-` properties) at any node level
12 + - Automatic type casting for extension values (number, boolean, null)
13 + - Dedicated UI section for managing custom extensions with a new 'tag' icon
14 +
8 15 ## [0.9.7] - 2026-01-27
9 16
10 17 ### Added
  @@ -15,6 +15,7 @@ A Phoenix LiveComponent for visually building, editing, and validating JSON Sche
15 15 - **Undo/Redo**: Full history support for all schema modifications.
16 16 - **Real-time Validation**: In-editor logic checking (e.g., `min <= max`) with immediate visual feedback.
17 17 - **Draft 07 Support**: Includes constraints, enums, constants, `null` type, and $schema management.
18 + - **Custom Extensions**: Support for adding and editing custom `x-` properties at any node level.
18 19 - **Copy to Clipboard**: One-click export of the generated schema.
19 20 - **Lightweight**: Zero external JS dependencies (uses native Phoenix hooks), only requires `phoenix_live_view`.
20 21 - **JSON Viewer**: Dedicated component for displaying JSON with syntax highlighting and indentation guides.
  @@ -29,7 +30,7 @@ This library uses a small CSS file for styling and a JavaScript hook for clipboa
29 30 ```elixir
30 31 def deps do
31 32 [
32 - {:json_schema_editor, "~> 0.9.6"}
33 + {:json_schema_editor, "~> 0.9.7"}
33 34 ]
34 35 end
35 36 ```
  @@ -85,9 +86,14 @@ end
85 86 module={JSONSchemaEditor}
86 87 id="json-editor"
87 88 schema={@my_schema}
89 + on_change={fn updated_schema -> send(self(), {:schema_changed, updated_schema}) end}
88 90 on_save={fn updated_schema -> send(self(), {:schema_saved, updated_schema}) end}
91 + class="my-custom-theme"
92 + header_class="bg-gray-100 p-4"
93 + toolbar_class="flex gap-2"
89 94 />
90 95 ```
96 + *Note: Both `on_save` and `on_change` are optional. If `on_save` is omitted, the "Save" button will not be rendered. `on_change` triggers on every valid update.*
91 97
92 98 ### 3. Handle Updates
93 99
  @@ -96,6 +102,11 @@ def handle_info({:schema_saved, updated_schema}, socket) do
96 102 # The schema is guaranteed to be logically consistent here
97 103 {:noreply, assign(socket, my_schema: updated_schema)}
98 104 end
105 +
106 + def handle_info({:schema_changed, updated_schema}, socket) do
107 + # Triggered whenever the schema is modified and valid
108 + {:noreply, assign(socket, my_schema: updated_schema)}
109 + end
99 110 ```
100 111
101 112 ### JSON Viewer
  @@ -118,12 +129,30 @@ The library also includes a dedicated JSON Editor component for editing JSON dat
118 129 id="json-data-editor"
119 130 schema={@my_schema}
120 131 json={@my_data}
132 + on_change={fn updated_json -> send(self(), {:json_changed, updated_json}) end}
121 133 on_save={fn updated_json -> send(self(), {:json_updated, updated_json}) end}
122 134 />
123 135 ```
124 136
125 137 It features real-time validation, a visual tree editor, and a live preview of the edited data.
126 138
139 + ## Theming
140 +
141 + The editor uses CSS variables for styling, which can be easily overridden in your project's CSS or via the `class` attribute.
142 +
143 + ```css
144 + .my-custom-theme {
145 + /* Override main colors */
146 + --jse-primary: #ec4899;
147 + --jse-primary-hover: #db2777;
148 +
149 + /* Override layout dimensions */
150 + --jse-tree-row-height: 2rem;
151 + }
152 + ```
153 +
154 + See `assets/css/json_schema_editor.css` for the full list of available variables.
155 +
127 156 ## Development
128 157
129 158 ### Running Tests
  @@ -432,7 +432,7 @@
432 432 border-color: var(--_jse-border-focus);
433 433 }
434 434
435 - .jse-constraints-container {
435 + .jse-constraints-container, .jse-extensions-container {
436 436 background-color: var(--_jse-bg-secondary);
437 437 border: 1px solid var(--_jse-border);
438 438 border-radius: var(--_jse-radius);
  @@ -440,6 +440,81 @@
440 440 margin-bottom: 1rem;
441 441 }
442 442
443 + .jse-extensions-header {
444 + display: flex;
445 + align-items: center;
446 + justify-content: space-between;
447 + margin-bottom: 0.75rem;
448 + padding-bottom: 0.25rem;
449 + border-bottom: 1px dashed var(--_jse-border);
450 + }
451 +
452 + .jse-extensions-list {
453 + display: flex;
454 + flex-direction: column;
455 + gap: 0.5rem;
456 + }
457 +
458 + .jse-extension-item {
459 + background-color: var(--_jse-bg);
460 + border: 1px solid var(--_jse-border);
461 + border-radius: var(--_jse-radius);
462 + padding: 0.25rem 0.5rem;
463 + }
464 +
465 + .jse-extension-row {
466 + display: flex;
467 + align-items: center;
468 + gap: 0.5rem;
469 + }
470 +
471 + .jse-extension-key-input {
472 + background: transparent;
473 + border: none;
474 + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
475 + font-size: 0.875rem;
476 + font-weight: 600;
477 + color: var(--_jse-syntax-key);
478 + width: 150px;
479 + border-radius: 2px;
480 + }
481 +
482 + .jse-extension-value-input {
483 + flex: 1;
484 + background: transparent;
485 + border: none;
486 + font-size: 0.875rem;
487 + color: var(--_jse-text-primary);
488 + border-radius: 2px;
489 + }
490 +
491 + .jse-extension-key-input:hover, .jse-extension-value-input:hover {
492 + background-color: var(--_jse-bg-secondary);
493 + }
494 +
495 + .jse-extension-key-input:focus, .jse-extension-value-input:focus {
496 + outline: none;
497 + background-color: var(--_jse-bg-secondary);
498 + box-shadow: 0 0 0 1px var(--_jse-primary);
499 + }
500 +
501 + .jse-no-data {
502 + font-size: 0.875rem;
503 + color: var(--_jse-text-tertiary);
504 + font-style: italic;
505 + text-align: center;
506 + padding: 0.5rem;
507 + }
508 +
509 + .jse-btn-toggle-extensions.jse-active {
510 + color: var(--_jse-primary);
511 + background-color: var(--_jse-bg-secondary);
512 + }
513 +
514 + .jse-btn-toggle-extensions.jse-has-data {
515 + color: var(--_jse-primary);
516 + }
517 +
443 518 .jse-constraints-grid {
444 519 display: grid;
445 520 grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  @@ -1,26 +1,27 @@
1 1 {<<"links">>,
2 2 [{<<"GitHub">>,<<"https://github.com/preciz/json_schema_editor">>}]}.
3 3 {<<"name">>,<<"json_schema_editor">>}.
4 - {<<"version">>,<<"0.9.7">>}.
4 + {<<"version">>,<<"0.9.8">>}.
5 5 {<<"description">>,
6 6 <<"A Phoenix LiveComponent for visually building, editing, and validating JSON Schemas.">>}.
7 7 {<<"elixir">>,<<"~> 1.18">>}.
8 8 {<<"app">>,<<"json_schema_editor">>}.
9 9 {<<"files">>,
10 10 [<<"lib">>,<<"lib/json_schema_editor">>,
11 - <<"lib/json_schema_editor/components.ex">>,
12 11 <<"lib/json_schema_editor/validator.ex">>,
13 12 <<"lib/json_schema_editor/viewer.ex">>,
14 13 <<"lib/json_schema_editor/ui_state.ex">>,
15 - <<"lib/json_schema_editor/schema_mutator.ex">>,
16 14 <<"lib/json_schema_editor/simple_validator.ex">>,
17 15 <<"lib/json_schema_editor/schema_generator.ex">>,
18 16 <<"lib/json_schema_editor/pretty_printer.ex">>,
19 17 <<"lib/json_schema_editor/schema_utils.ex">>,
20 - <<"lib/json_schema_editor/json_editor.ex">>,<<"lib/json_schema_editor.ex">>,
21 - <<"assets">>,<<"assets/css">>,<<"assets/css/json_schema_editor.css">>,
22 - <<"assets/js">>,<<"assets/js/json_schema_editor.js">>,<<".formatter.exs">>,
23 - <<"mix.exs">>,<<"README.md">>,<<"LICENSE">>,<<"CHANGELOG.md">>]}.
18 + <<"lib/json_schema_editor/json_editor.ex">>,
19 + <<"lib/json_schema_editor/components.ex">>,
20 + <<"lib/json_schema_editor/schema_mutator.ex">>,
21 + <<"lib/json_schema_editor.ex">>,<<"assets">>,<<"assets/css">>,
22 + <<"assets/css/json_schema_editor.css">>,<<"assets/js">>,
23 + <<"assets/js/json_schema_editor.js">>,<<".formatter.exs">>,<<"mix.exs">>,
24 + <<"README.md">>,<<"LICENSE">>,<<"CHANGELOG.md">>]}.
24 25 {<<"licenses">>,[<<"MIT">>]}.
25 26 {<<"requirements">>,
26 27 [[{<<"name">>,<<"phoenix_live_view">>},
  @@ -4,7 +4,7 @@ defmodule JSONSchemaEditor do
4 4
5 5 It provides a rich interface for creating and modifying JSON Schemas (Draft 07), supporting
6 6 nested structures, arrays, validation constraints, logical composition (oneOf, anyOf, allOf),
7 - and conditional logic (if, then, else, not).
7 + conditional logic (if, then, else, not), and custom extensions (x- properties).
8 8
9 9 The library also includes a dedicated `JSONSchemaEditor.JSONEditor` component for editing
10 10 JSON data according to a provided schema.
  @@ -282,9 +282,14 @@ defmodule JSONSchemaEditor do
282 282 new_ui_state =
283 283 Map.update(socket.assigns.ui_state, "#{t}:#{p}", true, fn v -> !v end)
284 284
285 - # If we are expanding constraints, logic or description, ensure the node itself is not collapsed
285 + # If we are expanding constraints, logic, extensions or description, ensure the node itself is not collapsed
286 286 final_ui_state =
287 - if t in ["expanded_constraints", "expanded_logic", "expanded_description"] and
287 + if t in [
288 + "expanded_constraints",
289 + "expanded_logic",
290 + "expanded_description",
291 + "expanded_extensions"
292 + ] and
288 293 Map.get(new_ui_state, "#{t}:#{p}") do
289 294 Map.put(new_ui_state, "collapsed_node:#{p}", false)
290 295 else
  @@ -294,6 +299,42 @@ defmodule JSONSchemaEditor do
294 299 {:noreply, assign(socket, :ui_state, final_ui_state)}
295 300 end
296 301
302 + def handle_event("add_extension", %{"path" => path}, socket) do
303 + {new_schema, _new_key} = SchemaMutator.add_extension(socket.assigns.schema, path)
304 + {:noreply, update_schema_and_notify(socket, new_schema)}
305 + end
306 +
307 + def handle_event("delete_extension", %{"path" => path, "key" => key}, socket) do
308 + {:noreply,
309 + update_schema_and_notify(
310 + socket,
311 + SchemaMutator.delete_extension(socket.assigns.schema, path, key)
312 + )}
313 + end
314 +
315 + def handle_event(
316 + "update_extension_key",
317 + %{"path" => path, "old_key" => old, "value" => new},
318 + socket
319 + ) do
320 + case SchemaMutator.update_extension_key(socket.assigns.schema, path, old, new) do
321 + {:ok, new_schema} -> {:noreply, update_schema_and_notify(socket, new_schema)}
322 + _ -> {:noreply, socket}
323 + end
324 + end
325 +
326 + def handle_event(
327 + "update_extension_value",
328 + %{"path" => path, "key" => key, "value" => value},
329 + socket
330 + ) do
331 + {:noreply,
332 + update_schema_and_notify(
333 + socket,
334 + SchemaMutator.update_extension_value(socket.assigns.schema, path, key, value)
335 + )}
336 + end
337 +
297 338 # --- Simple Mutations (Schema update only) ---
298 339
299 340 def handle_event(event, params, socket) do
Loading more files…