Javascript

Customize MaterializeCSS Before Download in Browser

How about we Customize MaterializeCSS before we download, right from our browser? This is a two-part article, which will delve into the building of my recent project, Materialize Custom Download.

Looking for the Part 2?

MaterializeCSS is one of the popular Material Design frameworks out there that does a good job in getting things right. Led by a team of 4 students and many more contributors around the world, it has grown so huge, with over 20k stars on Github, and for good reasons.

Article difficulty somewhere Intermediate Level

One of my favorite front-end frameworks is Foundation for Sites. What I always enjoyed about Foundation for Sites was the ability to quickly check some boxes and download customized version of their CSS with or without certain packages.

Unfortunately, since I started using MaterializeCSS, I have been looking for a similar, in-browser CSS Customizer for Materialize. I couldn’t, and so, I spent one weekend building it for personal use, as well as sharing with anyone online who might be interested.

Gave birth to Custom Download Materialize. I hope you find it useful too

materialize.khophi.co

Why you’re Here?

This article isn’t about telling you how good and cool the Custom Download Materialize site is, or how clean-looking and gentle it stands to be.

So, we’ll be using basically these tools:

  • NodeJS
  • ExpressJS
  • Node-Sass
  • MaterializeCSS (of course)

In this article, I share how to customize your CSS and download from the browser. I will share code snippets enough to guide you in building, hopefully, something similar to Custom Download Materialize.

Express Part

Express JS was the workable go-to solution that came to my mind when I planned on doing the project last week.

Did AngularJS cross my mind? Yep. I came across medialize/sass.js which allow you to compile Sass snippets into CSS right within the browser. The approach looked awesome. The challenges?

Network overhead was one. Bringing the ability to compile Sass to one’s browser can be heavy, and even if 500 kb per each visit, that is huge, both for the end user, and my server that’ll be serving these requests.

By the way, if you don’t have Devdocs, fully enabled in your browsers, then what kind of Web Developer are you? A Yoda’veloper?

In the long run, I felt such an approach will overwhelm my meager server specs and reduce scalability. For local use, that might not be an issue, but since I planned to deploy the solution onto my server for anyone interested in using.

The next issue I encountered after playing with the Sass.js was the lack thereof integration with AngularjS (or my knowing of how to). I opened a ticket about that, and the response I received felt more of a prank.

And so, instead of having to re-invent many things, probably, trying to use the Sass.js in my browser without AngularJS, I rethought about the whole process, and realize, it’ll be best letting the server handle the building process, besides, that is how I guess Foundation Zurb does theirs (??).

What about Express?

I won’t be teaching you ExpressJS in this article. You have the entire internet minus this blog to learn about Express, for example: devdocs.io/express.

Quickly, let us prepare our app.js

// OUR IMPORTS OF PACKAGES
var compression = require('compression');
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var bodyParser = require('body-parser');
var sass = require('node-sass');
var fs = require('fs-extra');
var replace = require('replace');
var uuid = require('node-uuid');

// our router app
var app = express();
app.use(compression()); // they say this is good for production?
app.set('views', path.join(__dirname, 'views/'));

// with my background in Django Templates,
// Twig template engine for Express was icing on cake for me
// Use whatever you like instead of twig.
app.set('view engine', 'twig'); 

app.use(favicon(path.join(__dirname, 'public', 'favicon.png')));
app.use(logger('dev')); // log to console
app.use(bodyParser.json()); // receive json posts
app.use(bodyParser.urlencoded({ extended: false })); // receive form submission
app.use(express.static(path.join(__dirname, 'public'))); // static directory


// MORE CODE WILL BE PUT HERE IN A JIFFY

// our homepage
app.get('/', function(req, res) {
  res.render('index'); 
});

app.post('/', function(req, res) {
// we'll do this in a moment
});

var port = process.env.PORT || 3000;

app.listen(port);
console.log('Up and running at ' + port);

I assume you know at least how to setup NodeJS Express apps, so I will be skipping the pleasantries.

However, all you should know is, with the above piece of code, and all our dependencies required in their respective directories, installed via NPM into their proper location, we are good to go.

Firing up nodemon app.js will be all it takes to have our server running on port 3000

 

[wp_ad_camp_1]

 

So the How?

At this point, let us consider the process responsible for the proper working of the app.

  1. User visits the homepage. Checks a bunch of boxes in a form, then submits
  2. The Server receives the form post
  3. The Server looks through the form post object, then appends the incoming data to a .scss file.
  4. The Server runs a compiling spree on the .scss file.
  5. The output is pushed as a response to the browser in the form of attachment.
  6. The User is happy.

Although the above steps are oversimplified, they are basically what the building process involves.

From the previous section, we already have the number 1) already happening; Serving the user with a homepage with a bunch of checkboxes in a form with a submit button.

1) The Homepage

In our index.twig, we can add something like this. For brevity, many parts that are repeating are cut.

<!DOCTYPE html>
<html>
<head>
    <title>Custom Download MaterializeCSS</title>
    <link rel="stylesheet" href="./vendor/materialize/dist/css/materialize.min.css">
<body>
    <div class="container">
        <div class="row">
            <form action="/" method="post" id="form">
                <div class="col s12">
                    <p style="margin-left:12px;">
                        <input type="checkbox" class="filled-in" id="all" name="all" />
                        <label for="all" class="flow-text">Download Everything Materialize</label>
                    </p>
                    <div class="divider"></div>
                    <div class="col m4">
                        <h5>General</h5>
                        <p>
                            <input type="checkbox" class="filled-in" id="grid" name="grid" />
                            <label for="grid">Grid</label>
                        </p>
                        
                    </div>
                    <div class="col s12" style="margin-top:20px;">
                        <button type="submit" class="btn waves-effect waves-light">Download Custom</button>
                    </div>
                </div>
            </form>
        </div>
</body>
<script src="./vendor/jquery/dist/jquery.min.js"></script>
<script src="./vendor/materialize/dist/js/materialize.min.js"></script>
<script>
$(document).ready(function() {
    $('#all').click(function(event) {
        if (this.checked) {
            $(':checkbox').each(function() {
                this.checked = true;
            })
        } else {
            $(':checkbox').each(function() {
                this.checked = false;
            })
        }
    });

    $('#form').submit(function() {
        var valid = 0;
        $(this).find('input[type=checkbox]:checked').each(function() {
            if ($(this).val() !== "") valid += 1;
        });

        if (valid) {
            // alert(valid + " inputs have been filled");
            Materialize.toast('Download begins', 5000);
            return true;
        } else {
            // alert("error: you must fill in at least one field");
            Materialize.toast("Duh! Download Everything, or at least check a box!", 7000);
            return false;
        }
    })
});
</script>
</html>

Take your time to read through the above snippet. Here’s what is happening, in union with our process steps listed above.

  • We have a form with many input checkboxes (only showing one above) with a name, and id attributes specified.
  • Take note of their names as they match what is found In the MaterializeCSS materialize.scss file. So grid is same as @import "components/grid"; in the .scss file.
  • Down in the script tag area, we see two scripts, one for ensuring that at least a single box is checked before the form can be submitted, and the other toggles all the checkboxes on the page.

You might be wondering why the choice of naming for the checkboxes are in the way they are at the moment. A view at the number 2) of our processes will shed some light on that.

 

[wp_ad_camp_1]

 

2) Server Receives form Post

First of all, if the ‘Download Everything’ box is checked, we want to build everything MaterializeCSS, at least that is what the user requests.

With that in mind, something like this comes first within the app.post(...) of Express. Bringing in our snippets from app.js, here we go:

app.post('/', function(req, res) {
  if (req.body.all === 'on') {
    console.log('Building for everything MaterializeCSS');
    buildSass('./public/vendor/materialize/sass/materialize.scss', res);
    return true;
  }

// some more code here coming later
});

A few notes. Our form action was directed to the same homepage. Therefore, the app.post('/', function(...) { .. }); targets the same homepage

Our submitted form data is attached to the req.body object. In there, we have access to each of the keys in the object, just like any other usual Javascript object.

And so, we go ahead to see, if the request body has a all key (as in a form checkbox called all) comes in, with a value of on, as in, that checkbox is checked, then we run a buildSass() function. More on that buildSass() function later.

We return true; so that the code ends within the if block and does not proceed to hunt the other subsequent code. Within the buildSass(), a response is given, although.

buildSass();

Absorb the snippet below and let us discuss afterwards.

function buildSass(destination, res) {
  sass.render({
    file: destination,
    includePaths: ['./public/vendor/materialize/sass/components/'],
    outputStyle: 'compressed'
  }, function(error, result) { // node-style callback from v3.0.0 onwards 
    if (error) {
      console.log(error.status); // used to be "code" in v2x and below 
      console.log(error.column);
      console.log(error.message);
      console.log(error.line);
      res.render('index', { title: 'Bad Error Express', result: 'Error only' });
    } else {
      console.log(result.stats);
      res.set({
        'Content-Type': 'text/css',
        'Content-disposition': 'attachment; filename=custom-download.css'
      });
      houseCleaning(destination);
      return res.send(result.css);
    }
  });
}

buildSass() takes two arguments: one is the location of the MaterializeCSS file to use for building the custom CSS, and the other argument is the response object. We include the response object so that we can send the user a response directly from the buildSass() wherever we call it.

The buildSass() is nothing special. All it does it use the sass.render synchronous function from node-sass

Nothing else happening there. Check the node-sass documentation for more details on how to use it. Upon error, we choose where to send the user to, or otherwise.

The file and includePaths are important. The file refers to the source materialize file to use in building, and the includePaths refers to what other places the rendering process can/should look for imported files.

At this point, some questions arise. What is the destination and houseCleaning()? I answer them in the next part of this tutorial.

Related Articles

Back to top button