Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Phone Number Input #170

Open
ucod3 opened this issue Jan 25, 2024 · 1 comment
Open

Phone Number Input #170

ucod3 opened this issue Jan 25, 2024 · 1 comment

Comments

@ucod3
Copy link

ucod3 commented Jan 25, 2024

Hey Fabian,

I'm testing modular forms with Qwik. I'm using the component from the playground Git repository and have successfully created a custom toPhoneNumber function. It formats a 10-digit number correctly when I type it in. However, I've noticed that I can still type in a string, which is not the default behavior for phone number inputs on the web. Do I need to implement separate logic to handle this, or is there a way to accomplish this through the form API?

index.tsc

import { type DocumentHead } from "@builder.io/qwik-city";
import type { SubmitHandler } from "@modular-forms/qwik";

import { $, component$ } from "@builder.io/qwik";
import { routeLoader$ } from "@builder.io/qwik-city";
import type { InitialValues } from "@modular-forms/qwik";
import { formAction$, valiForm$, useForm } from "@modular-forms/qwik";
import {
  email,
  type Input,
  minLength,
  object,
  string,
  regex,
} from "valibot";
import { TextInput } from "~/components/TextInput";
import { toPhoneNumber } from "~/components/toPhoneNumber";

const LoginSchema = object({
  name: object({
    first: string([minLength(1, "Please enter first name.")]),

    last: string([minLength(1, "Please enter last name.")]),
  }),
  email: string([
    minLength(1, "Please enter an email."),
    email("The email address is badly formatted."),
  ]),
  phone: string([
    minLength(1, "Please enter your phone number."),
    regex(/^\+?(?:[0-9] ?){6,14}[0-9]$/, "Please enter a valid phone number."),
  ]),
});

type LoginForm = Input<typeof LoginSchema>;

const getInitialValues = (): InitialValues<LoginForm> => ({
  name: {
    first: "",
    last: "",
  },
  email: "",
  phone: "",
});


export const useFormLoader = routeLoader$<InitialValues<LoginForm>>(() =>
  getInitialValues(),
);

// export const useFormAction = formAction$<LoginForm>((values) => {
//   // Runs on server
// }, valiForm$(LoginSchema));

export default component$(() => {
  const [loginForm, { Form, Field }] = useForm<LoginForm>({
    loader: useFormLoader(),
    // action: useFormAction(),
    validate: valiForm$(LoginSchema),
  });

  const handleSubmit: SubmitHandler<LoginForm> = $((values, event) => {
    console.log(object(values));
  });

  return (
    <Form onSubmit$={handleSubmit} class="container flex flex-col gap-4">
      <Field name="name.first">
        {(field, props) => (
          <TextInput
            {...props}
            value={field.value}
            error={field.error}
            type="text"
            // label="First Name"
            placeholder="First Name"
            required
          />
        )}
      </Field>
      <Field name="name.last">
        {(field, props) => (
          <TextInput
            {...props}
            value={field.value}
            error={field.error}
            type="text"
            placeholder="Last Name"
            required
          />
        )}
      </Field>
      <Field name="email">
        {(field, props) => (
          <TextInput
            {...props}
            value={field.value}
            error={field.error}
            type="email"
            placeholder="example@email.com"
            required
          />
        )}
      </Field>

      <Field
        name="phone"
        transform={toPhoneNumber({ on: "input" })}
      >
        {(field, props) => (
          <TextInput
            {...props}
            value={field.value}
            error={field.error}
            type="tel"
            placeholder="(000) 000-0000"
            required
            maxlength={14}
          />
        )}
      </Field>
      <div>{loginForm.response.message}</div>
      <button type="submit">Login</button>
    </Form>
  );
});

export const head: DocumentHead = {
  title: "Modular Forms",
  meta: [
    {
      name: "description",
      content: "Modular Form  description",
    },
  ],
};

toPhoneNumber.tsx

import type { TransformOptions } from "@modular-forms/qwik";
import { toCustom$ } from "@modular-forms/qwik";

export function toPhoneNumber(options: TransformOptions) {
  return toCustom$<string>((value) => {
    if (value === undefined) return;
    // Remove everything that is not a number
    const numbers = value.replace(/\D/g, "");

    // Continue if string is not empty
    if (numbers) {
      // Extract area, first 3 and last 4
      const matchResult = numbers.match(/(\d{0,3})(\d{0,3})(\d{0,4})/);

      if (matchResult !== null) {
        const [, area, first3, last4] = matchResult;

        // If length or first 3 is less than 1
        if (first3.length < 1) {
          return `(${area}`;
        }

        // If length or last 4 is less than 1
        if (last4.length < 1) {
          return `(${area}) ${first3}`;
        }

        // Otherwise return full US number
        return `(${area}) ${first3}-${last4}`;
      }
    }

    // Otherwise return an empty string
    return "";
  }, options);
}

TextInput.tsx

import { component$, type QRL, useSignal, useTask$ } from "@builder.io/qwik";
import clsx from "clsx";
import { InputError } from "./InputError";
import { InputLabel } from "./InputLabel";

type TextInputProps = {
  ref: QRL<(element: HTMLInputElement) => void>;
  type: "text" | "email" | "tel" | "password" | "url" | "number" | "date";
  name: string;
  value: string | number | undefined;
  onInput$: (event: Event, element: HTMLInputElement) => void;
  onChange$: (event: Event, element: HTMLInputElement) => void;
  onBlur$: (event: Event, element: HTMLInputElement) => void;
  placeholder?: string;
  required?: boolean;
  class?: string;
  label?: string;
  error?: string;
  form?: string;
  maxlength?: number;
};

/**
 * Text input field that users can type into. Various decorations can be
 * displayed in or around the field to communicate the entry requirements.
 */
export const TextInput = component$(
  ({ label, value, error, ...props }: TextInputProps) => {
    const { name, required } = props;
    const input = useSignal<string | number>();
    useTask$(({ track }) => {
      if (!Number.isNaN(track(() => value))) {
        input.value = value;
      }
    });
    return (
      <div class={clsx("px-8 lg:px-10", props.class)}>
        <InputLabel name={name} label={label} required={required} />
        <input
          {...props}
          class={clsx(
            "h-14 w-full rounded-2xl border-2 bg-white px-5 text-amber-800 outline-none placeholder:text-slate-500 dark:bg-gray-900 md:h-16 md:text-lg lg:h-[70px] lg:px-6 lg:text-xl",
            error
              ? "border-red-600/50 dark:border-red-400/50"
              : "border-slate-200 hover:border-slate-300 focus:border-sky-600/50 dark:border-slate-800 dark:hover:border-slate-700 dark:focus:border-sky-400/50",
          )}
          id={name}
          value={input.value}
          aria-invalid={!!error}
          aria-errormessage={`${name}-error`}
        />
        <InputError name={name} error={error} />
      </div>
    );
  },
);
@fabian-hiller
Copy link
Owner

fabian-hiller commented Jan 26, 2024

You can try to filter and remove letters in toPhoneNumber with value.replace(/[a-z]/gi, '') or value.replace(/\D/g, ''). But it seems like that's already in your code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants