felte icon indicating copy to clipboard operation
felte copied to clipboard

Initial data with optional types becomes non-optional

Open kevinrenskers opened this issue 2 years ago • 2 comments

Is your feature request related to a problem? Please describe. While it makes sense that the form's $data store gets turned into non-optional types, this isn't always so great especially for values that are handled outside of form elements. For example I have a form where I can edit a user, and the user can upload an avatar or clear out the avatar. The avatar is optional, because it can be cleared by sending null to the server (not an empty string). See this example:

<script lang="ts">
  type CreateOrUpdateUser = {
    name: string;
    bio: string;
    avatar?: string;
  };

  export let user: CreateOrUpdateUser;

  const { form, data, isValid, isSubmitting } = createForm({
    initialValues: user,
    onSubmit: values => {
      console.log(values);
    },
  });

  function fileUploaded(file: string) {
    $data.avatar = file;
  }

  function clearAvatar() {
    $data.avatar = undefined;
  }
</script>

<form class="form" use:form>
  // A mixture of input fields for `name` and `bio`, and custom components that call `fileUploaded` and `clearAvatar`
</form>

I am getting a TypeScript error here, because $data.avatar has becomes string, rather than string | undefined.

Describe the solution you'd like If I declare a field as optional, I would want the form library to respect that type and internally deal with it. It shouldn't modify the type I have chosen for a good reason.

Describe alternatives you've considered I could use empty strings and then in the end use transformations to turn empty strings into undefined. But that adds boilerplate which wouldn't be necessary if felte wouldn't modify my types. But much worse, adding a transform function completely gets rid of all types altogether, which really isn't great.

For me this is a dealbreaker to using felte, sadly.

kevinrenskers avatar Sep 02 '22 12:09 kevinrenskers

I agree it would be nice if this was a simple flag in the createForm options that would handle empty strings as undefined.

my workaround right now is:


const felteTransformEmptyStringToUndefined = (v: any) => {
  for (const key in v) {
    const value = v[key]
    if (value === "") v[key] = undefined;
    if (typeof value === "object") felteTransformEmptyStringToUndefined(value)
  }
  return v;
}


const { form, errors, isSubmitting, data } = createForm({
    onSubmit: handleSubmit,
    transform: felteTransformEmptyStringToUndefined,
});

@kevinrenskers is there another Svelte Forms Libary you are using which has this functionallity out of the box?

ralphwest1 avatar Mar 10 '23 18:03 ralphwest1

I'm not using a library but this bit of code:

export function copy<T>(value: T): T {
  return JSON.parse(JSON.stringify(value));
}

type FormConfig<T> = {
  initialValues: T;
  required?: Array<keyof T>;
  validate?: (values: T) => boolean;
  onError?: (error: unknown) => void;
  onSubmit: (values: T) => Promise<void> | undefined;
};

export function createForm<T>(config: FormConfig<T>) {
  const values = copy(config.initialValues);
  const required = config.required || [];

  const form = writable(values);
  const isSubmitting = writable(false);

  const isValid = derived(form, $form => {
    return formIsValid($form);
  });

  const onError = config.onError || handleError;

  function formIsValid(values: T) {
    let valid = true;

    required.forEach(field => {
      const value = values[field];
      if (typeof value === "undefined" || (typeof value === "string" && value === "")) {
        valid = false;
      }
    });

    if (!valid) {
      return false;
    }

    if (config.validate) {
      valid = config.validate(values);
    }

    return valid;
  }

  function handleError(error: unknown) {
    // Custom logic to show the errors
  }

  function handleSubmit(ev: Event & { currentTarget: EventTarget & HTMLFormElement }) {
    if (ev && ev.preventDefault) {
      ev.preventDefault();
    }

    if (!formIsValid(values)) {
      // Invalid form
      return;
    }

    return Promise.resolve()
      .then(() => isSubmitting.set(true))
      .then(() => config.onSubmit(values))
      .finally(() => isSubmitting.set(false))
      .catch(onError);
  }

  return {
    form,
    isValid,
    isSubmitting,
    handleSubmit,
  };
}

kevinrenskers avatar Mar 11 '23 09:03 kevinrenskers