20. May 2017
While developing web applications I have often needed to change the style of one or more elements based on the state of the data model used by the application. I have an inherent aversion to conditionals in views so went looking for other options. I came up with templated styles that works with both server and client side templating systems.
The code examples use Angular and can be found here
The example displays a traffic light which has 4 states
stop
prepare-to-go
go
prepare-to-stop
[!NOTE] Some traffic light systems jump from stop to go and don’t use the prepare-to-go state. Adding this state for the example shows how differences between the view and data model could be handled.
There are 3 lights red, amber, green. Each light is either on or off based on the status but there are really three states - one for each light.
e.g. for Red
The status of the red light when the red light is lit
The status of the red light when the amber light is list.
The status of the red light when the green light is list.
This model was used in coming up with the styles to be applied.
Probably the most obvious approach is to use a conditional in the template based on the state:
<!doctype html>
<html ng-app="templatedApp">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
<Link href="/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="/css/starter-template.css" rel="stylesheet">
<link href="/css/font-awesome.min.css" rel="stylesheet">
<link rel="stylesheet" href="/css/pygments-emacs.css">
<link rel="stylesheet" href="/css/screen.css">
<link rel="stylesheet" href="/css/asciidoc.css">
<link rel="stylesheet" type="text/css" href="templated.css"/>
</head>
<body>
<div class="container" ng-controller="TrafficLightsController as TrafficLights">
<div class="row">
<div class="col-md-8">
<input type="radio" ng-model="TrafficLights.status" value="stop"> Stop <br/>
<input type="radio" ng-model="TrafficLights.status" value="prepare-to-go"> Prepare to Go <br/>
<input type="radio" ng-model="TrafficLights.status" value="go"> Go <br/>
<input type="radio" ng-model="TrafficLights.status" value="prepare-to-stop"> Prepare to Stop <br/>
<p>Status: {{TrafficLights.status}} Color: {{TrafficLights.color}}</p>
<p>Traffic lights using templated conditional</p>
<!-- tag::conditional-lights[] -->
<div ng-switch on="TrafficLights.status">
<div class="animate-switch" ng-switch-when="prepare-to-go|prepare-to-stop" ng-switch-when-separator="|">
<div class="circle traffic-light-red-amber"></div>
<div class="circle traffic-light-amber-amber"></div>
<div class="circle traffic-light-green-amber"></div>
</div>
<div class="animate-switch" ng-switch-when="stop">
<div class="circle traffic-light-red-red"></div>
<div class="circle traffic-light-amber-red"></div>
<div class="circle traffic-light-green-red"></div>
</div>
<div class="animate-switch" ng-switch-when="go">
<div class="circle traffic-light-red-green"></div>
<div class="circle traffic-light-amber-green"></div>
<div class="circle traffic-light-green-green"></div>
</div>
<div class="animate-switch" ng-switch-default>
<p>Unknown state<p>
</div>
</div>
<!-- end::conditional-lights[] -->
<p>Traffic lights using templated CSS styles</p>
<!-- tag::styled-conditional-lights[] -->
<div>
<div class="circle traffic-light-red-{{TrafficLights.color}}"></div>
<div class="circle traffic-light-amber-{{TrafficLights.color}}"></div>
<div class="circle traffic-light-green-{{TrafficLights.color}}"></div>
</div>
<!-- end::styled-conditional-lights[] -->
<a href="/2017/05/20/templated-styles.html">Back to article</a>
</div>
</div>
</div>
<script src="templated.js"></script>
</body>
</html>
There is no change to the model. The view interprets the state and represents that state as an appropriate view of the traffic light.
The code is pretty verbose. Adding traffic light states means changes to the view.
This approach removes all the conditional code from the view and uses a new color view model attribute to control the style applied to each light.
Templated style traffic light
<!doctype html>
<html ng-app="templatedApp">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
<Link href="/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="/css/starter-template.css" rel="stylesheet">
<link href="/css/font-awesome.min.css" rel="stylesheet">
<link rel="stylesheet" href="/css/pygments-emacs.css">
<link rel="stylesheet" href="/css/screen.css">
<link rel="stylesheet" href="/css/asciidoc.css">
<link rel="stylesheet" type="text/css" href="templated.css"/>
</head>
<body>
<div class="container" ng-controller="TrafficLightsController as TrafficLights">
<div class="row">
<div class="col-md-8">
<input type="radio" ng-model="TrafficLights.status" value="stop"> Stop <br/>
<input type="radio" ng-model="TrafficLights.status" value="prepare-to-go"> Prepare to Go <br/>
<input type="radio" ng-model="TrafficLights.status" value="go"> Go <br/>
<input type="radio" ng-model="TrafficLights.status" value="prepare-to-stop"> Prepare to Stop <br/>
<p>Status: {{TrafficLights.status}} Color: {{TrafficLights.color}}</p>
<p>Traffic lights using templated conditional</p>
<!-- tag::conditional-lights[] -->
<div ng-switch on="TrafficLights.status">
<div class="animate-switch" ng-switch-when="prepare-to-go|prepare-to-stop" ng-switch-when-separator="|">
<div class="circle traffic-light-red-amber"></div>
<div class="circle traffic-light-amber-amber"></div>
<div class="circle traffic-light-green-amber"></div>
</div>
<div class="animate-switch" ng-switch-when="stop">
<div class="circle traffic-light-red-red"></div>
<div class="circle traffic-light-amber-red"></div>
<div class="circle traffic-light-green-red"></div>
</div>
<div class="animate-switch" ng-switch-when="go">
<div class="circle traffic-light-red-green"></div>
<div class="circle traffic-light-amber-green"></div>
<div class="circle traffic-light-green-green"></div>
</div>
<div class="animate-switch" ng-switch-default>
<p>Unknown state<p>
</div>
</div>
<!-- end::conditional-lights[] -->
<p>Traffic lights using templated CSS styles</p>
<!-- tag::styled-conditional-lights[] -->
<div>
<div class="circle traffic-light-red-{{TrafficLights.color}}"></div>
<div class="circle traffic-light-amber-{{TrafficLights.color}}"></div>
<div class="circle traffic-light-green-{{TrafficLights.color}}"></div>
</div>
<!-- end::styled-conditional-lights[] -->
<a href="/2017/05/20/templated-styles.html">Back to article</a>
</div>
</div>
</div>
<script src="templated.js"></script>
</body>
</html>
Using template substitution for the element class removes the need for a conditional. But we have to update model to include a color attribute derived from the status.
First we need to change the controller attribute into a property so when the value changes we can recognize the change.
Property definition
angular.module('templatedApp', [])
.controller('TrafficLightsController', function() {
var trafficLight = this;
// trafficLight.status = "stop";
var _status = null;
trafficLight.color = "unknown";
// tag::update-function[]
function updateColor() {
console.log("Update color called");
switch (_status) {
case "go":
trafficLight.color = "green";
break;
case "stop":
trafficLight.color = "red";
break;
default:
trafficLight.color = "amber";
break;
}
}
// end::update-function[]
// tag::color-attribute[]
Object.defineProperty(trafficLight, 'status', {
get: function () {
return _status;
},
set: function (value) {
_status = value;
updateColor(); // <1>
}
});
// end::color-attribute[]
trafficLight.status = "stop";
});
The ’updateColor’ function looks very similar to the template conditional
angular.module('templatedApp', [])
.controller('TrafficLightsController', function() {
var trafficLight = this;
// trafficLight.status = "stop";
var _status = null;
trafficLight.color = "unknown";
// tag::update-function[]
function updateColor() {
console.log("Update color called");
switch (_status) {
case "go":
trafficLight.color = "green";
break;
case "stop":
trafficLight.color = "red";
break;
default:
trafficLight.color = "amber";
break;
}
}
// end::update-function[]
// tag::color-attribute[]
Object.defineProperty(trafficLight, 'status', {
get: function () {
return _status;
},
set: function (value) {
_status = value;
updateColor(); // <1>
}
});
// end::color-attribute[]
trafficLight.status = "stop";
});
A key difference is that JavaScript is much easier to unit test.
The CSS
.circle {
width: 50px;
height: 50px;
border-radius: 50%;
display: inline-block;
margin-right: 20px;
}
.traffic-light-red-red {
background-color: #a02128;
}
.traffic-light-red-amber {
background-color: gray;
}
.traffic-light-red-green {
background-color: gray;
}
.traffic-light-amber-red {
background-color: gray;
}
.traffic-light-amber-amber {
background-color: #F7BA0B;
}
.traffic-light-amber-green {
background-color: gray;
}
.traffic-light-green-red {
background-color: gray;
}
.traffic-light-green-amber {
background-color: gray;
}
.traffic-light-green-green {
background-color: #317f43;
}
Wrapping up
By creating a view model with a view attribute for the color the view becomes simpler - zero code
Being able to test views is key differentiator. Unit tests are far faster than functional tests so feedback is faster. The view is logic free.
Each combination can be styled differently so the view model is more explicit.
As I mentioned at the beginning this technique can be applied with any server or client side templating systems that allow attributes to be templated.