Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion sdk/src/components/channel-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
useContext,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState,
Expand Down Expand Up @@ -74,6 +75,12 @@ const ChannelForm = forwardRef<ChannelFormHandle, Props>(
onChannelPropertiesChanged(channelProperties);
}, [getChannelProperties, onChannelPropertiesChanged]);

// call onChannelPropertiesChanged once on init
Comment thread
ndyhrdy marked this conversation as resolved.
useLayoutEffect(() => {
handleFieldChanged();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const filteredForm = useFilteredFormFields(
session,
form,
Expand Down Expand Up @@ -234,7 +241,13 @@ function formKvToChannelProperties(

// assign next value to channel properties
const nextValue = valueAsArray.length ? valueAsArray.shift() : "";
cursor[parts[0]] = nextValue;

// wrap in array if the subkey ends in []
if (parts[0].endsWith("[]")) {
cursor[parts[0].slice(0, -2)] = [nextValue];
} else {
cursor[parts[0]] = nextValue;
}
}
}

Expand Down
14 changes: 13 additions & 1 deletion sdk/src/components/field-country.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export const CountryField: FunctionComponent<FieldProps> = (props) => {

const [selectedCountry, setSelectedCountry] = useState<
CountryCode | undefined
>(undefined);
>(field.initial_value as CountryCode | undefined);

const selectedCountryIndex = COUNTRIES_AS_DROPDOWN_OPTIONS.findIndex(
(option) => option.value === selectedCountry,
);
Expand All @@ -63,6 +64,17 @@ export const CountryField: FunctionComponent<FieldProps> = (props) => {
[onChange],
);

// on first render populate hidden field with initial value and notify parent of change
useLayoutEffect(() => {
if (field.initial_value) {
if (hiddenFieldRef.current) {
hiddenFieldRef.current.value = selectedCountry || "";
}
onChange();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div>
<input type="hidden" name={name} defaultValue="" ref={hiddenFieldRef} />
Expand Down
26 changes: 19 additions & 7 deletions sdk/src/components/field-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ const toDropdownOptions = (
title: opt.label,
description: opt.subtitle,
value: opt.value,
leadingAsset: opt.icon_url ? (
<img
style={{
height: "16px",
width: "16px",
objectFit: "contain",
}}
src={opt.icon_url}
/>
) : null,
}));
};

Expand All @@ -37,7 +47,7 @@ export const DropdownField: FunctionComponent<FieldProps> = (props) => {
}, [field.type.options]);

const [selectedItemValue, setSelectedItemValue] = useState<string>(
dropdownItems[0]?.value ?? "",
field.initial_value ?? "",
);

const onChangeWrapper = useCallback(
Expand All @@ -51,16 +61,18 @@ export const DropdownField: FunctionComponent<FieldProps> = (props) => {
[onChange],
);

useLayoutEffect(() => {
// first render only, force select first option
if (dropdownItems.length) onChangeWrapper(dropdownItems[0]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const selectedIndex = dropdownItems.findIndex(
(opt) => opt.value === selectedItemValue,
);

// call onChange once on init
useLayoutEffect(() => {
if (field.initial_value) {
onChangeWrapper(dropdownItems[selectedIndex]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<>
<Dropdown
Expand Down
56 changes: 50 additions & 6 deletions sdk/src/components/field-phone-number.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import parsePhoneNumberFromString, {
import examples from "libphonenumber-js/mobile/examples";
import { useSession } from "./session-provider";
import { formFieldId, formFieldName } from "../utils";
import { useCallback, useMemo, useRef, useState } from "preact/hooks";
import {
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "preact/hooks";
import { FunctionComponent, TargetedEvent, TargetedFocusEvent } from "preact";
import { InternalSetFieldTouchedEvent } from "../private-event-types";

Expand All @@ -25,7 +31,12 @@ export const PhoneNumberField: FunctionComponent<FieldProps> = (props) => {

const hiddenFieldRef = useRef<HTMLInputElement>(null);

const [countryCode, setCountryCode] = useState(session.country);
const initial = useMemo(
() => initialValues(field.initial_value, session.country),
[field.initial_value, session.country],
);

const [countryCode, setCountryCode] = useState(initial.country);
const countryCodeIndex = useMemo(() => {
const index = COUNTRIES_WITH_DIAL_CODES_AS_DROPDOWN_OPTIONS.findIndex(
(r) => r.value === countryCode,
Expand All @@ -36,7 +47,7 @@ export const PhoneNumberField: FunctionComponent<FieldProps> = (props) => {
const country =
COUNTRIES_WITH_DIAL_CODES_AS_DROPDOWN_OPTIONS[countryCodeIndex];

const [localNumber, setLocalNumber] = useState("");
const [localNumber, setLocalNumber] = useState(initial.localNumber);
const inputRef = useRef<HTMLInputElement>(null);

const formatPhoneNumber = useCallback(
Expand Down Expand Up @@ -104,20 +115,31 @@ export const PhoneNumberField: FunctionComponent<FieldProps> = (props) => {
);
}

function formatForUser() {
const phoneNumber = sanitizePhoneNumber(country, localNumber);
function formatForUser(_country = country, _localNumber = localNumber) {
const phoneNumber = sanitizePhoneNumber(_country, _localNumber);
if (phoneNumber) {
const international = phoneNumber.formatInternational();
// remove country dial code from displayed local number
setLocalNumber(
international.replace(
`+${getCountryCallingCode(country.value as CountryCode)} `,
`+${getCountryCallingCode(_country.value as CountryCode)} `,
"",
),
);
}
}

// on first render, populate hidden input and notify parent component of initial value
useLayoutEffect(() => {
if (field.initial_value) {
if (hiddenFieldRef.current) {
hiddenFieldRef.current.value = field.initial_value;
}
onChange();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div className="xendit-input-phone">
<Dropdown
Expand Down Expand Up @@ -174,3 +196,25 @@ const sanitizePhoneNumber = (

return null;
};

function initialValues(initial: string | undefined, sessionCountry: string) {
const defaultInitial = {
country: sessionCountry,
localNumber: "",
};
if (!initial) return defaultInitial;
const parsed = parsePhoneNumberFromString(initial);
if (!parsed) return defaultInitial;
const countryOption = COUNTRIES_WITH_DIAL_CODES_AS_DROPDOWN_OPTIONS.find(
(option) => option.value === parsed.country,
);
if (!countryOption) return defaultInitial;
const sanitized = sanitizePhoneNumber(countryOption, parsed.nationalNumber);
if (!sanitized) return defaultInitial;
const international = parsed.formatInternational();
const countryCode = getCountryCallingCode(countryOption.value as CountryCode);
return {
country: countryOption.value as string,
localNumber: international.replace(`+${countryCode} `, ""),
};
}
39 changes: 35 additions & 4 deletions sdk/src/components/field-province.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useCallback, useLayoutEffect } from "preact/hooks";
import { useRef, useCallback, useLayoutEffect, useState } from "preact/hooks";
import { FieldProps } from "./field";
import { CountryCode } from "libphonenumber-js";
import { Dropdown, DropdownOption } from "./core/dropdown";
Expand Down Expand Up @@ -27,9 +27,12 @@ export const ProvinceField: FunctionComponent<FieldProps> = (props) => {
const allFields = useChannel()?.form;
const channelProperties = useChannelProperties();

const [value, setValue] = useState(field.initial_value as string);

const hiddenFieldRef = useRef<HTMLInputElement>(null);

const clearValue = useCallback(() => {
setValue("");
if (hiddenFieldRef.current) {
hiddenFieldRef.current.value = "";
}
Expand All @@ -38,6 +41,7 @@ export const ProvinceField: FunctionComponent<FieldProps> = (props) => {

const onChangeDropdown = useCallback(
(option: DropdownOption) => {
setValue(option.value);
if (hiddenFieldRef.current) {
hiddenFieldRef.current.value = option.value;
}
Expand All @@ -49,6 +53,7 @@ export const ProvinceField: FunctionComponent<FieldProps> = (props) => {

const onChangeInput = useCallback(
(e: TargetedEvent<HTMLInputElement>) => {
setValue(e.currentTarget.value);
if (hiddenFieldRef.current) {
hiddenFieldRef.current.value = (e.target as HTMLInputElement).value;
}
Expand All @@ -67,14 +72,38 @@ export const ProvinceField: FunctionComponent<FieldProps> = (props) => {
session,
),
);
const selectedOptionIndex = options
? options.findIndex((option) => option.value === value)
: -1;

// if the option list changes, clear the selection
// if the options list changes, clear the value,
// but not on first render,
// or if the current value happens to be a valid option in the new list
const previousOptions = usePrevious(options);
const didRenderOnce = useRef(false);
useLayoutEffect(() => {
if (previousOptions !== options) {
if (!didRenderOnce.current) {
didRenderOnce.current = true;
return;
}

// if options list changes, clear the selected value
if (options !== previousOptions) {
if (selectedOptionIndex !== -1) return; // ok, this is still valid
clearValue();
}
}, [clearValue, options, previousOptions]);
}, [clearValue, options, previousOptions, selectedOptionIndex]);

// on first render, populate hidden field and notify parent of initial value
useLayoutEffect(() => {
if (field.initial_value) {
if (hiddenFieldRef.current) {
hiddenFieldRef.current.value = value;
}
onChange();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<>
Expand All @@ -84,6 +113,7 @@ export const ProvinceField: FunctionComponent<FieldProps> = (props) => {
key={objectId(options)}
id={id}
options={options}
selectedIndex={selectedOptionIndex}
onChange={onChangeDropdown}
placeholder={field.placeholder}
enableSearch
Expand All @@ -93,6 +123,7 @@ export const ProvinceField: FunctionComponent<FieldProps> = (props) => {
<input
type="text"
id={id}
value={value}
onChange={onChangeInput}
placeholder={field.placeholder}
className={`xendit-form-field-inner xendit-text-14`}
Expand Down
6 changes: 5 additions & 1 deletion sdk/src/components/field-text.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChannelFormField } from "../backend-types/channel";
import { FieldProps } from "./field";
import { formFieldId, formFieldName } from "../utils";
import { useRef } from "preact/hooks";
import { useRef, useState } from "preact/hooks";
import { FunctionComponent, TargetedEvent, TargetedFocusEvent } from "preact";
import { InternalSetFieldTouchedEvent } from "../private-event-types";

Expand All @@ -11,7 +11,10 @@ export const TextField: FunctionComponent<FieldProps> = (props) => {
const name = formFieldName(field);
const inputRef = useRef<HTMLInputElement>(null);

const [value, setValue] = useState(field.initial_value ?? "");

function handleChange(event: TargetedEvent<HTMLInputElement>): void {
setValue(event.currentTarget.value);
onChange();
}

Expand All @@ -29,6 +32,7 @@ export const TextField: FunctionComponent<FieldProps> = (props) => {
type="text"
placeholder={field.placeholder}
className={`xendit-form-field-inner xendit-text-14`}
value={value}
onBlur={handleBlur}
onChange={handleChange}
minLength={isTextField(field) ? field.type.min_length : undefined}
Expand Down
Loading
Loading