Frontend Development - civiform/civiform GitHub Wiki

Frontend Code

As of Q32024, CiviForm is migrating to Thymeleaf. As the migration proceeds, this page will be updated. Please see this presentation for an overview of the migration: Frontend Overhaul.

Legacy Frontend Code

In the legacy stack, everything is mostly Java.

Our frontend is weird.
Nope! No two ways about it.
Just... it's really weird.

The HTML? Java (J2HTML)
The CSS? Java (Class wrappers around Tailwind styles)
The JavaScript? Well, okay... that part isn't Java, but it's pretty minimal.

Client-side code (TypeScript)

CiviForm uses Typescript client-side code to add dynamic behavior to the app. The code is located in server/app/assets/javascripts.

For more information:

HTMX

HTMX is a Javascript library that we use to create dynamic components without needing client side scripting. At a high level, it allows HTTP requests to be sent in response to various events (page load, click, etc) and replace the contents of an element with the response of that HTTP request. To use HTMX, we need to import it in the appropriate bundle (via the appropriate entry point).

A typical usage of HTMX would consist of something like:

  1. An element with HTMX attributes on it in the DOM: <button hx-get="some/route" hx-trigger="click">I have not been clicked</button>
  2. A route matching the value of the hx-get attribute (or different AJAX request).
  3. Methods in the controller and view to render a new piece of DOM to put in place

The most commonly used HTMX attributes are:

  • AJAX requests (hx-get, hx-post, hx-put, px-patch, or hx-delete). hx-get and hx-post are by far the most common values
  • A trigger condition hx-trigger.
  • A target hx-target. You can specify a CSS selector for this attribute to have the HTTP response update the content of a different element than the one that triggered the request. For instance, imagine clicking a tab header, which would render the tab content in a different container.
  • Swapping hx-swap. By default, the response will become the innerHTML of the target element, but that can be altered.

Code organization

There are no strict rules around TypeScript code organization. This codebase is relatively small so use your own judgement. If you are adding new functionality and that functionality is 100+ lines and focuses on a specific feature - consider adding it as a separate file.

Bundling

Client-side code is bundled into a few bundle JS files and a CSS file. Our custom TypeScript files are bundled into admin.bundle.js and applicant.bundle.js(see below). Only one of those two bundles should be loaded on any given CiviForm page. Each of those two bundles is built from a file ending with_entry_point.ts. Entry point file imports a subset of TS files and calls corresponding init()functions. We also bundle USWDS JS and CSS, mostly directly from the@uswds` node module but with some customization within the CiviForm project. At the moment we have the following bundles:

  • admin.bundle.js - loaded on admin pages (CiviForm admin, Program admin). Entry point: admin_entry_point.ts.
  • applicant.bundle.js - loaded on applicant pages. Should be kept small. Entry point: applicant_entry_point.ts.
  • uswds.bundle.js - loaded on all pages. Directly copied from node_modules. Entry point: ./node_modules/@uswds/uswds/dist/js/uswds.min.js.
  • uswds.min.css - loaded on all pages. Consists of compiled CSS which Webpack builds from Sass. Entry point: styles.scss.

Bundling is done using webpack. Webpack config is webpack.config.js. Webpack bundling is part of the bundleWebAssets SBT task which is defined here: WebAssetsBundler.scala.

Content Security Policy

CiviForm has a strict Content Security Policy (CSP), which means:

  • Inline javascript is forbidden
  • All script tags must have a "nonce" attribute set to the nonce value generated by Play. CspUtil provides easy access to the nonce value.

Any other javascript will be blocked by the browser, with an error message in the console. For more details, check out the original design doc.

Prototypes (HTML and Tailwind CSS)

If you're really comfortable in HTML and CSS, the best place to get started is through developing tailwind prototypes. You can view some examples of tailwind component here. Feel free to implement a mock or just start from scratch and roll your own. Tailwind Play is an excellent resource for creating and sharing quick tailwind mocks in your browser. We've also provided links to some of our Tailwind prototypes as a springboard for you to get started.

Tailwind Play Examples

TODO

Implementation

Once we've got Tailwind prototypes in place, it all comes down to the implementation phase. Do you like slinging Java code? Well have we got a job for you! Convert the DOM structure to J2HTML then use the Tailwind prototypes as a reference for the CSS.

Sample implementation

This Tailwind example

<div class="absolute transform -translate-x-1/2 left-1/2">
  <div class="relative flex flex-row bg-red-400 border border-red-500 bg-opacity-90 px-2 py-1 mb-4 text-gray-700 top-4 rounded-sm shadow-lg">
    <div class="flex-none pr-2">
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="inline-block h-6 w-6" fill="currentColor">
        <path fill-rule="evenodd" 
            d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
      </svg>
    </div>
    <span>Do not enter actual or personal data in this demo site.</span>
    <span class="font-bold pl-6 opacity-40 hover:opacity-100">x</span>
  <div>
</div>

Becomes this Java code

    ContainerTag wrappedWarningSvg =
        div()
            .withClasses(Styles.FLEX_NONE, Styles.PR_2)
            .with(
                Icons.svg(Icons.WARNING_SVG_PATH, 20)
                    .attr("fill-rule", "evenodd")
                    .withClasses(Styles.INLINE_BLOCK, Styles.H_6, Styles.W_6));
    ContainerTag messageSpan = span(BANNER_TEXT);
    ContainerTag dismissButton =
        div("x")
            .withId("warning-message-dismiss")
            .withClasses(
                Styles.FONT_BOLD,
                Styles.PL_6,
                Styles.OPACITY_40,
                Styles.CURSOR_POINTER,
                StyleUtils.hover(Styles.OPACITY_100));

    return div(wrappedWarningSvg, messageSpan, dismissButton)
        .withId("warning-message")
        .withClasses(
            Styles.ABSOLUTE,
            Styles.FLEX,
            Styles.FLEX_ROW,
            Styles.BG_RED_400,
            Styles.BORDER_RED_500,
            Styles.BG_OPACITY_90,
            Styles.MAX_W_MD,
            Styles.PX_2,
            Styles.PY_2,
            Styles.TEXT_GRAY_700,
            Styles.TOP_2,
            Styles.ROUNDED_SM,
            Styles.SHADOW_LG,
            Styles.TRANSFORM,
            Styles._TRANSLATE_X_1_2,
            Styles.LEFT_1_2,
            Styles.HIDDEN);
  }

U.S. Web Design System (USWDS)

USWDS is a web design system and component library that is maintained by the U.S. General Services Administration. It aims to provide familiar design across government sites through a common design language. Many government sites already make use of USWDS. The USWDS component library is a package with styles, scripts, fonts and images that any project can import with a package manager such as npm. Our Renovate tool will alert us when there is an update to the USWDS package and we can update it like we do other dependencies.

Why We Include It

USWDS follows UX best practices. It is based on human-centered design. Every component is 508-compliant (follows WCAG accessibility standards). It also closely follows the 21st Century IDEA law and the associated memo, which are guidance for how government websites should provide a better user experience. By integrating USWDS components into CiviForm, we are delegating UI work to specialists in accessibility and UX best practices. We are also partnering with an organization that is doing the same thing we are, rather than working in isolation.

Where We Use It

USWDS is meant to be adopted incrementally and flexibly. We are gradually replacing our existing components with USWDS components, focusing on applicant-facing content first. Right now, it is a tool in our toolbox. Our Tailwind CSS styles still take precedence, so we can easily customize the USWDS styles.

How to Bring In a USWDS Component

  1. Find the component on the USWDS component list and click into it.
  2. Open the “Component code” section.
  3. Translate the HTML into j2html, if needed, and include it in our code.
  4. Customize in one of these 3 ways:
    • By adding our base styles or Tailwind styles to the HTML element
    • If the component you're working on has component-specific settings (look for a "settings" section on the component page), you can go to USWDS settings, find the setting you need to change and add the setting and its new value to _uswds-theme.scss.
    • By adding your style changes to _uswds-theme-custom-styles.scss, either as regular CSS or as Sass, using a USWDS mixin or not. There are mixins for each of the USWDS Utilities, which are categories like: border, color, margin and padding, flex, etc. As a rule-of-thumb, use a mixin if you need to reference a USWDS design token (for example, to get a particular color).

If the USWDS component differs from what you see on the USWDS website, it is because one of our project styles or scripts is overriding it. For example, a button will have a blue background and other differing styles because of our base stylesheet. To overcome this, you may want to apply specific Tailwind styles at the component level.

Modifying USWDS

If you need to make changes to the USWDS library, the GitHub repository has some instructions. The uswds-sandbox project also provides a simpler test environment for running locally than the uswds one.

To test your version of USWDS with CiviForm, you can follow these steps:

  1. In your fork of USWDS, push your changes to a branch on GitHub.
  2. In your local CiviForm working copy, run npm install "https://github.com/YOUR_USERNAME/uswds/tree/YOUR_BRANCH_NAME" --save with your username and branch filled in.
  3. Stop and remove the existing docker containers that have the standard version of USWDS: docker rm -f $(docker ps --filter=name=civiform --quiet).
  4. Run bin/build-dev and bin/build-browser-tests to rebuild the civiform-dev image for running the application and browser tests, respectively, which picks up the new version of USWDS.
  5. Run bin/run-dev and/or bin/run-browser-test-env as normal, and they should reflect the version of USWDS on your branch.

Note: theoretically you can use npm link to link directly to a local working copy of USWDS in place of step 2, but I was not able to get this to work and it's more complicated than the method here anyway.

More details

⚠️ **GitHub.com Fallback** ⚠️