Table of Contents
You can download the demo code here: TwitterBootstrapDemo_ASPMVC4.zip.
Introduction
At work I do a mixture of ASP MVC / WCF and WPF. We have pretty much
embraced MVxx frameworks for JavaScript such as
Knockout.js and
Angular.js, but we recently had to
do some truly responsive design web work, which needed to target loads of
different form factors. We have good software guys where I work, for sure, but
none of us are exceptional with CSS, as we prefer to code I guess. So we set
about hiring some very UI/Ux people. The first thing that the new UI/Ux guys did
was introduce the use of
Bootstrap which is an HTML framework made by some of the guys that work for
Twitter.
For those that have not heard of a responsive framework, it is typically one
for working with HTML, that has lots of helpers, CSS, JavaScript, and well tried
and tested patterns for developing websites that behave in a well known manner
on different form factors. These responsive frameworks typically have these
sorts of things:
- A responsive grid
- Navigation menus
- Pop-ups / Dialogs
- Standard set of CSS controls / styles
They may have more or less, but these are the basic building blocks of these
frameworks.
I had obviously heard of
Bootstrap and other big
responsive frameworks like Foundation.js. However the new guys went down the
Bootstrap route. Now, not
having played with Bootstrap
so much, I decided to have a play around with it, and came up with some code I
decided to share. The code in this article is not clever at all, but if you are
new to Bootstrap (or even
responsive HTML design) it may have some bits and pieces in it which you will find
useful. For seasoned web developers that may have already used
Bootstrap or another
responsive framework, I don't know if you will get that much out of this
article.
Just before we get into the guts of the demo code, I just wanted to point out
that the demo app I decided to do was something that I may end up using as a
companion (linked from if you like) site to my blog. As such it contains a lot
of information that is kind of only relevant to me, such as links to articles
that I have written, etc.
Like I say, sorry about the shameless self promotion thing there, I just
figured if I was going to write something I might as well make it something that
I could actually make use of, so that is what I decided to do, I hope people are
OK with that.
That said, I think the demo code still shows you some nice introductory
material on using
Bootstrap and also
Knockout.js if you have not used
them before.
Pre-Requisites
The demo app uses SQL Server, and expects you to change the connection
string in the Web.Config to your own SQL Server connection instance.
The Demo App
As I have stated, the demo app is very focused around the needs I personally
had, to make a kind of companion site to my blog. So what does that mean exactly?
Well, for me, that meant that I wanted a small micro site that fulfilled the
following requirements:
- Site would make use of ASP MVC / Entity Framework / Web API /
Bootstrap /
Knockout.js
- I wanted it to be a small site, nothing too fancy
- I wanted to be able to add content to it pretty quickly
- I wanted it show a list of categories, such as C# / WPF etc., which
would each show some images of articles I have written in these areas
- Each article image should show a tooltip
- Each article image should be clickable to show a more detailed
description about the article, and should provide a mechanism to view the
full article
- Wanted it to work across different form factors (desktop / mobile /
tablet)
Loading
Here is what it looks like when it is first run, where the loading icon is
shown while the database is created and seeded with data (that's an Entity
Framework thing, more on this later).
Click the image for a larger view
After Data Is Loaded
Here is what it looks like when it has loaded everything:
Click the image for a larger view
And here is what it looks like when we have clicked on one of the article
images:
Click the image for a larger view
Responsive Design
And this is what it would look like on a Smartphone or small browser window.
There are several things to note here, such as:
- The navigation bar (NavBar is
Bootstrap lingo) has changed (more on this
later)
- The images are no longer several per row, they are in a single column
(more on this later)
- The content fits still just fine
And with an article image clicked, see how the popup dialog is still OK and
doesn't overflow the screen bounds, and that the popup dialog content is also
fitting nicely to its new size.
As I say, I am fully aware that this demo app is of a very personal nature
(i.e., personal to me), but I still feel it is a
useful vehicle to teach some of the workings of Knockout.js and
Bootstrap even though it
is obviously a demo that is very focused towards my own needs.
How Does It Work
The following sections will outline the inner workings of the demo app.
The Database
One of the things I wanted to do was to use SQL Server and Entity Framework
(Code First), where I wanted to seed the database with some initial seed data.
This is what the specific DbContext
for the demo application looks
like:
public class DatabaseContext : DbContext
{
public DatabaseContext() : base("DefaultConnection")
{
this.Configuration.LazyLoadingEnabled = true;
this.Configuration.ProxyCreationEnabled = true;
}
protected override void OnModelCreating(DbModelBuilder mb)
{
}
public DbSet<Article> Articles { get; set; }
public DbSet<Category> Categories { get; set; }
}
It can be seen that it is pretty simple and just has two DbSet
s, one for
categories and one for articles, where the Category
and Article
classes look like this:
Category
[Table("Categories")]
public class Category
{
[Key]
public int Id { get; set; }
[Required(ErrorMessage = "SlideOrder is a required field.")]
public int SlideOrder { get; set; }
[Required(ErrorMessage = "Title is a required field.")]
public string Title { get; set; }
[Required(ErrorMessage = "Description is a required field.")]
public string Description { get; set; }
public virtual ICollection<Article> Articles { get; set; }
}
Article
[Table("Articles")]
public class Article
{
[Key]
public int Id { get; set; }
[Required(ErrorMessage = "Title is a required field.")]
public string Title { get; set; }
[Required(ErrorMessage = "ShortDescription is a required field.")]
public string ShortDescription { get; set; }
[Required(ErrorMessage = "LongDescription is a required field.")]
public string LongDescription { get; set; }
[Required(ErrorMessage = "ImageUrl is a required field.")]
public string ImageUrl { get; set; }
[Required(ErrorMessage = "ArticleUrl is a required field.")]
public string ArticleUrl { get; set; }
[ForeignKey("Category")]
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
}
The use of the DataAnnotation
attributes here is a bit of an overkill for the demo application, since it never allows the entry of new
Category
/ Article
objects, but as this demo app was something
for me, I thought I may expand upon it in the future to allow new
Category
/
Article
objects to be created by the user, and as such I
thought the use of these attributes may prove useful further down the line.
Database Creation
For the seed data, I could obviously have run some SQL scripts, but this time I
decided to use the built-in Entity Framework database initializer stuff, so I
wrote this simple initializer that will drop and recreate the database from
scratch each time, and will also seed the database using the seed data that was
passed in via the constructor:
public class DatabaseInitializer : DropCreateDatabaseAlways<DatabaseContext>
{
private List<Category> categoriesSeedData;
public DatabaseInitializer(List<Category> categoriesSeedData)
{
this.categoriesSeedData = categoriesSeedData;
}
protected override void Seed(DatabaseContext context)
{
foreach (var category in categoriesSeedData)
{
context.Categories.Add(category);
}
context.SaveChanges();
}
}
And we need to make sure this database initializer is used, so to do that we
need to set it up in global.asax.cs, which is done as follows:
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
var categoriesSeedData = XmlParser.ObtainSeedData();
Database.SetInitializer(new DatabaseInitializer(categoriesSeedData));
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
We will see how the seed data is generated next.
XML Seed Data
Now we know that we are expecting to seed the database with some initial
data, where does this data come from?
Well, the answer to that lies in a small XML file which looks like this:
="1.0"="utf-8"
<seedData>
<categories>
<category>
<slideOrder>1</slideOrder>
<title>About</title>
<description>
<![CDATA[
</description>
</category>
<category>
<slideOrder>2</slideOrder>
<title>C#</title>
<description>
<![CDATA[
</description>
</category>
....
....
....
....
....
....
....
</categories>
<articles>
<article>
<title>Threading 1 of 5</title>
<shortDescription>Beginners guide to threading in .NET (Part 1)</shortDescription>
<longDescription><![CDATA[
</longDescription>
<imageUrl>Content/images/Thread1.png</imageUrl>
<articleUrl>http://www.codeproject.com/Articles/.......</articleUrl>
</article>
....
....
....
....
....
....
....
</articles>
</seedData>
We then parse this seed data using this tiny little helper class, where the return value of the ObtainSeedData()
method
is used to initialize the database (see the global.asax.cs code we saw before):
public static class XmlParser
{
public static List<Category> ObtainSeedData()
{
var appPath = HttpRuntime.AppDomainAppPath;
var doc = XElement.Load(string.Format(@"{0}\App_Data\SeedData.xml",appPath));
var serverUtility = HttpContext.Current.Server;
var categories = doc.Descendants("category").Select(x =>
{
Category category = new Category
{
SlideOrder = int.Parse(x.Element("slideOrder").Value),
Title = x.Element("title").Value.Trim(),
Description = x.Element("description").Value.Trim()
};
var articles = x.Descendants("articles").Descendants("article").Select(y =>
{
return new Article
{
Title = y.Element("title").Value.Trim(),
ShortDescription = y.Element("shortDescription").Value.Trim(),
LongDescription = y.Element("longDescription").Value.Trim(),
ArticleUrl = y.Element("articleUrl").Value,
ImageUrl = y.Element("imageUrl").Value,
Category = category
};
});
category.Articles = articles.ToList();
return category;
}).ToList();
return categories;
}
}
Web API
To expose the Category
and Article
objects, I chose to use the
WebAPI. There
were a couple of decisions that came into play here, namely:
- Should I allow auto content negotiation
- I chose not to, and forced the results to be JSON serialized as I
knew I wanted to use the results with
Knockout.js which would
just work much better with JSON. And also, if I allowed XML, I had to
worry about Lazy/Greedy loading.
- Should I try and stick to pure REST verbs such as PUT / POST /
GET etc.
- I opted for creating a custom route for the articles, as I
essentially wanted lazy loading of the articles collection, but only
when I wanted them from JavaScript. So for that requirement, in my eyes
it made sense to create a new custom route for the
ArticleController
.
CategoryController
Here is what the CategoryController
looks like:
public class CategoryController : ApiController
{
public HttpResponseMessage Get()
{
IEnumerable<Category> categories;
using (var context = new DatabaseContext())
{
context.Categories.Include("Articles");
categories = context.Categories.OrderBy(x => x.SlideOrder).ToList();
if (categories.Any())
{
var slimCats = categories.Select(x => new
{
Id = x.Id,
Title = x.Title,
Description = x.Description,
ArticleIds = x.Articles.Select(y => y.Id).ToList()
}).ToList();
return Request.CreateResponse(HttpStatusCode.OK, slimCats,
Configuration.Formatters.JsonFormatter);
}
throw new HttpResponseException(HttpStatusCode.NotFound);
}
}
}
ArticleController
Here is what the ArticleController
looks like:
public class ArticleController : ApiController
{
public HttpResponseMessage GetAll(int categoryId)
{
IEnumerable<Article> articles;
using (var context = new DatabaseContext())
{
articles = context.Articles.Where(x => x.CategoryId == categoryId).ToList();
var slimArticles = articles.Select(x => new
{
x.CategoryId,
x.Id,
x.Title,
x.ImageUrl,
x.ShortDescription,
x.LongDescription,
x.ArticleUrl
}).ToList();
return Request.CreateResponse(HttpStatusCode.OK, slimArticles,
Configuration.Formatters.JsonFormatter);
}
}
}
WebAPI Routing
And here is the routing setup for both of these Web API controllers, where it
can be seen that there is the standard route, and also a specific one that I
created for the ArticleController
:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "Articles",
routeTemplate: "api/Article/GetAll/{categoryId}",
defaults: new
{
controller = "Article",
action = "GetAll",
categoryId = 1
}
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Knockout JavaScript ViewModels
As has previously been stated, I decided to use
Knockout.js for the client
side MVVM bindings. Before we get into the code for the ViewModel(s), I just
want to go through what the ViewModel(s) should be trying to do. I decided to
create the following
Knockout.js ViewModel(s).
DemoMainPageViewModel
This view model is the one that is controlled by the
Bootstrap NavBar and
Bootstrap Carousel, which
essentially means it has to load the Article
s
for the currently selected
Bootstrap Carousel or
clicked
Bootstrap NavBar item.
The idea being that a single Category is represented either as a Bootstrap NavBar menu
item or a single
Bootstrap Carousel slide.
Each time of one of those two things is clicked, we fetch the Article
s for the current Category
.
var DemoMainPageViewModel = function () {
this.Categories = ko.observableArray();
this.Loading = ko.observable(true);
this.Loaded = ko.observable(false);
this.HasArticle = ko.observable(false);
this.CurrentArticle = ko.observable();
this.SetArticle = function (article) {
this.HasArticle(true);
this.CurrentArticle(article);
$('#articleModal').modal({
keyboard: true
}).modal('show');
};
this.Initialise = function(jsonCats) {
this.Loading(false);
this.Loaded(true);
for (var i = 0; i < jsonCats.length; i++) {
this.Categories.push(new CategoryViewModel(this, jsonCats[i]));
}
this.Categories()[0].loadArticles();
};
this.mainActionClicked = function (data, event) {
var sourceElement = event.srcElement;
var slideNumber = $(sourceElement).data('maincarouselslide');
$('#mainCarousel').carousel(slideNumber);
this.Categories()[slideNumber].loadArticles();
};
};
CategoryViewModel
The CategoryViewModel
is a simple one that is capable of holding
a number of Article
s, and is also responsible for showing the data
related to a clicked Article
. The basic idea is that the Article
s
are lazy loaded the first time, and only the first time,
were the call to fetch them will hit the WebAPI ArticleController.
Where we make use of the lovely jQuery when
/the
syntax. I love that, coming from
C# and Task Parallel Library and its use of continuations, it makes a lot of
sense to me. Anyway, here is the
CategoryViewModel
code:
var CategoryViewModel = function (parent, jsonCat) {
this.Id = jsonCat.Id;
this.Parent = parent;
this.LoadedArticles = ko.observable(false);
this.Title = ko.observable(jsonCat.Title);
this.Description = ko.observable(jsonCat.Description);
this.Articles = ko.observableArray();
var context = this;
this.hasArticles = ko.computed(function() {
return context.Articles().length > 0;
}, this);
this.articleClicked = function (article) {
context.Parent.SetArticle(article);
};
this.loadArticles = function () {
if (this.LoadedArticles() == true) {
return;
}
this.LoadedArticles(true);
$.when($.ajax('/api/article/GetAll/' + this.Id))
.then(function (data, textStatus, jqXHR) {
for (var i = 0; i < data.length; i++) {
context.Articles.push(data[i]);
$('#img' + data[i].Id).tooltip({
title: data[i].ShortDescription,
trigger: 'hover focus'
});
}
});
};
};
Kicking It All Off
In order to kick off the DemoMainPageViewModel
initialization
(i.e., loading it with the Category
data from
CategoryController
) we use the following code, which is a simple
JavaScript self executing function and an initial jQuery AJAX call to fetch the
Category
data. This code essentially shows a busy indicator while
the Category data is being fetched, and
only when the Category data is available
will the busy indicator be hidden and the main user interface shown.
(function ($) {
$(document).ready(function () {
createViewModel();
});
var DemoMainPageViewModel = function () {
....
....
....
};
function hookUpCarouselControls(demoVM) {
$('#mainCarousel').carousel({
interval: false,
pause:true
}).on('slid.bs.carousel', function (event) {
var active = $(event.target)
.find('.carousel-inner > .item.active');
var from = active.index();
demoVM.Categories()[from].loadArticles();
});
}
function createViewModel() {
var demoVM = new DemoMainPageViewModel();
ko.applyBindings(demoVM);
$.when($.ajax("/api/category"))
.then(function (data, textStatus, jqXHR) {
demoVM.Initialise(data);
hookUpCarouselControls(demoVM);
});
}
})(jQuery);
_Layout.cshtml
This is the main ASP MVC layout page that makes use of the
DemoMainPageViewModel
. Though, you will see some more usage of the
DemoMainPageViewModel
when we discuss the carousel below. It can be seen
below that there is a
Bootstrap NavBar, which
makes use of some of the
Knockout.js bindings within
the
DemoMainPageViewModel
. We will be covering this in more detail in a
while.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
</head>
<body>
<div id="wrap">
<div class="container mainContainer">
<div class="row">
<div class="col-xs-8 col-md-8 col-xs-offset-2 col-md-offset-2">
<div class="navbar navbar-default navbar-inverse navbar-static-top"
data-bind="visible: Loaded">
<div class="container">
<div class="navbar-header">
<button type="button"
class="navbar-toggle"
data-toggle="collapse"
data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">MY PORTFOLIO</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="active">
<a data-bind="click: mainActionClicked"
href="#"
data-maincarouselslide="0">About</a>
</li>
<li>
<a data-bind="click: mainActionClicked"
href="#"
data-maincarouselslide="1">C#</a>
</li>
<li><a data-bind="click: mainActionClicked"
href="#" data-maincarouselslide="2">Web</a></li>
<li class="dropdown">
<a href="#"
class="dropdown-toggle"
data-toggle="dropdown">XAML <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a data-bind="click: mainActionClicked"
href="#" data-maincarouselslide="3">WPF</a></li>
<li><a data-bind="click: mainActionClicked"
href="#" data-maincarouselslide="4">Silverlight</a></li>
<li><a data-bind="click: mainActionClicked"
href="#" data-maincarouselslide="5">WinRT</a></li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="seperator"></div>
@RenderBody()
</div>
<div id="footer" data-bind="visible: Loaded">
<div class="container">
<div class="row">
<div class="col-xs-8 col-md-8 col-xs-offset-2 col-md-offset-2">
<span>
<img src="~/Content/images/Info.png"
width="30px"
height="30px"></img><span>Sacha Barbers portfolio</span>
</span>
</div>
</div>
<div class="row">
<div class="col-xs-8 col-md-8 col-xs-offset-2 col-md-offset-2">
<span>
<img src="~/Content/images/Link.png"
width="30px" height="30px">
<a class="footerAnchor"
href="http://www.codeproject.com/script/Articles/MemberArticles.aspx?amid=569009"
target="_blank">View my articles here</a>
</img>
</span>
</div>
</div>
</div>
</div>
@Scripts.Render("~/bundles/Js")
@RenderSection("scripts", required: false)
</body>
</html>
Boostrap Bits
As I stated at the start of this article,
Bootstrap is one of a
breed of emerging (emerged) HTML frameworks that provide a core set of CSS /
JavaScript libraries and helpers that allow a developer to get up and running
quite quickly. There are generally lots of different controls included in these
libraries. I will certainly not be covering all of the functionality within
Bootstrap, but we will
visit a couple of the core items.
The first steps to starting out with
Bootstrap is to grab the
code. Then we need to do the following things:
Include the JavaScript/CSS Stuff
As I am working with ASP MVC, for me this literally meant including the
correct files in a bundle like this:
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/Js").Include(
"~/Scripts/jquery-{version}.js",
"~/Scripts/bootstrap.js",
"~/Scripts/knockout-2.1.0.js",
"~/Scripts/TwitterBoostrapDemo.js"));
bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
"~/Scripts/modernizr-*"));
bundles.Add(new StyleBundle("~/Content/css").Include(
"~/Content/bootstrap-3.0.1/css/bootstrap.css",
"~/Content/bootstrap-3.0.1/css/bootstrap-theme.css",
"~/Content/site.css"));
}
}
Navbar
Bootstrap comes with a
very cool (at least in my opinion) feature (OK, it has a few but this one is my
favorite). So what is it you ask?
Well conceptually, it's a menu, but what is cool about it is, it is completely
responsive.
For example, here is what is looks like on a big screen:
And this is what we get when we might view it on a tablet / smart phone, see
how it is all compact and in a single column:
And the code that makes this all happen could not be easier. Here it is:
<div class="navbar navbar-default navbar-inverse navbar-static-top"
data-bind="visible: Loaded">
<div class="container">
<div class="navbar-header">
<button type="button"
class="navbar-toggle"
data-toggle="collapse"
data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">MY PORTFOLIO</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="active">
<a data-bind="click: mainActionClicked"
href="#"
data-maincarouselslide="0">About</a>
</li>
<li>
<a data-bind="click: mainActionClicked"
href="#"
data-maincarouselslide="1">C#</a>
</li>
<li><a data-bind="click: mainActionClicked"
href="#" data-maincarouselslide="2">Web</a></li>
<li class="dropdown">
<a href="#"
class="dropdown-toggle"
data-toggle="dropdown">XAML <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a data-bind="click: mainActionClicked"
href="#" data-maincarouselslide="3">WPF</a></li>
<li><a data-bind="click: mainActionClicked"
href="#" data-maincarouselslide="4">Silverlight</a></li>
<li><a data-bind="click: mainActionClicked"
href="#" data-maincarouselslide="5">WinRT</a></li>
</ul>
</li>
</ul>
</div>
</div>
</div>
It can be seen that it is simply a matter of using a couple of
Bootstrap classes, such
as:
- navbar
- navbar-default
- navbar-inverse
- navbar-static-top
- navbar-header
- navbar-brand
- navbar-collapse
- nav navbar-nav
Full details of which can be found at the
Bootstrap site.
Responsive Layout AKA "The Grid"
The other killer feature of
Bootstrap is its
responsive layout feature, otherwise known as "The Grid". The grid is one
clever piece of kit for sure.
The key to using "The Grid" correctly boils down to the usage of a couple of
different container DIV
s and a couple of different CSS classes.
I think the best way to learn about the grid, is for me to just include this
little description from the
Bootstrap site:
Bootstrap includes a responsive, mobile first fluid grid system that
appropriately scales up to 12 columns as the device or viewport size increases.
It includes predefined classes for easy layout options, as well as powerful
mix-ins for generating more semantic layouts.
Introduction
- Grid systems are used for creating page layouts through a series of
rows and columns that house your content. Here's how the Bootstrap grid
system works:
- Rows must be placed within a .container for proper alignment and
padding.
- Use rows to create horizontal groups of columns.
- Content should be placed within columns, and only columns may be
immediate children of rows.
- Predefined grid classes like .row and .col-xs-4 are available for
quickly making grid layouts. LESS mixins can also be used for more semantic
layouts.
- Columns create gutters (gaps between column content) via padding.
That padding is offset in rows for the first and last column via negative
margin on .rows.
- Grid columns are created by specifying the number of twelve
available columns you wish to span. For example, three equal columns would
use three .col-xs-4.
Grids and full-width layouts
Folks looking to create fully fluid layouts (meaning your site stretches
the entire width of the viewport) must wrap their grid content in a containing
element with padding: 0 15px; to offset the margin: 0 -15px; used on .rows.
Media Queries
We use the following media queries in our LESS files to create the key
breakpoints in our grid system.
/* Extra small devices (phones, less than 768px) */
/* No media query since this is the default in Bootstrap */
/* Small devices (tablets, 768px and up) */
@media (min-width: @screen-sm-min) { ... }
/* Medium devices (desktops, 992px and up) */
@media (min-width: @screen-md-min) { ... }
/* Large devices (large desktops, 1200px and up) */
@media (min-width: @screen-lg-min) { ... }
We occasionally expand on these media queries to include a max-width
to limit
the CSS to a narrower set of devices.
@media (max-width: @screen-xs-max) { ... }
@media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { ... }
@media (min-width: @screen-md-min) and (max-width: @screen-md-max) { ... }
@media (min-width: @screen-lg-min) { ... }
Grid Options
See how aspects of the Bootstrap grid system work across multiple devices with a handy table
CLICK THE IMAGE FOR A LARGER VIEW
So that is how to use the grid system in
Bootstrap's own words. Let's
see an example of working with the grid system. Here is a small excerpt of the
the Carousel code that we will be looking at in just a minute:
<div class="container">
<div class="row">
<div class="col-xs-8 col-md-8 col-xs-offset-2 col-md-offset-2">
<p class="lead" data-bind="text: Title" />
<p data-bind="text: Description" />
</div>
<div data-bind="visible: hasArticles" >
<div data-bind="foreach: $data.Articles"
class="row col-xs-8 col-md-8 col-xs-offset-2 col-md-offset-2 articleRow">
<div class="col-xs-12 col-md-3">
<img data-bind="attr: {src: ImageUrl, id: 'img' + Id},
click: $parent.articleClicked"
class="img-responsive articleImage"
alt="Responsive image" />
</div>
</div>
</div>
</div>
</div>
In the example code above, what we can see is that we have a main
Container
, which in turn has two
Row
s.
Each Row
has some content that will take up eight columns on
"Extra Small" and "Medium" devices, and in each case will have a
two column offset
on the left and right sides. This layout does work very well (at least in my
testing) across all the different form factors.
Carousel
At the heart of the demo app is the usage of a
Bootstrap Carousel (see
http://getbootstrap.com/javascript/#carousel), which is used to host a slide
per Category
obtained via the Web API CategoryController
.
This is then bound to the the
Knockout.js
DemoMainPageViewModel
which hosts a number of
Knockout.js
CategoryViewModel
(s). Each
CategoryViewModel
also hosts a
number of
Knockout.js
ArticleViewModel
(s).
The inner workings of the different viewmodels was covered earlier in this
article, so I won't repeat that. Instead we will just look at the markup for the
Bootstrap Carousel, where
you should pay special attention to the
Knockout.js bindings in
there.
Essentially, it is pretty simple: one Carousel slide = one CategoryViewModel
.
One image within the slide = one ArticleViewModel
. Obviously as the
slide changes, so does the CategoryViewModel
it represents and
the CategoryViewModel
Article
s that belong to the
newly shown CategoryViewModel
.
Here is the HTML code and
Knockout.js bindings for
the
Bootstrap Carousel:
<div id="mainCarousel" data-wrap="false" class="slide" data-bind="visible: Loaded">
<div class="carousel-inner" data-bind="foreach: Categories">
<div class="item" data-bind="css: {'active': $index()==0}">
<div class="container">
<div class="row">
<div class="col-xs-8 col-md-8 col-xs-offset-2 col-md-offset-2">
<p class="lead" data-bind="text: Title" />
<p data-bind="text: Description" />
</div>
<div data-bind="visible: hasArticles" >
<div data-bind="foreach: $data.Articles"
class="row col-xs-8 col-md-8 col-xs-offset-2 col-md-offset-2 articleRow">
<div class="col-xs-12 col-md-3">
<img data-bind="attr: {src: ImageUrl, id: 'img' + Id},
click: $parent.articleClicked"
class="img-responsive articleImage" alt="Responsive image" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<a id="leftCaroselControl" class="left carousel-control" href="#mainCarousel"
data-slide="prev"><span
class="glyphicon glyphicon-chevron-left"></span></a>
<a id="rightCaroselControl" class="right carousel-control" href="#mainCarousel"
data-slide="next"><span
class="glyphicon glyphicon-chevron-right"></span></a>
</div>
Tooltips
Tooltips are very easy once you have included all the stuff you need to for
Bootstrap,
it is simply a case of some JavaScript like this:
$('#img' + data[i].Id).tooltip({
title: data[i].ShortDescription,
trigger: 'hover focus'
});
Which results in a nice shiny tooltip like this being shown on element hover:
Popup Dialogs
Bootstrap makes it very
easy to show popup dialogs. Shown below is how it works when an Article
is clicked (where the parent of the CategoryViewModel
will
be the singleton instance of the DemoMainPageViewModel
):
var CategoryViewModel = function (parent, jsonCat) {
this.articleClicked = function (article) {
context.Parent.SetArticle(article);
};
}
var DemoMainPageViewModel = function () {
this.SetArticle = function (article) {
this.HasArticle(true);
this.CurrentArticle(article);
$('#articleModal').modal({
keyboard: true
}).modal('show');
};
}
That's It
Anyway that is all I wanted to say this time. If you like the article, a vote
comment is always welcome.
By the way, I am going to be taking a break from writing articles for a
while, as I have decided to learn F#, as such I will not be writing many
articles, you can however expect a few more blog posts about my journey into a
new language, which I hope may prove useful for other folk that are new to F#,
like me. These blog posts will be at a very basic level, as that is where I will
be coming from with F#. Still you got to start somewhere, right?