19 Feb 2015
Exploring Boostrap Angular UI components

AngularJs is starting to impose itself as the reference JavaScript framework to develop SPA - Single Page (Web) Applications. And probably for a reason, that stuff acts like magic :)
Boostrap, another world famous framework for building websites, allows you to quickly create nice looking UI components.
I'm working on building a demo website featuring our viewing technology entirely relying on Angular-friendly components, so today I'm taking a look at two Bootstrap libraries offering Angular UI directives:
Both offer some pretty cool UI components and you can directly test them on their webpage, which makes it very explicit to see what you can achieve.
I decided to pick up one and integrate our viewer in it to see how they can get along: a nice one is the Bootstrap UI Carousel, a component that displays a collection of slides rotating on a timer. You can see a plunker demo of the carousel there. Not that I don't like kittens, but I think 3D models on the web are cooler ;)
The integration ended up being more challenging and instructive than I initially thought. Here are the highlights of the sample:
Following code illustrates how to perform a jsonP call from the angular controller:
1 //jsonP call to get total number of models in the Gallery 2 function getGalleryModelCount(onSuccess) { 3 4 var url = 'http://gallery.autodesk.io/api/models/count'; 5 6 $http.jsonp(url + "?callback=JSON_CALLBACK"). 7 success(function(data, status, headers, config) { 8 onSuccess(data.count); 9 }). 10 error(function(data, status, headers, config) { 11 console.log('Error: ' + status); 12 }); 13 }
This is needed because invoking the Gallery REST API from a different domain requires a cross domain call. Also needed, the activation of cors and jsonp on my node.js server:
1 var app = express(); 2 3 //CORS middleware 4 var cors = function (req, res, next) { 5 6 res.header("Access-Control-Allow-Origin", "*"); 7 8 res.header("Access-Control-Allow-Headers", 9 "Origin, X-Requested-With, Content-Type, Accept"); 10 11 res.header('Access-Control-Allow-Methods', 12 'GET,PUT,POST,DELETE'); 13 14 next(); 15 } 16 17 app.use(cors); 18 19 app.set("jsonp callback", true);
An angular filter is also required because I bind the iframe ng-source to a scope.member, so the url as to be trusted...
1 // needs that filter to bind iframe ng-src to scope member 2 app.filter('trustAsResourceUrl', ['$sce', function($sce) { 3 return function(url) { 4 return $sce.trustAsResourceUrl(url); 5 }}]);
Just for fun, I wanted to set up a listener for the carousel slide changed event, that thread gives a pretty exhaustive solution:
1 // a directive to watch carousel slide changed event 2 app.directive('onCarouselChange', function ($parse) { 3 return { 4 require: 'carousel', 5 link: function (scope, 6 element, 7 attrs, 8 carouselCtrl) { 9 var fn = $parse(attrs.onCarouselChange); 10 var origSelect = carouselCtrl.select; 11 carouselCtrl.select = 12 function (nextSlide, direction) { 13 if (nextSlide !== this.currentSlide) { 14 fn(scope, { 15 nextSlide: nextSlide, 16 direction: direction 17 }); 18 } 19 return origSelect.apply(this, arguments); 20 }; 21 } 22 }; 23 });
Here is the full code of the final result and the live demo: that carousel will fetch iframe slides of models from my Gallery, you can hit the "Add Slide" button to randomly add a new model slide.
<!--/////////////////////////////////////////////////////////////////////////// | |
// Copyright (c) Autodesk, Inc. All rights reserved | |
// Written by Philippe Leefsma 2015 - ADN/Developer Technical Services | |
// | |
// Permission to use, copy, modify, and distribute this software in | |
// object code form for any purpose and without fee is hereby granted, | |
// provided that the above copyright notice appears in all copies and | |
// that both that copyright notice and the limited warranty and | |
// restricted rights notice below appear in all supporting | |
// documentation. | |
// | |
// AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS. | |
// AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF | |
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE. AUTODESK, INC. | |
// DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE | |
// UNINTERRUPTED OR ERROR FREE. | |
///////////////////////////////////////////////////////////////////////////--> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<meta name="viewport" content="width=device-width"> | |
<title>Bootstrap UI Carousel Demo</title> | |
<link type="text/css" rel="stylesheet" href="https://developer.api.autodesk.com/viewingservice/v1/viewers/style.css"/> | |
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap.min.css"> | |
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap-theme.min.css"> | |
</head> | |
<body> | |
<div ng-app="Autodesk.ADN.Demo.Bootstrap.App"> | |
<div ng-controller="Autodesk.ADN.Demo.Bootstrap.Controller" | |
style="height: 490px; width: 490px"> | |
<div > | |
<!-- a bootstrap ui carousel with custom directive for onSildeChanged event --> | |
<carousel interval="myInterval" | |
on-carousel-change="onSlideChanged(nextSlide, direction)"> | |
<slide ng-repeat="slide in slides" active="slide.active"> | |
<iframe | |
width='100%' height='100%' frameborder='0' | |
allowFullScreen webkitallowfullscreen mozallowfullscreen | |
ng-src='{{slide.url | trustAsResourceUrl}}'> | |
</iframe> | |
<div class="carousel-caption"> | |
<h4>Model {{$index+1}}</h4> | |
<p>{{slide.name}}</p> | |
</div> | |
</slide> | |
</carousel> | |
</div> | |
<div class="row"> | |
<br> | |
<div class="col-md-6"> | |
<button type="button" class="btn btn-info" ng-click="addSlide()">Add Slide</button> | |
</div> | |
<div class="col-md-6"> | |
Interval, in milliseconds: <input type="number" class="form-control" ng-model="myInterval"> | |
<br />Enter a negative number or 0 to stop the interval. | |
</div> | |
</div> | |
</div> | |
</div> | |
<script src="http://code.jquery.com/jquery-2.1.3.min.js"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.10/angular.min.js"></script> | |
<script src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.12.0.min.js"></script> | |
<script> | |
var app = angular.module( | |
'Autodesk.ADN.Demo.Bootstrap.App', | |
['ui.bootstrap']); | |
app.controller('Autodesk.ADN.Demo.Bootstrap.Controller', | |
['$scope', '$http', function ($scope, $http) { | |
$scope.myInterval = 8000; | |
$scope.slides = []; | |
$scope.count = 1; | |
// add slide from modelId - internal method | |
function addSlide(index) { | |
getGalleryModel(index, function(model){ | |
var url = 'http://viewer.autodesk.io/node/gallery/embed/'; | |
$scope.slides.push({ | |
url: url + model._id, | |
name: model.name | |
}); | |
$scope.slides[$scope.slides.length - 1].active = true; | |
}); | |
}; | |
// on slide changed event - just for testing | |
$scope.onSlideChanged = function (nextSlide, direction) { | |
//console.log('onSlideChanged:', direction, nextSlide); | |
}; | |
// returns random int in [min, max] | |
function randomInt(min, max) { | |
return Math.floor(Math.random() * (max - min)) + min; | |
} | |
// jsonP call to get total number of models in the Gallery | |
function getGalleryModelCount(onSuccess) { | |
var url = 'http://gallery.autodesk.io/api/modelcount'; | |
$http.jsonp(url + "?callback=JSON_CALLBACK"). | |
success(function(data, status, headers, config) { | |
onSuccess(data.count); | |
}). | |
error(function(data, status, headers, config) { | |
console.log('Error: ' + status); | |
}); | |
} | |
// get model data from index | |
function getGalleryModel(index, onSuccess) { | |
// use Gallery REST API to pick up one model | |
var url = 'http://gallery.autodesk.io/api/models?skip=' | |
+ index + '&limit=1'; | |
$http.jsonp(url + "&callback=JSON_CALLBACK"). | |
success(function(data, status, headers, config) { | |
onSuccess(data.models[0]); | |
}). | |
error(function(data, status, headers, config) { | |
console.log('Error: ' + status); | |
}); | |
} | |
// add slide - scope method | |
$scope.addSlide = function() { | |
// picks a random model from the gallery | |
var index = randomInt(0, $scope.count-1); | |
addSlide(index); | |
} | |
// stores model count | |
getGalleryModelCount(function(count) { | |
$scope.count = count; | |
}); | |
// Adds first slide | |
addSlide(0); | |
}]); | |
// a directive to watch carousel slide changed event | |
app.directive('onCarouselChange', function ($parse) { | |
return { | |
require: 'carousel', | |
link: function (scope, element, attrs, carouselCtrl) { | |
var fn = $parse(attrs.onCarouselChange); | |
var origSelect = carouselCtrl.select; | |
carouselCtrl.select = function (nextSlide, direction) { | |
if (nextSlide !== this.currentSlide) { | |
fn(scope, { | |
nextSlide: nextSlide, | |
direction: direction | |
}); | |
} | |
return origSelect.apply(this, arguments); | |
}; | |
} | |
}; | |
}); | |
// needs that filter to bind iframe ng-src to scope member | |
app.filter('trustAsResourceUrl', ['$sce', function($sce) { | |
return function(url) { | |
return $sce.trustAsResourceUrl(url); | |
}}]); | |
</script> | |
</body> | |
</html> |
style="height: 490px; width: 490px">
on-carousel-change="onSlideChanged(nextSlide, direction)">
Model {{$index+1}}
{{slide.name}}
Enter a negative number or 0 to stop the interval.