🐛 Fix show error on name-input

This commit is contained in:
Eva Marco 2026-05-26 12:40:26 +02:00 committed by Elena Torró
parent 05cceab768
commit ba39600192
18 changed files with 232 additions and 99 deletions

View File

@ -6,7 +6,7 @@ import {
setupTypographyTokensFileRender,
unfoldTokenType,
createToken,
createSet
createSet,
} from "./helpers";
test.beforeEach(async ({ page }) => {

View File

@ -1818,21 +1818,6 @@ test("User disables the current set but token still have resolved values shown i
});
test.describe("User can't create groups that clash with token names", () => {
const changeSetInput = async (sidebar, setName, finalKey = "Enter") => {
const setInput = sidebar.locator("input:focus");
await expect(setInput).toBeVisible();
await setInput.fill(setName);
await setInput.press(finalKey);
};
const createSet = async (sidebar, setName, finalKey = "Enter") => {
const tokensTabButton = sidebar
.getByRole("button", { name: "Add set" })
.click();
await changeSetInput(sidebar, setName, (finalKey = "Enter"));
};
const createBadToken = async (page, type, name, textFieldName, value) => {
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@ -2102,7 +2087,7 @@ test("User can't create Text Decoration token with group name that clashes with
});
const errorNode = tokensUpdateCreateModal.getByText("Group name of typ1.bad conflicts with a token of the same name in another active set.");
await expect(errorNode).toHaveCount(6);
await expect(errorNode).toHaveCount(1);
await expect(submitButton).toBeDisabled();
});
@ -2160,7 +2145,7 @@ test("User can't create Text Decoration token with group name that clashes with
});
const errorNode = tokensUpdateCreateModal.getByText("Group name of sha1.bad conflicts with a token of the same name in another active set.");
await expect(errorNode).toHaveCount(5);
await expect(errorNode).toHaveCount(1);
await expect(submitButton).toBeDisabled();
});
});

View File

@ -360,20 +360,20 @@ const createToken = async (page, type, name, textFieldName, value) => {
await expect(tokensUpdateCreateModal).not.toBeVisible();
};
const changeSetInput = async (sidebar, setName, finalKey = "Enter") => {
const setInput = sidebar.locator("input:focus");
await expect(setInput).toBeVisible();
await setInput.fill(setName);
await setInput.press(finalKey);
};
const changeSetInput = async (sidebar, setName, finalKey = "Enter") => {
const setInput = sidebar.locator("input:focus");
await expect(setInput).toBeVisible();
await setInput.fill(setName);
await setInput.press(finalKey);
};
const createSet = async (sidebar, setName, finalKey = "Enter") => {
const tokensTabButton = sidebar
.getByRole("button", { name: "Add set" })
.click();
const createSet = async (sidebar, setName, finalKey = "Enter") => {
const tokensTabButton = sidebar
.getByRole("button", { name: "Add set" })
.click();
await changeSetInput(sidebar, setName, (finalKey = "Enter"));
};
await changeSetInput(sidebar, setName, (finalKey = "Enter"));
};
export {
setupEmptyTokensFile,

View File

@ -1,7 +1,12 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
import { createToken, setupTokensFileRender, unfoldTokenType } from "./helpers";
import {
createToken,
setupTokensFileRender,
unfoldTokenType,
createSet,
} from "./helpers";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
@ -174,14 +179,14 @@ test.describe("Tokens - node tree", () => {
// Rename to move it into the collapsed light group
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("light.base");
await tokensUpdateCreateModal
.getByRole("button", { name: "Save" })
.click();
await tokensUpdateCreateModal.getByRole("button", { name: "Save" }).click();
// After rename, light group should be auto-expanded and both tokens visible
await expect(lightGroup).toBeVisible();
await expect(lightAccentToken).toBeVisible();
await expect(tokensSidebar.getByRole("button", { name: "base" })).toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "base" }),
).toBeVisible();
});
test("User removes node and all child tokens", async ({ page }) => {
@ -228,3 +233,64 @@ test.describe("Tokens - node tree", () => {
await expect(tokenTypeButton).toHaveAttribute("aria-expanded", "false");
});
});
test("User can see an error on token pill and token modal form when token has an error", async ({
page,
}) => {
const {
tokensSidebar,
tokensUpdateCreateModal,
tokenContextMenuForToken,
tokenThemesSetsSidebar,
} = await setupTokensFileRender(page);
await createSet(tokenThemesSetsSidebar, "set/first");
await tokenThemesSetsSidebar.getByRole("button", { name: "first" }).click();
await tokenThemesSetsSidebar
.getByRole("button", { name: "first" })
.getByRole("checkbox")
.click();
await createSet(tokenThemesSetsSidebar, "set/second");
await tokenThemesSetsSidebar.getByRole("button", { name: "second" }).click();
await tokenThemesSetsSidebar
.getByRole("button", { name: "second" })
.getByRole("checkbox")
.click();
await createToken(page, "Border radius", "a.b", "Value", "23");
await tokenThemesSetsSidebar.getByRole("button", { name: "first" }).click();
await createToken(page, "Border radius", "a", "Value", "25");
await tokenThemesSetsSidebar.getByRole("button", { name: "second" }).click();
const brokenTokenPill = tokensSidebar.getByRole("button", {
name: "Group name of a.b conflicts",
});
await expect(brokenTokenPill).toBeVisible();
await brokenTokenPill.click({ button: "right" });
const editTokenButton = page
.getByRole("listitem")
.filter({ hasText: "Edit token" });
await expect(editTokenButton).toBeVisible();
await editTokenButton.click();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await expect(nameField).toBeVisible();
await expect(nameField).toHaveValue("a.b");
const errorMessage = tokensUpdateCreateModal.getByText(
"Group name of a.b conflicts",
);
await expect(errorMessage).toBeVisible();
await nameField.fill("new-name");
await expect(errorMessage).not.toBeVisible();
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await expect(submitButton).toBeEnabled();
});

View File

@ -26,6 +26,7 @@
(get-in @form [:touched input-name]))
error (get-in @form [:errors input-name])
extra-error (get-in @form [:extra-errors input-name])
value (get-in @form [:data input-name] "")
@ -41,9 +42,9 @@
:value value})
props
(if (and error touched?)
(if (or extra-error (and error touched?))
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
:hint-message (:message (or error extra-error))})
props)]
[:> input* props]))

View File

@ -333,7 +333,7 @@
(generic-attribute-actions #{:y} "Y" (assoc context-data :on-update-shape dwta/update-shape-position)))
(clean-separators)))}))
(defn default-actions [{:keys [token selected-token-set-id on-delete-token]}]
(defn default-actions [{:keys [token selected-token-set-id on-delete-token errors]}]
(let [{:keys [modal]} (dwta/get-token-properties token)
on-copy-name #(clipboard/to-clipboard (:name token))
on-duplicate-token #(st/emit! (dwtl/duplicate-token (:id token)))]
@ -347,6 +347,7 @@
:y (.-clientY ^js event)
:position :right
:fields fields
:initial-errors errors
:action "edit"
:selected-token-set-id selected-token-set-id
:token token}))))}

View File

@ -24,6 +24,7 @@
[app.main.ui.forms :as fc]
[app.main.ui.workspace.colorpicker :as colorpicker]
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]]
[app.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
@ -282,10 +283,13 @@
(let [touched? (get-in @form [:touched input-name])]
(when touched?
(if error
(do
(swap! form assoc-in [:extra-errors input-name] {:message error})
(swap! form assoc-in [:data :color-result] "")
(reset! hint* {:message error :type "error"}))
(if (csu/group-name-conflict-error? error token-name)
(swap! form assoc-in [:extra-errors ""] {:message error})
(do
(swap! form assoc-in [:extra-errors input-name] {:message error})
(swap! form assoc-in [:data :color-result] "")
(reset! hint* {:message error :type "error"})))
(let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value))]
(swap! form update :extra-errors dissoc input-name)
(swap! form assoc-in [:data :color-result] value)
@ -312,7 +316,8 @@
(-> state
(assoc-in [:data :value value-subfield index field] (if trim? (str/trim value) value))
(update :errors clean-errors)
(update :extra-errors clean-errors)))))))
(update :extra-errors clean-errors)
(update :extra-errors dissoc "")))))))
(mf/defc indexed-color-input*
[{:keys [name tokens token index value-subfield] :rest props}]
@ -452,10 +457,12 @@
(some? error)
(let [error' (:message error)]
(do
(swap! form assoc-in [:extra-errors :value value-subfield index input-name] {:message error'})
(swap! form assoc-in [:data :value value-subfield index :color-result] "")
(reset! hint* {:message error' :type "error"})))
(if (csu/group-name-conflict-error? error token-name)
(swap! form assoc-in [:extra-errors ""] {:message error})
(do
(swap! form assoc-in [:extra-errors :value value-subfield index input-name] {:message error'})
(swap! form assoc-in [:data :value value-subfield index :color-result] "")
(reset! hint* {:message error' :type "error"}))))
:else
(let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value))

View File

@ -78,6 +78,9 @@
error
(get-in @form [:errors name])
extra-error
(get-in @form [:extra-errors name])
value
(get-in @form [:data name] "")
@ -264,12 +267,11 @@
:on-mouse-down dom/prevent-default
:on-click toggle-dropdown}]))})
props
(if (and error touched?)
(if (or extra-error (and error touched?))
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
:hint-message (:message (or error extra-error))})
props)
{:keys [style ready?]} (use-floating-dropdown is-open input-wrapper-ref wrapper-ref dropdown-ref)]
(mf/with-effect [resolve-stream tokens token name token-name]
@ -284,9 +286,11 @@
(let [touched? (get-in @form [:touched name])]
(when touched?
(if error
(do
(swap! form assoc-in [:extra-errors name] {:message error})
(reset! hint* {:message error :type "error"}))
(if (csu/group-name-conflict-error? error token-name)
(swap! form assoc-in [:extra-errors ""] {:message error})
(do
(swap! form assoc-in [:extra-errors name] {:message error})
(reset! hint* {:message error :type "error"})))
(let [message (tr "workspace.tokens.resolved-value" value)]
(swap! form update :extra-errors dissoc name)
(reset! hint* {:message message :type "hint"}))))))))]

View File

@ -20,6 +20,7 @@
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.forms :as fc]
[app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]]
[app.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
@ -175,9 +176,11 @@
(rx/subs! (fn [{:keys [error value]}]
(when touched?
(if error
(do
(swap! form assoc-in [:extra-errors input-name] {:message error})
(reset! hint* {:message error :type "error"}))
(if (csu/group-name-conflict-error? error token-name)
(swap! form assoc-in [:extra-errors ""] {:message error})
(do
(swap! form assoc-in [:extra-errors input-name] {:message error})
(reset! hint* {:message error :type "error"})))
(let [message (tr "workspace.tokens.resolved-value" value)]
(swap! form update :extra-errors dissoc input-name)
(reset! hint* {:message message :type "hint"})))))))]
@ -205,7 +208,8 @@
(-> state
(assoc-in [:data :value field] (if trim? (str/trim value) value))
(update :errors clean-errors)
(update :extra-errors clean-errors)))))))
(update :extra-errors clean-errors)
(update :extra-errors dissoc "")))))))
(mf/defc composite-fonts-combobox*
[{:keys [token tokens name] :rest props}]
@ -306,8 +310,11 @@
(some? error)
(let [error' (:message error)]
(swap! form assoc-in [:extra-errors :value input-name] {:message error'})
(reset! hint* {:message error' :type "error"}))
(if (csu/group-name-conflict-error? error' token-name)
(swap! form assoc-in [:extra-errors ""] {:message error'})
(do
(swap! form assoc-in [:extra-errors :value input-name] {:message error'})
(reset! hint* {:message error' :type "error"}))))
:else
(let [message (tr "workspace.tokens.resolved-value" value)

View File

@ -17,6 +17,7 @@
[app.main.data.workspace.tokens.format :as dwtf]
[app.main.ui.ds.controls.input :as ds]
[app.main.ui.forms :as fc]
[app.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
@ -248,9 +249,11 @@
(let [touched? (get-in @form [:touched input-name])]
(when touched?
(if error
(do
(swap! form assoc-in [:extra-errors input-name] {:message error})
(reset! hint* {:message error :type "error"}))
(if (csu/group-name-conflict-error? error token-name)
(swap! form assoc-in [:extra-errors ""] {:message error})
(do
(swap! form assoc-in [:extra-errors input-name] {:message error})
(reset! hint* {:message error :type "error"})))
(let [message (tr "workspace.tokens.resolved-value" value)]
(swap! form update :extra-errors dissoc input-name)
(reset! hint* {:message message :type "hint"}))))))))]
@ -274,7 +277,8 @@
(assoc-in [:data :value field] (if trim? (str/trim value) value))
(assoc-in [:touched :value field] true)
(update :errors clean-errors)
(update :extra-errors clean-errors)))))))
(update :extra-errors clean-errors)
(update :extra-errors dissoc "")))))))
(mf/defc input-composite*
[{:keys [name tokens token] :rest props}]
@ -286,6 +290,9 @@
error
(get-in @form [:errors :value input-name])
extra-error
(get-in @form [:extra-errors :value input-name])
value
(get-in @form [:data :value input-name] "")
@ -319,9 +326,9 @@
:hint-message (:message hint)
:hint-type (:type hint)})
props
(if (and touched? error)
(if (or extra-error (and touched? error))
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
:hint-message (:message (or error extra-error))})
props)
props (if (and (not error) (= input-name :reference))
@ -347,8 +354,11 @@
(some? error)
(let [error' (:message error)]
(swap! form assoc-in [:extra-errors :value input-name] {:message error'})
(reset! hint* {:message error' :type "error"}))
(if (csu/group-name-conflict-error? error' token-name)
(swap! form assoc-in [:extra-errors ""] {:message error'})
(do
(swap! form assoc-in [:extra-errors :value input-name] {:message error'})
(reset! hint* {:message error' :type "error"}))))
:else
(let [input-value (get-in @form [:data :value input-name] "")
@ -386,7 +396,8 @@
(-> state
(assoc-in [:data :value value-subfield index field] (if trim? (str/trim value) value))
(update :errors clean-errors)
(update :extra-errors clean-errors)))))))
(update :extra-errors clean-errors)
(update :extra-errors dissoc "")))))))
(mf/defc input-indexed*
[{:keys [name tokens token index value-subfield] :rest props}]
@ -398,6 +409,9 @@
error
(get-in @form [:errors :value value-subfield index input-name])
extra-error
(get-in @form [:extra-errors :value value-subfield index input-name])
value-from-form
(get-in @form [:data :value value-subfield index input-name] "")
@ -428,9 +442,9 @@
:hint-message (:message hint)
:hint-type (:type hint)})
props
(if error
(if (or error extra-error)
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
:hint-message (:message (or error extra-error))})
props)
props
@ -457,8 +471,11 @@
(some? error)
(let [error' (:message error)]
(swap! form assoc-in [:extra-errors :value value-subfield index input-name] {:message error'})
(reset! hint* {:message error' :type "error"}))
(if (csu/group-name-conflict-error? error' token-name)
(swap! form assoc-in [:extra-errors ""] {:message error'})
(do (swap! form assoc-in [:extra-errors :value value-subfield index input-name] {:message error'})
(reset! hint* {:message error' :type "error"}))))
:else
(let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value))

View File

@ -37,7 +37,8 @@
(mf/deps input-name)
(fn [id]
(let [is-inner? (= id "inner")]
(swap! form assoc-in [:data :value indexed-type index input-name] is-inner?))))
(swap! form assoc-in [:data :value indexed-type index input-name] is-inner?)
(swap! form update :extra-errors dissoc ""))))
props (mf/spread-props props {:default-selected (if value "inner" "drop")
:variant "ghost"

View File

@ -119,4 +119,9 @@
(not-empty)))))
(defn focusable-options [options]
(filter #(= (:type %) :token) options))
(filter #(= (:type %) :token) options))
(defn group-name-conflict-error?
[error token-name]
(let [translated-string (tr "errors.tokens.name-collision" token-name)]
(= error translated-string)))

View File

@ -20,7 +20,7 @@
[rumext.v2 :as mf]))
(mf/defc form-container*
[{:keys [token token-type] :rest props}]
[{:keys [token token-type initial-errors] :rest props}]
(let [token-type
(or (:type token) token-type)
@ -38,7 +38,8 @@
props
(mf/spread-props props {:token-type token-type
:tokens-tree-in-selected-set tokens-tree-in-selected-set
:token token})
:token token
:initial-errors initial-errors})
text-case-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-case-value-enter")})
text-decoration-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")})
font-weight-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")})

View File

@ -24,6 +24,7 @@
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.forms :as fc]
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
[app.main.ui.workspace.tokens.management.forms.validators :refer [default-validate-token]]
@ -61,6 +62,7 @@
make-schema
input-component
initial
initial-errors
value-type
value-subfield
input-value-placeholder] :as props}]
@ -112,10 +114,21 @@
:value (:value token "")
:description (:description token "")}))
initial-general-errors (mf/with-memo [token initial initial-errors]
(when initial-errors
(if (= :error.style-dictionary/missing-reference (:error/code (first initial-errors)))
(if (or (= value-type :composite)
(= value-type :indexed))
{:value {:reference {:message (wte/resolve-error-message (first initial-errors))}}}
{:value {:message (wte/resolve-error-message (first initial-errors))}})
{"" {:message (wte/resolve-error-message (first initial-errors))}})))
form
(fm/use-form :schema schema
:initial-errors initial-general-errors
:initial initial)
general-errors (get-in @form [:extra-errors ""])
on-toggle-tab
(mf/use-fn
(mf/deps form)
@ -288,6 +301,10 @@
:max-length max-input-length
:variant "comfortable"
:is-optional true}]]
(when (some? general-errors)
[:> context-notification* {:level :warning
:appearance :ghost}
(:message general-errors)])
[:div {:class (stl/css-case :button-row true
:with-delete (= action "edit"))}

View File

@ -75,7 +75,7 @@
(mf/defc token-update-create-modal
{::mf/wrap-props false}
[{:keys [x y position token token-type action selected-token-set-id] :as _args}]
[{:keys [x y position token token-type action selected-token-set-id initial-errors] :as _args}]
(let [wrapper-style (use-viewport-position-style x y position token-type)
modal-size-large* (mf/use-state (or (= token-type :typography)
(= token-type :color)
@ -102,6 +102,7 @@
:action action
:selected-token-set-id selected-token-set-id
:token-type token-type
:initial-errors initial-errors
:on-display-colorpicker update-modal-size}]]))
;; Modals ----------------------------------------------------------------------

View File

@ -100,18 +100,19 @@
tokens
(mf/with-memo [tokens]
(vec (sort-by :name tokens)))
expandable? (d/nilv (seq tokens) false)
on-pill-context-menu
(mf/use-fn
(mf/deps active-theme-tokens)
(fn [event token]
(dom/prevent-default event)
(st/emit! (dwtl/assign-token-context-menu
{:type :token
:position (dom/get-client-position event)
:errors (:errors token)
:token-id (:id token)}))))
(let [resolved-token (get active-theme-tokens (:name token))]
(st/emit! (dwtl/assign-token-context-menu
{:type :token
:position (dom/get-client-position event)
:errors (:errors resolved-token)
:token-id (:id token)})))))
on-node-context-menu
(mf/use-fn

View File

@ -16,6 +16,7 @@
[app.config :as cf]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.color :as dwtc]
[app.main.data.workspace.tokens.errors :as wte]
[app.main.data.workspace.tokens.format :as dwtf]
[app.main.refs :as refs]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
@ -101,7 +102,7 @@
(defn- generate-tooltip
"Generates a tooltip for a given token"
[is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set]
[is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set is-name-collision errors]
(let [{:keys [name type resolved-value value]} token
resolved-value-theme (:resolved-value theme-token)
resolved-value (or resolved-value-theme resolved-value)
@ -128,6 +129,9 @@
ref-not-in-active-set
(tr "workspace.tokens.ref-not-valid")
is-name-collision
(wte/resolve-error-message (first errors))
no-valid-value
(tr "workspace.tokens.value-not-valid")
@ -177,7 +181,6 @@
{::mf/wrap [mf/memo]}
[{:keys [on-click token on-context-menu selected-shapes is-selected-inside-layout active-theme-tokens]}]
(let [{:keys [name value type]} token
resolved-token (get active-theme-tokens (:name token))
errors (:errors resolved-token)
@ -218,11 +221,17 @@
(and is-reference?
(not (contains-reference-value? value active-theme-tokens))))
name-collision (->> errors
(first)
(:error/code)
(= :error.token/name-collision))
no-valid-value (seq errors)
errors?
(or ref-not-in-active-set
no-valid-value)
no-valid-value
name-collision)
color
(when (cfo/color-token? token)
@ -266,12 +275,12 @@
on-hover
(mf/use-fn
(mf/deps selected-shapes is-viewer? active-theme-tokens token half-applied? no-valid-value ref-not-in-active-set)
(mf/deps selected-shapes is-viewer? active-theme-tokens token half-applied? no-valid-value ref-not-in-active-set name-collision errors)
(fn [event]
(let [node (dom/get-current-target event)
theme-token (get active-theme-tokens name)
title (generate-tooltip is-viewer? (first selected-shapes) theme-token token
half-applied? no-valid-value ref-not-in-active-set)]
half-applied? no-valid-value ref-not-in-active-set name-collision errors)]
(dom/set-attribute! node "title" title))))]
[:button {:class (stl/css-case
@ -301,7 +310,9 @@
[:> icon*
{:icon-id i/broken-link
:class (stl/css :token-pill-icon)
:aria-label (tr "workspace.tokens.missing-reference")}]
:aria-label (if name-collision
(wte/resolve-error-message (first errors))
(tr "workspace.tokens.missing-reference"))}]
color
[:> swatch* {:background color

View File

@ -45,11 +45,15 @@
valid?)))))
(defn- make-initial-state
[initial-data]
[initial-data initial-errors]
(let [initial (if (fn? initial-data) (initial-data) initial-data)
initial (d/nilv initial {})]
initial (d/nilv initial {})
initial-errors (if (fn? initial-errors) (initial-errors) initial-errors)
initial-errors (d/nilv initial-errors {})]
{:initial initial
:initial-errors initial-errors
:data initial
:extra-errors initial-errors
:errors {}
:touched {}}))
@ -64,9 +68,11 @@
(-reset! [_ new-value]
(if (nil? new-value)
(let [initial (-> (mf/ref-val internal-state)
(get :initial)
(make-initial-state))]
(mf/set-ref-val! internal-state initial))
(get :initial))
initial-errors (-> (mf/ref-val internal-state)
(get :initial-errors))
state (make-initial-state initial initial-errors)]
(mf/set-ref-val! internal-state state))
(mf/set-ref-val! internal-state new-value))
(rerender-fn))
@ -92,12 +98,12 @@
(rerender-fn)))))
(defn use-form
[& {:keys [initial schema validators] :as opts}]
[& {:keys [initial initial-errors schema validators] :as opts}]
(let [rerender-fn (use-rerender-fn)
initial
(mf/with-memo [initial]
(make-initial-state initial))
(mf/with-memo [initial initial-errors]
(make-initial-state initial initial-errors))
internal-state
(mf/use-ref initial)
@ -131,14 +137,16 @@
(assoc-in [:touched field] true)
(assoc-in [:data field] (if trim? (str/trim value) value))
(update :errors clean-errors)
(update :extra-errors clean-errors)))))))
(update :extra-errors clean-errors)
(update :extra-errors dissoc "")))))))
(defn update-input-value!
[form field value]
(swap! form (fn [state]
(-> state
(assoc-in [:data field] value)
(update :errors dissoc field)))))
(update :errors dissoc field)
(update :extra-errors dissoc "")))))
(defn on-input-blur
[form field]