ciscoheat / sveltekit-superforms

Making SvelteKit forms a pleasure to use!
https://superforms.rocks
MIT License
2.18k stars 65 forks source link

Dynamic Default Values? #356

Closed kigathi-chege closed 7 months ago

kigathi-chege commented 8 months ago

When editing an existing record instead of adding a new one, is it possible to set the default values to dynamic?

For example, I have:

import { z } from "zod";
export const formSchema = z.object({
  name: z.string().min(2).max(50),
  description: z.string().nullable(),
});
export type FormSchema = typeof formSchema;

My initial idea was to do this:


import { z } from "zod";
export const formSchema = (initialValues?: {
  name: string;
  description: string | null;
}) =>
  z.object({
    name: z
      .string()
      .min(2)
      .max(50)
      .default(initialValues?.name ?? ""),
    description: z
      .string()
      .nullable()
      .default(initialValues?.description ?? null),
  });
export type FormSchema = typeof formSchema;

This however doesn't seem to work, and I am left wondering whether I am facing a sveltekit reactivity issue, or whether I am doing this the wrong way.

Either way, is there a way to dynamically set these default values?

ciscoheat commented 8 months ago

Setting default values for fields will make them optional in Superforms, so this is probably not the best approach, and I'm not sure you want it anyway, if you're editing existing records. Doesn't it work to populate it as usual when you call superValidate? https://superforms.rocks/get-started#populate-form-from-database

ndom91 commented 8 months ago

I was thinking of posting a similar question here as I have a similar use-case I'm not sure I'm handling correctly.

I have a form to edit the metadata of an item. However, the form only appears when the user (from a list of items) presses the "Edit" button on one of them.

So the default values should be being set then. Client-side.

Does this make sense then? When submitting, none of the data is inclnuded in the POST body. I've removed the name attributes from the form field as I see they're not required in dataType: 'json' mode. But there doesn't seem to be a replacement JSON Body of the data on the POST request when submitting then :thinking:

import { zodClient } from "sveltekit-superforms/adapters"
import SuperDebug, { defaults, superForm } from "sveltekit-superforms"

// `ui` is a global $state "store" that gets filled when an item from the list is selected to be edited.
const defaultData = {
  id: ui.metadataSidebarData.bookmark?.id,
  url: ui.metadataSidebarData.bookmark?.url,
  title: ui.metadataSidebarData.bookmark?.title,
  description: ui.metadataSidebarData.bookmark?.desc,
  category: ui.metadataSidebarData.bookmark?.category,
  tags: ui.metadataSidebarData.bookmark?.tags,
}

const superformInstance = superForm(defaults(defaultData, zodClient($page.data.metadataForm)), {
  resetForm: false,
  dataType: "json",
  validators: zodClient(metadataSchema),
  onUpdated: ({ form }) => {
    if (form.message?.text) {
      toast.success(form.message.text)
    }
  },
  onError: ({ result }) => {
    if (result.type === "error") {
      toast.error(result.error.message)
    }
  },
})

Where $page.data.metadataForm is the this return value from +page.server.ts:

return {
  metadataForm: await superValidate(zod(metadataSchema))
}

And this is the schema:

import { z } from "zod"

export const metadataSchema = z.object({
  id: z.string().max(500).cuid(),
  title: z.string({ required_error: "A title is required" }).min(2).max(100),
  url: z.string({ required_error: "A URL is required" }).url().max(100),
  description: z.string().max(500).optional(),
  image: z.string().url().optional(),
  category: z.object({
    id: z.string().cuid(),
    name: z.string(),
    description: z.string().optional(),
    userId: z.string().min(2).max(50),
    createdAt: z.string().datetime(),
    updatedAt: z.string().datetime(),
  }),
  tags: z.object({
    id: z.string().cuid(),
    name: z.string(),
    userId: z.string().min(2).max(50),
    createdAt: z.string().datetime(),
    updatedAt: z.string().datetime(),
  }),
})

export type FormSchema = typeof formSchema

There seems to be an additional problem, where as soon as I begin editing/typing into one of the input fields for one of the simple string fields, for example like title, I begin getting svelte too-many-updates errors in the console :/

Svelte component looks like this:

<input
  type="text"
  id="title"
  readonly={!isEditMode}
  bind:value={$form.data.title}
  aria-invalid={$errors.title ? "true" : undefined}
  {...$constraints.title}
  class="bunch of tailwind classes.."
/>

image

ciscoheat commented 8 months ago

@ndom91 It's hard to say without more context, so please make an MRE here on Stackblitz, and then post in one of the support channels on Discord, then I can take a closer look!

ndom91 commented 8 months ago

Sounds good, thanks!

ndom91 commented 8 months ago

Okay so I got it working, but there was one thing which deviates from the docs that I either misunderstood, or might need to be updated. After fixing this all the other issues went away as well :joy:

Using client-side defaults() as the first arguemnt to superForm results in the returned form value (i.e. const { form } = superForm()) to be the entire form, incl. id, errors fields, etc. which messes up the rest of the behaviour. This returned $form store should be an object with only the form data, right?

I got it to work by setting the defaults().data as the first argument to superForm(), not the entire return value of defaults():

  const superformInstance = superForm(
+    defaults(defaultData, zodClient($page.data.metadataForm)).data,
-    defaults(defaultData, zodClient($page.data.metadataForm)),
    {
      resetForm: false,
      dataType: "json",
      validators: zodClient(metadataSchema),
    }
)
ciscoheat commented 8 months ago

defaults returns a SuperValidated type, which is used to instantiate superForm. If you do it as you do now, you'll get no constraints and could have problem with error mapping, as they are a part of the SuperValidated type.

ndom91 commented 8 months ago

Hmm okay, so what would you recommend?

Doing this

const superformInstance = superForm(
    defaults(defaultData, zodClient($page.data.metadataForm)),
    {
      resetForm: false,
      dataType: "json",
      validators: zodClient(metadataSchema),
    }
)

Results in:

const { form } = superForm(..)
console.log($form)

{
  id: '',
  valid: true,
  posted: true,
  errors: {},
  data: {
    id: 'clsp4u0050008ave480bxp3ly',
    title: 'Engadget',
    url: 'https://engadget.com',
    description: 'Find the latest technology news....',
    image: 'https://s.yimg.c..../b.jpg',
    category: {
      id: 'clsordi7y0005ave4risddzf5',
      name: 'Category2',
      description: 'Cat2',
      userId: 'cls57rev90000iw28cg9fl2nr',
      createdAt: 2024-02-16T14:41:59.422Z,
      updatedAt: 2024-02-16T14:41:59.422Z
    },
    tags: []
  }
}

So then I have to go through all my form fields and set <Input bind:value={$form.data.title} /> instead of bind:value={$form.title}. Then whenever I update the data with jsonData in onSubmit upon return the $form is just the "normal" form data object containing only the data key/values (i.e. data object from above) :thinking:

I feel like I'm misunderstanding something critical here maybe haha. Will try to put together a simple stackblitz repro when I have a minute :+1:

ndom91 commented 8 months ago

Interestingly this validation onBlur and error mapping / rendering does seem to work in that case of using .data

image

ciscoheat commented 8 months ago

There is something strange about how you use zodClient. Here you should send a Zod schema to it, but it looks like you're passing a SuperValidated object:

zodClient($page.data.metadataForm)

There should be no Zod schemas in $page.data, as they cannot be serialized, so something is wrong with how you call it. It looks more like you should just call superForm directly.

ndom91 commented 8 months ago

Ah good catch. But fixing that still results in my superForm returned form to basically also be a SuperValidated type, instead of SuperFormData :thinking:

ciscoheat commented 8 months ago

Compare your code with the getting started tutorial on the website, and see what differs. If you can't figure it out, make an MRE.

ndom91 commented 8 months ago

I've been going throguh the "getting started" and comparing stuff all day :sweat_smile:

Anyway, here's a super simple repro: https://stackblitz.com/edit/sveltejs-kit-template-default-z8bhfv?file=src%2Froutes%2F%2Bpage.svelte

It's having the same issue where superForms.form is an instance of SuperValidated (instead of SuperFormData) and my defaultData isn't appearing in the bound input fields.

I couldn't test the form submission stuff here though beacuse as soon as I submit the data, which does get included in the __superform_json formData field, it crashes with an odd error i've never seen before, Error:linemust be greater than 0 (lines start at line 1). Seems like it might not be related to superforms though

ciscoheat commented 8 months ago

Long story short, defaults failed the check for a SuperValidated object. You should be able to use zod instead of zodClient to make it work, until it's fixed in the next release.

ciscoheat commented 8 months ago

If it still doesn't work, try adding an id as an option.

ndom91 commented 8 months ago

Thanks for taking a closer look! Using the non-client version of the zod adapter worked for now :+1:

ciscoheat commented 8 months ago

Should be fixed now with 2.6.1.