This respository is no longer used or maintained.
It's functionality has been split into two other repositories:
- The library: from-flow
- A starter app that uses the library: form-flow-starter-app
This is a standard Spring Boot application that uses the form-flows
Java package as a library. It
can be customized to meet the needs of a web app, and is meant to be built upon. It's a plain,
boring (but modern) Spring app that uses common, frequently-used libraries throughout.
It contains example code for a simple, generic application for public benefits. An applicant can fill out screens with their basic info, upload supporting documents, then submit it all. Upon submission, they receive a simple SMS confirmation and a receipt email with a filled-in application PDF. The entire experience is in both English and Spanish.
To power the form flow logic, this app depends on the form-flows
Java library. That library is
included in build.gradle
along with all other dependencies. The codebase for the form-flows
package is open source (TBD).
Out-of-the-box, integrations can be set up with common third-party services:
- Intercom
- Google Analytics
- Mixpanel
- Optimizely
- Google Ads
- Facebook Ads
The relevant service keys and other settings are configurable in application.yaml
.
- Flows
- Inputs
- Screens
- Conditions
- Validations
Flows are the top-level construct that define the navigation between a collection of screens. A flow can have many inputs to accept user data (e.g. first name, zip code, email, file upload). Each input can have zero to many validations.
A flow also has many screens. Each screen can be made up of zero or more inputs. A flow has an ordering of screens, and can use defined conditions to control navigation. Conditions use submitted inputs to make a logical decision about showing or not showing a screen / part of a screen.
erDiagram
Flow ||--|{ Screen : "ordered collection of"
Flow ||--o{ Input : "collection of"
Screen ||--o{ Input : displays
Input ||--o{ Validation : "validated by"
Input }|--o{ Condition: "determines"
To start, create a flow-config.yaml
in src/main/resources
.
You can define multiple flows by separating them with ---
.
At it's base a flow as defined in yaml has a name, a flow object, and a collection of screens, their next screens, any conditions for navigation between those screens, and optionally one or more subflows.
A basic flow configuration could look like this:
name: exampleFlow
flow:
firstScreen:
nextScreens:
- name: secondScreen
secondScreen:
nextScreens:
- name: thirdScreen
- name: otherScreen
condition: userSelectedExample
thirdScreen:
nextScreens:
- name: success
otherScreen:
nextScreens:
- name: success
success:
nextScreens: null
___
name: someOtherFlow
flow:
otherFlowScreen:
All screens must have an entry in the flows-config in order to be rendered. Additionally, each screen
should have its own template defined in a folder respective to the flow that screen is contained within.
Example /src/resources/templates/<flowName>/<templateName>
.
We have provided a number of IntelliJ Live templates to make the creation of screens faster and easier. More on Live Templates here.
When setting up a new flow, create a folder in src/main/resources/templates
to hold all HTML files.
In the starter app, we name the respective template folders after their respective flows.
For example, add an HTML file such as about-you.html
in the flow's templates folder. Here is an example using our live templates for a form screen:
<th:block th:replace="'fragments/form' :: form(action=${formAction}, content=~{::formContent})">
<th:block th:ref="formContent">
<div class="form-card__content">
<th:block th:replace="'icons' :: 'clipboard'"></th:block>
<th:block th:replace="'content' :: cardHeader(header='Tell us about yourself')"/>
<th:block th:replace="'inputs' :: textInput(name='firstName', label='What's your first name?')"/>
<th:block th:replace="'inputs' :: textInput(name='lastName', label='What's your last name?')"/>
<th:block th:replace="'inputs' :: textInput(name='emailAddress', label='What's your email address?')"/>
</div>
<div class="form-card__footer">
<th:block th:replace="'fragments/continueButton' :: continue" />
</div>
</th:block>
</th:block>
We use Thymeleaf for frontend views. Thymeleaf is a Java based HTML framework for frontend templating. You can learn more about Thymeleaf here.
We use Thymeleaf's concept of fragments to store complex mark up into simple reusable imports.
Fragments simplify the process of creating more complex HTML pages. Some places we use fragments include input types, forms, page headers and footers, error handlers, etc. You can view these fragments here.
Thymeleaf is also capable of making direct calls to Java class methods using what is known as the Spring Expression Language T operator. This allows you to implement Java code in your Thymeleaf templates. We provide two classes for this purpose:
- ConditionDefinitions
- Houses methods which should always return Booleans and can be used to conditionally show or hide sections of a Thymeleaf template
- ViewUtilities
- Houses methods for general purpose manipulation of data to display on the frontend in Thymeleaf templates
An example of using the T operator can be found in the incomeAmounts
template from the starter app.
<main id="content" role="main" class="form-card spacing-above-35"
th:with="selectedSelf=${T(org.codeforamerica.formflowstarter.app.config.ConditionDefinitions).incomeSelectedSelf(submission, uuid)},
houseHoldMemberName=${T(org.codeforamerica.formflowstarter.app.data.Submission).getSubflowEntryByUuid('income', uuid, submission).householdMember}">
...
</main>
We provide some data to the model for ease of use access in Thymeleaf templates. Below are the data types we pass and when they are available.
Name | Type | Availability | Description |
---|---|---|---|
flow |
String | Always available | The name of the flow the screen is contained within. |
screen |
String | Always available | the name of the screen. |
inputData |
HashMap<String, Object> | Always available | inputData is a HashMap of user submitted input data. If editing a subflow, inputData will only contain the data for that specific iteration within the subflow. |
submission |
Submission | Always available | submission is the entire Submission object that contains a single users submission data. |
formAction |
String | Always available | Is the correct endpoint for the forms POST action if flows-config is set up correctly. |
errorMessages |
ArrayList | On screens that fail validation | A list of error messages for inputs that failed validation. |
subflow |
String | On deleteConfirmationScreen screens |
This is the name of the subflow that the deleteConfirmationScreen screen belongs to. |
noEntryToDelete |
Boolean | On deleteConfirmationScreen screens if corresponding uuid is no longer there |
Indicates that the subflow entry containing a uuid is no longer available. |
reviewScreen |
String | On deleteConfirmationScreen screens if corresponding uuid is no longer there |
Name of the review screen for the subflow that the deleteConfirmationScreen belongs to. |
subflowIsEmpty |
Boolean | On deleteConfirmationScreen screens if no entries in a subflow exist |
Indicates that the subflow being accessed no longer has entries. |
entryScreen |
String | On deleteConfirmationScreen screens if no entries in a subflow exist |
Name of the entry screen for the subflow that the deleteConfirmationScreen belongs to. |
For more information on the T Operator see section 6.5.8 here.
There's /icons
endpoint to view all available icon fragments
Inputs are defined in two places - the template in which they are rendered, and in a separate class for validation.
The inputs class is defined in /src/main/java/app/inputs/<nameOfFlowAsNameOfInputsClass>
When defining inputs we have provided a suite of input based Live Templates, more on that here.
Live templates are provided for the following input types:
Checkbox
Date
Fieldset
Money
Number
Radio
Select
SelectOption
Text
TextArea
Phone
Ssn
YesOrNo
Submit
FileUpload
(TBD)
An example inputs class can be seen below, with example validations.
Please note that for single value inputs the type when defining the input is String. However, for input types that can contain more than one value, the type is ArrayList.
class Apply {
@NotBlank(message = "{personal-info.provide-first-name}")
String firstName;
@NotBlank(message = "{personal-info.provide-last-name}")
String lastName;
String emailAddress;
String phoneNumber;
@NotEmpty(message = "{personal-info.please-make-a-gender-selection}")
ArrayList<String> gender;
}
Validations for inputs use the JSR-303 bean validation paradigm, more specifically, Hibernate validations. For a list of validation decorators, see Hibernate's documentation.
Submission data is stored in the Submission
object, persisted to PostgreSQL via the Hibernate ORM.
class Submission {
@Id
@GeneratedValue
private Long id;
private String flow;
@CreationTimestamp
@Temporal(TIMESTAMP)
private Timestamp createdAt;
@UpdateTimestamp
@Temporal(TIMESTAMP)
private Timestamp updatedAt;
@Temporal(TIMESTAMP)
private Timestamp submittedAt;
@Type(JsonType.class)
private Map<String, String> inputData = new HashMap<>();
}
The inputData
field is a JSON object that stores input data from the inputs as a given
flow progresses. It can be used for defining conditions.
An instance variable currentSubmission
is available for use in the ScreenController
and
inputData
is placed on the Thymeleaf model.
Subflows are repeating sections of one or more screens within a regular flow. These can be things like household builders that ask a repeating set of questions about members of a household. Subflows represent an array of screens and their respective inputs (represented as a HashMap) where each item in the array is one iteration.
These are screens that every subflow must have.
Here is an example of a subflow yaml:
subflow:
docs:
entryScreen: docsEntry
iterationStartScreen: docsStart
reviewScreen: docsReview
deleteConfirmationScreen: docsDeleteConfirmation
This screen represents the entry point to a subflow, it is usually the point at which a user makes a decision to enter the subflow or not. Example: a screen that asks "Would you like to add household members?" could be the entry screen for a household based subflow.
The entry screen is not part of the repeating
set of pages internal to the subflow and as such does not need to be demarked with subflow: subflowName
in the flows-config.yaml
.
This screen is the first screen in a subflows set of repeating screens. When this screen is submitted, it creates a new iteration which is then saved to the subflow array within the Submission object.
Because this screen is part of the repeating screens within the subfow, it should be denoted with
subflow: subflowName
in the flows-config.yaml
.
This is the last screen in a subflow. This screen lists each iteration completed within a subflow, and provides options to edit or delete a single iteration.
This screen does not need to be demarked with subflow: subflowName
in the flows-config.yaml
. It is not technically part of the repeating screens within a subflow, however,
you do visit this screen at the end of each iteration to show iterations completed so far and ask the
user if they would like to add another?
This screen appears when a user selects delete
on a iteration listed on the review screen. It asks
the user to confirm their deletion before submitting the actual deletion request to the server.
This page is not technically part of the subflow and as such, does not need to be demarked with subflow: subflowName
in the flows-config.yaml
.
What do you need to do to create a subflow?
- In
flows-config.yaml
:- Define a
subflow
section - Create a name for your subflow in the
subflow
section - Define
entryScreen
,iterationStartScreen
,reviewScreen
, anddeleteConfirmationScreen
in thesubflow
section - Add all subflow screens into the
flow
, withsubflow: <subflow-name>
unless otherwise noted above (for dedicated subflow screens) - Note for screens that aren't ever defined in
NextScreens
(delete confirmation screen), they still need to be somewhere in theflow
- Define a
- Define
fields
that appear in subflow screens just like you would in ascreen
, in your flow Java Class (e.g. Ubi.java in the starter app) - Define
screen
templates inresources/templates/<flow-name>
name: docFlow
flow:
first:
nextScreens:
- name: second
second:
nextScreens:
- name: docsEntry
docsEntry:
nextScreens:
- name: docsStart
docsStart:
subflow: docs
nextScreens:
- name: docsInfo
docsInfo:
subflow: docs
nextScreens:
- name: docsReview
docsReview:
nextScreens:
- name: success
success:
nextScreens:
# NOTE: this screen still needs to be defined in `flow` to be rendered even though
# it isn't the nextScreen of any other Screen
docsDeleteConfirmation:
nextScreens:
subflow:
docs:
entryScreen: docsEntry
iterationStartScreen: docsStart
reviewScreen: docsReview
deleteConfirmationScreen: docsDeleteConfirmation
Conditions are defined in Java as methods, and can read from the currentSubmission
object. When
defining new conditions as methods, the instance variable inputData
is accessible.
public class ApplyConditions extends FlowConditions {
public boolean isGmailUser() {
return inputData.get('emailAddress').contains("gmail.com");
}
}
You can pull in conditions into a Thymeleaf with the T operator, then use the variable to define show logic:
<div th:with="showCondition=${T(org.codeforamerica.formflowstarter.app.config.ConditionDefinitions).<show-method>()}">
<h1 th:if="showCondition">Conditionally show this element</h1>
</div>
Unlike Screens, Static Pages are HTML content not part of a flow. Examples include the home page,
privacy policy, or FAQ. This starter app contains a home page (index.html
) and FAQ (faq.html
)
as examples in the resources/templates
folder.
To add a new Static Page:
- Add an annotated method (
@GetMapping
) to theStaticPageController
- Create a page template in
src/resources/templates
.
The template HTML can look like:
<!DOCTYPE html>
<html th:lang="${#locale.language}">
<head th:replace="fragments/head :: head(title='')"></head>
<body>
<div class="page-wrapper">
<th:block th:replace="fragments/toolbar :: toolbar"/>
<th:block th:replace="fragments/demoBanner :: demoBanner"/>
<section class="slab">
<div class="grid">
<div class="grid__item">
<h1 class="spacing-below-35"></h1>
</div>
</div>
</section>
<main id="content" role="main" class="slab slab--white">
</main>
</div>
<th:block th:replace="fragments/footer :: footer"/>
</body>
</html>
The IntelliJ Live Template for the above example can be generated with cfa:staticPage
.
Note: these instructions are specific to macOS, but the same dependencies do need to be installed on Windows as well.
brew install openjdk
First run brew install jenv
.
Add the following to your ~/.bashrc
or ~/.zshrc
:
export PATH="$HOME/.jenv/bin:$PATH"
eval "$(jenv init -)"
Reload your terminal, then finally run this from the repo's root directory:
jenv add /Library/Java/JavaVirtualMachines/openjdk.jdk/Contents/Home/
brew install gradle
- Install PostgreSQL 14 via an official download
- Or on macOS, through homebrew:
brew install postgresql@14
- Or on macOS, through homebrew:
- Create the database using the command line:
$ createuser -s formflow
$ createdb formflow
- Enable annotation processing in
Preferences -> Build, Execution, Deployment -> Compiler -> Annotation Processor
- Set the Gradle JVM version to 17 in
Preferences -> Build, Execution, Deployment -> Build Tools -> Gradle
- Set the Project SDK to Java 17 in
File > Project Structure
- Run the application using the
FormflowstarterApplication
configuration
From the project root invoke
./gradlew clean test
You can run tests directly in IntelliJ by running tests from test folder (via right click or ctrl + shift + r
).
We use an automatic form filler to make manual test easier.
- Install Fake Filler for Chrome or Fake Filler for FireFox
- Go to fakeFillerConfig.txt, click on "Raw", then save the file to your computer.
- Open the Fake Filler Options then click on Backup and Restore (chrome)
- Click on "Import Settings" and upload the config file that you saved above.
- Click on Keyboard Shortcuts (chrome) to choose the shortcut you want to use to fill out the page.
As a team, we use IntelliJ and can use the Live Templates feature to quickly build Thymeleaf templates.
Support for importing/exporting these Live Templates is a buggy process that can sometimes wipe away all of your previous settings. So we're going to use a copy/paste approach.
- Open the intellij-live-templates/CfA.xml from the root of this repo
- Copy the whole file
- Open Preferences (
cmd + ,
), search or find the section "Live Templates" - If there isn't a template group already called CfA, create one by pressing the "+" in the top right area and selecting "Template group..."
- Highlight the template group "CfA", right click and "Paste"
- You should now see Live templates with the prefix "cfa:" populated in the template group
Once you have Live Templates installed on your IntelliJ IDE, in (.html
, .java
) files you can use our
Live Templates by typing cfa:
and a list of templates to autofill will show itself.
- Open Preferences (
cmd + ,
), search or find the section "Live Templates" - Find the Live Template you want to contribute
- Right click and "Copy" (this will copy the Live Template in XML form)
- Open intellij-live-templates/CfA.xml in this repo
- Paste at the bottom of the file
- Commit to GitHub
- Now others can copy/paste your Live Templates
We use JSON schema to autocomplete and validate the flows-config.yaml
file.
You must manually connect the schema to the local file in your instance of IntelliJ IDE.
- Open IntelliJ preferences (
Cmd + ,
on mac) - Navigate to "JSON Schema Mappings"
- Select the "+" in the top left to add a new mapping
- Name can be anything (I use "flow config")
- "Schema file or URL" needs to be set to the
src/main/resources/flows-config-schema.json
- "Schema version" set to "JSON Schema version 7"
- Use the "+" under schema version to add:
- a new file and connect to
src/main/resources/flows-config.yaml
- a folder and connect to
src/test/resources/flows-config
- a new file and connect to
To confirm that the connection is work, go into flows-config.yaml
and see if autocomplete is appearing for you.