Joseph Post

The No Framework SPA Tutorial, part 1

2017, July 17th

JavaScript developers are nuts.

It's ok, I'm one of them and I'm nuts too.

I want to show you something that I think is less nuts. Working without a framework or ay third party libraries. Some folks don't realize how realistic of an option that is.

If you are a javascript developer and never built an SPA without a framework, please try it. Afterward, you can explore the massive ecosystem of JavaScript libraries and be able to fine-tune the dependencies on your codebase, rather than tuning your thinking to someone else's framework.

Ok, glad to hear you'll do it!

No more time to waste, let's get started. We are going to build a page that gives us information about countries, using the very nice REST Countries API. It will end up being around 50 lines of code, including our Vanilla JS dependency.

First off, you'll need an index.html page. Let's fill it with the normal junk:

<!doctype html>
<html>
<head>
  <title>List of Countries</title>
</head>
<body></body>
<script>
// Woohoo, I'm ready when you are!
</script>
</html>

Looks great, that's all we need to start writing a web app. We'll be doing everything in this single file for now (Single File App! SFA?). Assume all following JS examples are in the <script> tag.

Look ma, no React/Vue/Angular/Webpack/Rollup/Gulp/Typescript/Babel/Config! (Yet...)

We'll be writing ES2015+ without a modern-to-legacy-javascript compiler.

This is ok.

You should be developing with a modern browser anyway, like Chrome or Firefox. Even Edge and Safari will work. Don't bother with support for IE right now, this is supposed to be fun.

Let's build a little helper function, because we're going to be creating a lot of elements.

function create(element, text) {
  const el = document.createElement(element)
  el.innerText = text
  return el
}

Not a bad start. Let's build a table.

const mockData = [
  { fish: 'one' },
  { fish: 'two' },
  { fish: 'red' },
  { fish: 'blue' }
]

function Table (data) {
  const table = create('table')
  const thead = create('thead')
  const tbody = create('tbody')
  const tr = create('tr')
  const columns = Object.keys(data.reduce((a, v) => Object.assign(a, v), {}))

  columns.forEach(column => tr.appendChild(create('th', column)))
  thead.appendChild(tr)
  table.appendChild(thead)
  data.forEach(record => {
    const tr = create('tr')
    columns.forEach(column => tr.appendChild('td', record[column]))
    tbody.appendChild(tr)
  }

  return table.appendChild(tbody)
}

Let's make it make our element factory a little more robust, and call it h() so it can be à la mode.

function h(element = 'div', classes, children) {
  const el = document.createElement(element)

  if (Array.isArray(classes)) {
    classes.forEach(c => el.classList.add(c))
  } else if (typeof classes === 'string') {
    el.classList.add(classes)
  }

  if (Array.isArray(children)) {
    children.forEach(c => {
      if (c instanceof HTMLElement) {
        el.append(c)
      }
    })
  } else if (children instanceof HTMLElement) {
    el.append(children)
  } else if (typeof children === 'string') {
    el.innerText = children
  }

  return el
}

This provides functionality comparable to element-creating functions in various libraries and frameworks. Let's disect it a bit:

This is pretty powerful stuff. YNow we can write a table like this:

// Nice table ya got there...
h('table', 'table', [ 
  h('thead', null,
    h('tr', null, [
      h('th', null, 'name'),
      h('th', null, 'capital')
    ])
  ),
  h('tbody', null, [
    h('tr', null, [
      h('td', null, 'Belgium'),
      h('td', null, 'Brussels')
    ]),
    h('tr', null, [
      h('td', null, 'Chile'),
      h('td', null, 'Santiago')
    ])
  ])
])

In part 2, we'll fetch data and display it.