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
.
var viewModel = window.viewModel = {
"@body-bg": ko.observable('#ffffff'),
"@text-color": ko.observable('#777777'),
"@brand-primary": ko.observable('#337ab7')
},
To just:
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.
$(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
.
$.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) === "@") {
nameValue = line.split(":");
name = nameValue[0].trim();
value = nameValue[1].trim();
if (value.substr(0, 1) === "#") {
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.
<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;
<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:
ko.applyBindings(viewModel);
Testing
Running the site, it should look something like this:
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:
if (value.substr(0, 1) === "#") {
viewModel[name] = ko.observable(value);
}
And change them to this:
if (value.substr(0, 1) === "#") {
value = value.replace(";", "");
if (value.length === 4) {
value += value.substr(1, 3);
}
viewModel[name] = ko.observable(value);
}
Run again and:
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
variables
The bare viewModel
is declared like this:
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:
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();
viewModel.categories[category] = {
variables: ko.observableArray()
};
continue;
}
if (line.substr(0, 1) === "@") {
nameValue = line.split(":");
name = nameValue[0].trim();
value = nameValue[1].trim();
value = value.replace(";", "");
if (value.substr(0, 1) === "#") {
if (value.length === 4) {
value += value.substr(1, 3);
}
viewModel.categories[category].variables.push(name);
viewModel.variables[name] = {
type: "color",
value: ko.observable(value)
}
}
}
}
console.log(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:
<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:
<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:
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.
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
.
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.
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
;
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:
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
:
ko.applyBindings(viewModel);
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.