top of page

Drag-N-Form: a pure javascript forms designer with direct PDF export

Writer's picture: Alejandro GaioAlejandro Gaio

Updated: Dec 31, 2024



DragNForm designer
DragNForm designer

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:

  1. Design: allows creating/editing forms.

  2. Run: allows a user to interact with a form by filling the fields and triggering validations (via code).

  3. 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

  1. Clone the repository

  2. If using VS Code, install the Live Server extension

  3. Look for demo.html in the root folder, right click -> Open with Live Server


How to use it in other projects

  1. Copy the following files to your JS folder:

    1. dragnform.min.js,

    2. dnf-icons.min.js,

    3. dnf-strings.[en | es].js // select the language of your choice

    4. dnf-functions.js .

  2. Copy the following files to your CSS folder:

    1. widgets-theme-[light | dark].min.css // select your theme of choice

  3. 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.

    Widget's grip element
    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
    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!

10 views0 comments

Recent Posts

See All

Comments


©2022 by Alejandro Gaio.

bottom of page