We are still discussing how to Customize MaterializeCSS Before Downloading in the browser. The materialize.khophi.co application gave birth to this and the part one articles
In the previous part, we considered up until the point of opening up what the buildSass()
does. The buildSass()
takes an destination
argument. Where from it?
User visits the homepage. Checks a bunch of boxes in a form, then submitsThe Server receives the form post- The Server looks through the form post object, then appends the incoming data to a
.scss
file. - The Server runs a compiling spree on the .scss file.
- The output is pushed as a response to the browser in the form of attachment.
- The User is happy.
The stroked through bullet points were discussed in the previous article of this two-part article
The Actual Issue
Multiple users are going to request to build a custom version of Materialize in real-time with their personal configurations which will differ entirely from one another.
Article difficulty somewhere Intermediate Level
Because of this, it will in our best interest, as a developer, to generate on the fly, new temporal .scss files per each user’s immediate configuration, use the file to build our .css and then clean up the file, as in delete it.
This create-use-delete process will happen for every request that hits the server. With that in mind, it is important to generate really random numbers hard to repeat, to ensure it almost never happens a file I generated before I could use to build, is overridden by another user’s settings because their file name happened to be same as mine.
Should that happen, it’ll be bad for business, and might lead to confusion. We don’t want that, so I brought in a little bit of UUID. You can read more on that, but the first time I heard of it was with using MongoDB.
UUIDs are meant to be random, but random in a way that means they’ll never, ever, ever be the same, in a perfect world.
For instance, MongoDBs UUID is generated using your computer’s timestamp, along with certain unique details from your machine. And so, I doubt probably using details like your computer’s hostname, model, brand name, timestamp (date inclusive), and pure random numbers will ever generate two strings the same.
I am no genius with UUIDs, but they are intentionally designed in a way to achieve extreme, kinda universal uniqueness.
Although the use case for UUIDs are overkill for our use case here beside each file after use is deleted, at least you’ve heard about UUIDs.
Complete app.post()
Here is our complete app.post()
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; } newSassFile(req, res); });
The newSass()
function is responsible for generating a new file and passes in the generated file to the buildSass()
as destination
for use in building our custom CSS.
I believe the whole picture is beginning to form at this point, how the pieces fit together?
5) newSassFile()
function newSassFile(req, res) { var filename = uuid.v1(); // a random generated file var destination = './public/vendor/materialize/sass/' + filename + '.scss'; console.log('Copying begins'); fs.copy('./public/vendor/materialize/sass/barebone.scss', destination, function(err) { if (err) return console.log(err); console.log('File created successfuly'); // tucked in here because of race conditions. // The tend to run before the copy happens. console.log('Looping begins'); doLoop(req, destination); console.log('Sass building begins'); buildSass(destination, res); }); }
Ooh! This is getting exciting. The newSassFile()
has a couple of tricks up its sleeves. First, we generate the unique filename in creating a destination reference to a file using the uuid.v1()
function from the node-uuid package.
We, therefore, employ the services of NodeJS’s File System, however through fs-extra
which blesses us with a clean way of copying files from one point to another. In this case, we copy from our barebone.scss file into the just generated file reference within the destination variable.
Race Conditions
Savvy readers will by now be asking, ‘Why am I waiting for file copying to finish before doing anything else, like invoking buildSass()
?’
Well, I learned it the hard way: Race conditions. Nodejs is single threaded, for the most part. However, when it comes to I/O processing, it doesn’t always happen the way it should. I am no guru to delve deeper into the whys and hows, as they are already nicely deliberated in this Stack Overflow question:
In the instance above, the answer to the question is a big fat YES. Because, without tucking the build process code within AFTER the copying is complete, on my HDD, almost 100 out of 100 times, the build process begins without the copying complete.
[wp_ad_camp_1]
Now, we are talking of milliseconds, however, in that milliseconds, the copying operation is slow, resulting in parts of the code running in memory to outrun the hard drive operation.
I learned this the hard way! Although your code is written to be synchronous and arranged in a synchronous manner, with regards to I/O, there isn’t a 100% guarantee they’ll follow the order you wish them to follow.
In the above scenario, I saved myself the troubles of guess works by making sure they all kick into action ONLY AFTER the copying is done. That solved everything.
Maybe there’s a better way to achieve the same effect without having to tuck the other code within the finished copying part, but hey, it works for now.
3) & 4) doLoop()? What for?
For something beautiful, like this:
function doReplace(oldString, newString, toFile) { // handles the replacement of the strings in // the 'toFile' console.log(toFile); replace({ regex: oldString, replacement: newString, paths: [toFile], recursive: true, silent: true, }); } function doLoop(req, toFile) { for (key in req.body) { if (req.body.hasOwnProperty(key)) { console.log('//' + key, '@import "' + 'components/' + key + '";'); if (key === 'forms') { console.log('Forms were involved'); // console.log('//' + key, '@import "' + 'components/' + key + '/' + key + '";'); doReplace('//' + key, '@import "' + 'components/' + key + '/' + key + '";', toFile); } doReplace('//' + key, '@import "' + 'components/' + key + '";', toFile); } } };
The doLoop();
is the guy responsible for taking the form data submitted, stripping off the name parts, and feeding them to a function called doReplace()
which takes an oldString
and newString
parameters to replace an existing string with a new one.
The toFile
refers to the file on which the string replacements should happen. And so since we’re looking at a for loop, the string replacements will keep happening until we’ve run out of keys in our req.body
object.
The doReplace()
is nothing but a mere replace
function from the replace
node package. That’s all. Nothing fancy happening there.
Everything together
Below is everything we needed for the app.post
route to function properly and return our .css
as a result, free of comments and console logs:
function doReplace(oldString, newString, toFile) { console.log(toFile); replace({ regex: oldString, replacement: newString, paths: [toFile], recursive: true, silent: true, }); } 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); } }); } function houseCleaning(destination) { fs.remove(destination, function(err) { if(err) return console.error(err); console.log('House cleaning done!'); }) } function doLoop(req, toFile) { for (key in req.body) { if (req.body.hasOwnProperty(key)) { console.log('//' + key, '@import "' + 'components/' + key + '";'); if (key === 'forms') { console.log('Forms were involved'); doReplace('//' + key, '@import "' + 'components/' + key + '/' + key + '";', toFile); } doReplace('//' + key, '@import "' + 'components/' + key + '";', toFile); } } }; function newSassFile(req, res) { var filename = uuid.v1(); // a random generated file var destination = './public/vendor/materialize/sass/' + filename + '.scss'; console.log('Copying begins'); fs.copy('./public/vendor/materialize/sass/barebone.scss', destination, function(err) { if (err) return console.log(err); doLoop(req, destination); buildSass(destination, res); }); } 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; } newSassFile(req, res); });
Conclusion
This article, although went straight to the points without explaining much, I believe you found it useful, and in a position to go out there and keep customizing your CSS in browser easily.
Remember, the principles used within this article applies to any .scss out there. So if your framework of choice has a way of cherry picking what modules of the framework to use via a Sass approach or even another approach, you should be able to apply this principle in getting something up and running easily.
I’ll be more than happy to see what you build with concepts from this article. Leave your comments, feedback, questions and suggestions in the comments section below and I’ll be happy to read and respond to them.
Thanks for joining me, and hope to see you in the next one