@salesforcedevs/dw-components 1.3.175 → 1.3.176-canary.1

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.
@@ -0,0 +1,353 @@
1
+ import { toJson } from "dxUtils/normalizers";
2
+ import { stateMap } from "dwUtils/forms";
3
+ import { api } from "lwc";
4
+ import debounce from "debounce";
5
+ import { LightningElementWithState } from "dxBaseElements/lightningElementWithState";
6
+ import { DrmFormData } from "dw/j2sCardSequence";
7
+
8
+ // Hard-coding for now because we can't get information about an endpoint for this
9
+ const tbidValidRoles = [
10
+ "Developer",
11
+ "Administrator",
12
+ "Architect",
13
+ "Sales Representative / Manager",
14
+ "Marketer",
15
+ "Service Agent / Supervisor",
16
+ "Data Analyst",
17
+ "Consultant",
18
+ "Business Operations Manager",
19
+ "Designer",
20
+ "Educator",
21
+ "Student",
22
+ "Executive",
23
+ "Product Manager"
24
+ ];
25
+
26
+ export type J2sFormState = DrmFormData & {
27
+ legalText: string;
28
+ marketingText: string;
29
+ learnMore: string;
30
+ showLegalCheckbox: boolean;
31
+ showMarketingText: boolean;
32
+ isFormValid: boolean;
33
+ };
34
+
35
+ const debounceCallback = (callback: Function) =>
36
+ debounce(callback, 1000 / 15 /* 15fps roughly */);
37
+
38
+ export default class J2sForm extends LightningElementWithState<J2sFormState> {
39
+ private _countriesJson = "";
40
+ private _countriesLookup: any = {}; // TODO typings
41
+ private _didSetFormId = false;
42
+ private _isInitialRender = true;
43
+ private _recaptchaToken = "";
44
+
45
+ @api formMetadata: any = {};
46
+ @api id = "";
47
+ @api incomingUserInfo = {} as DrmFormData;
48
+ @api referralText = "";
49
+
50
+ @api
51
+ get countries() {
52
+ return this._countriesJson || "[]";
53
+ }
54
+ set countries(value: string) {
55
+ this._countriesJson = value;
56
+
57
+ // Store a lookup table of country data by value so that we don't have to loop all the time
58
+ const countries = toJson(value);
59
+ const countriesLookup: any = {};
60
+ countries.forEach((country: any) => {
61
+ countriesLookup[country.value] = country;
62
+ });
63
+ this._countriesLookup = countriesLookup;
64
+ }
65
+
66
+ @api
67
+ get formDataForDrm(): Partial<DrmFormData> {
68
+ // We should be able to just get the formData from the form automatically using `new FormData(theForm)`,
69
+ // but our custom form elements don't seem to implement what it is required for that currently. See:
70
+ // https://web.dev/more-capable-form-controls/#defining-a-form-associated-custom-element
71
+ const formData: any = {};
72
+ this.template
73
+ .querySelectorAll("form [name]")
74
+ .forEach((namedElement) => {
75
+ const typedElement = namedElement as HTMLInputElement;
76
+ formData[typedElement.name] =
77
+ typeof typedElement.checked === "boolean"
78
+ ? typedElement.checked
79
+ : typedElement.value;
80
+ });
81
+ return formData;
82
+ }
83
+
84
+ @api
85
+ get recaptchaToken() {
86
+ return this._recaptchaToken;
87
+ }
88
+ set recaptchaToken(value: string) {
89
+ this._recaptchaToken = value;
90
+ if (!this._isInitialRender) {
91
+ // Treat recaptcha checkbox as a required form field even though not technically marked as such:
92
+ this.setState({
93
+ isFormValid: this.checkValidity()
94
+ });
95
+ }
96
+ }
97
+
98
+ private participatingCountryCodes = ["US", "CA", "IN"];
99
+ private countriesWithReferralInput = ["IN"];
100
+ private requiredFormFields: HTMLElement[] = [];
101
+ private showSubmitFailureAlert = false;
102
+
103
+ get isNotParticipatingCountry() {
104
+ return !this.isParticipatingCountry(this.state.MailingCountry);
105
+ }
106
+
107
+ get disableCheckboxes() {
108
+ return this.isSalesforceEmployee || this.isNotParticipatingCountry;
109
+ }
110
+
111
+ get isCountryWithReferralInput() {
112
+ return this.countriesWithReferralInput.includes(
113
+ this.state.MailingCountry
114
+ );
115
+ }
116
+
117
+ get isSalesforceEmployee() {
118
+ return this.state.RelationshipToSalesforce === "Salesforce Employee";
119
+ }
120
+
121
+ get notAvailableAlertInfo() {
122
+ return this.formMetadata.notAvailableAlertInfo;
123
+ }
124
+
125
+ get salesforceEmployeeAlertInfo() {
126
+ return this.formMetadata.salesforceEmployeeAlertInfo;
127
+ }
128
+
129
+ get formSubmitFailureAlertInfo() {
130
+ return this.formMetadata.formSubmitFailureAlertInfo;
131
+ }
132
+
133
+ get jobRoles() {
134
+ return tbidValidRoles.map((role) => ({
135
+ label: role,
136
+ value: role
137
+ }));
138
+ }
139
+
140
+ get showStateProvince(): boolean {
141
+ return this.state.MailingCountry in stateMap;
142
+ }
143
+
144
+ get states(): { id: string; label: string; value: string }[] {
145
+ return (
146
+ stateMap[this.state.MailingCountry as keyof typeof stateMap] || []
147
+ );
148
+ }
149
+
150
+ connectedCallback() {
151
+ const initialState: Partial<J2sFormState> = {
152
+ ...this.incomingUserInfo,
153
+ isFormValid: false,
154
+ showMarketingText: true
155
+ };
156
+ const initialCountry = initialState.MailingCountry;
157
+ const initialCountryData =
158
+ initialCountry && this._countriesLookup[initialCountry];
159
+ if (initialCountryData) {
160
+ initialState.showLegalCheckbox =
161
+ initialCountryData.showLegalCheckbox;
162
+ initialState.showMarketingText =
163
+ initialCountryData.showMarketingText;
164
+ initialState.legalText = initialCountryData.legalText;
165
+ initialState.marketingText = initialCountryData.marketingText;
166
+ initialState.learnMore = initialCountryData.learnMore;
167
+ }
168
+
169
+ this.state = initialState as J2sFormState;
170
+ }
171
+
172
+ renderedCallback(): void {
173
+ if (this._isInitialRender) {
174
+ this.updateInnerHtml("span.j2s-terms", this.formMetadata.j2sTerms);
175
+ this._isInitialRender = false;
176
+ }
177
+
178
+ if (
179
+ this.state.MailingCountry &&
180
+ this.state.MailingCountry !== this.prevState.MailingCountry
181
+ ) {
182
+ // Re-render new country's terms
183
+ this.updateInnerHtml(
184
+ "span.marketing-terms",
185
+ this.state.marketingText
186
+ );
187
+ this.updateInnerHtml("span.legal-terms", this.state.legalText);
188
+ if (this.state.learnMore) {
189
+ this.updateInnerHtml(
190
+ "span.legal-terms-learn-more",
191
+ this.state.learnMore
192
+ );
193
+ } else {
194
+ this.updateInnerHtml("span.legal-terms-learn-more", "");
195
+ this.template
196
+ .querySelector(".legal-terms-learn-more")
197
+ ?.classList.add("hidden");
198
+ }
199
+ }
200
+
201
+ if (this.state.isFormValid !== this.prevState.isFormValid) {
202
+ this.dispatchEvent(
203
+ new CustomEvent("formvaliditychange", {
204
+ detail: {
205
+ isValid: this.state.isFormValid
206
+ },
207
+ bubbles: true,
208
+ composed: true
209
+ })
210
+ );
211
+ }
212
+
213
+ this.requiredFormFields = Array.from(
214
+ this.template.querySelectorAll("[required]")
215
+ );
216
+
217
+ if (this.id && !this._didSetFormId) {
218
+ this._didSetFormId = true;
219
+ this.template.querySelector("form")?.setAttribute("id", this.id);
220
+ }
221
+ }
222
+
223
+ handleLearnMore() {
224
+ this.template
225
+ .querySelector(".legal-terms-learn-more")
226
+ ?.classList.toggle("hidden");
227
+ }
228
+
229
+ debouncedFormFieldChangeHandler = debounceCallback(
230
+ (targetName: string, value: any, isRequiredField = false) => {
231
+ if (targetName === "MailingCountry") {
232
+ this.handleCountryChange(value);
233
+ } else {
234
+ const nextState: Partial<J2sFormState> = {};
235
+ if (targetName) {
236
+ nextState[targetName as keyof J2sFormState] = value;
237
+ }
238
+ if (isRequiredField) {
239
+ nextState.isFormValid = this.checkValidity();
240
+ }
241
+ this.setState(nextState);
242
+ }
243
+
244
+ this.unsetSubmitFailure();
245
+ }
246
+ );
247
+
248
+ handleFormFieldChange = (evt: CustomEvent) => {
249
+ // Event target information must be read prior to debouncing; the target is gone after the debounce.
250
+ const { target = {}, detail } = evt;
251
+ const { name: targetName, required: isRequiredField } =
252
+ target as HTMLInputElement;
253
+ const value =
254
+ typeof detail === "string"
255
+ ? detail
256
+ : typeof detail.checked === "boolean"
257
+ ? detail.checked
258
+ : detail.value;
259
+
260
+ // Perform the real work after a debounce, in order to prevent unnecessary work on rapid form value changes:
261
+ this.debouncedFormFieldChangeHandler(
262
+ targetName,
263
+ value,
264
+ isRequiredField
265
+ );
266
+ };
267
+
268
+ @api
269
+ notifySubmitFailure(formSubmissionFailureState: Partial<DrmFormData>) {
270
+ this.showSubmitFailureAlert = true;
271
+ // After form submission failure, restore at most the following values to the form:
272
+ const {
273
+ FirstName,
274
+ LastName,
275
+ Email,
276
+ MailingCountry,
277
+ MailingState,
278
+ Company__c,
279
+ DRM_Role__c,
280
+ Referral_Email__c
281
+ } = formSubmissionFailureState;
282
+ this.setState({
283
+ FirstName,
284
+ LastName,
285
+ Email,
286
+ MailingCountry,
287
+ MailingState,
288
+ Company__c,
289
+ DRM_Role__c,
290
+ Referral_Email__c
291
+ });
292
+ }
293
+
294
+ private unsetSubmitFailure() {
295
+ if (this.showSubmitFailureAlert) {
296
+ this.showSubmitFailureAlert = false;
297
+ }
298
+ }
299
+
300
+ private handleCountryChange(countryCode: string) {
301
+ const countryData = this._countriesLookup[countryCode];
302
+
303
+ this.setState({
304
+ MailingCountry: countryCode,
305
+ MailingState: undefined,
306
+ learnMore: countryData.learnMore,
307
+ legalText: countryData.legalText,
308
+ marketingText: countryData.marketingText,
309
+ showLegalCheckbox: countryData.showLegalCheckbox,
310
+ showMarketingText: countryData.showMarketingText
311
+ });
312
+
313
+ this.dispatchEvent(
314
+ new CustomEvent("countrychange", {
315
+ bubbles: true,
316
+ composed: true,
317
+ detail: countryCode
318
+ })
319
+ );
320
+
321
+ // Because updating the country can update the required fields, we need to re-set form
322
+ // validity _after_ render
323
+ requestAnimationFrame(() =>
324
+ this.setState({
325
+ isFormValid: this.checkValidity()
326
+ })
327
+ );
328
+ }
329
+
330
+ private updateInnerHtml(selector: string, html: string): void {
331
+ const element = this.template.querySelector(selector);
332
+ if (element) {
333
+ element.innerHTML = html;
334
+ }
335
+ }
336
+
337
+ private isParticipatingCountry(country: string) {
338
+ return this.participatingCountryCodes.includes(country);
339
+ }
340
+
341
+ private checkValidity() {
342
+ return (
343
+ Boolean(this.recaptchaToken) &&
344
+ !this.isSalesforceEmployee &&
345
+ this.isParticipatingCountry(this.state.MailingCountry) &&
346
+ this.requiredFormFields.every(
347
+ (requiredFormField) =>
348
+ // @ts-ignore
349
+ requiredFormField.checkValidity?.() === true
350
+ )
351
+ );
352
+ }
353
+ }