
Let me start saying out loud what you're thinking right now: "Oh cr@p! yet another HTML forms designer... 😒"
Well, yes... but this designer is frameworks/libraries free: no Bootstrap, Tailwind, jQuery, React, Node needed. Everything is done in the frontend, without any APIs or backend, just with pure javascript and love ❤️ (and it's free).
This module has only one external dependency: jsPDF for rendering PDFs. Beyond that, all you need is a browser to get started.
You can find the project's repository at https://github.com/alegaio7/dragnform
Background
While working on a project that required a forms designer, I spent countless hours searching but couldn’t find a product that met my needs.
Every one had one or more of the follwing flaws/issues:
expensive
unwanted dependencies (like jQuery, or Bootstrap)
needed a backend
didn't have the right type of controls (like email, dates, etc.)
couldn't export the form to PDF
So I decided to create my own forms designer from scratch with these goals in mind:
open source
only vanilla javascript
backend-less
export to PDF
export to / import from json
easily customizable
input data validation
Maybe this is not the best form designer out there, but I believe it achieves those goals, and it's free to use.
Features
Widgets
The designer provides the following form elements, called widgets.:
Labels
Inputs: text, number, date, email and paragraph.
Images
Radio buttons
Checkboxes
Buttons (1)
Invisible spacers to align content
(1) Buttons are used/displayed only in Run mode (i.e. to allow some custom user interaction with the form), but are not rendered in View mode. More on "modes" later.
Interface
The interface allows reordering elements by dragging them around the canvas and provides a convenient 12-column layout for better control alignment.

Validations
The input widgets provide validations like min and max for numbers, or min length, max length for texts, etc.
Modes
The designer uses 3 working modes:
Design: allows creating/editing forms.
Run: allows a user to interact with a form by filling the fields and triggering validations (via code).
View: allows a user to see a read-only, rendered version of the form, also used when exporting to PDF.
Import/export
The form definition can be exported to / imported from json.
A form can be exported (rendered) to PDF.
Customization
The designer provides an embedded toolbar which is fully customizable (text and icons) and can be hidden if needed.

The default icon font used is Font Awesome (https://fontawesome.com/), but you can switch it to any other font/icon library as you wish.
Easy widget's customization via CSS (Sass)
Localization
The current version is localized to english and spanish, and can be adapted to any other languages by creating and importing the corresponding resource files.
Integration
The designer provides several callbacks for functions like creating a new form, saving forms, exporting to pdf, notifying when widgets are modified, etc.
How to use
Using the demo
Clone the repository
If using VS Code, install the Live Server extension
Look for demo.html in the root folder, right click -> Open with Live Server
How to use it in other projects
Copy the following files to your JS folder:
dragnform.min.js,
dnf-icons.min.js,
dnf-strings.[en | es].js // select the language of your choice
dnf-functions.js .
Copy the following files to your CSS folder:
widgets-theme-[light | dark].min.css // select your theme of choice
Add the following tags to the page where you want to use the form editor:
<link href="[css folder]/widgets-theme-[light|dark].min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="[js folder]/dnf-strings.[language].min.js"></script>
<script src="[js folder]/dnf-icons.min.js"></script>
<script src="[js folder]/dragnform.js"></script>
Then, create an instance of DragNForm inside a script tag:
<script type="text/javascript">
var designer = new dragnform.Designer({
containerId: "formDesignerDiv",
toolbar: {
visible: true,
compact: false,
buttons: {
button: true,
checkbox: true,
dateField: true,
emailField: true,
export: true,
image: true,
label: true,
load: true,
new: true,
numberField: true,
paragraph: true,
radio: true,
renderCurrentMode: true,
renderValidateForm: true,
savepdf: true,
select: true,
spacer: true,
textField: true,
}
},
widgetPaths: {
widgetTemplates: "/lib/dragnform/widgets",
widgetFormEditors: "/lib/dragnform/editors"
},
widgetRenderOptions: {
enableInlineEditor: false,
renderGrip: true,
renderRemove: true,
renderTips: true,
globalClasses: {
button: "widget-button",
checkbox: "custom-control-input",
checkboxLabel: "custom-control-label",
input: "form-control",
inputLabel: "widget-label",
radio: "custom-control-input",
radioLabel: "custom-control-label",
select: "form-control",
textarea: "form-control",
valueControl: "widget-value",
},
},
renderMode: dragnform.constants.WIDGET_MODE_RUN
});
</script>
Note: Replace paths and keywords inside braces according to your own environment!
Customization
Customizing widget's appearance
If you want to modify the widgets' appearance or the designer itself, start from widgets.scss where you'll find mostly sizing/spacing sass variables, font family and some screen breakpoint rules (you'll need a sass compiler like Live Sass Compiler).
If you want to customize colors, then modify either widgets-theme-light.scss or widgets-theme-dark.scss.
Advise!: I've provided the dark-theme version just for fun. But considering this is a forms designer, and most likely the final result of using this component will be a rendered PDF, I strongly recommend using the light theme. It's ackward to see a textbox (or any other control) with dark background and light text in a PDF file, and this will happen because the same CSS used for the UI is used to render the exported PDF.
Font used in exported PDFs
The pdf file created by calling exportPdf uses the Poppins font. PDF files must include all non-standard fonts inside the file, otherwise they won't show properly.
If you want to use other fonts, you'll have to get the font files (ttf, otf, etc.) and convert them to a base64 string (check poppins.normal.js in the repo for an example). Check the section Convert a font for jsPDF later in this post.
Once you get the js files with the encoded fonts, update jspdf-exporter.js to reference them:
Update the imports at the top of the file
Update the method exportPDF to reference the new font:
doc.addFileToVFS('MyCuteFont-normal.ttf', MyCuteFont)
doc.addFont('MyCuteFont-normal.ttf', 'MyCuteFont', 'normal')
doc.addFileToVFS('MyCuteFont-bold.ttf', MyCuteFont_bold)
doc.addFont('MyCuteFont-bold.ttf', 'MyCuteFont', 'bold')
doc.addFileToVFS('MyCuteFont-italic.ttf', MyCuteFont_italic)
doc.addFont('MyCuteFont-italic.ttf', 'MyCuteFont', 'italic')
doc.addFileToVFS('MyCuteFont-bold-italic.ttf', MyCuteFont_bold_italic)
doc.addFont('MyCuteFont-bold-italic.ttf', 'MyCuteFont', 'bolditalic')
You can omit the styles that you won't need, for instance bold-italic, etc.
After updating jspdf-exporter.js, open widgets.scss and update the $font-familiy variable at the top of the file to the name of your new font.
Convert a font for jsPDF
In order to use a custom font for your PDFs, you'll need a base-64 representation of the font data.
Check these online resources for more information on converting fonts for use with jsPdf.
How PDF rendering works
Feature extraction
Rendering PDFs from HTML pages can be quite challenging.
HTML is inherently designed for flow-based layouts, where absolute positioning is rarely used. In contrast, PDFs rely on an absolute coordinate system, with the origin located at the bottom of the page rather than the top.
Ensuring an HTML layout looks the same—or at least similar—in a PDF presents additional challenges, such as:
Consistent use of fonts.
Maintaining the appearance of native HTML controls like radio buttons and checkboxes.
Applying HTML styling, typically managed through CSS stylesheets.
Accurately positioning content.
For the font problem, we must select which fonts we want to use for PDF and embed them into the generated PDF document, then follow the approach described earlier in order to incorporate those resources into the rendering process.
For some native HTML controls, like checkboxes and radio buttons, the DragNForm renderer module must draw them manually in the PDF canvas, so depending on which browser/OS you use, they might not look exactly the same in the web page as in the PDF file.
HTML styling (and its cascading nature) is also handled by the renderer, which must take into account every HTML element's parent in order to know how to render the corresponding item in PDF.
Content positioning
For this process, the renderer relies in a feature-extraction process which reads all element properties in the designer's DOM and extracts their features like:
Bounding rectangle (aka: position and size)
Scroll offset
Style
All of those features are obtained with native web apis like element.getBoundingClientRect or window.getComputedStyle, and converted to a json definition that's then passed to the renderer.
JsPDF
Once a json representation of the form is built, a pdf exporter component is used, which is a wrapper over the jsPDF library.
The pdf exporter component is also responsible of detecting whether contents exceed the default page size (which is set to 8.5" x 11") and adds a new page to the PDF document when needed.
For every rendered widget, the positioning and styling is considered as described in the json definition.
Integration in other projects
Designer constructor parameters
The designer's constructor accepts an options parameter with the following properties:
containerId
The element Id in the DOM where the designer will be placed.
liveEditsPreview
When true, widget property changes made through an editor dialog are shown live in the designer. Otherwise, the changes are applied only when the dialog is accepted.
renderMode
The initial rendering mode for the designer.
The renderMode value can be one of the following:
WIDGET_MODE_DESIGN: allows designing forms, i.e. adding or removing widgets, changing their properties, reordering them, etc.
WIDGET_MODE_RUN: allows a user to interact with the form by filling the input fields, but the form itself cannot be modified by adding or removing widgets, or updating their properties.
WIDGET_MODE_VIEW: allows a user to see a readonly version of the form, which is also used when exporting to PDF by the rendering process.
toolbar
Allows configuring the designer's toolbar.
The toolbar object has this child properties:
buttonClass: the CSS class used to style the toolbar buttons
compact: true for showing a more compact toolbar
visible: true for showing the toolbar in the designer
buttons: a collection of the buttons that should be rendered:
button: true | false
checkbox: true | false
dateField: true | false
emailField: true | false
export: true | false
image: true | false
label: true | false
load: true | false
new: true | false
numberField: true | false
paragraph: true | false
radio: true | false
renderCurrentMode: true | false
renderValidateForm: true | false
savepdf: true | false
select: true | false
spacer: true | false
textField: true | false
widgetPaths
widgetTemplates: the folder where the widget templates are stored. Each widget relies on a template file which contains the html definition of the component.
widgetFormEditors: the folder where the widget dialog editors are stored. Each widget has its own editor, which allows setting the component propertes during design mode.
widgetRenderOptions
enableInlineEditor (*): true if the flyter inline editor is enabled. Flyter is used to allow for widget label editing without opening the widget editor dialog. For more info on flyter, check https://github.com/ovesco/flyter.
(*) This feature is experimental. Disable it if you find strange behavior.
renderGrip: true if the grip element is rendered on widgets. The grip element allows the user to drag-and-drop widgets in the canvas.
The grip element renderRemove: true if the X button is rendered on widgets. This button allows elements to be removed from the canvas.
renderTips: true if widget tips are rendered. Tips are help texts that some widgets accept in order to give clues to the user.
Widget's tip globalClasses: an object that defines custom CSS class names that widgets will use when rendered. If no custom class names are specified, the original styles defined in the DragNFrom stylesheets will be used. Classes can be defined for these HTML elements:
button
checkbox
checkboxLabel: labels associated with checkboxes
input
inputLabel: labels associated with input fields
radio
radioLabel: labels associated with radio buttons
select
textarea
validationError: used to style a widget when it doesn't pass a validation check, i.e. a red surrounding border.
valueControl: used when rendering the value of input controls (i.e. textbox, dates, numbers) in VIEW mode, usually as SPANs.
Callbacks
The designer supports a few callbacks that allow the caller to handle these events:
onNewForm: called when the 'New form' toolbar button is clicked. Use the preventDefault() method from the event to avoid creating a new form.
onExportToJson: called when the 'Export form to json' button is clicked. If the event is not prevented, a dialog will be shown to allow the user select the destination for the exported file. The event also contains a parameter with the form's json definition.
onLoadJson: called when the 'Import form from json' button is clicked. This event gives the caller an oportunity to load json contents from an external source and return it to the designer using the e.json event property.
onDesignModified: this event occurrs whenever a change is made to a widget inside the designer. The event's value property contains a boolean value indicating whether the designer was modified (true) or when the canvas is cleared (false).
onLoadJsonCompleted: called when the designer completed rendering the form. The json contents used for rendering are sent as an event parameter.
onRenderModeChanged: called when the 'change design mode' button is clicked. The event sends a parameter with a property named 'intendedMode' that tells the event handler which mode the designer will change to if the event is not prevented.
onWidgetAdd: this event occurrs whenever a new widget is added to the canvas. It contains the new widget as a parameter.
onWidgetDelete: this event occurrs whenever a widget is about to be deleted from the canvas. The event contains a reference the widget and an event parameter that, when preventDefault() is called, cancels the delete operation.
Designer properties
canvas get
Returns a reference to the underlying canvas used by the designer to render widgets. The designer is just a wrapper with added functionality over the canvas (i.e. the designer provides the toolbar, but the rendering and widget's management happens in the canvas).
renderMode get/set
This property allows setting or reading the current rendering mode. As stated before, the designer supports 3 modes:
Design: used by the person who wants to create or edit new forms. It allows adding, removing or updating widgets.
Run: used by the users that will fill a form.
View: used by the designer component when a form is filled and it has to be rendered to a PDF file. The designer converts all editable widgets to their 'view' version and starts the feature extraction and json export process that will be sent to jsPDF for PDF creation.
widgets get
Returns a reference to the widgets collection. It's recommended that you don't modify this collection directly, but instead use the canvas provided methods like addWidget, clearCanvas, removeWidget, etc.
Designer methods
clearCanvas
This method removes all widgets from the canvas and resets the modified property to false.
exportJson
This methods turns the designer to design mode temporarily in order to allow extracting widget properties, then returns the designer back to whatever mode it was before.
The result is a json object that can be used to realod a form from it in a later time.
extractFeatures
You can use this method to inspect the json definition that contains a representation of the widgets suitable for rendering a PDF. Please note that this definition is different from what exportJson returns.
exportPdf(saveTofile)
This method creates a PDF from the form. The saveToFile parameter will tell the designer whether to just return the PDF blob (false) or to trigger a save file dialog (true).
findWidget(id)
Returns a widget by passing its Id.
loadForm(json)
This method will load a form from a previously exported json object.
validate
Validates the widgets in the form.
This method requires that the designer is switched to run mode in order to be able to validate the widgets.
Validation will loop though the widgets collection and, for those that have configured validations (like min/max for number field, or required, or min length for text fields), the proper validation will be checked.
If a widget property does not pass a validation, the result of this call will be an object with a result property set to false, and a validations array filled with the failed validations information.
Final words
Creating this component was both fun and challenging at the same time.
What began as a personal need quickly evolved into a concrete, tangible project.
Choosing to make it open source was an easy decision. Having benefited greatly from open-source libraries myself, this is my way of giving back to the community. I hope it proves helpful to someone who was once in my shoes.
Anyway, leave me a note if you find this useful, found bugs or just have something to say!
Comments