6 min read

Vue Cli and Bulma Starter PWA

Etienne Marais

Vue Bulma Starter App

Ideas spring up at the weirdest times. They rarely keep office hours and keep me awake unless I action at least a prototype level of it. The thing I find that generally takes the longest is setting up a clean workspace and setup that will satisfy a deployed prototype. I use Netlify to host my static apps.

TL;DR

I created a starter vue cli + bulma single page progressive web application that I can clone for faster setup to get going on my idea rather than to spend time setting up without a line of code.

Github | Demo


I want to talk about the features I used and the flexibility this provides when getting started.

Custom Service Worker

A basic requirement for me was the reload content capability using the service worker to help the user reload to fresh content when a deploy goes live. Due to the rapid nature of prototyping things, changes occur very often so making that easy for people to see adds a lot of value. I adapted the code from an article I read over custom service workers in a vue cli 3 app. The article explains fully the steps it takes to build this and why they are necessary.

vue.config.js

module.exports = {
    pwa: {
        workboxPluginMode: "InjectManifest",
        workboxOptions: {
            swSrc: "src/service-worker.js"
        }
    }
}

/src/service-worker.js

self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});

// install new service worker when ok, then reload page.
self.addEventListener("message", msg=>{
    if (msg.data.action=='skipWaiting'){
        self.skipWaiting()
    }
})

/src/registerServiceWorker.js

/* eslint-disable no-console */

import { register } from 'register-service-worker'

if (process.env.NODE_ENV === 'production') {
    register(`${process.env.BASE_URL}service-worker.js`, {
        ready() {
            console.log(
                'App is being served from cache by a service worker.\n' +
                    'For more details, visit https://goo.gl/AFskqB'
            )
        },
        registered() {
            console.log('Service worker has been registered.')
        },
        cached() {
            console.log('Content has been cached for offline use.')
        },
        updatefound() {
            console.log('New content is downloading.')
        },
        updated(registration) {
            console.log('New content is available; please refresh.')
            let confirmationResult = confirm(
                'New content found! Do you want to reload the app?'
            )
            if (confirmationResult)
                registration.waiting.postMessage({ action: 'skipWaiting' })
        },
        offline() {
            console.log(
                'No internet connection found. App is running in offline mode.'
            )
        },
        error(error) {
            console.error('Error during service worker registration:', error)
        }
    })

    let refreshing
    navigator.serviceWorker.addEventListener('controllerchange', e => {
        if (refreshing) return
        window.location.reload()
        refreshing = true
    })
}

Extending Bulma

This was the main focus for me to automate. Extending bulma is tricky but not impossible. Bulma also has documentation on how to extend their framework.

/src/assets/scss/app.scss

@charset "utf-8";

@import './_shared';
@import './_mixins';

@import 'node_modules/bulma/sass/base/_all';
@import 'node_modules/bulma/sass/elements/_all';
@import 'node_modules/bulma/sass/grid/_all';
@import 'node_modules/bulma/sass/components/_all';
@import 'node_modules/bulma/sass/layout/_all';

/* Buefy UI components */
//@import 'node_modules/buefy/src/scss/buefy'; // Uncomment to use

/* Better row level spacing so no more overlap */
.columns {
    margin-top: 0;

    &:last-child {
        margin-bottom: 0;
    }
}

/* 4K screens override */
.container {
    @include desktop() {
        &.is-ultra-widescreen-fluid {
            margin-left: $gap;
            margin-right: $gap;
            max-width: none;
            width: auto;
        }
    }
    @include ultra-widescreen() {
        &.is-ultra-widescreen-fluid {
            margin: auto;
            max-width: $ultra-widescreen;
            width: auto;
        }
    }
}

/* Components */
@import 'components/_all';

app.scss is the main entrypoint into the application stylesheet and it references the various areas of concern while allowing you to extend things.

/src/assets/scss/_shared.scss

This is where most of the framework level overrides and customisations happen. You have to load the bulma files in order and drop in your replacements and/or overrides for the rest of the framework to propagate from.

// Import business fonts
@import "_fonts";

@import "node_modules/bulma/sass/utilities/initial-variables";
@import "node_modules/bulma/sass/utilities/functions";

$border: $grey-lighter !default;
@import "node_modules/bulma/sass/utilities/mixins";

// Import Business main color scheme
@import "_colours";

/** Application variables and overrides */
$family-primary: "Avenir-Book", -apple-system, system-ui, "Helvetica Neue", "Helvetica", "Arial", sans-serif !default;
$family-light: "Avenir-Light", -apple-system, system-ui, "Helvetica Neue", "Helvetica", "Arial", sans-serif !default;
$family-medium: "Avenir-Medium", -apple-system, system-ui, "Helvetica Neue", "Helvetica", "Arial", sans-serif !default;
$family-heavy: "Avenir-Heavy", -apple-system, system-ui, "Helvetica Neue", "Helvetica", "Arial", sans-serif !default;
$family-black: "Avenir-Black", -apple-system, system-ui, "Helvetica Neue", "Helvetica", "Arial", sans-serif !default;
$body-background-color: hsl(0, 0%, 99%) !default;
$body-size: 16px !default;
$body-rendering: optimizeLegibility !default;
$body-family: $family-primary !default;
$body-color: $grey-heading !default;
$body-line-height: 1.618 !default;

$hr-background-color: hsl(0, 0%, 95%) !default;
$hr-height: 1px !default;

$table-background-color: $body-background-color !default;
$table-cell-border: 0 !default;
$table-cell-padding: 0.5em 0;

$weight-hairline: 100 !default;
$weight-lighter: 200 !default;

/* Framework overrides */
$navbar-box-shadow-size: 0;

@import "node_modules/bulma/sass/utilities/derived-variables";
@import "node_modules/bulma/sass/utilities/controls";
@import "node_modules/bulma/sass/utilities/animations";

/** Framework overrides */
$link: $grey-heading;
$ultra-widescreen: 1680px + (2 * $gap) !default;

/src/assets/scss/_fonts.scss

Example of the custom loaded fonts that your application will use.

@font-face {
    font-family: 'Avenir-Book';
    src: url('assets/fonts/Avenir-Book.eot');
    src: url('assets/fonts/Avenir-Book.eot?#iefix') format('embedded-opentype'),
    url('assets/fonts/Avenir-Book.svg#Avenir-Book') format('svg'),
    url('assets/fonts/Avenir-Book.ttf') format('truetype'),
    url('assets/fonts/Avenir-Book.woff') format('woff'),
    url('assets/fonts/Avenir-Book.woff2') format('woff2');
    font-weight: normal;
    font-style: normal;
}

/src/assets/scss/_mixins.scss

Example of your mixins file.

@import './_shared';

@mixin ultra-widescreen() {
    @media screen and (min-width: $ultra-widescreen) {
        @content
    }
}

@mixin rounded($radius: 3px) {
    -webkit-border-radius: $radius;
    -moz-border-radius: $radius;
    border-radius: $radius;
}

/src/assets/scss/_colours.scss

Example of your colours file. This is where you would add more color variables if your palette requires.

$grey-lightest: hsl(0, 0%, 95%) !default;
$grey-heading: hsl(0, 0%, 28%) !default; //#484848

.has-background-grey-lightest {
    background-color: $grey-lightest;
}

/src/assets/scss/components/Home.scss

You would extract all the component level styles into their own files.

.home {
    .hero {
        -webkit-background-size: cover;
        -moz-background-size: cover;
        -o-background-size: cover;
        background-size: cover;
        background: linear-gradient(
            to top,
            rgba(0, 0, 0, 0),
            rgba(0, 0, 0, 0.8)
          ), url('/img/responsive/kara-eads-1297668-unsplash_w_2180.jpg') no-repeat 45% 45%;
        background-color: $grey-darker;
    }

    h1 {
        font-family: $family-black;
        font-weight: 800;
    }

    h2 {
        font-family: $family-black;
        font-weight: 800;
    }
}

scss-structure

Putting it all together

Loading your extended Bulma is as simple as importing your base scss into the top level vue component and letting webpack take care of the rest.

/src/App.vue

<template>
    <div id="app">
        <router-view/>
    </div>
</template>

<style lang="scss">
    @import "assets/scss/app";
</style>

Summary

A simple but effective way to get started using vue cli 3 alongside bulma that can easily be extended. Sometimes you only want to use the flex layout and other times the form components. I hope to extend this even more to be able to satisfy many more use cases but for the time being I got three prototypes up in a weekend and deployed to Netlify.

I consider that a win.


Cover photo by Jan Antonin Kolar.