Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/javub25/formup-live-app
Dynamic Form builder to export CSV data
https://github.com/javub25/formup-live-app
react shadcn-ui typescript zustand
Last synced: 2 days ago
JSON representation
Dynamic Form builder to export CSV data
- Host: GitHub
- URL: https://github.com/javub25/formup-live-app
- Owner: javub25
- Created: 2025-01-28T08:35:47.000Z (2 days ago)
- Default Branch: master
- Last Pushed: 2025-01-28T10:30:57.000Z (2 days ago)
- Last Synced: 2025-01-28T11:32:00.257Z (2 days ago)
- Topics: react, shadcn-ui, typescript, zustand
- Language: TypeScript
- Homepage: https://formup-live.vercel.app/
- Size: 180 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# FormUp live App
You can create custom forms in the Build tab by selecting elements, validating fields without coding, and editing fields.
Use the Preview tab to adjust settings and export form results in a CSV file.## Benefits ⭐
1️⃣ Real-time validation of custom elements.
2️⃣ CSV data export for Excel analysis or database integration.
3️⃣ Real use case as job application form, surveys.## Demo
![Alt text](https://res.cloudinary.com/dukcdzezp/image/upload/v1737995587/msedge_n0xr86r1Bv_oogrwz.gif "Demo FormUp")## Stack
1️⃣ React + TypeScript - Build a typed Formup UI.
2️⃣ Shadcn/ui + TailwindCSS - Library components for:
1️⃣ Form fields elements
2️⃣ Build and Preview tabs
3️⃣ Dndkit - Drag and drop functionality to move FieldLibrary elements in the FormBuilder area.
4️⃣ Zustand - Synchronize Droppable items data between Build and Preview Tabs.
5️⃣ React Hook Form - Validate form fields.
## Structure
```css
public/ # Public assets accessible from the root of the server.
├── drag.svg # Icon for dragging functionality.
src/ # Main source code of the application.
├── assets/ # Static assets like images, icons, and fonts.
│ └── svg/ # SVG icons used in the app.
│ ├── alert.svg
│ ├── download.svg
│ ├── pencil.svg
│ ├── plus.svg
│ ├── save.svg
│ ├── trash.svg
├── components/
│ ├── Draggable/ # Components for draggable items.
│ │ ├── Draggable.tsx # List of draggable items based on id and type
│ │ └── DraggableItem.tsx # Render a draggable item
│ ├── Droppable/
│ │ └── DroppableEditor.tsx # Edit a droppable element
│ │ └── DroppableList.tsx # Render each droppable item into Preview tab
│ ├── FieldLibrary/ # Display form field elements.
│ │ └── FieldLibrary.tsx
│ ├── Fields/ # Form field components
│ │ ├── Email.tsx
│ │ ├── FullName.tsx
│ │ ├── MultipleChoice.tsx
│ │ └── SingleChoice.tsx
│ ├── FormBuilder/ # Form builder component.
│ │ └── FormBuilder.tsx
│ └── TabsNavigation/ # Allows to change between Build and Preview tab.
│ └── TabsNavigation.tsx
│
├── hooks/ # Custom hooks for reusable logic.
│ ├── ActiveDraggable/
│ │ └── useActiveDraggable.tsx # Allows to save the current active draggable id.
│ ├── CreateDraggable/
│ │ └── useCreateDraggable.tsx # Create draggable items for form elements.
│ ├── CreateDroppable/
│ │ └── useCreateDroppable.tsx # Create droppable area for formBuilder.
│ ├── OverDroppable/
│ │ └── useOverDroppable.tsx # Handle droppable area interaction.
│ └── Form/
│ └── useFormDroppable.ts # Create form for DroppableEditor component.
│ └── useFormOptions.ts # Create dynamic form options.
│ └── useFormPreview.ts # Create form for Preview tab.
│ └── useValidateRules.ts # Create Validation for each droppable item.
├── lib/ # Auxiliary libraries and reusable utilities.
│
├── data/
│ └── models/
│ └── DraggableList.tsx # Define each Draggable item with id and label.
│
├── pages/ # Main pages of the application.
│ ├── Build.tsx # Page to build the form.
│ └── Preview.tsx # Page to preview the form.
│
├── store/ # Zustand Global state management
│ └── utils # Methods to use for Zustand store
│ └── index.ts # File to export Zustand utils and middleware.
│ ├── useDroppableStore.ts # Store to manage droppable state.
│
├── types/ # TypeScript types
│ ├── DndContext # Drag and drop context types.
│ └── Draggable # Draggable elements types.
│ └── Droppable # Droppable elements and store types.
│ └── Form
│ └── CSVData.ts # Type of data sent to the CSV File
│ └── Form.ts # Control Type and Form Data.
│ └── ValidationRules.ts # Validation form field rules.
├── utils/ # Reusable utility functions.
│ ├── Draggable/
│ │ └── Attributes/
│ │ └── updateDescription.tsx # Update draggable item name.
│ ├── Events/
│ │ ├── handleEvents.tsx # Event handling utility.
│ │── getActiveElement.tsx # Gets the active element.
│ │── getUniqueID.tsx # Utility to generate unique IDs.
│ ├── Form/
│ │ ├── exportCSV.ts # Allows you to download the form data in a CSV file.
│ │ ├── getCurrentExpression.ts # Check if FullName or Email are required.
│ │ ├── getCurrentRequired.ts # Check if Single/MultipleChoice are required.
│ │ ├── getErrors.ts # Allows you to show an error notification.
│ │ ├── getValidationClicked.ts # Allows you to show a custom error message.
│ │ ├── hasAvailableOptions.ts # Validate options to enable Export CSV button.
│ │ ├── index.ts # Export custom hooks from react-hook-form.
│ │── ShadcnElements.ts # Shadcn/ui form components.
│
├── App.tsx # Main app entry point.
├── index.css # Global styles.
├── main.tsx
├── vite-env.d.ts # Vite configuration for TypeScript.
├── components.json # Configuration for shadcn/ui components.
```## How to install 🛠️
1️⃣ Clone repository
```git
git clone "https://github.com/javub25/formup-live-app.git"
```2️⃣ Install all dependencies from package.json
```npm
npm install
```3️⃣ Run project development mode
```npm
npm run dev
```4️⃣ Run project production mode (optional)
```npm
npm run build
npm run preview
```## Sections 📋
### 1️⃣ Pages
### Build
#### Key concepts
● useActiveDraggable
Custom hook that gives me the current draggable id has already started to move
to the formBuilder area.● useOverDroppable
Custom hook that stores a boolean to handle whenever a draggable item is up from
the formBuilder area.
In that case, it will display a green border.
This behavior is handled through onDragOver Event.● addDroppable
Zustand method to add a new Droppable item to the store, in case that Draggable item is dropped into to the formBuilder.
This behavior is handled through onDragEnd Event.● DndContext
Context that allows to synchronize between draggable item to droppable area.
In this case, FieldLibrary which contains every form field to the FormBuilder.● DragOverlay
Allows you to create a layer as if the draggable element has been moved from its original position.
This works by using the draggable id saved from the useActiveDraggable hook.### Preview
#### Key concepts
● DroppableItems
It will render every draggable item dropped into the form builder based on
its current type (Email, SingleChoice, MultipleChoice...) and dynamic label and options.● DroppableList
This is the place where we render each form field based on the draggable item
moved to the form builder area.
For example, whether user wants to build a custom form with single choice and multiple choice fields, only both fields will be displayed.### 2️⃣ Types
#### DraggableTypes.tsx
● DescriptionType
```ts
export interface DescriptionType {
attributes: {
"aria-roledescription": string;
};
description: string;
}
```Defines the structure of a draggable item's description:
● attributes: An object with an accessibility attribute aria-roledescription
● description: A string describing the unique purpose of the draggable element.
An example in the description of form field cases would be:
● draggable Email
● draggable FullName
● draggable SingleChoice
● draggable MultipleChoice
● DraggableType
```ts
export interface DraggableType {
id: string;
}
```
Represents a basic type for a draggable item it only contains and id field, which uniquely identifies the item.● DraggableItemType
```ts
export interface DraggableItemType extends DraggableType {
key?: string;
label: string;
}```
Extends DraggableType to render Draggable items in FieldLibrary component:● key: An optional property used to uniquely render each draggable element to the FieldLibrary.
● label: The name of the draggable item.
#### DroppableStore.ts
```ts
export type DroppableStore = {
DroppableItems: DroppableItemsType[],
addDroppable: (currentDraggable: DraggableItemType) => void,
removeDroppable: (id: DroppableItemsType["id"]) => void,
updateDroppable: (formData: DroppableItemsType ) => void
}
```Defines the Zustand store, which includes:
● DroppableItems: An array that stores every draggable item in the build tab.
● addDroppable: Method which stores every draggable item in DroppableItem.
● removeDroppable: Method which removes a droppable item by its id.
● updateDroppable: Method which update droppable element through form fields#### DroppableType.ts
● DroppableType
```ts
export type DroppableType = {
IsOver: boolean;
}
```
This property is used to check if a droppable area is being ‘overlapped’ by a draggable element.● DroppableItemsType
```ts
export type DroppableItemsType = {
id: DraggableType["id"],
type: string;
label: string;
options?: {value: string} [],
validation?: ValidateDroppable,
register?: UseFormRegisterCSV
};
```
This type indicates the structure that each droppable element must follow:● id: unique id from the type field (FullName, Email...), followed by a random number.
● type: represents the allowed types (FullName, Email, SingleChoice, MultipleChoice).
● label: represents the name of each field chosen by the user.
● options: List to store options.
It's optional because it's only required in SingleChoice and MultipleChoice to choose one or multiple values.
● validation: Object to store the expression and custom error message for each form field.
It's optional because user could store their fields without validate each other.
● register: Allows to store each field sent to CSV file.● DroppableListType
```ts
export type DroppableListType = Omit
```
Represents each droppable item inside Preview tab.
I use Omit, because I need each props from DroppableItemsType except id.● DroppableField
```ts
export type DroppableField = Pick;```
Each field such as FullName, SingleChoice..., have dynamic title (label) and options and the user can validate their fields and register them in the Preview tab.I use Pick, to store only 4 DroppableItemsType props.
● DroppableEditorProps
```ts
export type DroppableEditorProps = Pick;
```
Sends the name of the form element to be displayed to the DroppableEditor● DroppableValidationProps
```ts
export type DroppableValidationProps = Pick & {
register: UseFormRegisterType
}
```
It is used to know which field of the form is written depending on its type.
register: we will store all the data of the DroppableEditor component in the form.
This way we will be able to access them through the Preview tab.4️⃣ Form
● Form.ts
```ts
export type ControlType = Control;
```
ControlType --> Responsible for connecting form input to the form's state management.● ValidationRules.ts
```ts
export type ValidationRules = {
[key: string]: {
field: string,
expression?: string,
shownMessageError: boolean,
messageError?: string,
}
}export type ValidateDroppable = Pick
export type ValidationClicked = {
e: React.ChangeEvent,
type: string,
setValidateRules: (update: (rules: ValidationRules) => ValidationRules) => void
}
```#### Key concepts
- ValidationRules: Type for each form Field.
field: form field name.
expression: regular expression is only required in Email and FullName fields.
shownMessageError: boolean that tell us whether the button is pressed for the user to write the error message.
messageError: a string indicating the custom message error, otherwise it shows a default message.
Form fields
Email: Invalid Email (in case Validate as Email was marked and the user didn't write anything in the error section).
FullName: Invalid Fullname (in case Validate as fullname was marked and the user didn't write anything in the error section).
SingleChoice / MultipleChoice: (SingleChoice / MultipleChoice) required.- ValidateDroppable: The way we validate each form field
- ValidationClicked: Type to updated shownMessageError from true to false and opposite.
- e: Event is triggered once Input element changes.
- type: form field name (SingleChoice, MultipleChoice...).
- setValidateRules: change showMessageError state to show message error input.
5️⃣ DndContext / EventsType.tsx
● EventsType
```tsx
export type EventsType = {
handleDragStart: (
event: DragStartEvent,
setActiveDraggable: (currentDraggable : {id: string}) => void
) => void;handleDragOver: (event: DragOverEvent,
setIsOverDroppable: (IsOver: boolean) => void
) => void;
handleDragEnd: (event: DragEndEvent,
setIsOverDroppable: (IsOver: boolean) => void,
addItems: DroppableStore["addDroppable"]
) => void;
}
```
Defines the event handler types for the drag-and-drop functionality.● handleDragStart: A handler that fires when a drag operation starts.
● handleDragOver: A handler that fires when a draggable item is over a droppable area.
● handleDragEnd: A handler that fires when the drag operation ends.
### 3️⃣ Store
● index.ts
```ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { useShallow } from "zustand/react/shallow";export {
create,
persist,
useShallow
}
```
#### Key concepts- create: Create a zustand store
- persist: Middleware to save all data in localStorage by default
- useShallow: Allows to optimize unnecessary re-renders, rendering the property only when it has changed.● useDroppableStore.ts
```ts
import {create, persist} from "@/store/index.ts"
import { DroppableStore } from "@/types/Droppable/DroppableStore.ts";
import { findDroppableIndex } from "@/store/utils/findDroppableIndex.ts";
import { addDroppableElement } from "@/store/utils/addDroppableElement.ts";export const useDroppableStore = create()(
persist(
(set, get) => ({
DroppableItems: [],addDroppable: (currentDraggable) =>
{
const {id, label} = currentDraggable;
const currentDroppable = get().DroppableItems;const {droppableIndex} = findDroppableIndex(currentDroppable, label);
if (currentDroppable.length === 0 || droppableIndex === -1) {
const {newDroppable} = addDroppableElement({id, label});
set({ DroppableItems: [...currentDroppable, newDroppable] });
}
},removeDroppable(id) {
const currentDroppable = get().DroppableItems;
const updatedDroppable = currentDroppable.filter((item) => item.id !== id);
set({ DroppableItems: updatedDroppable });
},
updateDroppable: (formData) => {
const {type, label, options, validation} = formData;
const currentDroppable = get().DroppableItems;const {droppableIndex} = findDroppableIndex(currentDroppable, type);
if(droppableIndex !== -1) {
currentDroppable[droppableIndex] = {
...currentDroppable[droppableIndex],
label,
options,
validation
}
set({ DroppableItems: [...currentDroppable] });
}
}
}),{
name: 'droppable-storage'
}
),
)
```#### Key concepts
- addDroppable: Add unique items to form builder area.
- removeDroppable: Delete an existing item.
- updateDroppable: Update an existing item with custom title, options and validation.#### Zustand methods
- set: allows you to update the status
- get: allows you to get the current state.### 4️⃣ Hooks
● useFormDroppable.ts
```ts
import { useForm } from "@/utils/Form/index.ts";
import { DroppableItemsType } from "@/types/Droppable/DroppableType";export const useFormDroppable = () =>
{
const { register, handleSubmit, control } = useForm();return {register, handleSubmit, control};
}
```#### Purpose
it allows to register form fields with their current settings and chosen validation.
● useFormOptions.ts
```ts
import { useFieldArray } from "@/utils/Form/index.ts";
import { ControlType } from "@/types/Form/Form.ts";export const useFormOptions = (control: ControlType) => {
const { fields, append, remove } = useFieldArray({
control,
name: "options"
});const addOption = () => append({value: `Option ${fields.length + 1}`});
const removeOption = (index: number) => remove(index);
return {fields, addOption, removeOption};
}
```#### Purpose
it allows to add dynamic options for SingleChoice and MultipleChoice fields.
#### Key concepts
- useFieldArray:
it allows me to add dynamic options to the form.
name --> "options": Array where the SingleChoice and MultipleChoice options will be stored.- addOption: it allows me to add a new Option with an incremented number.
- removeOption: it allows me to remove an option using its index.● useFormPreview.ts
```ts
import { useForm } from "@/utils/Form/index.ts";
import { CSVData } from "@/types/Form/CSVData.ts"
export const useFormPreview = () =>
{
const { register, handleSubmit} = useForm({
reValidateMode: "onSubmit"
});
return {register, handleSubmit};
}
```#### Purpose
it allows to register form fields to be sent to CSV file.● useValidateRules.ts
```ts
import {useState} from "react";
import { ValidationRules } from "@/types/Form/ValidationRules";export const useValidateRules = () => {
const [validateRules, setValidateRules] = useState(
{
FullName: {
field: "Validate as a full name",
expression: "/^[A-ZÁÉÍÓÚÑa-záéíóúñ]+ [A-ZÁÉÍÓÚÑa-záéíóúñ]+$/",
shownMessageError: false,
messageError: "",
},
Email: {
field: "Validate as an email",
expression: "/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/",
shownMessageError: false,
messageError: "",
},
SingleChoice: {
field: "Required",
shownMessageError: false,
messageError: "",
},
MultipleChoice: {
field: "Required",
shownMessageError: false,
messageError: "",
},
}
);
return {validateRules, setValidateRules}
}
```#### Purpose
it allows to handle the list of rules for each field in a form.
#### Key concepts
- field: This is the text that is displayed based on the form field type.
- shownMessageError: If the user has clicked the checkbox rules for each form field, it will be updated to true
and "Write your custom message error" will be displayed.
Otherwise, it must be hidden.- messageError: This is the custom validation text, by default it would be an empty string.
## Manage FormUp Errors ❌
1️⃣ Prevent horizontal scrolling in draggable elements 🛠️
1️⃣ Solution ✅
Updated the main of the whole application to disable horizontal scrolling when dragging draggable elements.
2️⃣ Implementation 🔧
```css
main
{
width: 100vw;
overflow: hidden;
}
```
3️⃣ Result 🎯
If the user drags each draggable element out of reach of the Form Builder without releasing it:
No bottom scroll sidebar is displayed on desktop or mobile.
No interface whitespace is created.
2️⃣ Mobility of Draggable elements in the Droppable zone (Mobile) 🛠️
1️⃣ Solution ✅
A class called draggable-item was created for each element.
The property touch-action: none was added to enable dragging functionality on mobile.2️⃣ Implementation 🔧
```css
.draggable-item
{
touch-action: none;
}
``````tsx
//@/components/Draggable/DraggableItem.tsxreturn (
{label}
)
```
3️⃣ Result 🎯In mobile, we can drag the draggable elements found in the FieldLibrary to the FormBuilder.
3️⃣ Sync between Build Droppable Zone and Preview 🛠️
1️⃣ Solution ✅
I use Zustand for global state management to synchronize draggable items in the Build tab with the Preview tab.
Persist middleware ensures that the state is saved in localStorage and persists even when the browser is closed.2️⃣ 🤔 Why Zustand instead of Context API or Redux?
In my case, when I was developing my prototype for personal use I was looking for a global state with a focus on simplicity and keeping things clean as the code base grows, avoiding unnecessary bundle size in my project.
Also, Zustand gives a middleware (persist) to save all the data in localStorage automatically.## Wiki 📖
1️⃣ Dnd-kit
How can I create Draggable items?
https://docs.dndkit.com/api-documentation/draggableHow can I create the Droppable area?
https://docs.dndkit.com/api-documentation/droppableHow can I communicate Draggable items with the Droppable area?
https://docs.dndkit.com/api-documentation/context-provider2️⃣ Form
How can I write validation and message error with register -->
https://www.react-hook-form.com/api/useform/register/#options
How can I send data to form?
https://www.react-hook-form.com/api/useform/handlesubmit/How can I solve useFieldArray: Type String is not assignable to never?
https://github.com/orgs/react-hook-form/discussions/75863️⃣ Zustand
How can I create a store with TypeScript?
https://zustand.docs.pmnd.rs/guides/typescript#slices-patternHow can I store a state in localStorage?
https://zustand.docs.pmnd.rs/middlewares/persist#persisting-a-state4️⃣ Sooner
Reference API documentation
https://sonner.emilkowal.ski/toastHow can I use TailwindCSS classes?
https://sonner.emilkowal.ski/styling#tailwind-css