@madojs/mado 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,43 +1,74 @@
1
- // <x-input label name type placeholder required value @input @blur>
1
+ // <x-input label name type placeholder required value error @input @blur>
2
2
  //
3
3
  // Labeled input that proxies its events. Use inside `useForm()`:
4
4
  //
5
5
  // <x-input name="email" type="email" required
6
6
  // @input=${form.onInput} @blur=${form.onBlur}></x-input>
7
+ //
8
+ // Shadow DOM integration notes:
9
+ // - `name` and `value` are exposed as DOM properties on the host so that
10
+ // event retargeting (e.target → <x-input>) still works with useForm().
11
+ // useForm.onInput reads e.target.name and e.target.value — without these
12
+ // getters the form silently receives undefined.
13
+ // - The inner <input> dispatches its events with `composed: true` (native
14
+ // behaviour), so @input/@blur bubble through the shadow boundary.
7
15
 
8
16
  import { component, css, html } from "@madojs/mado";
9
17
 
10
18
  component(
11
19
  "x-input",
12
- ({ host }) => () => {
13
- const label = host.getAttribute("label") ?? "";
14
- const name = host.getAttribute("name") ?? "";
15
- const type = host.getAttribute("type") ?? "text";
16
- const placeholder = host.getAttribute("placeholder") ?? "";
17
- const required = host.hasAttribute("required");
18
- const value = host.getAttribute("value") ?? "";
19
- const error = host.getAttribute("error");
20
+ ({ host, attr }) => {
21
+ const label = attr("label", "");
22
+ const name = attr("name", "");
23
+ const type = attr("type", "text");
24
+ const placeholder = attr("placeholder", "");
25
+ const required = attr("required");
26
+ const value = attr("value", "");
27
+ const error = attr("error");
28
+
29
+ // Proxy properties so useForm().onInput can read e.target.name / .value
30
+ // even after Shadow DOM retargets e.target from <input> to <x-input>.
31
+ Object.defineProperty(host, "name", {
32
+ get: () => host.getAttribute("name") ?? "",
33
+ configurable: true,
34
+ });
35
+ Object.defineProperty(host, "value", {
36
+ get: () => host.shadowRoot?.querySelector("input")?.value ?? "",
37
+ set: (v: string) => {
38
+ const input = host.shadowRoot?.querySelector("input");
39
+ if (input) input.value = v;
40
+ },
41
+ configurable: true,
42
+ });
20
43
 
21
- return html`
44
+ return () => html`
22
45
  <label>
23
- ${label
24
- ? html`<span class="lbl">${label}${required ? html`<em>*</em>` : null}</span>`
25
- : null}
46
+ ${() =>
47
+ label()
48
+ ? html`<span class="lbl"
49
+ >${label}${() =>
50
+ required() !== "" ? html`<em>*</em>` : null}</span
51
+ >`
52
+ : null}
26
53
  <input
27
54
  name=${name}
28
55
  type=${type}
29
56
  placeholder=${placeholder}
30
- ?required=${required}
57
+ ?required=${() => required() !== ""}
31
58
  .value=${value}
32
- >
33
- ${error ? html`<small class="err">${error}</small>` : null}
59
+ />
60
+ ${() => (error() ? html`<small class="err">${error}</small>` : null)}
34
61
  </label>
35
62
  `;
36
63
  },
37
64
  {
38
65
  styles: css`
39
- :host { display: block; }
40
- label { display: block; }
66
+ :host {
67
+ display: block;
68
+ }
69
+ label {
70
+ display: block;
71
+ }
41
72
  .lbl {
42
73
  display: block;
43
74
  font-size: 12px;
@@ -71,4 +102,4 @@ component(
71
102
  }
72
103
  `,
73
104
  },
74
- );
105
+ );
@@ -71,9 +71,9 @@ export function createApiClient(baseUrl: string) {
71
71
  credentials: "include",
72
72
  });
73
73
  if (!res.ok) return false;
74
- const data = (await res.json().catch(() => null)) as
75
- | { accessToken?: string }
76
- | null;
74
+ const data = (await res.json().catch(() => null)) as {
75
+ accessToken?: string;
76
+ } | null;
77
77
  if (!data?.accessToken) return false;
78
78
  accessToken.set(data.accessToken);
79
79
  return true;
@@ -118,7 +118,11 @@ export function createApiClient(baseUrl: string) {
118
118
  }
119
119
  if (!res.ok) {
120
120
  const body = await res.json().catch(() => null);
121
- throw new ApiError(res.status, body, `HTTP ${res.status} ${res.statusText}`);
121
+ throw new ApiError(
122
+ res.status,
123
+ body,
124
+ `HTTP ${res.status} ${res.statusText}`,
125
+ );
122
126
  }
123
127
  if (res.status === 204) return null as unknown as T;
124
128
  return (await res.json()) as T;
@@ -131,3 +135,50 @@ export function createApiClient(baseUrl: string) {
131
135
 
132
136
  /** Default app-wide client. Change the base URL via mado.config.json dev.proxy. */
133
137
  export const api = createApiClient("/api");
138
+
139
+ /**
140
+ * Fetcher for resource() that attaches the Bearer token automatically.
141
+ * Use this instead of jsonFetcher() for protected endpoints:
142
+ *
143
+ * const stats = resource(() => "/api/admin/stats", apiFetcher<Stats>());
144
+ *
145
+ * Unlike jsonFetcher(), this:
146
+ * - Sends the access token from memory (no cookie-based auth needed).
147
+ * - Does NOT prepend a base URL — the key is the full URL (matches
148
+ * resource key semantics).
149
+ * - Throws ApiError on non-2xx (consistent with api()).
150
+ */
151
+ export function apiFetcher<T>(): (
152
+ url: string,
153
+ signal: AbortSignal,
154
+ ) => Promise<T> {
155
+ return async (url, abortSignal) => {
156
+ const token = accessToken();
157
+ const headers = new Headers();
158
+ headers.set("accept", "application/json");
159
+ if (token) headers.set("authorization", `Bearer ${token}`);
160
+
161
+ const res = await fetch(url, {
162
+ signal: abortSignal,
163
+ headers,
164
+ credentials: "include",
165
+ });
166
+
167
+ if (!res.ok) {
168
+ let body: unknown = null;
169
+ try {
170
+ const ct = res.headers.get("content-type") ?? "";
171
+ body = ct.includes("json") ? await res.json() : await res.text();
172
+ } catch {
173
+ body = null;
174
+ }
175
+ throw new ApiError(
176
+ res.status,
177
+ body,
178
+ `HTTP ${res.status} ${res.statusText}`,
179
+ );
180
+ }
181
+ if (res.status === 204) return null as unknown as T;
182
+ return (await res.json()) as T;
183
+ };
184
+ }