Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web

BootBrander a bootstrap .less Generator UI (Part 2 / Parsing variables.less)

4.14/5 (3 votes)
2 Mar 2015CPOL8 min read 17.2K   143  
Creates a MVC site with user inputs to change the bootstrap variables and generate a custom branded bootstrap.css

Introduction

If you didn't read part 1 of this series, you should read it here.

We're building a user interface to generate a custom bootstrap.css by manipulating the variable colors in the variables.less file.

In this article, we will parse this file and expose all the colors in it to the user. I'm assuming he's allowed to change the colors but not things like font-size and padding.

Article Index

The Variables

I tried finding out what variables there are through the less.js library. But I couldn't find any documentation. So I decided to fetch the variables.less file and parse this myself.

Making a code parser is really difficult to do. You have go through the code character by character to do it right.

But we are lucky. The variables.less file the boys and girls of the bootstrap team give us, is really nicely formatted. There are never spaces where they shouldn't be and everytime a section is declared, it has the exact same syntax.

Al the variables are declared in categories. Which I'm going to utilize. A category looks like this:

//== Colors

So if I were reading this line, I could easily look at the first 4 characters to identify it.

A variable looks like this:

@gray-base:              #000;

That's even easier.

To Start

If you have the code of the previous article, you should change a few things. First, change the declaration of the viewModel.

JavaScript
var viewModel = window.viewModel = {
    "@body-bg": ko.observable('#ffffff'),
    "@text-color": ko.observable('#777777'),
    "@brand-primary": ko.observable('#337ab7')
},

To just:

JavaScript
var viewModel = {
},

And also find the line where the knockout binding is done and throw it out. It will brake before we are completely done. We will put it back later.

Fetch and Parse

So let's just get on with it and write an Ajax function that gets the variables.less file.

JavaScript
$(document).ready(function () {

     $.ajax({
         url: "/Content/bootstrap/variables.less",
         success: function (responseText) {
             var lines = responseText.split('\n'),
                 line,
                 i;

             for (i = 0, j = lines.length; i < j; i++) {
                 line = lines[i];
                 if (line.substr(0, 1) === "@") {
                     console.log(line);
                 }
             }
         }
     })
 });

Testing

If we run this code, it should give the following output:

...Lots up here
main.js:52 @brand-success:         #5cb85c;
main.js:52 @brand-info:            #5bc0de;
main.js:52 @brand-warning:         #f0ad4e;
main.js:52 @brand-danger:          #d9534f;
main.js:52 @body-bg:               #fff;
... and more down here 

It's printing everything that is a variable. There are lots of different type of variables in it. They can be functions like darken(), they can be pixels, numbers and references to other variables.

For now, we will focus on the ones that have value that starts with #. Those are colors for sure.

Anyone of those we will add to the viewModel.

JavaScript
$.ajax({
     url: "/Content/bootstrap/variables.less",
     success: function (responseText) {
         var lines = responseText.split('\n'),
             line,
             i,
             nameValue,
             name,
             value;

         for (i = 0, j = lines.length; i < j; i++) {

             line = lines[i];

             if (line.substr(0, 1) === "@") {
                 //this is a variable
                 nameValue = line.split(":");
                 name = nameValue[0].trim();
                 value = nameValue[1].trim();

                 if (value.substr(0, 1) === "#") {
                     //this is color
                     viewModel[name] = ko.observable(value);
                 }

             }
         }
         console.log(viewModel);
     }
 });

If we run this, we can see that the viewModel output to the console contains lots of variables now.

Bind the viewModel

In the previous article, we hand coded a piece of HTML with the knockout binding in it for each individual color variable. This won't be very useful and we will replace it.

To do that, we will create an HTML template that we will feed to knockout.

Template for a Single Color

We'll use a template and send in the knockout binding variable later. Create next piece of HTML code at the bottom of the body.

HTML
<script type="text/html" id="color">
    <div class="row">
        <div class="col-xs-9" style="padding-right: 0;">
            <input type="text" class="form-control" data-bind="value: $data" />
        </div>
        <div class="col-xs-3" style="padding-left: 0;">
            <input type="color" class="form-control" data-bind="value: $data" />
        </div>
    </div>
</script>

Now, in the left panel with the id toolbar-container, we are going to iterate over the properties of the viewModel. In every iteration, we will set a label and call our template;

HTML
<div class="col-xs-2" id="toolbar-container">
    <div class="form-group" data-bind="foreach: {data: Object.keys($data), as: '_propkey'}">
        <label data-bind="text: _propkey"></label>
        <div data-bind="template: { name: 'color', data: $root[_propkey] }"></div>
    </div>
</div>

Now we can start the knockout binding again. Do this at the end of the Ajax call's success handler. Right after the loop that parsed out the variables.less:

JavaScript
ko.applyBindings(viewModel);

Testing

Running the site, it should look something like this:

Image 1

We get a nice list of all the ones that seem to be a color. But there is an issue. Or actually two.

All the color spinners are black, this is because of the values. First of the end in a ";". The second problem is the shorthand notations of colors like white #fff. The input type="color" needs them to be written out fully.

Let's find these lines:

JavaScript
if (value.substr(0, 1) === "#") {
    //this is color
    viewModel[name] = ko.observable(value);
}

And change them to this:

JavaScript
if (value.substr(0, 1) === "#") {
    //this is color
    value = value.replace(";", "");
    if (value.length === 4) {
        value += value.substr(1, 3);
    }
    viewModel[name] = ko.observable(value);
}

Run again and:

Image 2

Nice!

It seems iterating over the properties does break knockouts two way binding though. But let us not worry about that for now. Because we will first add our categories and that will change things anyway.

The Categories

Right now, it's just a long list of colors. Some context would be nice. As said before, the variables are put in the variables.less in categories which are declared as comment:

//== Colors

So first, we'll see a category and then a list of variables that belong to these categories. In order to reflect this, we'll first have to change our viewModel a bit. It should get the following form:

  • viewModel
    • categories
      • category
        • variableName
        • variableName
      • category
        • variableName
    • variables
      • variable
      • variable

The bare viewModel is declared like this:

C#
var viewModel = window.viewModel = {
categories: {},
variables: {}
}

We will also change the variable object itself. If you looked at the list we've now got, we are missing a few variables, @text-color is gone. This is because that color didn't start with a # but was declared as being inherited from a different variable. We will deal with that later. But for now, we will split a single variable in value and a type.

Our parsing code should now look like this:

JavaScript
   var lines = responseText.split('\n'),
        line,
        i,
        nameValue,
        name,
        value,
        category
    ;

    for (i = 0, j = lines.length; i < j; i++) {

        line = lines[i];

        if (line.substr(0, 4) === "//==") {
            category = line.substr(5, line.length).trim();
            //console.log(line.substr(5, line.length).trim());
            viewModel.categories[category] = {
                variables: ko.observableArray()
            };
            continue;
        }

        if (line.substr(0, 1) === "@") {
            //this is a variable
            nameValue = line.split(":");
            name = nameValue[0].trim();
            value = nameValue[1].trim();
            value = value.replace(";", ""); 

            if (value.substr(0, 1) === "#") {
                //this is color
 
                if (value.length === 4) {
                    value += value.substr(1, 3);
                }

                //add the name to the categories
                viewModel.categories[category].variables.push(name);

                //add the variable to the variables object
                viewModel.variables[name] = {
                    type: "color",
                    value: ko.observable(value)
                }
            }
        }
    }

    console.log(viewModel);

    //Apply the viewModel
    ko.applyBindings(viewModel);
}

You may notice that I'm not simply saving the variable in the category itself but just the name. And I'm adding the variable itself to an object.

This is for 2 reasons.

The first reason is the reference variables (the ones that have another variable name as their value). To resolve these, an object is var easier to look them up in.

The second reason is serializing. I cannot save a reference in the category.variables list, because that would create a circular object, which we cannot put through JSON.stringify.

Update the UI

Now we need to change our HTML and knockout binding to reflect this change. First our template:

HTML
<script type="text/html" id="color">
    <div class="row">
        <div class="col-xs-9" style="padding-right: 0;">
            <input type="text" class="form-control" 
            data-bind="value: value" />
        </div>
        <div class="col-xs-3" style="padding-left: 0;">
            <input type="color" class="form-control" 
            data-bind="value: value" />
        </div>
    </div>
</script>

And now the left column. I choose to put the categories and variable in details sections. But you could improve on that by using an accordion:

HTML
<div class="col-xs-2" id="toolbar-container">

    <div data-bind="foreach: {data: Object.keys(categories), as: '_propkey'}">
        <details>
            <summary data-bind="text: _propkey"></summary>

            <div data-bind="foreach: $root.categories[_propkey].variables">
                <div class="form-group">

                    <label data-bind="text: $data"></label>

                    <div data-bind="template: 
                    { name: $root.variables[$data].type, data: $root.variables[$data] }">
                    </div>

                </div>

            </div>

        </details>
    </div>

</div>

Testing

Run the page and the UI should look something like this:

Image 3

But if we run a test like in the first article:

  • Change the @body-bg variable by typing "red" in the text field
    • The background of your page should change to red
  • Change the @body-bg variable through the color selector
    • The background of your page should change to the selected color
  • Change the @brand-primary variable by typing "red" in the text field
    • The primary button should change to "blue"
    • The UI should retain the previously set @body-bg
  • Change the @brand-primary variable through the color selector
    • The primary button should change to the selected color
    • The UI should retain the previously set @body-bg

None of this works. This is because we didn't set up the subscription to our observables yet. In the previous article, we had the following code for that.

JavaScript
function onViewModelChanged() {
    var viewData = ko.toJS(viewModel);
    less.modifyVars(viewData);
    localStorage.setItem("viewData", JSON.stringify(viewData));
};
for (var prop in viewModel) {
    if (viewModel.hasOwnProperty(prop)) {
        viewModel[prop].subscribe(onViewModelChanged);
        if (storedViewData.hasOwnProperty(prop)) {
            viewModel[prop](storedViewData[prop]);
        };
    }
}

I then said it was fundamentally wrong. The reason that it is this way is it sets the value for the stored data. The subscription came first. And then for every variable, it sets the stored value. This will cause the onViewModelChanged function to fire for every color in the viewModel. In our previous example, this didn't matter too much since we had just 3 colors. But now, we might start noticing this.

But first, let's wrap the loop that does the subscription in a function, let's call this applySubscriptions.

JavaScript
function applySubscriptions() {
    var observableVars = viewModel.variables;

    for (var prop in observableVars) {

        if (observableVars.hasOwnProperty(prop)) {

            observableVars[prop].value.subscribe(onViewModelChanged);
        }
    }
}

Now, we need something to deserialize our viewModel. We've split the variable into a type and a value. So just calling less.modifyVars(ko.toJS(viewModel)) isn't going to work.

JavaScript
function viewModelToJS() {
    var obj = {},
        observableVars = viewModel.variables
        ;
    for (var prop in observableVars) {

        if (observableVars.hasOwnProperty(prop)) {

            obj[prop] = observableVars[prop].value();

        }
    }
    return obj;
}

And then, we change the onViewModelChanged function to use this instead of ko.toJS;

JavaScript
function onViewModelChanged() {

    var viewData = viewModelToJS();

    less.modifyVars(viewData);

    localStorage.setItem("viewData", JSON.stringify(viewData));
};

Run our test:

  • Change the @body-bg variable by typing "red" in the text field
    • The background of your page should change to red
  • Change the @body-bg variable through the color selector
    • The background of your page should change to the selected color
  • Change the @brand-primary variable by typing "red" in the text field
    • The primary button should change to "blue"
    • The UI should retain the previously set @body-bg
  • Change the @brand-primary variable through the color selector
    • The primary button should change to the selected color
    • The UI should retain the previously set @body-bg
  • Move to a different page in top navigation
    • The UI should retain the previously set @body-bg
    • The UI should retain the previously set @brand-primary

Our test worked up to the point where we switch pages. As said, this MVC site is not ajaxy, so it will pull the page form the server.

We need to fix resetting the variables form storage. The code from the previous article that got the stored variables and put them in the storedViewData should still work, so in the parser loop, before we check for the variable type, we could restore the previous value:

JavaScript
value = value.replace(";", "");

if (storedViewData.hasOwnProperty(name)) {
    value = storedViewData[name];
}

if (value.substr(0, 1) === "#") {

And then at the end of the Ajax success handler, call less.modifyVars to apply all the variables; Just below the applyBindings:

JavaScript
//Apply the viewModel
ko.applyBindings(viewModel);

//Set the current values
less.modifyVars(viewModelToJS());

If we now perform our testplan, it should work.

Concluding

I'm going to end this article now. Because I see that it's becoming rather long. Finding our reference variables like @text-color and also dealing with functions like 'darken' and 'lighten' will be in the next post.

In the download, you'll find a somewhat optimized version with jsdoc comments in it. It will be the starting point of our next article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)