@webstudio-is/sdk-components-react-remix 0.74.0 → 0.76.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,117 +1,8 @@
1
1
  import type { PropMeta } from "@webstudio-is/generate-arg-types";
2
2
 
3
3
  export const props: Record<string, PropMeta> = {
4
- slot: { required: false, control: "text", type: "string" },
5
- style: { required: false, control: "text", type: "string" },
6
- title: { required: false, control: "text", type: "string" },
7
- download: { required: false, control: "text", type: "string" },
8
- href: { required: false, control: "text", type: "string" },
9
- hrefLang: { required: false, control: "text", type: "string" },
10
- media: { required: false, control: "text", type: "string" },
11
- ping: { required: false, control: "text", type: "string" },
12
- type: { required: false, control: "text", type: "string" },
13
- referrerPolicy: {
14
- required: false,
15
- control: "select",
16
- type: "string",
17
- options: [
18
- "",
19
- "no-referrer",
20
- "no-referrer-when-downgrade",
21
- "origin",
22
- "origin-when-cross-origin",
23
- "same-origin",
24
- "strict-origin",
25
- "strict-origin-when-cross-origin",
26
- "unsafe-url",
27
- ],
28
- },
29
- defaultChecked: { required: false, control: "boolean", type: "boolean" },
30
- defaultValue: { required: false, control: "text", type: "string" },
31
- suppressContentEditableWarning: {
32
- required: false,
33
- control: "boolean",
34
- type: "boolean",
35
- },
36
- suppressHydrationWarning: {
37
- required: false,
38
- control: "boolean",
39
- type: "boolean",
40
- },
41
- accessKey: { required: false, control: "text", type: "string" },
42
- autoFocus: { required: false, control: "boolean", type: "boolean" },
43
- className: { required: false, control: "text", type: "string" },
44
- contentEditable: { required: false, control: "text", type: "string" },
45
- contextMenu: { required: false, control: "text", type: "string" },
46
- dir: { required: false, control: "text", type: "string" },
47
- draggable: { required: false, control: "boolean", type: "boolean" },
48
- hidden: { required: false, control: "boolean", type: "boolean" },
49
- id: { required: false, control: "text", type: "string" },
50
- lang: { required: false, control: "text", type: "string" },
51
- nonce: { required: false, control: "text", type: "string" },
52
- placeholder: { required: false, control: "text", type: "string" },
53
- spellCheck: { required: false, control: "boolean", type: "boolean" },
54
- tabIndex: { required: false, control: "number", type: "number" },
55
- translate: {
56
- required: false,
57
- control: "radio",
58
- type: "string",
59
- options: ["yes", "no"],
60
- },
61
- radioGroup: { required: false, control: "text", type: "string" },
62
- role: { required: false, control: "text", type: "string" },
63
4
  about: { required: false, control: "text", type: "string" },
64
- content: { required: false, control: "text", type: "string" },
65
- datatype: { required: false, control: "text", type: "string" },
66
- inlist: { required: false, control: "text", type: "string" },
67
- prefix: { required: false, control: "text", type: "string" },
68
- property: { required: false, control: "text", type: "string" },
69
- rel: { required: false, control: "text", type: "string" },
70
- resource: { required: false, control: "text", type: "string" },
71
- rev: { required: false, control: "text", type: "string" },
72
- typeof: { required: false, control: "text", type: "string" },
73
- vocab: { required: false, control: "text", type: "string" },
74
- autoCapitalize: { required: false, control: "text", type: "string" },
75
- autoCorrect: { required: false, control: "text", type: "string" },
76
- autoSave: { required: false, control: "text", type: "string" },
77
- color: { required: false, control: "color", type: "string" },
78
- itemProp: { required: false, control: "text", type: "string" },
79
- itemScope: { required: false, control: "boolean", type: "boolean" },
80
- itemType: { required: false, control: "text", type: "string" },
81
- itemID: { required: false, control: "text", type: "string" },
82
- itemRef: { required: false, control: "text", type: "string" },
83
- results: { required: false, control: "number", type: "number" },
84
- security: { required: false, control: "text", type: "string" },
85
- unselectable: {
86
- required: false,
87
- control: "radio",
88
- type: "string",
89
- options: ["on", "off"],
90
- },
91
- inputMode: {
92
- description:
93
- "Hints at the type of data that might be entered by the user while editing the element or its contents\n@see https://html.spec.whatwg.org/multipage/interaction.html#input-modalities:-the-inputmode-attribute",
94
- required: false,
95
- control: "select",
96
- type: "string",
97
- options: [
98
- "text",
99
- "none",
100
- "search",
101
- "tel",
102
- "url",
103
- "email",
104
- "numeric",
105
- "decimal",
106
- ],
107
- },
108
- is: {
109
- description:
110
- "Specify that a standard HTML element should behave like a defined custom built-in element\n@see https://html.spec.whatwg.org/multipage/custom-elements.html#attr-is",
111
- required: false,
112
- control: "text",
113
- type: "string",
114
- },
5
+ accessKey: { required: false, control: "text", type: "string" },
115
6
  "aria-activedescendant": {
116
7
  description:
117
8
  "Identifies the currently active element when DOM focus is on a composite widget, textbox, group, or application.",
@@ -462,19 +353,123 @@ export const props: Record<string, PropMeta> = {
462
353
  control: "text",
463
354
  type: "string",
464
355
  },
465
- target: {
356
+ autoCapitalize: { required: false, control: "text", type: "string" },
357
+ autoCorrect: { required: false, control: "text", type: "string" },
358
+ autoFocus: { required: false, control: "boolean", type: "boolean" },
359
+ autoSave: { required: false, control: "text", type: "string" },
360
+ className: { required: false, control: "text", type: "string" },
361
+ color: { required: false, control: "color", type: "string" },
362
+ content: { required: false, control: "text", type: "string" },
363
+ contextMenu: { required: false, control: "text", type: "string" },
364
+ datatype: { required: false, control: "text", type: "string" },
365
+ defaultChecked: { required: false, control: "boolean", type: "boolean" },
366
+ dir: { required: false, control: "text", type: "string" },
367
+ draggable: { required: false, control: "boolean", type: "boolean" },
368
+ hidden: { required: false, control: "boolean", type: "boolean" },
369
+ href: { required: false, control: "text", type: "string" },
370
+ hrefLang: { required: false, control: "text", type: "string" },
371
+ id: { required: false, control: "text", type: "string" },
372
+ inputMode: {
373
+ description:
374
+ "Hints at the type of data that might be entered by the user while editing the element or its contents\n@see https://html.spec.whatwg.org/multipage/interaction.html#input-modalities:-the-inputmode-attribute",
466
375
  required: false,
467
376
  control: "select",
468
377
  type: "string",
469
- options: ["_self", "_blank", "_parent", "_top"],
378
+ options: [
379
+ "text",
380
+ "none",
381
+ "search",
382
+ "tel",
383
+ "url",
384
+ "email",
385
+ "numeric",
386
+ "decimal",
387
+ ],
388
+ },
389
+ is: {
390
+ description:
391
+ "Specify that a standard HTML element should behave like a defined custom built-in element\n@see https://html.spec.whatwg.org/multipage/custom-elements.html#attr-is",
392
+ required: false,
393
+ control: "text",
394
+ type: "string",
470
395
  },
396
+ itemID: { required: false, control: "text", type: "string" },
397
+ itemProp: { required: false, control: "text", type: "string" },
398
+ itemRef: { required: false, control: "text", type: "string" },
399
+ itemScope: { required: false, control: "boolean", type: "boolean" },
400
+ itemType: { required: false, control: "text", type: "string" },
401
+ lang: { required: false, control: "text", type: "string" },
402
+ media: { required: false, control: "text", type: "string" },
403
+ nonce: { required: false, control: "text", type: "string" },
404
+ ping: { required: false, control: "text", type: "string" },
405
+ placeholder: { required: false, control: "text", type: "string" },
471
406
  prefetch: {
472
407
  required: false,
473
408
  control: "radio",
474
409
  type: "string",
475
410
  options: ["none", "intent", "render"],
476
411
  },
412
+ prefix: { required: false, control: "text", type: "string" },
413
+ preventScrollReset: { required: false, control: "boolean", type: "boolean" },
414
+ property: { required: false, control: "text", type: "string" },
415
+ radioGroup: { required: false, control: "text", type: "string" },
416
+ referrerPolicy: {
417
+ required: false,
418
+ control: "select",
419
+ type: "string",
420
+ options: [
421
+ "",
422
+ "no-referrer",
423
+ "no-referrer-when-downgrade",
424
+ "origin",
425
+ "origin-when-cross-origin",
426
+ "same-origin",
427
+ "strict-origin",
428
+ "strict-origin-when-cross-origin",
429
+ "unsafe-url",
430
+ ],
431
+ },
432
+ rel: { required: false, control: "text", type: "string" },
477
433
  reloadDocument: { required: false, control: "boolean", type: "boolean" },
478
434
  replace: { required: false, control: "boolean", type: "boolean" },
479
- preventScrollReset: { required: false, control: "boolean", type: "boolean" },
435
+ resource: { required: false, control: "text", type: "string" },
436
+ results: { required: false, control: "number", type: "number" },
437
+ rev: { required: false, control: "text", type: "string" },
438
+ role: { required: false, control: "text", type: "string" },
439
+ security: { required: false, control: "text", type: "string" },
440
+ slot: { required: false, control: "text", type: "string" },
441
+ spellCheck: { required: false, control: "boolean", type: "boolean" },
442
+ suppressContentEditableWarning: {
443
+ required: false,
444
+ control: "boolean",
445
+ type: "boolean",
446
+ },
447
+ suppressHydrationWarning: {
448
+ required: false,
449
+ control: "boolean",
450
+ type: "boolean",
451
+ },
452
+ tabIndex: { required: false, control: "number", type: "number" },
453
+ target: {
454
+ required: false,
455
+ control: "select",
456
+ type: "string",
457
+ options: ["_self", "_blank", "_parent", "_top"],
458
+ },
459
+ title: { required: false, control: "text", type: "string" },
460
+ translate: {
461
+ required: false,
462
+ control: "radio",
463
+ type: "string",
464
+ options: ["yes", "no"],
465
+ },
466
+ type: { required: false, control: "text", type: "string" },
467
+ typeof: { required: false, control: "text", type: "string" },
468
+ unselectable: {
469
+ required: false,
470
+ control: "radio",
471
+ type: "string",
472
+ options: ["on", "off"],
473
+ },
474
+ vocab: { required: false, control: "text", type: "string" },
480
475
  };
package/src/form.tsx CHANGED
@@ -1,109 +1,61 @@
1
1
  import {
2
- type ReactNode,
3
2
  type ElementRef,
4
3
  type ComponentProps,
5
- Children,
6
- cloneElement,
7
4
  forwardRef,
5
+ useRef,
6
+ useEffect,
7
+ useContext,
8
8
  } from "react";
9
- import { useFetcher } from "@remix-run/react";
9
+ import { useFetcher, type Fetcher } from "@remix-run/react";
10
10
  import { formIdFieldName } from "@webstudio-is/form-handlers";
11
- import { getInstanceIdFromComponentProps } from "@webstudio-is/react-sdk";
11
+ import {
12
+ ReactSdkContext,
13
+ getInstanceIdFromComponentProps,
14
+ } from "@webstudio-is/react-sdk";
12
15
 
13
16
  export const defaultTag = "form";
14
17
 
15
- const isComponentNode = (
16
- component: string,
17
- node: Exclude<ReactNode, null | number | string | boolean | undefined>
18
- ) => "props" in node && node.props.instance?.component === component;
19
-
20
- const onlyErrorMessage = (children: ReactNode) =>
21
- Children.map(children, (child): ReactNode => {
22
- if (typeof child !== "object" || child === null) {
23
- return null;
24
- }
25
-
26
- if (isComponentNode("ErrorMessage", child)) {
27
- return child;
28
- }
29
-
30
- if ("props" in child) {
31
- const newChildren = onlyErrorMessage(child.props.children);
32
- return Children.toArray(newChildren).some((child) => child !== null)
33
- ? cloneElement(child, { children: newChildren })
34
- : null;
35
- }
36
-
37
- return onlyErrorMessage(child);
38
- });
39
-
40
- const onlySuccessMessage = (children: ReactNode) =>
41
- Children.map(children, (child): ReactNode => {
42
- if (typeof child !== "object" || child === null) {
43
- return null;
44
- }
45
-
46
- if (isComponentNode("SuccessMessage", child)) {
47
- return child;
48
- }
49
-
50
- if ("props" in child) {
51
- const newChildren = onlySuccessMessage(child.props.children);
52
- return Children.toArray(newChildren).some((child) => child !== null)
53
- ? cloneElement(child, { children: newChildren })
54
- : null;
55
- }
56
-
57
- return onlySuccessMessage(child);
58
- });
59
-
60
- const withoutMessages = (children: ReactNode) =>
61
- Children.map(children, (child): ReactNode => {
62
- if (typeof child !== "object" || child === null) {
63
- return child;
64
- }
18
+ const useOnFetchEnd = <Data,>(
19
+ fetcher: Fetcher<Data>,
20
+ handler: (data: Data) => void
21
+ ) => {
22
+ const latestHandler = useRef(handler);
23
+ latestHandler.current = handler;
65
24
 
25
+ const prevFetcher = useRef(fetcher);
26
+ useEffect(() => {
66
27
  if (
67
- isComponentNode("ErrorMessage", child) ||
68
- isComponentNode("SuccessMessage", child)
28
+ prevFetcher.current.state !== fetcher.state &&
29
+ fetcher.state === "idle" &&
30
+ fetcher.data !== undefined
69
31
  ) {
70
- return null;
71
- }
72
-
73
- if ("props" in child) {
74
- return cloneElement(child, {
75
- children: withoutMessages(child.props.children),
76
- });
32
+ latestHandler.current(fetcher.data);
77
33
  }
34
+ prevFetcher.current = fetcher;
35
+ }, [fetcher]);
36
+ };
78
37
 
79
- return withoutMessages(child);
80
- });
38
+ type State = "initial" | "success" | "error";
81
39
 
82
40
  export const Form = forwardRef<
83
41
  ElementRef<typeof defaultTag>,
84
- Omit<ComponentProps<typeof defaultTag>, "method" | "action"> & {
85
- initialState?: "initial" | "success" | "error";
86
- }
87
- >(({ children, initialState = "initial", ...props }, ref) => {
42
+ ComponentProps<typeof defaultTag> & { state?: State }
43
+ >(({ children, action, method, state = "initial", ...rest }, ref) => {
44
+ const { setDataSourceValue } = useContext(ReactSdkContext);
45
+
88
46
  const fetcher = useFetcher();
89
47
 
90
- const state =
91
- fetcher.type === "done"
92
- ? fetcher.data?.success === true
93
- ? "success"
94
- : "error"
95
- : initialState;
48
+ const instanceId = getInstanceIdFromComponentProps(rest);
96
49
 
97
- const instanceId = getInstanceIdFromComponentProps(props);
50
+ useOnFetchEnd(fetcher, (data) => {
51
+ const state: State = data?.success === true ? "success" : "error";
52
+ setDataSourceValue(instanceId, "state", state);
53
+ });
98
54
 
99
55
  return (
100
- <fetcher.Form {...props} method="post" data-state={state} ref={ref}>
56
+ <fetcher.Form {...rest} method="post" data-state={state} ref={ref}>
101
57
  <input type="hidden" name={formIdFieldName} value={instanceId} />
102
- {state === "success"
103
- ? onlySuccessMessage(children)
104
- : state === "error"
105
- ? onlyErrorMessage(children)
106
- : withoutMessages(children)}
58
+ {children}
107
59
  </fetcher.Form>
108
60
  );
109
61
  });
package/src/form.ws.tsx CHANGED
@@ -1,9 +1,10 @@
1
1
  import { FormIcon } from "@webstudio-is/icons/svg";
2
2
  import { form } from "@webstudio-is/react-sdk/css-normalize";
3
- import type {
4
- PresetStyle,
5
- WsComponentMeta,
6
- WsComponentPropsMeta,
3
+ import {
4
+ type PresetStyle,
5
+ type WsComponentMeta,
6
+ type WsComponentPropsMeta,
7
+ showAttribute,
7
8
  } from "@webstudio-is/react-sdk";
8
9
  import type { defaultTag } from "./form";
9
10
  import { props } from "./__generated__/form.props";
@@ -18,6 +19,7 @@ const presetStyle = {
18
19
  export const meta: WsComponentMeta = {
19
20
  category: "forms",
20
21
  type: "container",
22
+ invalidAncestors: ["Form"],
21
23
  label: "Form",
22
24
  icon: FormIcon,
23
25
  presetStyle,
@@ -30,59 +32,103 @@ export const meta: WsComponentMeta = {
30
32
  {
31
33
  type: "instance",
32
34
  component: "Form",
33
- children: [
34
- {
35
- type: "instance",
36
- component: "Label",
37
- children: [{ type: "text", value: "Name" }],
38
- },
35
+ props: [
39
36
  {
40
- type: "instance",
41
- component: "Input",
42
- props: [{ type: "string", name: "name", value: "name" }],
43
- children: [],
44
- },
45
- {
46
- type: "instance",
47
- component: "Label",
48
- children: [{ type: "text", value: "Email" }],
49
- },
50
- {
51
- type: "instance",
52
- component: "Input",
53
- props: [{ type: "string", name: "name", value: "email" }],
54
- children: [],
55
- },
56
- {
57
- type: "instance",
58
- component: "Button",
59
- children: [{ type: "text", value: "Submit" }],
37
+ name: "state",
38
+ type: "string",
39
+ value: "initial",
40
+ dataSourceRef: {
41
+ type: "variable",
42
+ name: "formState",
43
+ },
60
44
  },
45
+ ],
46
+ children: [
61
47
  {
62
48
  type: "instance",
63
- component: "SuccessMessage",
49
+ label: "Form Content",
50
+ component: "Box",
51
+ props: [
52
+ {
53
+ name: showAttribute,
54
+ type: "boolean",
55
+ value: false,
56
+ dataSourceRef: {
57
+ type: "expression",
58
+ name: "formInitial",
59
+ code: `formState === 'initial' || formState === 'error'`,
60
+ },
61
+ },
62
+ ],
64
63
  children: [
65
64
  {
66
65
  type: "instance",
67
- component: "Text",
68
- children: [
69
- { type: "text", value: "Thank you for getting in touch!" },
70
- ],
66
+ component: "Label",
67
+ children: [{ type: "text", value: "Name" }],
68
+ },
69
+ {
70
+ type: "instance",
71
+ component: "Input",
72
+ props: [{ type: "string", name: "name", value: "name" }],
73
+ children: [],
74
+ },
75
+ {
76
+ type: "instance",
77
+ component: "Label",
78
+ children: [{ type: "text", value: "Email" }],
79
+ },
80
+ {
81
+ type: "instance",
82
+ component: "Input",
83
+ props: [{ type: "string", name: "name", value: "email" }],
84
+ children: [],
85
+ },
86
+ {
87
+ type: "instance",
88
+ component: "Button",
89
+ children: [{ type: "text", value: "Submit" }],
71
90
  },
72
91
  ],
73
92
  },
93
+
74
94
  {
75
95
  type: "instance",
76
- component: "ErrorMessage",
96
+ label: "Success Message",
97
+ component: "Box",
98
+ props: [
99
+ {
100
+ name: showAttribute,
101
+ type: "boolean",
102
+ value: false,
103
+ dataSourceRef: {
104
+ type: "expression",
105
+ name: "formSuccess",
106
+ code: `formState === 'success'`,
107
+ },
108
+ },
109
+ ],
77
110
  children: [
111
+ { type: "text", value: "Thank you for getting in touch!" },
112
+ ],
113
+ },
114
+
115
+ {
116
+ type: "instance",
117
+ label: "Error Message",
118
+ component: "Box",
119
+ props: [
78
120
  {
79
- type: "instance",
80
- component: "Text",
81
- children: [
82
- { type: "text", value: "Sorry, something went wrong." },
83
- ],
121
+ name: showAttribute,
122
+ type: "boolean",
123
+ value: false,
124
+ dataSourceRef: {
125
+ type: "expression",
126
+ name: "formError",
127
+ code: `formState === 'error'`,
128
+ },
84
129
  },
85
130
  ],
131
+ children: [{ type: "text", value: "Sorry, something went wrong." }],
86
132
  },
87
133
  ],
88
134
  },
@@ -91,5 +137,5 @@ export const meta: WsComponentMeta = {
91
137
 
92
138
  export const propsMeta: WsComponentPropsMeta = {
93
139
  props,
94
- initialProps: ["initialState"],
140
+ initialProps: ["state"],
95
141
  };
@@ -23,9 +23,14 @@ export const wrapLinkComponent = (BaseLink: typeof Link) => {
23
23
 
24
24
  if (href?.type === "page") {
25
25
  let to = href.page.path === "" ? "/" : href.page.path;
26
+ const urlTo = new URL(to, "https://any-valid.url");
27
+ to = urlTo.pathname;
28
+
26
29
  if (href.hash !== undefined) {
27
- to += `#${href.hash}`;
30
+ urlTo.hash = encodeURIComponent(href.hash);
31
+ to = `${urlTo.pathname}${urlTo.hash}`;
28
32
  }
33
+
29
34
  return <RemixLink {...props} to={to} ref={ref} />;
30
35
  }
31
36