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:
- Screw those pesky old browsers... 😈
- Write your frontend code in lowest common denominator standards 😳
- 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
See today's code: https://github.com/frameworkless-js/remind.ist/tree/stage/5
See today's lesson running live: https://part5.remind.ist
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