Single Page Voter

1. Introduction

Single Page Voter is a tool for performing small in person votes. Do not use this for anything to serious.

While implementing this tool I have decided to:

  1. Use literate programming as my intention is to convey more clearly my design choices to the reader. For install instructions see the literate GitHub

  2. Use Vanilla JS to convey my understanding of the fundamentals and the browser.

  3. Design for bleeding edge browsers only. Not cleaning up event listeners when an element is removed.

  4. Not focus on tools like frameworks, linters, prettifiers, transpilers and bundlers.

See single page voter in action over here, or keep reading to see how and why I have implemented the way I did.

Whenever you see text styled in this manner, it takes you into my mind. Most of the consideration does not end up as working code. These blocks explain some of my thought process and design choices.

2. Standard HTML template

Like any good frontend web application we start with a simple HTML template, then the code, followed by styling.

{./dist/voter.html 2}
<!DOCTYPE html>
<html>

  <head>
    <title>Single page voter</title>
    {Pure CSS Link, 23}
    <style>
      {Single Page Voter CSS rules, 24}
    </style>
  </head>

  <body>
    <h1>Single Page Voter</h1>
    {Single Page Voter HTML, 3}
  </body>

  <script>
    {Single Page Voter Script, 4}
  </script>

</html>

Notice that we define the script tag after the body. By doing this the Voter HTML has been registered and is accessible through the DOM API. It is also possible to listen to load events but I find this approach requiring the least amount of effort and code.

3. The Voter application HTML

All but the first element is generated using Javascript. This div contains an id which allows us to reference it from JS using document.getElementById.

{Single Page Voter HTML 3}
 <div id='poll-creator'></div>

Used in section 2

Why not append directly to the body? With the id approach you have more control in the HTML as to where the component should be rendered. It makes it easier when combining other components in the HTML.

4. Vanilla Javascript

We will use an IIFE. This allows one to define variables without changing any global variables/objects.

To prevent naming collisions within our own IIFE, we could define a namespace object. This can come in handy when in the future you or someone else pulls in a dependency in the global environment or the IIFE's code block. However, I did not do this. If a project requires this type of code separation it is better to use modules and bundling. Let's look at that IIFE.

{Single Page Voter Script 4}
(function() {
  {State Utility, 7}
  {Helpers, 18}
  {Initialize Components, 10}
})()

Used in section 2

Notice that the code is split up into a state utility, some helpers and the initialization of the components.

I could also have made a main or app function and then just call it. I would get the same benefit of the IIFE. I would however mutate the global environment with a reference to the main/app function.

5. Colors

Used to make the bar chart look prettier.

{Colors Array 5}
const colors = [
  '#EE82EE',
  '#EEE8AA',
  '#F08080',
  '#F0E68C',
  '#F0F8FF',
  '#F0FFF0',
  '#F0FFFF',
  '#F4A460',
  '#F5DEB3',
  '#F5F5DC',
  '#F5F5F5',
  '#F5FFFA',
  '#F8F8FF',
  '#FA8072',
]

Used in section 22

6. Fruits

We will use this list of fruits later in the program.

{Fruits Array 6}
const fruits = [
  'Apples ',
  'Apricots ',
  'Banana ',
  'Cantaloupe ',
  'Cherry ',
  'Avocado',
  'Carissa',
  'Carob',
  'Cattleya Guava',
]

Used in section 10

7. State Utility

We have some state to keep around. For this we make a little state utility. It allows transforming the state and listening to state changes.

{State Utility 7}
function state(initialState) {
  let state = initialState();
  const callbacks = {}
  let id = 0

  function transform(transformFn) {
    state = transformFn(state)

    // Call all listener callbacks when transform occurs.
    Object.keys(callbacks)
      .forEach(id => callbacks[id](state))
  }

  function addListener(callback) {
    id = id + 1
    callback(state)
    callbacks[id] = callback
    return id
  }

  function removeListener(id) {
    delete callbacks[id]
  }

  function reset() {
    transform(() => initialState())
  }

  return {
    reset,
    transform,
    addListener,
    removeListener,
  }
}

Used in sections 4 and 9

I went for the simplest thing I could think of.

8. Testing Our JS code

I have chosen a tool I find very useful which is Perl's prove. It is a test harness that will run all ./t/*.t files. It expects these scripts to output TAP.

To make this possible we need a JS library that supports tap. NPM's tape should do the job.

We create the package.json and register tape as one of the dev dependencies. We also register prove as the test script.

{./package.json 8}
{
  "name": "simple-page-voter",
  "version": "1.0.0",
  "description": "Vote on a single page",
  "author": "Bas Huis",
  "license": "GNU General Public License 3.0",
  "scripts": {
    "test": "prove"
  },
  "devDependencies": {
    "tape": "^5.0.1"
  }
}
{Tape Script 8}
#!/usr/bin/env node

const test = require('tape')

Used in sections 9 and 14

9. Testing the State Utility

Because we are using lit, we can just concat the State Utility section directly into our file. It is more common to see this utility be defined in a node module instead.

{./t/state.t 9}
{Tape Script, 8}
{State Utility, 7}

const always = v => () => v

test('Initial state stays the same after transform (reference)', t => {
  const init = {}
  const myState = state(always(init))

  myState.transform(state => {
    t.equals(state, init)
    return state
  })

  myState.transform(state => {
    t.equals(state, init)
    t.end()
  })
})

test('Event listener is called on register', t => {
  const init = {}
  const myState = state(always(init))

  myState.addListener(state => {
    t.equals(init, state)
    t.end()
  })
})

test('Event listener is called on state transform', t => {
  const init = {}
  const myState = state(always(init))

  t.plan(2)

  myState.addListener(state => {
    t.equals(state, init)
  })

  myState.transform(v => v)
  t.end()
})

test('Event listener is not called after remove', t => {
  const init = {}
  const myState = state(always(init))

  t.plan(1)

  const id = myState.addListener(state => {
    t.equals(init, state)
  })

  myState.removeListener(id)
  myState.transform(v => v)

  t.end()
})

test('Reset the state', t => {
  const initFn = always({ a: 1 })

  const myState = state(initFn)

  const changed = {a: 2}

  myState.transform(state => changed)

  myState.removeListener(
    myState.addListener(state => t.deepEquals(state, changed)))

  myState.reset()

  myState.addListener(state => t.deepEquals(state, initFn()))

  t.end()
})

Thinking of possible states an application can run into and writing tests for these cases is something that requires skill and time. I find generative and property based testing a great tool for putting your application to the test. For the sake of time I'll leave that for another day.

These tests can be run by executing the ./t/state.t file or by running the prove or npm t command. Do make sure to first install the tape dependency with npm i.

We have some tests for the state utility and we are ready to start initializing the components.

10. Initializing Components

The application consists out of three columns. Within these columns we can also sub divide the UI into simpler parts. From now on we name these columns and parts components.

Before we have a look at the individual components, we'll write the code necessary to wire up the components and append them to the page. You can consider this code block the main of the app.

{Initialize Components 10}
// common components
{Input Component, 11}

// first column
{Edit Poll Component, 12}

// second column
{Vote Component, 21}

// third column
{Result Component, 22}

function initialState() {
  {Fruits Array, 6}

  return {
    optionInput: '',
    question: 'What fruit is the best fruit?',
    options: fruits,
    votes: {},
  }
}

const myState = state(initialState)

{Computed State, 13}

const appElem = document.getElementById('poll-creator')

// columns
const editPoll = appElem.appendChild(editPollComponent(myState))
const vote = appElem.appendChild(voteComponent(myState))
const result = appElem.appendChild(resultComponent(myState))

{Hide Vote And Result Column, 10}
{Reset Votes On Poll Change, 10}

appElem.classList.add('pure-g')

editPoll.classList.add('pure-u-1')
vote.classList.add('pure-u-1')
result.classList.add('pure-u-1')

editPoll.classList.add('pure-u-sm-1-3')
vote.classList.add('pure-u-sm-1-3')
result.classList.add('pure-u-sm-1-3')

Used in section 4

Hide the vote and result column when there is no question or enough options defined.

{Hide Vote And Result Column 10}
myState.addListener(state => {
  const displayType = myState.computed.canVote(state) && state.question
    ? 'block'
    : 'none'

  vote.style.display = displayType
  result.style.display = displayType
})

Whenever the poll changes we want to reset the votes. We define this in a state listener in order to have the checking and changing all in one place.

Previously I had several state.votes = {} peppered throughout some callbacks. This goes against the DRY principle which makes it harder to maintain the code.

{Reset Votes On Poll Change 10}
function resetVotesOnPollChange() {
  let oldValue

  return ({options, question}) => {
    const newValue = JSON.stringify({
      options: options,
      question: question
    })

    if (oldValue === undefined)
      oldValue = newValue

    if (oldValue !== newValue) {
      oldValue = newValue
      myState.transform(state => {
        state.votes = {}
        return state
      })
    }
  }
}

myState.addListener(resetVotesOnPollChange())

I could have used an deep equality function or better suited data types for checking if the poll information has changed. Using JSON.stringify was however the most obvious approach.

11. Input Component

One of the requirements is that all input fields have an 80 character limit. We create a component with this common behavior and use it whenever we create an input field.

{Input Component 11}
function inputComponent() {
  const input = document.createElement('input')

  input.addEventListener('input', event => {
    input.value = input.value.substring(0, 80)
  })

  return input
}

Used in section 10

After writing this common component, I considered to instead define the input value limit using one or more state listeners. This would result in adding some extra code because all input fields that use this limit would require an event listener to update the value of that element with the sub-stringed value. The inputComponent approach is less code and solves the requirement for this project sufficiently.

12. Edit Poll Component

The first column is the poll editing form. The component is able to transform the poll. Just like every "component function", it returns an element.

{Edit Poll Component 12}
function editPollComponent(myState) {
  const form = document.createElement('form')
  const p = document.createElement('p')
  const question = inputComponent()
  const options = optionsComponent(myState)
  const optionsCap = optionsCapComponent(myState)
  const reset = document.createElement('button')

  reset.textContent = 'reset'

  myState.addListener(state => {
    question.value = state.question
  })

  function updateQuestion(event) {
    myState.transform(state => {
      state.question = event.target.value
      return state
    })
  }

  {Options Component, 15}
  {Options Cap Component, 12}
  {Add Option Component, 17}

  reset.addEventListener('click', event => {
    event.preventDefault()
    myState.reset()
    return false
  })

  question.addEventListener('input', updateQuestion)

  // Prevent enter from pressing any buttons.
  question.addEventListener('keypress', event => {
    if ((event.which || event.keyCode || event.charCode) === 13)
      event.preventDefault()
  })

  p.appendChild(question)

  form.appendChild(p)
  form.appendChild(options)
  form.appendChild(addOptionComponent(myState))
  form.appendChild(optionsCap)
  form.appendChild(reset)

  return form
}

Used in section 10

Then the component that shows how many options one has created and how many one is allowed to make. A basic component that demo's string interpolation.

{Options Cap Component 12}
function optionsCapComponent(myState) {
  const elem = document.createElement('div')

  myState.addListener(state => {
    const count = myState.computed.optionsCount(state)
    elem.innerHTML = `${count}/10 possible answers`
  })

  return elem
}

First I was using the older 'string' + 'string' string interpolation. In this case both approaches would suffice.

13. Computed State

Some values are derived from the state. We name these values computed. Here we list all the helpers for computed values.

{Computed State 13}
myState.computed = {
  options(state) {
    return compact(state.options)
  },
  canAddOption(state) {
    return (this.options(state).length < 10) && (state.optionInput !== '')
  },
  optionsCount(state) {
    return this.options(state).length
  },
  optionsWithIndex(state) {
    return compactWithIndex(state.options)
  },
  totalVotes(state) {
    return Object.values(state.votes).reduce((a, b) => a + b, 0)
  },
  maxVotes(state) {
    return Object.values(state.votes)
      .reduce((a, b) => Math.max(a, b), 0) || 0
  },
  optionVotes(state) {
    return toPairs(state.votes).map(optionVote =>
      [...optionVote, state.options[optionVote[0]]])
  },
  canVote(state) {
    return this.optionsCount(state) >= 2
  },
}

Used in section 10

I have considered defining the state transform callbacks in a similar object. Were the program to grow further, it would make sense to isolate all state related functionality from the components. This would improve the testability.

14. To Pairs Helper

The computed helpers depend on the toPairs helper. Here the implementation and some tests.

{To Pairs Helper 14}
function toPairs(object) {
  return Object.keys(object).reduce((array, property) => {
    return [...array, [property, object[property]]]
  }, [])
}

Used in section 18

{./t/to_pairs.t 14}
{Tape Script, 8}
{To Pairs Helper, 14}

test('Object is transformed to key value pairs', t => {
  t.deepEquals(toPairs({
    a: 1,
    b: 2,
  }), [
    ['a', 1],
    ['b', 2],
  ])
  t.end()
})

15. Options Components

The goal of Options Component is to list all the options and allow removal of one of the options. For now each option has its own event listener. This can be improved by using event bubbling and registering an event listener on a parent element. This would be more memory efficient in cases where one would have many elements. In this case we have a max of 10 options.

{Options Component 15}
function optionsComponent(myState) {
  const ul = document.createElement('ul')

  {Edit Option Component, 16}

  const mapEditOptionsComponent = mapComponents(
    editOptionComponent.bind(null, myState) // partial application
  )

  myState.addListener((state) => {
    mapEditOptionsComponent(myState.computed.optionsWithIndex(state))
      .forEach(e => ul.appendChild(e))
  })

  return ul
}

Used in section 12

16. Edit Options Component

An individual input element and remove button that allows editing and removal of an poll option.

{Edit Option Component 16}
function editOptionComponent(state, option) {
  const [index, value] = option
  const li = document.createElement('li')
  const input = inputComponent()
  const button = document.createElement('button')

  function removeOption(event) {
    event.preventDefault()

    state.transform(state => {
      state.options[index] = undefined
      return state
    })
  }

  function updateOption(event) {
    state.transform(state => {
      state.options[index] = event.target.value
      return state
    })
  }

  button.addEventListener('click', removeOption)
  button.textContent = 'x'

  input.value = value
  input.addEventListener('change', updateOption)

  li.appendChild(input)
  li.appendChild(button)

  return li
}

Used in section 15

17. Add Option Component

Allows the poll maker to create a new poll option.

{Add Option Component 17}
function addOptionComponent(myState) {
  const form = document.createElement('form')
  const input = inputComponent()
  const button = document.createElement('button')

  function onAddOption(event) {
      event.preventDefault()
      myState.transform(state => {
        state.optionInput = ''
        state.options.push(input.value)
        return state
      })
      return false
  }

  function onInput(event) {
    myState.transform(state => {
      state.optionInput = event.target.value
      return state
    })
  }

  form.addEventListener('submit', onAddOption)
  input.addEventListener('input', onInput)

  myState.addListener(state => {
    input.value = state.optionInput

    myState.computed.canAddOption(state)
      ? button.removeAttribute('disabled')
      : button.setAttribute('disabled', 'disabled')
  })

  form.appendChild(input)
  form.appendChild(button)

  button.textContent = 'add'

  return form
}

Used in section 12

18. Helpers

Where did mapComponents come from? It is a helper that allows us to create and update elements based on an array. There are also other helpers.

{Helpers 18}
{Map Components Helper, 19}
{Compact Helper, 20}
{To Pairs Helper, 14}

Used in section 4

19. Map Components Helper

I have chosen to make this an higher order function for several reasons.

Firstly because I need some state to keep reference to the current elements. Ideally I want this state to be isolated to prevent undesired mutation. I can achieve that by using the function scope.

Secondly I can get more code reuse, allowing me to create multiple functions that have similar behavior just by calling the mapComponents helper.

The inner mapComponents function returns the newly created elements for the user to append to a parent element. For sake of ease I remove all old elements before creating all new elements. This removes the hassle of having to mutate existing DOM elements. This might be less memory efficient but it is much easier to write.

{Map Components Helper 19}
function mapComponents(createComponent) {
  let components = []

  return function mapComponentsFn(array, ...rest) {
    const createdComponents = []

    // Removal introduces an issue where currently selected input elements are
    // removed. As a result the cursor disappears. Bad UX. Should be mutating
    // existing elements to coincide with the latest state. Not doing for now.
    components.forEach(elem => elem.remove())

    components = array
      .map((item, index) => createComponent(item, index, ...rest))

    return components
  }
}

Used in section 18

20. Compact Helper

Why the compact helper? The alternative would be implementing a Linked List. Why? Imagine we have an array of options and I remove an option from the poll somewhere in the middle of the list. Everything after that removed item is moved one index down the array. All event listeners registered and other parts of the applications that uses the index of these options will have to be updated.

So what if we do not change the indices of the items ever. We can remove things by just setting the value on a specific index to undefined. We then use the compact function to get all not yet removed items.

For sake of ease I chose to use a compact helper function instead of writing a linked list implementation. It's a time saver. The linked list could be implemented in the future. A linked list is the most elegant solution I can think of.

The compactWithIndex version is necessary to enable correct mutation of the original options array.

{Compact Helper 20}
function compact(arr) {
  return arr.filter(value => value != null)
}

function compactWithIndex(arr) {
  return arr.reduce((acc, value, index) => {
    if (value != null)
      acc.push([index, value])

    return acc
  }, [])
}

Used in section 18

21. Vote Component

The first column was a lot to take in. We created helpers and quite some components. This column should be much more concise because we get to reuse those helpers and this column has less components. It is just a form with radio buttons.

{Vote Component 21}
function voteComponent(myState) {
  const form = document.createElement('form')
  const question = document.createElement('p')
  const button = document.createElement('button')
  const ul = document.createElement('ul')

  let options = []

  function onVote (event) {
    event.preventDefault()
    let option = options.find(li => li.children[0].checked)

    if (option === undefined)
      return false

    const value = option.children[0].value

    myState.transform(state => {
      state.votes[value] = (state.votes[value] === undefined)
        ? 1
        : state.votes[value] + 1

      return state
    })

    return false
  }

  button.textContent = 'vote'

  form.addEventListener('submit', onVote)

  {Option Radio Component, 21}

  const mapOptionRadioComponent = mapComponents(
    optionRadioComponent.bind(null, myState)
  )

  myState.addListener(state => {
    question.textContent = state.question
    options = mapOptionRadioComponent(
      myState.computed.optionsWithIndex(state))

    options.forEach(elem => ul.appendChild(elem))
  })

  form.appendChild(question)
  form.appendChild(ul)
  form.appendChild(button)

  return form
}

Used in section 10

{Option Radio Component 21}
function optionRadioComponent(state, option) {
  const li = document.createElement('li')
  const input = inputComponent()
  const label = document.createElement('label')

  label.textContent = option[1]
  label.setAttribute('for', option[0])

  input.setAttribute('type', 'radio')
  input.setAttribute('id', option[0])
  input.setAttribute('name', 'vote')
  input.setAttribute('value', option[0])

  li.appendChild(input)
  li.appendChild(label)

  return li
}

22. Result Component

The third column reports on the votes. It is a bar chart where each bar displays the option and absolute amount of votes on every bar.

{Result Component 22}
function resultComponent(myState) {
  const question = document.createElement('p')
  const div = document.createElement('div')

  {Bar Chart Component, 22}
  {Total Votes Component, 22}

  myState.addListener(state =>
    question.textContent = state.question
  )

  div.appendChild(question)
  div.appendChild(barChartComponent(myState))
  div.appendChild(totalVotesComponent(myState))

  return div
}

Used in section 10

{Bar Chart Component 22}
function barChartComponent(myState) {
  const div = document.createElement('div')

  {Bar Component, 22}

  const mapBarComponent = mapComponents(barComponent)

  myState.addListener(state => {
    const maxVotes = myState.computed.maxVotes(state)

    mapBarComponent(myState.computed.optionVotes(state), maxVotes)
      .forEach(elem => div.appendChild(elem))
  })

  return div
}
{Bar Component 22}
function barComponent([_, votes, option], index, totalVotes) {
  const div = document.createElement('div')
  const label = document.createElement('div')
  const value = document.createElement('span')

  const percentage = (totalVotes
    ? votes / totalVotes
    : 0) * 100

  value.textContent = votes
  value.style.position = 'absolute'
  value.style.right = '0px'

  label.textContent = option

  {Colors Array, 5}

  div.title = option
  div.style.width = `${percentage}%`
  div.style.height = '1.5rem'
  div.style['background-color'] = colors[index]
  div.style.border = '1px solid black'
  div.style.position = 'relative'
  div.style.bottom = '0px'

  div.appendChild(value)
  div.appendChild(label)

  return div
}
{Total Votes Component 22}
function totalVotesComponent(myState) {
  const div = document.createElement('div')

  myState.addListener(state => {
    div.textContent = `Total votes: ${myState.computed.totalVotes(state)}`
  })

  return div
}

23. Pure CSS

I needed some columns and better styling for the forms. Not interested in spending to much time on that.

See https://purecss.io/

{Pure CSS Link 23}
<link
  rel="stylesheet"
  href="https://unpkg.com/purecss@2.0.3/build/pure-min.css"
  integrity="sha384-cg6SkqEOCV1NbJoCu11+bm0NvBRc8IYLRGXkmNrqUBfTjmMYwNKPWBTIKyw9mHNJ"
  crossorigin="anonymous"/>

<link
  rel="stylesheet"
  href="https://unpkg.com/purecss@2.0.3/build/grids-responsive-min.css"/>

Used in section 2

24. CSS rules

We keep it simple and make the default HTML styling do most of the heavy lifting.

{Single Page Voter CSS rules 24}
body {
  font-family: arial;
}

ul {
  list-style: none;
}

Used in section 2

25. Testing Components

It is relatively simple to unit test these components as they are just functions. The document global variable should be mocked when running in a node environment, or one can run the tests in a browser environment (which should be better). Because of lack of time I won't be testing component functions.

26. Conclusion

With this project I have showcased my understanding of the fundamentals of a Single Page Application. This basic knowledge is essential regardless of the SPA framework that is being used.

The use of Literate Programming is something new for me. I wonder if you the reader has benefited from splitting the code into sections with explanation. I, as the programmer, enjoyed writing code like this. It created small and readable code sections that I could easily tie into the eventual code.

The choice to use Vanilla JS has been a interesting test for myself. It has taken more time then I expected. SPA frameworks make templating a breeze, compared to .appendChild and .classList.add. Most SPAs have conventions for state management and the making of components that make it easier to work together and across projects.