Continuing Frameworkless.js

Part 5: Frontend ❤️

Recap of Last Time

  • We built a Node.js app utilising the built-in http.createServer
  • Starts an HTTP server and serves static files out of the ./public directory
  • We implemented dynamic routing with a handy easy to define syntax
  • Our routes use Handlebars template files to display content that's not static
  • We also wrote a super simple form data parser that will allow us to capture user data
  • We connected up a database, using SQL queries mixed into "actions" to contain them

What are we building again?

Ever wanted to remind yourself of something, out of the blue? Well, we're building a quick tool to do that!

What's left to do?

Today, we're working on the frontend a bit.

 

Next session, we're going to write a cronjob and connect up the mailing functionality.

 

Then, it's launch time!

The state of frontend

Modern JS and CSS are not supported across all browsers. Your options are:

  1. Screw those pesky old browsers... 😈
  2. Write your frontend code in lowest common denominator standards 😳
  3. Add a compilation step allowing you to write modern code but have it come out as acceptable even to the oldest of the Internet Explorers

So what's a 'pipeline'?

  • Your app should serve static assets on production
  • In your development environment, it's helpful to "watch" for changes and not rely on static assets
  • An asset pipeline lets you have your watch for changes on dev, and compile assets on live

What we're actually going to do...

On live, a build step will happen before deploy completes with static assets.

 

On development, assets will be compiled and served from memory.

Install packages! 😿

$ npm i --save @babel/core @babel/polyfill @babel/preset-env babelify browserify uglifyify
$ npm i --save autoprefixer postcss postcss-cli postcss-import postcss-node-sass postcss-url

For JS, we will use Babel, Browserify & Uglify.

For CSS, we will use PostCSS with SASS rules.

* We install them as dependencies and not devDependencies because our production build step needs them!

Importing the new modules

// lib/responder.js

const autoprefixer = require('autoprefixer')
const postcss = require('postcss')
const cssImports = require('postcss-import')
const sass = require('postcss-node-sass')
const cssUrl = require('postcss-url')
const browserify = require('browserify')

const { NODE_ENV } = process.env

A new type of response is needed

// lib/responder.js

const serveAssetFromPipeline = async ({ file, extension, path }, response) => {
  if (extension === 'css') {
    const { css } = await postcss()
      .use(sass)
      .use(cssImports())
      .use(cssUrl({ url: 'inline' }))
      .use(autoprefixer)
      .process(file, { from: path, include: [ './node_modules' ] })

    return response.end(css)
  } else if (extension === 'js') {
    return browserify(path)
      .transform('babelify', { presets: [ '@babel/preset-env' ] })
      .bundle().pipe(response)
  }
}

Incorporating it into our statics

// lib/responder.js
// the try block in serveStaticFile

try {
  const pipelineAsset = NODE_ENV !== 'production' && [ 'css', 'js' ].indexOf(extension) > -1
  const realPath = pipelineAsset ? `./assets/${extension}${path}` : `./public${path}`

  fileHandle = await open(realPath, 'r')
  const staticFile = await fileHandle.readFile()
  const mime = lookup(extension)
  if (!mime) throw new Error('not_found')

  response.writeHead(statusCode || 200, {
    'Content-Type': mime
  })

  if (pipelineAsset) return serveAssetFromPipeline({ file: staticFile, extension, path: realPath }, response)

  return response.end(staticFile)
} catch ...

npm run build

"scripts": {
  "build": "npm run build:css && npm run build:js",
  "prebuild": "rm -f ./public/style.css && rm -f ./public/app.js",
  "build:css": "postcss ./assets/css/style.css -u postcss-node-sass postcss-import autoprefixer -o ./public/style.css",
  "build:js": "browserify -t [ babelify --presets [ @babel/preset-env ] ] -g [ uglifyify ] -o ./public/app.js -e ./assets/js/app.js"
}

Ok, let's make some files now

$ mkdir -p assets/css
$ mkdir assets/js
// assets/js/app.js

const init = () => {
  console.log('Wow this is working omg omg omg')
}

init()
/* assets/css/style.css */

body {
  background: #dd04b5;
  color: 'white';
}

🚨 Moar styles! 🚨

Code's too long to put in the slides, but here you go:

 

https://gist.github.com/mtimofiiv/e0db0d7c372558573ed9b56c2146db7c

/* assets/css/style.css */

@import './variables.scss';
@import './base.scss';
@import './form.scss';

date picking for everyone

Unfortunately, input type="datetime-local" only works on new browsers...

$ npm i --save datepickr
/* assets/css/style.css */

@import '../../node_modules/flatpickr/dist/flatpickr.css';
// assets/js/app.js

import flatpickr from 'flatpickr'

const init = () => {
  const pickers = document.querySelectorAll('.flatpickr')

  for (const el of pickers) {
    flatpickr(el, {
      minDate: new Date()
    })
  }
}

init()

Handlebar helpers in templates

Helpers can simplify a lot of things in views.

$ npm i --save timeago.js
// lib/handlebar-helpers.js

const timeago = require('timeago.js')

exports.timeago = timeago.format
{{!-- templates/new_reminder.hbs --}}

<h1>Awesome, I will remind you {{timeago send_at}}.</h1>
// lib/responder.js

const hbsHelpers = require('./handlebar-helpers')
for (const helper of Object.keys(hbsHelpers)) Handlebars.registerHelper(helper, hbsHelpers[helper])

Wawaweewaa!

Look at that gorgeous thing...

Well done, you made it! 👏👏👏

See you next time where we look at Adding in email functionality and building a cronjob.

 

Please don't hesitate to contact me or leave feedback on the course:

Telegram channel: https://t.me/frameworkless

Twitter: @mtimofiiv

frameworkless.js -> Part 5

By Mike Timofiiv

frameworkless.js -> Part 5

Check out https://frameworkless.js.org/course/5 for the whole presentation

  • 1,468