@lub-crm/forms 1.0.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.
- package/README.md +298 -0
- package/dist/lub-forms.css +1 -0
- package/dist/lub-forms.es.js +5848 -0
- package/dist/lub-forms.es.js.map +1 -0
- package/dist/lub-forms.standalone.js +10 -0
- package/dist/lub-forms.standalone.js.map +1 -0
- package/dist/lub-forms.umd.js +227 -0
- package/dist/lub-forms.umd.js.map +1 -0
- package/package.json +68 -0
- package/src/api/client.ts +115 -0
- package/src/api/index.ts +2 -0
- package/src/api/types.ts +202 -0
- package/src/core/FormProvider.tsx +228 -0
- package/src/core/FormRenderer.tsx +134 -0
- package/src/core/LubForm.tsx +476 -0
- package/src/core/StepManager.tsx +199 -0
- package/src/core/index.ts +4 -0
- package/src/embed.ts +188 -0
- package/src/fields/CheckboxField.tsx +62 -0
- package/src/fields/CheckboxGroupField.tsx +57 -0
- package/src/fields/CountryField.tsx +43 -0
- package/src/fields/DateField.tsx +33 -0
- package/src/fields/DateTimeField.tsx +33 -0
- package/src/fields/DividerField.tsx +16 -0
- package/src/fields/FieldWrapper.tsx +60 -0
- package/src/fields/FileField.tsx +45 -0
- package/src/fields/HiddenField.tsx +18 -0
- package/src/fields/HtmlField.tsx +17 -0
- package/src/fields/NumberField.tsx +39 -0
- package/src/fields/RadioField.tsx +57 -0
- package/src/fields/RecaptchaField.tsx +137 -0
- package/src/fields/SelectField.tsx +49 -0
- package/src/fields/StateField.tsx +84 -0
- package/src/fields/TextField.tsx +51 -0
- package/src/fields/TextareaField.tsx +37 -0
- package/src/fields/TimeField.tsx +33 -0
- package/src/fields/index.ts +84 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useConditionalLogic.ts +59 -0
- package/src/hooks/useFormApi.ts +118 -0
- package/src/hooks/useFormDesign.ts +48 -0
- package/src/hooks/useMultiStep.ts +98 -0
- package/src/index.ts +101 -0
- package/src/main.tsx +40 -0
- package/src/styles/index.css +707 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/countries.ts +163 -0
- package/src/utils/css-variables.ts +63 -0
- package/src/utils/index.ts +3 -0
- package/src/validation/conditional.ts +170 -0
- package/src/validation/index.ts +2 -0
- package/src/validation/schema-builder.ts +327 -0
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lub-crm/forms",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Embeddable form library for Lub CRM",
|
|
6
|
+
"main": "./dist/lub-forms.umd.js",
|
|
7
|
+
"module": "./dist/lub-forms.es.js",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.ts",
|
|
12
|
+
"import": "./dist/lub-forms.es.js",
|
|
13
|
+
"require": "./dist/lub-forms.umd.js"
|
|
14
|
+
},
|
|
15
|
+
"./styles": "./dist/lub-forms.css"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": [
|
|
22
|
+
"*.css"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "vite",
|
|
26
|
+
"build": "bun run build:lib && bun run build:umd && bun run build:standalone && bun run build:clean",
|
|
27
|
+
"build:clean": "rm -f dist/forms.css dist/vite.svg",
|
|
28
|
+
"build:lib": "vite build --config vite.lib.config.ts",
|
|
29
|
+
"build:umd": "vite build --config vite.umd.config.ts",
|
|
30
|
+
"build:standalone": "vite build --config vite.standalone.config.ts",
|
|
31
|
+
"lint": "eslint .",
|
|
32
|
+
"preview": "vite preview",
|
|
33
|
+
"typecheck": "tsc --noEmit"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@hookform/resolvers": "^3.9.0",
|
|
37
|
+
"react-hook-form": "^7.53.0",
|
|
38
|
+
"zod": "^3.23.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
42
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@eslint/js": "^9.39.1",
|
|
46
|
+
"@types/node": "^24.10.1",
|
|
47
|
+
"@types/react": "^19.2.5",
|
|
48
|
+
"@types/react-dom": "^19.2.3",
|
|
49
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
50
|
+
"babel-plugin-react-compiler": "^1.0.0",
|
|
51
|
+
"eslint": "^9.39.1",
|
|
52
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
53
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
54
|
+
"globals": "^16.5.0",
|
|
55
|
+
"react": "^19.2.0",
|
|
56
|
+
"react-dom": "^19.2.0",
|
|
57
|
+
"typescript": "~5.9.3",
|
|
58
|
+
"typescript-eslint": "^8.46.4",
|
|
59
|
+
"vite": "^7.2.4"
|
|
60
|
+
},
|
|
61
|
+
"keywords": [
|
|
62
|
+
"forms",
|
|
63
|
+
"embed",
|
|
64
|
+
"crm",
|
|
65
|
+
"lub"
|
|
66
|
+
],
|
|
67
|
+
"license": "MIT"
|
|
68
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PublicFormResponse,
|
|
3
|
+
SubmitFormRequest,
|
|
4
|
+
SubmitFormResponse,
|
|
5
|
+
ApiError,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
export class LubFormsClient {
|
|
9
|
+
private baseUrl: string;
|
|
10
|
+
|
|
11
|
+
constructor(baseUrl: string) {
|
|
12
|
+
// Remove trailing slash
|
|
13
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fetch a form by ID for public display
|
|
18
|
+
*/
|
|
19
|
+
async getForm(formId: string): Promise<PublicFormResponse> {
|
|
20
|
+
const url = `${this.baseUrl}/public/forms/${formId}`;
|
|
21
|
+
|
|
22
|
+
const response = await fetch(url, {
|
|
23
|
+
method: "GET",
|
|
24
|
+
headers: {
|
|
25
|
+
Accept: "application/json",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
const error = await this.parseError(response);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return response.json() as Promise<PublicFormResponse>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Submit form data
|
|
39
|
+
*/
|
|
40
|
+
async submitForm(
|
|
41
|
+
formId: string,
|
|
42
|
+
data: SubmitFormRequest,
|
|
43
|
+
): Promise<SubmitFormResponse> {
|
|
44
|
+
const url = `${this.baseUrl}/public/forms/${formId}/submit`;
|
|
45
|
+
|
|
46
|
+
const response = await fetch(url, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
Accept: "application/json",
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify(data),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const error = await this.parseError(response);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return response.json() as Promise<SubmitFormResponse>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Confirm double opt-in
|
|
65
|
+
*/
|
|
66
|
+
async confirmOptIn(
|
|
67
|
+
token: string,
|
|
68
|
+
): Promise<{ success: boolean; message: string; submission_id: string }> {
|
|
69
|
+
const url = `${this.baseUrl}/public/forms/confirm/${token}`;
|
|
70
|
+
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
method: "GET",
|
|
73
|
+
headers: {
|
|
74
|
+
Accept: "application/json",
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const error = await this.parseError(response);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return response.json();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async parseError(response: Response): Promise<ApiError> {
|
|
87
|
+
try {
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
return {
|
|
90
|
+
error: data.error || data.message || "An error occurred",
|
|
91
|
+
code: data.code,
|
|
92
|
+
details: data.details,
|
|
93
|
+
};
|
|
94
|
+
} catch {
|
|
95
|
+
return {
|
|
96
|
+
error: `HTTP ${response.status}: ${response.statusText}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Default client instance (can be set via LubForms.init)
|
|
103
|
+
let defaultClient: LubFormsClient | null = null;
|
|
104
|
+
|
|
105
|
+
export function setDefaultClient(client: LubFormsClient): void {
|
|
106
|
+
defaultClient = client;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getDefaultClient(): LubFormsClient | null {
|
|
110
|
+
return defaultClient;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createClient(baseUrl: string): LubFormsClient {
|
|
114
|
+
return new LubFormsClient(baseUrl);
|
|
115
|
+
}
|
package/src/api/index.ts
ADDED
package/src/api/types.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Types matching backend marketing/forms DTOs
|
|
2
|
+
|
|
3
|
+
// Field types
|
|
4
|
+
export type FieldType =
|
|
5
|
+
| "text"
|
|
6
|
+
| "textarea"
|
|
7
|
+
| "email"
|
|
8
|
+
| "phone"
|
|
9
|
+
| "number"
|
|
10
|
+
| "url"
|
|
11
|
+
| "date"
|
|
12
|
+
| "time"
|
|
13
|
+
| "datetime"
|
|
14
|
+
| "select"
|
|
15
|
+
| "radio"
|
|
16
|
+
| "checkbox"
|
|
17
|
+
| "checkbox_group"
|
|
18
|
+
| "file"
|
|
19
|
+
| "hidden"
|
|
20
|
+
| "country"
|
|
21
|
+
| "state"
|
|
22
|
+
| "html"
|
|
23
|
+
| "divider"
|
|
24
|
+
| "recaptcha";
|
|
25
|
+
|
|
26
|
+
// Field width
|
|
27
|
+
export type FieldWidth = "full" | "half" | "third" | "quarter";
|
|
28
|
+
|
|
29
|
+
// Conditional operators
|
|
30
|
+
export type Operator =
|
|
31
|
+
| "equals"
|
|
32
|
+
| "not_equals"
|
|
33
|
+
| "contains"
|
|
34
|
+
| "not_contains"
|
|
35
|
+
| "greater_than"
|
|
36
|
+
| "less_than"
|
|
37
|
+
| "is_empty"
|
|
38
|
+
| "is_not_empty";
|
|
39
|
+
|
|
40
|
+
// Form layout
|
|
41
|
+
export type FormLayout = "vertical" | "horizontal" | "two_column";
|
|
42
|
+
|
|
43
|
+
// Validation rules
|
|
44
|
+
export interface ValidationRules {
|
|
45
|
+
min_length?: number;
|
|
46
|
+
max_length?: number;
|
|
47
|
+
min?: number;
|
|
48
|
+
max?: number;
|
|
49
|
+
pattern?: string;
|
|
50
|
+
allowed_types?: string[];
|
|
51
|
+
max_file_size?: number;
|
|
52
|
+
custom_error?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Select option
|
|
56
|
+
export interface SelectOption {
|
|
57
|
+
value: string;
|
|
58
|
+
label: string;
|
|
59
|
+
selected?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Field options (for select, radio, checkbox_group)
|
|
63
|
+
export interface FieldOptions {
|
|
64
|
+
options: SelectOption[];
|
|
65
|
+
allow_other?: boolean;
|
|
66
|
+
other_label?: string;
|
|
67
|
+
multiple?: boolean;
|
|
68
|
+
searchable?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Conditional logic
|
|
72
|
+
export interface Condition {
|
|
73
|
+
field_name: string;
|
|
74
|
+
operator: Operator;
|
|
75
|
+
value: unknown;
|
|
76
|
+
and?: Condition[];
|
|
77
|
+
or?: Condition[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ConditionalLogic {
|
|
81
|
+
show_if?: Condition;
|
|
82
|
+
hide_if?: Condition;
|
|
83
|
+
required_if?: Condition;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// CRM mapping
|
|
87
|
+
export interface CRMMapping {
|
|
88
|
+
object_type: string;
|
|
89
|
+
field_name: string;
|
|
90
|
+
is_custom_field?: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Form field response
|
|
94
|
+
export interface FormField {
|
|
95
|
+
id: string;
|
|
96
|
+
field_type: FieldType;
|
|
97
|
+
name: string;
|
|
98
|
+
label: string;
|
|
99
|
+
placeholder?: string;
|
|
100
|
+
default_value?: string;
|
|
101
|
+
help_text?: string;
|
|
102
|
+
display_order: number;
|
|
103
|
+
required: boolean;
|
|
104
|
+
validation_rules?: ValidationRules;
|
|
105
|
+
options?: FieldOptions;
|
|
106
|
+
conditional_logic?: ConditionalLogic;
|
|
107
|
+
crm_mapping?: CRMMapping;
|
|
108
|
+
width: FieldWidth;
|
|
109
|
+
css_class?: string;
|
|
110
|
+
is_active: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Button style
|
|
114
|
+
export interface ButtonStyle {
|
|
115
|
+
background_color?: string;
|
|
116
|
+
text_color?: string;
|
|
117
|
+
border_radius?: string;
|
|
118
|
+
size?: "small" | "medium" | "large";
|
|
119
|
+
full_width?: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Form design
|
|
123
|
+
export interface FormDesign {
|
|
124
|
+
layout: FormLayout;
|
|
125
|
+
theme: string;
|
|
126
|
+
background_color?: string;
|
|
127
|
+
text_color?: string;
|
|
128
|
+
primary_color?: string;
|
|
129
|
+
border_color?: string;
|
|
130
|
+
font_family?: string;
|
|
131
|
+
font_size?: string;
|
|
132
|
+
padding?: string;
|
|
133
|
+
field_spacing?: string;
|
|
134
|
+
button_style?: ButtonStyle;
|
|
135
|
+
custom_css?: string;
|
|
136
|
+
show_logo?: boolean;
|
|
137
|
+
logo_url?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Form step (for multi-step forms)
|
|
141
|
+
export interface FormStep {
|
|
142
|
+
id: string;
|
|
143
|
+
name: string;
|
|
144
|
+
description?: string;
|
|
145
|
+
field_ids: string[];
|
|
146
|
+
display_order: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Public form settings
|
|
150
|
+
export interface PublicFormSettings {
|
|
151
|
+
submit_button_text: string;
|
|
152
|
+
success_message: string;
|
|
153
|
+
enable_recaptcha: boolean;
|
|
154
|
+
recaptcha_site_key?: string;
|
|
155
|
+
show_consent_checkbox: boolean;
|
|
156
|
+
consent_text?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Public form response (from GET /public/forms/:id)
|
|
160
|
+
export interface PublicFormResponse {
|
|
161
|
+
id: string;
|
|
162
|
+
name: string;
|
|
163
|
+
description: string;
|
|
164
|
+
fields: FormField[];
|
|
165
|
+
design: FormDesign;
|
|
166
|
+
is_multi_step: boolean;
|
|
167
|
+
steps?: FormStep[];
|
|
168
|
+
settings: PublicFormSettings;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// UTM parameters
|
|
172
|
+
export interface UTMParameters {
|
|
173
|
+
source?: string;
|
|
174
|
+
medium?: string;
|
|
175
|
+
campaign?: string;
|
|
176
|
+
term?: string;
|
|
177
|
+
content?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Submit form request
|
|
181
|
+
export interface SubmitFormRequest {
|
|
182
|
+
data: Record<string, unknown>;
|
|
183
|
+
utm_parameters?: UTMParameters;
|
|
184
|
+
referrer?: string;
|
|
185
|
+
recaptcha_token?: string;
|
|
186
|
+
honeypot?: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Submit form response
|
|
190
|
+
export interface SubmitFormResponse {
|
|
191
|
+
success: boolean;
|
|
192
|
+
submission_id: string;
|
|
193
|
+
message: string;
|
|
194
|
+
requires_confirmation?: boolean;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// API Error response
|
|
198
|
+
export interface ApiError {
|
|
199
|
+
error: string;
|
|
200
|
+
code?: string;
|
|
201
|
+
details?: Record<string, unknown>;
|
|
202
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useForm,
|
|
4
|
+
FormProvider as RHFFormProvider,
|
|
5
|
+
type UseFormReturn,
|
|
6
|
+
type FieldValues,
|
|
7
|
+
} from "react-hook-form";
|
|
8
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
9
|
+
import type { z } from "zod";
|
|
10
|
+
import type { PublicFormResponse, FormField } from "@/api/types";
|
|
11
|
+
|
|
12
|
+
// Form context types
|
|
13
|
+
interface LubFormContextValue {
|
|
14
|
+
form: PublicFormResponse;
|
|
15
|
+
fields: FormField[];
|
|
16
|
+
currentStep: number;
|
|
17
|
+
totalSteps: number;
|
|
18
|
+
isMultiStep: boolean;
|
|
19
|
+
isSubmitting: boolean;
|
|
20
|
+
isSuccess: boolean;
|
|
21
|
+
error: string | null;
|
|
22
|
+
goToStep: (step: number) => void;
|
|
23
|
+
nextStep: () => void;
|
|
24
|
+
prevStep: () => void;
|
|
25
|
+
getFieldsForCurrentStep: () => FormField[];
|
|
26
|
+
getVisibleFields: (formValues: FieldValues) => FormField[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const LubFormContext = createContext<LubFormContextValue | null>(null);
|
|
30
|
+
|
|
31
|
+
export function useLubFormContext(): LubFormContextValue {
|
|
32
|
+
const context = useContext(LubFormContext);
|
|
33
|
+
if (!context) {
|
|
34
|
+
throw new Error("useLubFormContext must be used within a LubFormProvider");
|
|
35
|
+
}
|
|
36
|
+
return context;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Props
|
|
40
|
+
interface LubFormProviderProps {
|
|
41
|
+
form: PublicFormResponse;
|
|
42
|
+
schema: z.ZodSchema;
|
|
43
|
+
defaultValues?: Record<string, unknown>;
|
|
44
|
+
currentStep: number;
|
|
45
|
+
totalSteps: number;
|
|
46
|
+
isSubmitting: boolean;
|
|
47
|
+
isSuccess: boolean;
|
|
48
|
+
error: string | null;
|
|
49
|
+
onStepChange: (step: number) => void;
|
|
50
|
+
children: ReactNode;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function LubFormProvider({
|
|
54
|
+
form,
|
|
55
|
+
schema,
|
|
56
|
+
defaultValues,
|
|
57
|
+
currentStep,
|
|
58
|
+
totalSteps,
|
|
59
|
+
isSubmitting,
|
|
60
|
+
isSuccess,
|
|
61
|
+
error,
|
|
62
|
+
onStepChange,
|
|
63
|
+
children,
|
|
64
|
+
}: LubFormProviderProps) {
|
|
65
|
+
const methods = useForm({
|
|
66
|
+
resolver: zodResolver(schema),
|
|
67
|
+
defaultValues: defaultValues ?? {},
|
|
68
|
+
mode: "onBlur",
|
|
69
|
+
reValidateMode: "onChange",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const isMultiStep = form.is_multi_step && (form.steps?.length ?? 0) > 0;
|
|
73
|
+
|
|
74
|
+
const goToStep = (step: number) => {
|
|
75
|
+
if (step >= 0 && step < totalSteps) {
|
|
76
|
+
onStepChange(step);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const nextStep = () => {
|
|
81
|
+
if (currentStep < totalSteps - 1) {
|
|
82
|
+
onStepChange(currentStep + 1);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const prevStep = () => {
|
|
87
|
+
if (currentStep > 0) {
|
|
88
|
+
onStepChange(currentStep - 1);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const getFieldsForCurrentStep = (): FormField[] => {
|
|
93
|
+
if (!isMultiStep || !form.steps) {
|
|
94
|
+
return form.fields.filter((f) => f.is_active);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const step = form.steps[currentStep];
|
|
98
|
+
if (!step) return [];
|
|
99
|
+
|
|
100
|
+
const stepFieldIds = new Set(step.field_ids);
|
|
101
|
+
return form.fields.filter((f) => f.is_active && stepFieldIds.has(f.id));
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const getVisibleFields = (formValues: FieldValues): FormField[] => {
|
|
105
|
+
const stepFields = getFieldsForCurrentStep();
|
|
106
|
+
return stepFields.filter((field) => {
|
|
107
|
+
if (!field.conditional_logic) return true;
|
|
108
|
+
return evaluateFieldVisibility(field, formValues);
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const contextValue = useMemo(
|
|
113
|
+
(): LubFormContextValue => ({
|
|
114
|
+
form,
|
|
115
|
+
fields: form.fields,
|
|
116
|
+
currentStep,
|
|
117
|
+
totalSteps,
|
|
118
|
+
isMultiStep,
|
|
119
|
+
isSubmitting,
|
|
120
|
+
isSuccess,
|
|
121
|
+
error,
|
|
122
|
+
goToStep,
|
|
123
|
+
nextStep,
|
|
124
|
+
prevStep,
|
|
125
|
+
getFieldsForCurrentStep,
|
|
126
|
+
getVisibleFields,
|
|
127
|
+
}),
|
|
128
|
+
[form, currentStep, totalSteps, isSubmitting, isSuccess, error],
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<LubFormContext.Provider value={contextValue}>
|
|
133
|
+
<RHFFormProvider {...methods}>{children}</RHFFormProvider>
|
|
134
|
+
</LubFormContext.Provider>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Helper to evaluate field visibility based on conditional logic
|
|
139
|
+
function evaluateFieldVisibility(
|
|
140
|
+
field: FormField,
|
|
141
|
+
formValues: FieldValues,
|
|
142
|
+
): boolean {
|
|
143
|
+
const logic = field.conditional_logic;
|
|
144
|
+
if (!logic) return true;
|
|
145
|
+
|
|
146
|
+
// Check show_if
|
|
147
|
+
if (logic.show_if) {
|
|
148
|
+
return evaluateCondition(logic.show_if, formValues);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check hide_if
|
|
152
|
+
if (logic.hide_if) {
|
|
153
|
+
return !evaluateCondition(logic.hide_if, formValues);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function evaluateCondition(
|
|
160
|
+
condition: NonNullable<FormField["conditional_logic"]>["show_if"],
|
|
161
|
+
formValues: FieldValues,
|
|
162
|
+
): boolean {
|
|
163
|
+
if (!condition) return true;
|
|
164
|
+
|
|
165
|
+
const fieldValue = formValues[condition.field_name];
|
|
166
|
+
let result = compareValues(fieldValue, condition.operator, condition.value);
|
|
167
|
+
|
|
168
|
+
// Evaluate AND conditions
|
|
169
|
+
if (condition.and && condition.and.length > 0) {
|
|
170
|
+
for (const andCond of condition.and) {
|
|
171
|
+
if (!evaluateCondition(andCond, formValues)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Evaluate OR conditions
|
|
178
|
+
if (condition.or && condition.or.length > 0) {
|
|
179
|
+
let orResult = false;
|
|
180
|
+
for (const orCond of condition.or) {
|
|
181
|
+
if (evaluateCondition(orCond, formValues)) {
|
|
182
|
+
orResult = true;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
result = result && orResult;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function compareValues(
|
|
193
|
+
fieldValue: unknown,
|
|
194
|
+
operator: string,
|
|
195
|
+
compareValue: unknown,
|
|
196
|
+
): boolean {
|
|
197
|
+
switch (operator) {
|
|
198
|
+
case "equals":
|
|
199
|
+
return fieldValue === compareValue;
|
|
200
|
+
case "not_equals":
|
|
201
|
+
return fieldValue !== compareValue;
|
|
202
|
+
case "contains":
|
|
203
|
+
return String(fieldValue ?? "").includes(String(compareValue));
|
|
204
|
+
case "not_contains":
|
|
205
|
+
return !String(fieldValue ?? "").includes(String(compareValue));
|
|
206
|
+
case "greater_than":
|
|
207
|
+
return Number(fieldValue) > Number(compareValue);
|
|
208
|
+
case "less_than":
|
|
209
|
+
return Number(fieldValue) < Number(compareValue);
|
|
210
|
+
case "is_empty":
|
|
211
|
+
return fieldValue == null || fieldValue === "";
|
|
212
|
+
case "is_not_empty":
|
|
213
|
+
return fieldValue != null && fieldValue !== "";
|
|
214
|
+
default:
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Export React Hook Form methods hook
|
|
220
|
+
export function useFormMethods(): UseFormReturn {
|
|
221
|
+
const methods = useContext(
|
|
222
|
+
RHFFormProvider as unknown as React.Context<UseFormReturn | null>,
|
|
223
|
+
);
|
|
224
|
+
if (!methods) {
|
|
225
|
+
throw new Error("useFormMethods must be used within a LubFormProvider");
|
|
226
|
+
}
|
|
227
|
+
return methods;
|
|
228
|
+
}
|