Click here to Skip to main content
15,879,239 members
Articles / Web Development

BootBrander - A Bootstrap .less Generator UI (Part 1 / Set Up the UI)

Rate me:
Please Sign up or sign in to vote.
4.33/5 (2 votes)
1 Mar 2015CPOL8 min read 17.3K   131   14  
Creates a MVC site with user inputs to change the bootstrap variables and generate a custom branded bootstrap.css

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)

CSS
//
// Variables
// --------------------------------------------------


//== Colors
//
//## Gray and brand colors for use across Bootstrap.

@gray-base:              #000;
@gray-darker:            lighten(@gray-base, 13.5%); // #222
@gray-dark:              lighten(@gray-base, 20%);   // #333
@gray:                   lighten(@gray-base, 33.5%); // #555
@gray-light:             lighten(@gray-base, 46.7%); // #777
@gray-lighter:           lighten(@gray-base, 93.5%); // #eee

//@brand-primary:         darken(#428bca, 6.5%); // #337ab7
@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:

HTML
@Styles.Render("~/Content/css")

And we'll add in these lines:

HTML
<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.

XML
<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.

  1. Start your website
    • It should show the ASP.NET welcome page
  2. 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:

HTML
    @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:

JavaScript
/* globals ko, less */
(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:

HTML
<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.

HTML
<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:

HTML
<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.

JavaScript
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:

Image 1

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

JavaScript
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:

HTML
<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:

JavaScript
viewModel["@text-color"].subscribe(function () {
    //there's your problem! you need the whole viewModel!
    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.

JavaScript
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:

JavaScript
/* globals ko, less */
(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.

JavaScript
/* globals ko, less */
(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.

License

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


Written By
Software Developer (Senior)
Netherlands Netherlands
I'm a developer with 22+ years of experience. Starting of on a MVS mainframe, moving to building big multi-tier ERP systems with unix backends, to building web-based BI-Portals. I've seen a lot of different languages, environments and paradigmes.

At this point my main interest is webdevelopment. Mainly javascript/typescript and ASP.NET. But I also like getting my hands dirty on some PHP.

My main focus has been shifting towards full javascript the past years. Almost anything can be accomplished now. From building full offline webapps, to IoT (Tessel), to server (node).

Comments and Discussions

 
-- There are no messages in this forum --