Introduction
I use bootstrap for most of my work. And I love it. I find that I don't have to write much CSS, since most things are already there.
If I need a different look, the first thing I do is change .less
variables in the variables.less file.
But the customer should have some control over the colors. The idea behind this project is to create a UI in which we can change the .less variables from the bootstrap variables.less file.
These changes should reflect live in the site.
This article will be part of a series. A demo of what I made of this after finishing this series can be found at http://bootbrander.azurewebsites.net/. At the end of this article series, you will not end up with precisely that, because I'm still improving on it.
Article Index
Prerequisites
I'm using Visual Studio 2013. In this, I create a standard MVC website. If you do this, you'll get a standard setup with bootstrap in it.
After this, I installed the following NuGet packages:
- Bootstrap Less Source
- less.js
- knockoutjs
The way bootstrap.less works is that all the components in bootstrap have their own .less file. But all the main colors are set up through a file called variables.less. Change a color there and it will reflect through the entire boostrap.css which is generated from bootstrap.less.
Bootstrap.less (partial)
@gray-base: #000;
@gray-darker: lighten(@gray-base, 13.5%);
@gray-dark: lighten(@gray-base, 20%);
@gray: lighten(@gray-base, 33.5%);
@gray-light: lighten(@gray-base, 46.7%);
@gray-lighter: lighten(@gray-base, 93.5%);
@brand-primary: #337ab7;
@brand-success: #5cb85c;
@brand-info: #5bc0de;
@brand-warning: #f0ad4e;
@brand-danger: #d9534f;
As you can see, all the colors are variables. This is just part of the file. But the point is these are the colors we'd like to gain access to.
Less.js
Less.js allows you to hook in the actual less file directly into your site. This way, you can change the less
variables on the fly. The Bootstrap Less Source package has created a folder called \Content\Bootstrap which contains the bootstrap.less file.
To hook everything up, we'll need to change the _Layout.cshtml file. We'll first remove this line:
@Styles.Render("~/Content/css")
And we'll add in these lines:
<link href="~/Content/bootstrap/bootstrap.less"
rel="stylesheet/less" type="text/css" />
<script src="~/Scripts/less-1.5.1.min.js"></script>
You might get into trouble because the .less extention will not be recognized by your IISExpress. If so, you'll get a 404 error on the bootstrap.less file. To change this, you'll have to declare its mimeType
in your web.config.
Put this in the system.webServer
tag.
<staticContent>
<mimeMap fileExtension=".less" mimeType="text/css" />
</staticContent>
Testing
If all has gone right, you should be able to change a less
variable realtime through the less.modifyVars
method.
- Start your website
- It should show the ASP.NET welcome page
- From the console, run the following command:
less.modifyVars({ "@body-bg": "#FF0000" });
- The background of the page should now be red;
Setting Up the userinterface
As we've seen in our little test, there is a variable called @body-bg
which controls the background color. Let's use this first to setup our interface.
But first, we'll need a source to stick our JavaScript code in. I've called mine "main.js" and stuck it in the root folder.
Next, we'll need to add it to the bottom of our _Layout.cshtml file. Just before the body
tag and underneath the other bundles. It should look like this:
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@Scripts.Render("~/bundles/knockout")
@Scripts.Render("~/main.js")
@RenderSection("scripts", required: false)
</body>
Now, we'll create a knockout viewModel
and stick a variable @body-bg
in it. And bind it to the HTML. Our main.js should look like this:
(function () {
var viewModel = window.viewModel = {
"@body-bg": ko.observable('#ffffff')
};
$(document).ready(function () {
ko.applyBindings(viewModel);
});
})();
Making the Variable Panel
I decided to have the panel containing the variables as a column on the left. I could achieve this by creating a partial view and sticking that in every other view, but since I'm going to need it on every page, I'll just stick it in the _Layout.cshtml.
Find this part:
<div class="container body-content">
@RenderBody()
</div>
We'll want to change a view bootstrap things. First, I want to use the full width of the page, so I'm going to change the class "container
" to "container-fluid
".
Next to that, I'll need to create a "row
" with 2 columns "col-xs-4
" and "col-xs-8
" in it. And then, put @RenderBody()
in the last.
<div class="container body-content">
<div class="row">
<div class="col-xs-2" id="toolbar-container">
</div>
<div class="col-xs-10">
@RenderBody()
</div>
</div>
</div>
So remember, our viewModel
has a field @body-bg
which is a color. I'd like it to be possible to edit this by typing the exact hex code in a text field, but I'd also like to utilize the new HTML5 input control type="color"
.
And I'd like to setup the knockout binding. So to the first column "toolbar-container
", I'm going to add:
<div class="form-group">
<label>@@body-bg</label>
<div class="row">
<div class="col-xs-9" style="padding-right: 0;">
<input type="text" class="form-control"
data-bind="value: $data['@@body-bg']" />
</div>
<div class="col-xs-3" style="padding-left: 0;">
<input type="color" class="form-control"
data-bind="value: $data['@@body-bg']" />
</div>
</div>
</div>
Note the double @ signs. The 'real' variable name has only one @ sign, but this sign has meaning to the Razor engine. To escape it, you'll need to add a double @.
Oh. And yes, that's an inline style. Because I'm lazy;)
Subscribe to the Change
So now, we'll have a variable that will be update through the UI. But we still need to call the less.modifyVars
method. To do this, we will have to subscribe to the change of our variable.
var viewModel = window.viewModel = {
"@body-bg": ko.observable('#ffffff')
}
viewModel["@body-bg"].subscribe(function () {
less.modifyVars({
"@body-bg": viewModel["@body-bg"]()
});
});
Testing
If you now start the site and change the color
field, the website should change color and look something like this:
Adding Values
So now, you could decide what variables the customer should be allowed to change. And add them to the view model.
So let's add @text-color
which is the main text color and @brand-primary
which has all the classes that end in "-primary
" like "btn-primary
".
main.js
var viewModel = window.viewModel = {
"@body-bg": ko.observable('#ffffff'),
"@text-color": ko.observable('#777777'),
"@brand-primary": ko.observable('#337ab7')
};
viewModel["@body-bg"].subscribe(function () {
less.modifyVars({
"@body-bg": viewModel["@body-bg"]()
});
});
viewModel["@text-color"].subscribe(function () {
less.modifyVars({
"@text-color": viewModel["@text-color"]()
});
});
viewModel["@brand-primary"].subscribe(function () {
less.modifyVars({
"@brand-primary": viewModel["@brand-primary"]()
});
});
HTML
Stick this underneath the @body-bg
definition:
<div class="form-group">
<label>@@text-color</label>
<div class="row">
<div class="col-xs-9" style="padding-right: 0;">
<input type="text" class="form-control"
data-bind="value: $data['@@text-color']" />
</div>
<div class="col-xs-3" style="padding-left: 0;">
<input type="color" class="form-control"
data-bind="value: $data['@@text-color']" />
</div>
</div>
</div>
<div class="form-group">
<label>@@brand-primary</label>
<div class="row">
<div class="col-xs-9" style="padding-right: 0;">
<input type="text" class="form-control"
data-bind="value: $data['@@brand-primary']" />
</div>
<div class="col-xs-3" style="padding-left: 0;">
<input type="color" class="form-control"
data-bind="value: $data['@@brand-primary']" />
</div>
</div>
</div>
Testing
Start your project and perform the following tests:
- 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
@text-color
variable by typing "white
" in the text field
- The text color of your page should change to white
- Change the
@text-color
variable through the color selector
- The text color 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
"
- Change the
@brand-primary
variable through the color selector
- The primary button should change to change to the selected color
Problem
The UI passes by test. But I'm seeing something I didn't intend. Every time I change a variable, the UI doesn't retain the value of a previously set variable. In other words, it keeps resetting everything I did.
Why?
But wait, before we dive into the why, let's change our testplan:
- 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
@text-color
variable by typing "white
" in the text field
- The text color of your page should change to white
- The UI should retain the previously set
@body-bg
- Change the
@text-color
variable through the color selector
- The text color of your page should change to the selected color
- The UI should retain the previously set
@body-bg
- 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
- The UI should retain the previously set @
text-color
- 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
- The UI should retain the previously set @
text-color
Performing the test again, it now fails after step 1.
Fix the Problem
The issue is with the way less.modifyVars
seems to work. Every time we call it, it will get all the variables of the original and apply the variable object you send in. So we need to send in all the colors everytime we call less.modifyVars
.
So let's find all of the subscriptions to the viewModel
, where less.modifyVars
is being called. I'll just show one of them:
viewModel["@text-color"].subscribe(function () {
less.modifyVars({
"@text-color": viewModel["@text-color"]()
});
});
As you can see, we are only posting @text-color
while it needs all our parameters.
To fix this, we are going to use a knockout feature called toJS
to deserialize our viewModel
to a normal JavaScript object and send this in.
viewModel["@body-bg"].subscribe(function () {
less.modifyVars(ko.toJS(viewModel));
});
viewModel["@text-color"].subscribe(function () {
less.modifyVars(ko.toJS(viewModel));
});
viewModel["@brand-primary"].subscribe(function () {
less.modifyVars(ko.toJS(viewModel));
});
Now, if we perform our last testplan, it should pass.
Optimization
I'm still not happy. I think for now it is ok that I have defined each variable I'd like expose to the user, but I don't want to repeat this subscription code for each variable.
So let's change that to an iteration on viewModel
, the whole thing will now look like this:
(function () {
var viewModel = window.viewModel = {
"@body-bg": ko.observable('#ffffff'),
"@text-color": ko.observable('#777777'),
"@brand-primary": ko.observable('#337ab7')
};
for (var prop in viewModel) {
if (viewModel.hasOwnProperty(prop)) {
viewModel[prop].subscribe(function () {
less.modifyVars(ko.toJS(viewModel));
});
}
}
$(document).ready(function () {
ko.applyBindings(viewModel);
});
})();
Testing
- Perform the previous testplan
- From the top navigation bar choose a different page
Problem
Again it threw everything I did away!
This is caused by the way .NET.MVC works, it's not an Ajax site. The whole thing will be refetched from the server. Actually saving the changes to the server will be the topic of another post. But for now, we will need to save something to localStorage
and recreate our viewModel
from there.
Let's add just one last optimization to our code using most of what we've learned.
(function () {
var viewModel = window.viewModel = {
"@body-bg": ko.observable('#ffffff'),
"@text-color": ko.observable('#777777'),
"@brand-primary": ko.observable('#337ab7')
},
storedViewData =
localStorage.getItem("viewData") !== null
? JSON.parse(localStorage.getItem("viewData"))
: {};
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]);
};
}
}
$(document).ready(function () {
ko.applyBindings(viewModel);
});
})();
Testing
Now performing all test plans, they all pass (at least in my house;).
Conclusion
I'm going to leave it at this for now. You could add other color variables to expose.
Generating and downloading the actual .css file will be discussed in another post. I will probably fix one other fundamental flaw in the code before then. Can you spot it?
Next to that, making the UI 2 columns on an extra small screen isn't really that nice. So maybe it could be little more responsive.
But for now, it will do.
History
I have added the next post on this subject here.