- Leverages components and utilities from the Login.gov Design System, which is based on the U.S. Web Design System.
- Uses Sass as the CSS preprocessor and Stylelint to keep files tidy.
- Uses well-structured, accessible, semantic HTML.
To the extent possible, use design system components and utilities when implementing designs.
Components are simple and consistent solutions to common user interface needs, like form fields, buttons, and icons. See the Components section below for more information.
Utilities are CSS classes which allow you to add consistent styling to an HTML element, such as margins or borders.
- All new code is expected to be written using TypeScript (
.ts
or.tsx
file extension) - The site should be functional even when JavaScript is disabled, with a few specific exceptions (identity proofing)
- The code follows TTS JavaScript standards, using a custom ESLint configuration
- Code styling is formatted automatically using Prettier
- Packages are managed with Yarn, organized using Yarn workspaces
- JavaScript is transpiled, bundled, and minified via Webpack and Babel
- Files within
app/javascript
should be named as kebab-case, e.g../path-to/my-javascript.ts
. - Variables and functions (excluding React components) should be named as camelCase, e.g.
const myFavoriteNumber = 1;
.- Only the first letter of an abbreviation should be capitalized, e.g.
const userId = 10;
. - All letters of an acronym should be capitalized, e.g.
const siteURL = 'https://example.com';
.
- Only the first letter of an abbreviation should be capitalized, e.g.
- Classes, React components, and TypeScript types should be named as PascalCase (upper camel case), e.g.
class MyCustomElement {}
. - Constants should be named as SCREAMING_SNAKE_CASE, e.g.
const MEANING_OF_LIFE = 42;
. - TypeScript enums should be named as PascalCase with SCREAMING_SNAKE_CASE members, e.g.
enum Color { RED = '#f00'; }
.
Related: Component Naming Conventions
Prettier is an opinionated code formatter which simplifies adherence to JavaScript style standards, both for the developer and for the reviewer. As a developer, it can eliminate the effort involved with applying correct formatting. As a reviewer, it avoids most debates over code style, since there is a consistent style being enforced through the adopted tooling.
Prettier is integrated with the project's linting setup. Most issues can be resolved
automatically by running yarn run lint --fix
. You may also consider one of the
available editor integrations, which can simplify your
workflow to apply formatting automatically on save.
Workspaces allow a developer to create and organize code which is used just like any other NPM package, but which doesn't require the overhead involved in publishing those modules and keeping versions in sync across multiple repositories. We use Yarn workspaces to keep JavaScript code organized, reusable, and to encourage good coding practices in abstractions.
In practice:
- All folders within
app/javascript/packages
are treated as workspace packages. - Each package should have its own
package.json
that includes...- ...a
name
starting with@18f/identity-
and ending with the name of the package folder. - ...a
private
value indicating whether the package is intended to be published to NPM. - ...a value for the
version
field, since it is required. The value value can be anything, and"1.0.0"
is a good default. - ...a
sideEffects
value listing files containing any side effects, used for Webpack's Tree Shaking optimization.
- ...a
- The package should be importable by its bare name, either with an
index.ts
or equivalent package entrypoints
As with any public NPM package, a workspace package should ideally be reusable and avoid direct
references to page elements. In order to integrate a package within a particular page, you should
either reference it within a ViewComponent component's accompanying script,
or by creating a new app/javascript/packs
file to be loaded on a page.
Because Yarn will alias workspace packages using symlinks, you can reference a package using the
name you assigned using the guidelines above for package.json
name
field (for example,
import { Button } from '@18f/identity-components';
).
While the project is not a Node.js application or library, the distinction between dependencies
and devDependencies
is important due to how assets are precompiled in deployed environments.
During a deployment, dependencies are installed using the --production
flag,
meaning that all dependencies which are required to build the project must be defined as
dependencies
, not as devDependencies
.
devDependencies
should be reserved for dependencies which are not required to compile application
assets, such as testing-related libraries or DefinitelyTyped
TypeScript declaration packages. When possible, it is still useful to define devDependencies
to
improve the performance of application asset compilation.
When installing new dependencies, consider whether the dependency is relevant for an individual workspace package, or for the entire project. By default, Yarn will warn when trying to install a dependency in the root package, since dependencies should typically be installed for a specific workspace.
To install a dependency to a workspace:
yarn workspace @18f/identity-build-sass add sass-embedded
To install a dependency to the project:
# Note the `-W` flag
yarn add -W webpack
As much as possible, try to use the same version of a dependency when it is used across multiple workspace packages. Otherwise, it can inflate the size of the compiled bundles and have a negative performance impact on users.
We use yarn-deduplicate
to deduplicate resolved package versions within the Yarn lockfile, and enforce it with
the make lint_yarn_lock
check.
See @18f/identity-i18n
package documentation.
See @18f/identity-analytics
package documentation for code examples detailing
how to track an event in JavaScript.
Any event logged from the frontend must be added to the ALLOWED_EVENTS
allowlist in FrontendLogController
.
This is an allowlist of events defined in AnalyticsEvents which are allowed
to be logged from the frontend. All properties will be passed automatically to the event from the
frontend as long as they are defined in the method argument signature.
There may be some situations where you need to append a value known by the server to an event logged in the frontend, such as an A/B test bucket descriptor. In these scenarios, you have a few options:
- Add the value to the page markup, such as through an HTML
data-
attribute, and reference that attribute in JavaScript. - Implement a mixin to intercept and override the default behavior of an analytics event, such as
how
Idv::AnalyticsEventEnhancer
is implemented.
Any of the U.S. Web Design system components are available to use. Through the Login.gov Design System, we have customized some of these components to suit our needs.
We use a mixture of complementary component implementation approaches to support both server-side and client-side rendering.
The ViewComponent gem is a framework for creating reusable, testable, and independent view components, rendered server-side.
For more information, refer to the components README.md
.
For non-trivial client-side interactivity, we use React to build and combine JavaScript components for stateful applications.
- Components should be implemented as function components, using hooks to manage the component lifecycle.
- Application state is managed using context, where domain-specific state is passed from a context provider to a child component.
- Client-side routing is not a concern that you should typically encounter, since the project is not
a single-page application. However, the
@18f/identity-form-steps
package is available if you need to implement a series of steps within a page.
For simple client-side interactivity tied to singular components (React or ViewComponent), we use native custom elements.
Custom elements provide several advantages in that they...
- can be initialized from any markup renderer, supporting both server-side (ViewComponent) and client-side (React) component implementations
- have no dependencies, limiting overall page size in the critical path
- are portable and avoid vendor lock-in
Each component should have a name that is used consistently in its implementation and which describes its purpose. This should be reflected in file names and the code itself.
- ViewComponent classes should be named
[ExampleName]Component
- ViewComponent classes should be defined in
app/components/[example_name]_component.rb
- ViewComponent stylesheets should be named
app/components/[example_name].scss
- ViewComponent scripts should be named
app/components/[example_name].ts
- Stylesheet selectors should use
[example-name]
as the "block name" in BEM - React components should be named
<[ExampleName] />
- React component files should be named
app/javascript/packages/[example-name]/[example-name].tsx
- Web components should be named
[ExampleName]Element
- Web components files should be named
app/javascript/packages/[example-name]/[example-name]-element.ts
For example, consider a Password Input component:
- A ViewComponent implementation would be named
PasswordInputComponent
- A ViewComponent classes would be defined in
app/components/password_input_component.rb
- A ViewComponent stylesheet would be named
app/components/password_input_component.scss
- A ViewComponent script would be named
app/components/password_input_component.ts
- A stylesheet selector would be named
.password-input
, with child elements prefixed as.password-input__
- A React component would be named
<PasswordInput />
- A React component file would be named
app/javascript/packages/password-input/password-input.tsx
- A web component would be named
PasswordInputElement
- A web components file would be named
app/javascript/packages/password-input/password-input-element.ts
Web graphic assets like images, GIFs, and videos are artifacts authored in other tools. As such, there is no need to keep multiple variants of an asset (e.g., SVG and PNG) in the repository if they are not in use.
Login.gov publishes and uses our own custom Stylelint configuration, which is based on TTS engineering best-practices and includes recommended Sass rules, applies Prettier formatting, and enforces BEM-style class naming conventions.
It may be useful to consider installing a Prettier editor integration to automatically format files on save. Similarly, a Stylelint editor integration can help identify issues in your code as you write.
Mocha is used as a test runner for JavaScript code.
JavaScript tests include a combination of unit tests and integration tests, with a heavier emphasis on integration tests since the bulk of our front-end code is in service of user interactivity.
To simplify common test behaviors and encourage best practices, we make extensive use of the Testing Library suite of testing libraries, which can be used to render and query basic DOM elements as well as advanced React components. Their APIs are designed in a way to accurately simulate real user behavior and support querying by accessible semantics.
To run all test specs:
yarn test
To run a single test file:
yarn mocha app/javascript/packages/analytics/index.spec.ts
You can also pass any Mocha command-line arguments.
For example, to watch a file and rerun tests after any change:
yarn mocha app/javascript/packages/analytics/index.spec.ts --watch
ESLint is used to ensure code quality and enforce styling conventions.
To analyze all JavaScript files:
yarn run lint
Many issues can be fixed automatically by appending a --fix
flag to the command:
yarn run lint --fix
Login.gov is a form-heavy application, and there are some conventions to consider when implementing a new form.
For details on back-end form processing, refer to the equivalent section of the Back-end Architecture document.
Simple Form is a wrapper which enhances Ruby on Rails' default form_for
helper,
including some nice conveniences:
- Standardizing markup layout for common input types
- Adding additional input types not available in Ruby on Rails
- Pre-filling values associated with form's associated record
- Displaying user-facing error messages after an invalid form submission
Typical usage should combine the simple_form_for
helper with a record and associated block of form content:
<%= simple_form_for(@reset_password_form, url: user_password_path) do |f| %>
<%= f.input :reset_password_token, as: :hidden %>
<% end >
If there is no record available, you can initialize simple_form_for
with an empty string:
<%= simple_form_for('', url: user_password_path) do |f| %>
<%= f.input :reset_password_token, as: :hidden %>
<% end >
Use standards-based client-side form validation
wherever possible. This is typically achieved using input attributes
to define validation constraints. For advanced validation, consider using the setCustomValidity
function to assign or remove validation messages when an input's value changes.
A form's contents are validated when a user submits the form. Errors messages should only be
displayed at this point, and the user's focus should be drawn to the first field with an error
present. Error messages should be removed from a field when that field's value changes. It's
recommended that you use ValidatedFieldComponent
, which automatically
manages these behaviors.
The ValidatedFieldComponent
View Component
is a wrapper component for Simple Form's f.input
helper. It enhances the behavior of an input by:
- Displaying an error message on the page when form submission results in a validation error
- Moving focus to the first invalid field when form submission results in a validation error
- Providing default error messages for common validation constraints (e.g. required field missing)
- Allowing you to customize error messages associated with default field validation
- Creating a relationship between an input and its error message to ensure that the error is announced to assistive technology
- Resetting the error state when an input value changes
JavaScript errors that occur in production environments are automatically logged to NewRelic. They are logged as an expected Ruby error with the class FrontendLoggerError::FrontendError
.
There are two ways you can view these errors:
- In the production APM "Errors" inbox, removing the filter which hides "expected" errors
- In the query builder, selecting from
TransactionError
with an error class ofFrontendErrorLogger::FrontendLogger
Each error includes a few details to help you debug:
message
: Corresponds toError#message
, and is usually a good summary to group byname
: The subclass of the error (e.g.TypeError
)stack
: A stacktrace of the individual error instancefilename
: The URL of the script where the error was raised, if it's an uncaught errorerror_id
: A unique identifier for tracing caught errors explicitly tracked
Note that NewRelic creates links in stack traces which are invalid, since they include the line and column number. If you encounter an "AccessDenied" error when clicking a stacktrace link, make sure to remove those details after the .js
in your browser URL.
If an error includes error_id
, you can use this to search in code for the corresponding call to trackError
including that value as its errorId
to trace where the error occurred.
Otherwise, debugging these stack traces can be difficult, since files in production are minified, and the stack traces include line numbers and columns for minified files. With the following steps, you can find a reference to the original code:
- Download the minified JavaScript file referenced in the stack trace
- Download the sourcemap file for the JavaScript by appending
.map
to the previous URL - Install the
sourcemap-lookup
npm packagenpm i -g sourcemap-lookup
- Open a terminal window to the directory where you downloaded the files in steps 1 and 2
- Example:
cd ~/Downloads
- Example:
- Clean the sourcemap file to remove Webpack protocol details
- Example:
sed -i '' 's/webpack:\/\/@18f\/identity-idp\///g' document-capture-e41c853e.digested.js.map
- Example:
- Run the
sourcemap-lookup
command with a reference to the JavaScript file, line and column number, and specifying the source path to your local copy ofidentity-idp
- Example:
sourcemap-lookup document-capture-e41c853e.digested.js:2:172098 --source-path=/path/to/identity-idp/
- Example:
The output of the sourcemap-lookup
command should include "Original Position" and "Code Section" of the code which triggered the error.
Font files are optimized to remove unused character data. If a new character is added to content, the font files must be regenerated:
- Download Public Sans and extract it to your project's
tmp/
directory - Install glyphhanger and its dependencies:
npm install -g glyphhanger
pip install fonttools brotli
- Scrape content for character data:
make lint_font_glyphs
- Subset the original Public Sans fonts to include only used character data:
glyphhanger --formats=woff2 --subset="tmp/public-sans-v2/fonts/ttf/PublicSans-*.ttf" --whitelist="$(cat app/assets/fonts/glyphs.txt)"
- Replace font files with new subset fonts:
cd tmp/public-sans-v2/fonts/ttf
find . -name "*-subset.woff2" -exec sh -c 'cp $1 "../../../../app/assets/fonts/public-sans/${1%-subset.woff2}.woff2"' _ {} \;
At this point, your working directory should reflect changes to all of the files within app/assets/fonts/public-sans
, and new or removed characters in app/assets/fonts/glyphs.txt
. These changes should be committed to resolve the lint failure for character data.
The application should support:
- All browsers with >1% usage according to our own analytics
- All device sizes
You can find additional frontend documentation in relevant places throughout the code: