Example 4 - Futurama

Corey Montella and Rob Attorri - 14 Mar 2017

Example 3

What is this?

This example demonstrates how to capture input from forms and create and explore records from that. It should also provide a look at some of the particular complications and pitfalls in dealing with forms. In the future it’ll most likely be useful to address a more complicated version of forms, but for now this is a good starting point to develop an intuition of the concepts. If anything seems oversimplified or susceptible to edge cases and user error, it’s most likely from the effort to avoid bugs and overly complicated layouts.

You can play with this example in your browser here.

Page Layout

Containers

This one’s pretty simple; I want a nav bar at the top to let me manually go to the different pages, and an app window where those pages are rendered.

commit @browser
  [#div class:"nav-bar"]
  [#div class:"app-window"]

Pages

For every page I want, I make a record with an attribute of target to specify which each page is called.

commit
  [#page target:"Start"]
  [#page target:"Register"]
  [#page target:"Summary"]

By abstracting this step out, I can define all the pages I want as in the block before, and then here find those pages and create a button on the nav bar for each of them.

search @session @browser
  nav-bar = [#div class:"nav-bar"]
  [#page target]

bind @browser
  nav-bar <- [children: [#button target text:target]]

This partner block works with the nav buttons by simply setting the app window to whichever nav button is clicked.

search @session @browser @event
  [#click element: [#button target]]
  window = [#app-window]

commit
  window.target := target

Starting Page

This commits a record called #app-window whose attribute target specifies which page gets rendered. In this case, when the app starts up, I want the landing view to be the Start page.

commit
  [#app-window target: "Start"]

Start Page

The starting page isn’t very complicated - it contains a thank you message for the civically engaged Earthicans, the flag of Earth, a button to start a new registration for another voter, and a subtle sponsorship message.

search @session @browser
  [#app-window target:"Start"]
  window = [#div class:"app-window"]

bind @browser
  window.class += "home-screen"
  window <- [children:
    [#div class:"thank-you" text:"Thank you for registering to vote!"]
    [#img class:"old-freebie" src:"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Futurama_flag_of_Earth.svg/720px-Futurama_flag_of_Earth.svg.png"]
    [#button target:"Register" class:"begin-btn" text:"Begin a new registration"]
    [#div class:"sponsor" text:"This election sponsored with love by MomCorp"]
  ]

Voter Registration Page

The voter registration page starts with questions that determine the eligibility of a registrant to complete the form. A voter must be both a citizen, and not convicted of a felony. This block sets up the form, and posses those two questions.

search @session @browser
  [#app-window target:"Register"]
  window = [#div class:"app-window"]

bind @browser
  window.class += "voter-registration"
  window <- [children: 
  [#form name: "Voter Registration" sections: 
  
    [#section sort: 1 name: "Qualifications" fields: 
      [#field #required sort: 1 type: "radio" label: "Are you a citizen?" field: "citizenship" options:   
        [label: "Yes"]
        [label: "No"]]
      [#field #required sort: 2 type: "radio" label: "Have you been convicted of a felony?" field: "felony" options: 
        [label: "Yes"]
        [label: "No"]]]]]

If either of the qualifications are not met in the submission, then a message is displayed informing the registrant. We can access the from submission through a #submission record. The attributes of the submission map to the field attribute in each of the #field records of the form. Form submissions can be either #valid, meaning the submission contains all of the required fields; or #invalid, meaning the submission is missing some of the required fields. This gives you flexibility on how to handle form submissions.

search @browser @session
  [#submission #valid form citizenship felony]
  form = [#form name: "Voter Registration"]
  not(form = [#qualified])
  
  disqualified = if citizenship != "Yes" then "disqualified"
                 else if felony != "No" then "disqualified"

  form-message = [#form-message form]
  
bind @browser
  form-message.children += [#div class: "error-message" text: "Sorry, you are not qualified to register to vote"]

The form is #qualified if the qualifications are met.

search @browser @session
  [#submission #valid form citizenship: "Yes" felony: "No"]
  form = [#form name: "Voter Registration"]

bind @browser
  form += #qualified

When the registrant is deemed qualified, the rest of the voter registration form is revealed. This is done by attaching additional sections to the already exiting form, defined above.

search @session @browser
  [#app-window target: "Register"]
  window = [#div class:"app-window"]
  [#party party-name priority]
  [#gender gender-name order]
  registration-form = [#form #qualified name: "Voter Registration"]
  
bind @browser
  registration-form.sections += (
  [#section sort: 2 name: "Personal Information" fields:
    [#field #required sort: 1 type: "input" label: "First Name" field: "first-name"]
    [#field sort: 2 type: "input" label: "Middle Name" field: "middle-name"]
    [#field #required sort: 3 type: "input" label: "Last Name" field: "last-name"]
    [#field #required sort: 4 type: "input" label: "Birthday" field: "birthday" placeholder:"MM-DD-YYYY"]
    [#field #required sort: 5 type: "radio" label: "Gender" field: "gender" options: 
      [label: gender-name]]
    [#field sort: 6 type: "input" label:"Phone Number" field: "phone" placeholder:"XXX-XXX-XXXX"]
    [#field #required sort: 7 type: "input" label:"Last 4 Digits of SSN" field: "ssn" placeholder:"XXXX"]
  ]
  
  [#section sort: 3 name: "Address" fields: 
    [#field #required sort: 1 type: "input" label: "Address Line 1" field: "address1"]
    [#field sort: 2 type: "input" label: "Address Line 2" field: "address2"]
    [#field #required sort: 3 type: "input" label: "City" field: "city"]
    [#field #required sort: 4 type: "input" label: "State" field: "state"]
    [#field #required sort: 5 type: "input" label: "Zip" field: "zip"]
  ]

  [#section sort: 4 name: "Political Affiliation" fields: 
    [#field #required sort: 1 type: "radio" label: "Of which party are you a member?" field: "party" options:
    [label: party-name]]])

This listens for a new submission on the registration form, and creates a new #voter record with the submitted information.

search @browser @session
  form = [#form #qualified]
  submission = [#valid form first-name middle-name last-name birthday gender phone ssn address1 address2 city state zip party]
  window = [#app-window]

// Reset the form for a new submission
commit @browser
  form -= #qualified
  form += #reset

// Save the submission as a new voter record, and change the page to a submission summary page
commit @session
  window.target := "Submission"
  window.voter := voter
  voter = [#voter first-name middle-name last-name birthday ssn phone gender party address: [address1 address2 city state zip]]

The registration summary page displays the voter’s new voter card, as and contains a button that redirects back to the starting page.

search @session @browser
  [#app-window target: "Submission" voter]
  window = [#div class: "app-window"]
  
bind @browser
  window.children += [#div children:
    [#h1 text: "Registration Summary"]
    [#voter-card voter]
    [#button target: "Start" class: "submit-btn" text: "Finish"]]

Gender options

Technically this question susses out both one’s gender and organic life versus robot status, and the end result is the need for a list of genders rather than a binary option. Instead of adding these manually in the voter registration page layout, I’ve made a list here, open to any additional genders that need to be added. Any gender on this list will get added to the voter registration page, which gets sorted by the order attribute.

commit
  [#gender gender-name:"Male" order:1]
  [#gender gender-name:"Female" order:2]
  [#gender gender-name:"Manbot" order:3]
  [#gender gender-name:"Fembot" order:4]

Political Party Options

While the Tastycrats and Fingerlicans are the primary parties, there are many third parties to choose from, and they all get listed here. Again, this makes it easy to add or remove political parties without having to mess around with the voter registration page.

commit
  [#party party-name:"Tastycrat Party" priority:1]
  [#party party-name:"Fingerlican Party" priority:2]
  [#party party-name:"One Cell, One Vote" priority:3]
  [#party party-name:"Green Party" priority:4]
  [#party party-name:"Brain Slug Party" priority:5]
  [#party party-name:"Dudes for the Legalization of Hemp" priority:6]
  [#party party-name:"Bull Space Moose Party" priority:7]
  [#party party-name:"National Ray-Gun Association" priority:8]
  [#party party-name:"People for the Ethical Treatment of Humans" priority:9]
  [#party party-name:"Voter Apathy Party" priority:10]
  [#party party-name:"Rainbow Whigs" priority:11]
  [#party party-name:"Antisocialists" priority:12]
  [#party party-name:"Reform Party" priority:13]
  [#party party-name:"No Party" priority:14]

Summary Page

This page gives an administrator an overview of all the Earthicans who have registered to vote, and drawing the contents is actually pretty easy. All we need is a #voter-card for each #voter record. The layout for what the #voter-card looks like gets handled in the next block, and the brunt of the work for this section is actually the styling.

search @session @browser
  [#app-window target:"Summary"]
  window = [#div class:"app-window"]
  voter = [#voter]  
  
bind @browser
  window.children += [#voter-card voter]

Voter Cards

Each voter gets a #voter-card in the browser, which displays the voter’s information.

search @session @browser
  voter-card = [#voter-card voter]
  voter = [#voter first-name last-name birthday gender ssn party address]
  phone = if voter.phone then "Phone: " else ""
  address2 = if address.address2 then "  " else " "
  middle-name = if voter.middle-name then "  " else " "

bind @browser
  voter-card <- [#div class:"voter-card" children:
    [#div class:"voter-name" text: ""]
    [#div class:"birth-info" children:
      [#div class:"voter-address" text: "  "]
      [#div class:"voter-birth" text:"Born: "]
      [#div class:"voter-sex" text:"Gender: "]
      [#div class:"voter-phone" text:phone]
      [#div class:"voter-ssn" text:"SSN: XXX-XX-"]]
    [#div class:"voter-party" text:"Affiliation: "]]

Appendix

Sample Data

Here’s a few sample registered voters to help populate the Summary page.

commit
  [#voter
   first-name: "Philip"
   middle-name: "J."
   last-name: "Fry"
   birthday: "08-14-1974"
   gender: "male"
   ssn: "0810"
   party: "Voter Apathy Party"
   address: [address1: "620 W 42nd St" address2: "Apt 00100100" city: "New New York" state: "NY" zip: "10036"]]
  
  [#voter
    first-name: "Hubert"
    middle-name: "J."
    last-name: "Farnsworth"
    birthday: "04-09-2841"
    gender: "male"
    phone: "04-09-2841"
    ssn: "2458"
    party: "National Ray-Gun Association"
    address: [address1: "650 W 57th S." city: "New New York" state: "NY" zip: "10036"]]

  [#voter
    first-name: "Turanga" 
    last-name: "Leela"
    birthday: "07-29-2975"
    gender: "Female"
    phone: "212-307-7760"
    ssn: "3001"
    party: "No party"
    address: [address1: "715 9th Ave" address2: "Apt 1I" city: "New New York" state: "NY" zip: "10036"]]

  [#voter 
    first-name: "Bender"
    middle-name: "Bending"
    last-name: "Rodriguez"
    birthday: "01-02-2996"
    gender: "Manbot"
    party: "No party"
    ssn: "BU22"
    address: [address1: "620 W 42nd St" address2: "Apt 00100100" city: "New New York" state: "NY" zip: "10036"]]

  [#voter
    first-name: "John"
    middle-name: "A."
    last-name: "Zoidberg"
    birthday: "05-05-2914"
    gender: "Male"
    ssn: "DXC7"
    party: "People for the Ethical Treatment of Humans"
    address: [address1: "The dumpster behind 650 W 57th St" city: "New New York" state: "NY" zip: "10036"]]
    
  [#voter
    first-name: "Hermes" 
    last-name: "Conrad"
    birthday: "07-15-2959"
    gender: "Male"
    ssn: "6789"
    party: "Brain Slug Party"
    address: [address1: "105 Duane St" address2: "Apt 3007" city: "New New York" state: "NY" zip: "10007"]]

  [#voter 
    first-name: "Amy"
    last-name: "Wong"
    birthday: "05-04-2978"
    gender: "Female"
    phone: "212-995-8833"
    ssn: "5523"
    party: "Dudes for the Legalization of Hemp"
    address: [address1: "10 West St." city: "New New York" state: "NY" zip: "10007"]]

A Custom Form Element

Forms have a title and one or more sections. Each section has an optional name, and contains one or more fields. Each field additionally has the input type of that field (input, radio button, drop down list, etc.).

A form starts as a #form record.

search @browser
  form = [#form]
  
bind @browser
  form += #div
  form.sort := 0
  form.class := "form"

Display the form name

search @browser
  form = [#form]
  
bind @browser
  form.children += [#div children:
    [#h1 class: "form-name" sort: 0 text: form.name]
    [#div #form-message form sort: 1 class: "form-message"]]

Display each section. To properly display sections, we need to add them to the children of the form.

search @browser
  form = [#form sections]
    
bind @browser
  form.children += [#div form section: sections class: "form-section" sort: sections.sort]
  sections.form := form

If the section has a name, display it

search @browser
  section-display = [#div section]
  
bind @browser
  section-display.children += [#h2 class: "section-name" text: section.name, sort: 0]

Display the fields in each section. As we did with sections, to display fields we need to move them over to the children of the section display.

search @browser
  section-display = [#div section]
  field = section.fields
  
bind @browser
  field.form := section.form
  section-display.children += 
    [#div #field field sort: field.sort form: section.form sort: 1]

Display a submit button at the end of the form

search @browser
  form = [#form]
  
bind @browser
  form.children += [#button #submit form sort: 100 text: "Submit"]

Handle form submission

When the submit button is clicked, the form enters a #validating stage, where all form data are collected and compared against the field specifications. If any field does not match, then the submission is tagged #invalid and those fields are called out to the user to correct.

If all fields are valid, then the submission is marked #valid and the data are stored in a new #submission record stamped with the submission time.

We start by saving the data in the submission record, and mark the submission as #validating. Validation is covered in the next section.

search @event @browser @session
  [#click element: [#submit form]]
  [#field form field]
  [#time timestamp: time]
  
  value = if field.value then field.value
          else ""
  
commit @browser
  submission = [#submission #validating form time]
  lookup[record: submission, attribute: field.field value]

Handle form validation

If a field is #required and a submission is #validating, then we check that the field is actually filled. If any required fields are not filled, then we mark the submission as #invalid`.

search @browser
  submission = [#submission #validating form]
  field = [#field #required form not(value)]
  
commit @browser
  submission -= #validating
  submission += #invalid

Give invalid fields a custom class. Also display a message at the top if any fields are required but missing. The bind here allows the form to be corrected dynamically, unmarking previously invalid fields as soon as they are filled.

search @browser
  [#submission #invalid form]
  field = [#field #required form not(value)]
  field-label = [#h3 field]
  form-message = [#form-message form]
  
bind @browser
  field-label.class += "required-field"
  form-message.children += [#div class: "error-message" text: "Please fill out all required fields"]

If all required fields are valid, then the submission is marked #valid

search @browser
  submission = [#submission #validating form]
  not([#field #required form not(value)])

commit @browser
  submission -= #validating
  submission += #valid

Reset the form

Resetting currently has a bug that affects multiple form submissions in succession.

search @browser
  form = [#form #reset]
  submission = [#submission form]

commit @browser
  form -= #reset
  submission := none

Custom Input Types

Custom input types are implemented by taking the value of the input (whatever that may be) and assigning it to the value attribute of its corresponding #field record. For example, the value of a text field is just value. However, the value of a radio field is the value of the option that is checked, so some additional logic needs to be in place to work with these various components. However, the interface to access these data from Eve is to uniformly look at the value on the #field. In this example, we implement both the radio and the input fields, but more can be implemented in this way.

Render radio buttons

search @browser
  field-display = [#div #field field form]
  field.type = "radio"

bind @browser
  field-display.children += [#div #radio field children: 
    [#h3 field text: field.label]
    [#div option: field.options children: 
      [#div class: "radio-label" text: field.options.label]
      [#input class: "radio-button" field form type: "radio" name: field.field value: field.options.label]]]

Save radio value in its respective #field

search @browser @session
  radio = [#input type: "radio" checked: true field value]
  field = [#field]
  
bind @browser
  field.value := value

Render input fields

search @browser
  field-display = [#div #field field form]
  field.type = "input"
  placeholder = if field.placeholder then field.placeholder
                else ""

bind @browser
  field-display.children += [#div children: 
    [#h3 field text: field.label]
    [#input class: "text-input" type: "input" form field name: field.field placeholder]]

Save input value to respective #field


search @browser @session
  input = [#input type: "input" field value]
  field = [#field]
  
bind @browser
  field.value := value

Render password fields

search @browser
  password = [#password]
  
bind @browser
  password += #input
  password.type := "password"
  password.class := "password"

Render custom button styles

search @browser
  button = [#button form]
  
bind @browser
  button.class += "submit-btn"

Styles

This app needs a good amount of CSS to style the layout of the registration form and the voter cards.

{for a good time, leave this here}
.radio-label {
 float: left;
 font-size: 13px; 
 width: 300px;
}

.radio-button {
  width: 100px;
}

.text-input {
 width: 350px; 
 border-radius: 5px;
 padding: 5px;
}

.error-message {
 background-color: 	#FA8072; 
 padding: 10px;
 color: white;
 font-size: 16px;
}

.required-field {
  color: red;
}
@font-face {
  font-family: "futurama";
  src: url("https://witheve.github.io/assets/fonts/fr-bold.ttf") format("truetype");
}

.nav-bar {
  order: 1;
  display: flex;
  flex-direction: row;
  margin-bottom: 10px;
  min-height: 44px;
}

.nav-bar button {
  padding: 10px;
  margin-right: 15px;
  border: 1px solid #555;
  border-radius: 5px;
  background: none;
  font-size: 14px;
  cursor: pointer;
}

.app-window.home-screen {
  background: radial-gradient(circle at bottom,#5ef5fc,#0b527a,#012535);
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  height: 100%;
  min-height: 1000px;
  order: 2;
}

.thank-you {
  font-family: "futurama";
  text-align: center;
  margin: 100px 0px;
  font-size: 48px;
  color: white;
}

.begin-btn {
  width: auto;
  flex: 0 0 50px;
  padding: 0px 20px;
  margin-top: 5vh;
  font-size: 16px;
  background: white;
  border: 1px solid #555;
  border-radius: 6px;
  text-transform: uppercase;
  cursor: pointer;
}

.sponsor {
  background: url(http://i.imgur.com/sWkxxNU.png) no-repeat;
  background-size: 60px;
  background-position: bottom center;
  width: auto;
  height: 100px;
  color: white;
  position: absolute;
  bottom: 30px;
  text-transform: uppercase;
  font-size: 24px;
  text-align: center;
}

.app-window {
  background: white;
  order: 2;
  display: flex;
  flex-direction: column;
  overflow: scroll;
  height: 100%;
}

.voter-registration > div {
  display: flex;
  border-top: 1px solid #0b527a;
  align-items: center;
  padding: 15px 0px;
  flex: 0 0 auto;
}

.voter-registration h1 {
  font-size: 80px;
  width: 150px;
  margin-right: 50px;
  padding-left: 20px;
  color: #0b527a;
}

.voter-registration input {
  margin-left: 8px;
}

.name-field {
  width: 300px;
  display: inline;
}

.address-field {
  width: 500px;
  display: inline;
}

.radio-field {
  margin: 6px 0px;
}

.gender-field {
  display: flex;
  align-items: center;
}

.gender-field div {
  margin-right: 15px;
}

.voter-card {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  margin-bottom: 10px;
  border: 1px solid #555;
  border-radius: 6px;
  padding: 10px;
}

.submit-btn {
  width: auto;
  height: 60px;
  margin-top: 20px;
  padding: 0px 60px;
  font-size: 16px;
  color: white;
  background: #0b527a;
  border: 1px solid #555;
  border-radius: 6px;
  text-transform: uppercase;
  align-self: center;
  flex: 0 0 auto;
}

.voter-name {
  font-size: 18px;
  margin-bottom: 10px;
  font-weight: 600;
}

.voter-address {
  font-size: 18px;
  margin-bottom: 10px;
}

.birth-info {
  display: flex;
}

.birth-info div {
  padding-right: 50px;
}

@media (max-width:2275px) {
  
.nav-bar {
  min-height: 25px;
}

.nav-bar button {
  padding: 0px 10px;
  margin-right: 10px;
  font-size: 10px;
}

.app-window.home-screen {
  min-height: 550px;
}
  
.thank-you {
  text-align: center;
  margin: 50px 0px;
  font-size: 26px;
}

.old-freebie {
  width: 80%;
}
  
.begin-btn {
  flex: 0 0 40px;
  padding: 0px 20px;
  margin-top: 50px;
  font-size: 14px;
}
  
.sponsor {
  background-size: 40px;
  width: auto;
  height: 70px;
  bottom: 20px;
  font-size: 14px;
}

.app-window {
  font-size: 12px;
} 
  
.voter-registration > div {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0px 10px 10px 10px;
  flex: 0 0 auto;
  min-width: 192px;
}
  
.voter-registration > div > div{
  width: 100%;
}   

.voter-registration h1 {
  font-size: 30px;
  flex: 0 0 20px;
  width: auto;
  margin-right: 0px;
  padding-left: 0px;
}
  
.radio-field input {
  margin-left: 6px;  
}
  
.name-field {
  width: 100%;
  display: flex;
}
  
.birth-field {
  width: 100%;
  display: flex;
}
  
.phone-field {
  width: 100%;
  display: flex;
}  

.voter-registration .address-field {
  width: 100%;
  display: inline;
  margin-left: 0px;
}
  
.voter-registration .ssn-field {
  margin-left: 0px;
  width: 100%;
  display: flex;
}

.radio-field {
  margin: 5px 0px;
}

.submit-btn {
  width: auto;
  height: 40px;
  margin: 25px 0px;
  padding: 0px 40px;
  font-size: 14px;
  cursor: pointer;
}
  
.political-party .radio-field {
  display: flex;
  flex-direction: column;
  margin-bottom: 10px;
}
  
.political-party input {
  margin-left: 0px;
}

.voter-name {
  font-size: 16px;
}

.voter-address {
  font-size: 16px;
}

.birth-info {
  flex-direction: column;
}

.birth-info div {
  padding-right: 0px;
}  
  
}