Skip to content

Commit a9a8c7c

Browse files
committed
Installer and Test
1 parent c058025 commit a9a8c7c

175 files changed

Lines changed: 4179 additions & 2594 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

assets/components/signature-pad.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ export class SignaturePad extends Component<Props, Api> {
4040
const hiddenInput = this.el.querySelector<HTMLInputElement>(
4141
'[data-scope="signature-pad"][data-part="hidden-input"]'
4242
);
43-
if (hiddenInput) hiddenInput.value = "";
43+
if (hiddenInput && hiddenInput.value !== "") {
44+
hiddenInput.value = "";
45+
}
4446
return;
4547
}
4648

assets/hooks/color-picker.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ type ColorPickerHookState = {
1717
onSetValue?: (event: Event) => void;
1818
};
1919

20+
function syncColorHiddenAndNotify(el: HTMLElement, valueAsString: string | undefined) {
21+
if (valueAsString === undefined) {
22+
return;
23+
}
24+
const hidden = el.querySelector<HTMLInputElement>(
25+
'[data-scope="color-picker"][data-part="hidden-input"]'
26+
);
27+
if (hidden) {
28+
hidden.value = valueAsString;
29+
hidden.dispatchEvent(new Event("input", { bubbles: true }));
30+
hidden.dispatchEvent(new Event("change", { bubbles: true }));
31+
}
32+
}
33+
2034
function readValueProps(el: HTMLElement): Pick<Props, "defaultValue"> {
2135
const defaultVal = getString(el, "defaultValue");
2236
return { defaultValue: defaultVal ? parse(defaultVal) : undefined };
@@ -44,6 +58,7 @@ const ColorPickerHook: Hook<object & ColorPickerHookState, HTMLElement> = {
4458
dir: getDir(el),
4559
positioning: readPositioningOptions(el),
4660
onValueChange: (details: ValueChangeDetails) => {
61+
syncColorHiddenAndNotify(el, details.valueAsString);
4762
notifyChange({
4863
el,
4964
canPushServer: canPush(),
@@ -57,6 +72,7 @@ const ColorPickerHook: Hook<object & ColorPickerHookState, HTMLElement> = {
5772
});
5873
},
5974
onValueChangeEnd: (details: ValueChangeDetails) => {
75+
syncColorHiddenAndNotify(el, details.valueAsString);
6076
notifyChange({
6177
el,
6278
canPushServer: canPush(),

assets/hooks/combobox.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ function buildComboboxProps(
9999
const list = details.value.map((v) => String(v));
100100
hidden.value =
101101
list.length === 0 ? "" : getBoolean(el, "multiple") ? list.join(",") : (list[0] ?? "");
102+
hidden.dispatchEvent(new Event("input", { bubbles: true }));
102103
hidden.dispatchEvent(new Event("change", { bubbles: true }));
103104
}
104105
}

assets/hooks/date-picker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ const DatePickerHook: Hook<object & DatePickerHookState, HTMLElement> = {
8282
placeholder: getString(el, "placeholder"),
8383
minView: getString<"day" | "month" | "year">(el, "minView"),
8484
maxView: getString<"day" | "month" | "year">(el, "maxView"),
85+
defaultOpen: false,
8586
inline: getBoolean(el, "inline"),
8687
positioning: readPositioningOptions(el),
8788
...resolveZagDatePickerTranslations(el),

assets/hooks/number-input.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ const NumberInputHook: Hook<object & NumberInputHookState, HTMLElement> = {
3636
);
3737
if (valueInput) {
3838
valueInput.value = details.value ?? "";
39+
valueInput.dispatchEvent(new Event("input", { bubbles: true }));
40+
valueInput.dispatchEvent(new Event("change", { bubbles: true }));
3941
}
4042
}
4143
notifyChange({

assets/hooks/radio-group.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ const RadioGroupHook: Hook<object & RadioGroupHookState, HTMLElement> = {
3434
dir: getDir(el),
3535
orientation: getString<"horizontal" | "vertical">(el, "orientation"),
3636
onValueChange: (details: ValueChangeDetails) => {
37+
const checked = el.querySelector<HTMLInputElement>(
38+
'[data-scope="radio-group"][data-part="item-hidden-input"]:checked'
39+
);
40+
if (checked) {
41+
checked.dispatchEvent(new Event("input", { bubbles: true }));
42+
checked.dispatchEvent(new Event("change", { bubbles: true }));
43+
}
3744
notifyChange({
3845
el,
3946
canPushServer: canPush(),

assets/hooks/select.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ const SelectHook: Hook<object & SelectHookState, HTMLElement> = {
9797
: details.value.length === 1
9898
? String(details.value[0])
9999
: details.value.map(String).join(",");
100+
valueInput.dispatchEvent(new Event("input", { bubbles: true }));
100101
valueInput.dispatchEvent(new Event("change", { bubbles: true }));
101102
}
102103

assets/hooks/signature-pad.ts

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type { Props } from "@zag-js/signature-pad";
66
import { getBoolean, getNumber, getString } from "../lib/util";
77
import { idMatches, readPayloadId } from "../lib/respond-to";
88

9+
const PHX_HAS_FOCUSED = "phx-has-focused";
10+
911
function parsePathsFromDataset(el: HTMLElement, key: "defaultPaths" | "paths"): string[] {
1012
const raw = el.dataset[key];
1113
if (!raw) return [];
@@ -15,6 +17,12 @@ function parsePathsFromDataset(el: HTMLElement, key: "defaultPaths" | "paths"):
1517
.filter(Boolean);
1618
}
1719

20+
function reapplyLiveViewValueInputUsage(input: HTMLInputElement) {
21+
const p = input as HTMLInputElement & { phxPrivate?: Record<string, boolean> };
22+
if (!p.phxPrivate) p.phxPrivate = {};
23+
p.phxPrivate[PHX_HAS_FOCUSED] = true;
24+
}
25+
1826
function buildDrawingOptions(el: HTMLElement): NonNullable<Props["drawing"]> {
1927
const o: Record<string, unknown> = {
2028
fill: getString(el, "drawingFill"),
@@ -29,16 +37,24 @@ function buildDrawingOptions(el: HTMLElement): NonNullable<Props["drawing"]> {
2937
return o as NonNullable<Props["drawing"]>;
3038
}
3139

32-
function queueFormBubblingInputForPhoenix(el: HTMLElement, getValue: () => string): void {
40+
function queueFormBubblingInputForPhoenix(
41+
el: HTMLElement,
42+
getValue: () => string,
43+
opts: { onPadTouched: () => void }
44+
): void {
3345
queueMicrotask(() => {
3446
const input = el.querySelector<HTMLInputElement>(
3547
'[data-scope="signature-pad"][data-part="hidden-input"]'
3648
);
37-
if (!input) return;
49+
if (!input) {
50+
return;
51+
}
3852
const v = getValue();
3953
if (String(input.value) !== String(v)) {
4054
input.value = v;
4155
}
56+
opts.onPadTouched();
57+
reapplyLiveViewValueInputUsage(input);
4258
input.dispatchEvent(new Event("input", { bubbles: true }));
4359
input.dispatchEvent(new Event("change", { bubbles: true }));
4460
});
@@ -48,14 +64,34 @@ type SignaturePadHookState = {
4864
signaturePad?: SignaturePad;
4965
handlers?: Array<CallbackRef>;
5066
onClear?: (event: Event) => void;
67+
padTouched: boolean;
5168
};
5269

5370
const SignaturePadHook: Hook<object & SignaturePadHookState, HTMLElement> = {
5471
mounted(this: object & HookInterface<HTMLElement> & SignaturePadHookState) {
5572
const el = this.el;
73+
const hook = this as object & SignaturePadHookState;
5674
const pushEvent = this.pushEvent.bind(this);
75+
hook.padTouched = false;
76+
const markTouched = () => {
77+
hook.padTouched = true;
78+
};
5779

5880
const defaultPaths = parsePathsFromDataset(el, "defaultPaths");
81+
{
82+
const input = el.querySelector<HTMLInputElement>(
83+
'[data-scope="signature-pad"][data-part="hidden-input"]'
84+
);
85+
if (String(input?.value ?? "") !== "" || defaultPaths.length > 0) {
86+
hook.padTouched = true;
87+
queueMicrotask(() => {
88+
const i = el.querySelector<HTMLInputElement>(
89+
'[data-scope="signature-pad"][data-part="hidden-input"]'
90+
);
91+
if (i) reapplyLiveViewValueInputUsage(i);
92+
});
93+
}
94+
}
5995

6096
const signaturePad = new SignaturePad(el, {
6197
id: el.id,
@@ -65,8 +101,10 @@ const SignaturePadHook: Hook<object & SignaturePadHookState, HTMLElement> = {
65101
onDrawEnd: (details) => {
66102
signaturePad.setPaths(details.paths);
67103

68-
queueFormBubblingInputForPhoenix(el, () =>
69-
details.paths.length > 0 ? details.paths.join("\n") : ""
104+
queueFormBubblingInputForPhoenix(
105+
el,
106+
() => (details.paths.length > 0 ? details.paths.join("\n") : ""),
107+
{ onPadTouched: markTouched }
70108
);
71109

72110
details.getDataUrl("image/png").then((url) => {
@@ -99,12 +137,11 @@ const SignaturePadHook: Hook<object & SignaturePadHookState, HTMLElement> = {
99137
} as Props);
100138
signaturePad.init();
101139
this.signaturePad = signaturePad;
102-
103140
this.onClear = (event: Event) => {
104141
const { id: targetId } = (event as CustomEvent<{ id: string }>).detail;
105142
if (targetId && targetId !== el.id) return;
106143
signaturePad.api.clear();
107-
queueFormBubblingInputForPhoenix(el, () => "");
144+
queueFormBubblingInputForPhoenix(el, () => "", { onPadTouched: markTouched });
108145
};
109146
el.addEventListener("corex:signature-pad:clear", this.onClear);
110147

@@ -114,14 +151,13 @@ const SignaturePadHook: Hook<object & SignaturePadHookState, HTMLElement> = {
114151
this.handleEvent("signature_pad_clear", (payload: unknown) => {
115152
if (!idMatches(el.id, readPayloadId(payload))) return;
116153
signaturePad.api.clear();
117-
queueFormBubblingInputForPhoenix(el, () => "");
154+
queueFormBubblingInputForPhoenix(el, () => "", { onPadTouched: markTouched });
118155
})
119156
);
120157
},
121158

122159
updated(this: object & HookInterface<HTMLElement> & SignaturePadHookState) {
123160
const el = this.el;
124-
const defaultPaths = parsePathsFromDataset(el, "defaultPaths");
125161
const name = getString(el, "name");
126162

127163
if (name) {
@@ -131,9 +167,20 @@ const SignaturePadHook: Hook<object & SignaturePadHookState, HTMLElement> = {
131167
this.signaturePad?.updateProps({
132168
id: el.id,
133169
name: name,
134-
...(defaultPaths.length > 0 ? { defaultPaths } : {}),
135170
drawing: buildDrawingOptions(el),
136171
} as Partial<Props>);
172+
173+
if (!this.padTouched) {
174+
return;
175+
}
176+
queueMicrotask(() => {
177+
const input = this.el.querySelector<HTMLInputElement>(
178+
'[data-scope="signature-pad"][data-part="hidden-input"]'
179+
);
180+
if (input) {
181+
reapplyLiveViewValueInputUsage(input);
182+
}
183+
});
137184
},
138185

139186
destroyed(this: object & HookInterface<HTMLElement> & SignaturePadHookState) {

config/config.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import Config
22

33
config :logger, :console,
44
colors: [enabled: false],
5-
format: "\n$time $metadata[$level] $message\n"
5+
format: "\n$time $metadata[$level] $message\n",
6+
metadata: :all
67

78
config :phoenix,
89
json_library: Jason,

e2e/AGENTS.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ custom classes must fully style the input
5050
- Ensure **clean typography, spacing, and layout balance** for a refined, premium look
5151
- Focus on **delightful details** like hover effects, loading states, and smooth page transitions
5252

53-
54-
<!-- usage-rules-start -->
55-
5653
<!-- phoenix:elixir-start -->
5754
## Elixir guidelines
5855

@@ -451,6 +448,4 @@ And **never** do this:
451448

452449
- You are FORBIDDEN from accessing the changeset in the template as it will cause errors
453450
- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/2` assigned in the LiveView module that is derived from a changeset
454-
<!-- phoenix:liveview-end -->
455-
456-
<!-- usage-rules-end -->
451+
<!-- phoenix:liveview-end -->

0 commit comments

Comments
 (0)