DEV Community

Cover image for I've open-sourced a form library — define metadata once, and it can be rendered anywhere.
wszgrcy
wszgrcy

Posted on

I've open-sourced a form library — define metadata once, and it can be rendered anywhere.

  • After reviewing all major form libraries on the market, I identified a critical flaw: they require repeated definitions to build a form.
  • For example, consider the following (pseudo-code):
interface Test { firstName: string; } 
Enter fullscreen mode Exit fullscreen mode
const form = useForm<Test>({ defaultValues: { firstName: "default", }, onSubmit: async ({ value }) => { console.log(value); }, }); 
Enter fullscreen mode Exit fullscreen mode
<form.Field name="firstName" //... /> 
Enter fullscreen mode Exit fullscreen mode
  • As we all know, the more times you define something, the higher the chance of introducing bugs.
  • In the above scenario, if we want to rename firstName to name, we’d need to update it in at least three places — significantly increasing code fragility.
  • That’s why I created Piying — a form library that achieves all the above functionality with just one definition.
v.object({ firstName: v.optional(v.string(), "default") }); 
Enter fullscreen mode Exit fullscreen mode

How Does Piying Achieve This?

  • First, thanks to valibot, the code above is simply a schema definition.
  • Piying implements a traversal engine that collects metadata from the schema.
  • This metadata is then transformed into components and form configurations, enabling full compatibility across any frontend framework.

But What About Layout Flexibility?

  • While the schema definition is fixed, Piying supports dynamic layout manipulation via the layout method.
  • You can move any field into a container schema that supports nesting — meaning you can freely rearrange the UI layout without changing the schema.
v.intersect([ v.pipe(v.object({}), setAlias("scope1")), v.object({ key1: v.pipe( v.object({ test1: v.pipe(v.optional(v.string(), "value1"), layout({ keyPath: ["#", "@scope1"] })), }), ), }), ]); 
Enter fullscreen mode Exit fullscreen mode
  • This effectively decouples definition from visual positioning.
  • Regarding field order in object, refer to the MDN documentation for details on JavaScript’s object property iteration order.

How to Achieve Advanced Layouts?

  • Sometimes, you need more than just field rendering — think labels, validation hints, tooltips, etc.
  • These can be achieved using wrappers:
v.pipe(v.number(), v.title("k2-label"), setWrappers(["label"])); 
Enter fullscreen mode Exit fullscreen mode
  • If you want to customize the styling of a group of fields, you can define a custom component directly: > While wrappers can be used for field groups, direct component customization is often more convenient.
v.pipe( v.object({ k1: v.pipe(v.string(), v.title("k1-label"), setWrappers(["label"])), k2: v.pipe(v.number(), v.title("k2-label"), v.minValue(10), setWrappers(["label", "validator"])), }), setComponent("fieldset"), ); 
Enter fullscreen mode Exit fullscreen mode

How to Customize Wrappers or Components?

  • The code examples above don’t specify a particular frontend framework — because they’re framework-agnostic.
  • However, wrapper and component definitions are framework-specific.
  • You can find setup instructions for your preferred framework in the Quick Start Guide. Currently supported: React, Vue, and Angular. If you need support for another framework, feel free to open an issue.

Is It Ready for Production?

Project Repository

Contact Me

  • If you have any feedback, suggestions, or questions, feel free to reach out: wszgrcy@gmail.com

Top comments (0)