JavaScript Framework Evaluation
- Design
- Active
- Deprecation of Prototype.js and the usage of JQuery as our default JS framework http://markmail.org/thread/vtmsymzdgl2jf252 (Feb 25, 2014)
- Experiment using emberJS http://markmail.org/thread/ihg5t5qtbp5yfe6f (Feb 2, 2014)
- Experiment with AngularJS and XWiki http://markmail.org/thread/o7xsxyqtuqlh7x44 (Apr 16, 2013)
- [PROPOSAL] Include require.js in XWiki by default and make jQuery available through require.js http://markmail.org/thread/dn4pqrcgvnk6qgyp (Nov 30, 2012)
- [Idea] Bundle jQuery ? http://markmail.org/message/roh55giixdyo2sux (Jun 15, 2012)
Description
Currently XWiki is still using prototype as its JS framework of choice. Extensions can be written using jquery.
Prototype has 2 limitations for us:
- It's now dead
- It's not enough to build modern web UIs
Possible list of JS MVC frameworks to check out:
- AngularJS (also check Angular 2.0 which is mobile first)
- EmberJS
- DurandalJS
- BackboneJS
- KnockoutJS
Discussions found on the web comparing them:
- http://eviltrout.com/2013/06/15/ember-vs-angular.html
- http://www.quora.com/Client-side-MVC/Is-Angular-js-or-Ember-js-the-better-choice-for-Javascript-frameworks
- http://readwrite.com/2014/02/06/angular-backbone-ember-best-javascript-framework-for-you
Experiments inside XWiki:
- With EmberJS: http://extensions.xwiki.org/xwiki/bin/view/Extension/AngularJSDemo (source: https://github.com/xwiki-contrib/application-emberjs-todolist and mail thread with explanations: http://markmail.org/message/ihg5t5qtbp5yfe6f)
- With AngularJS:
AngularJS (1.x)
Can be loaded with RequireJS.
require.config({
paths: {
angular: '//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min',
'angular-route': '//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular-route.min',
'angular-resource': '//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular-resource.min',
'angular-animate': '//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular-animate.min'
},
shim: {
angular: {
exports: 'angular'
},
'angular-route': {
deps: ['angular'],
exports: 'angular'
},
'angular-resource': {
deps: ['angular'],
exports: 'angular'
},
'angular-animate': {
deps: ['angular'],
exports: 'angular'
}
}
});
require(['angular', 'angular-route', 'angular-resource', 'angular-animate'], function(angular) {
// Define a new module and specify its dependencies.
var myApp = angular.module('myApp', [
'ngRoute',
'myAppControllers',
'myAppFilters',
'myAppServices',
'myAppAnimations']);
...
});We can load it also from a WebJar.
require(["$services.webjars.url('angularjs/1.2.16/angular.js')"], function() {
var myApp = angular.module('myApp', []);
...
});It can initialize itself automatically on page load if it finds an element marked with the ng-app attribute (the element that wraps the application UI):
<div ng-app="myApp">
...
</div>but it can also be lazy loaded:
angular.bootstrap($('#myAppContainer')[0], ['myApp']);It has its own Dependency Injection mechanism but it injects dependencies that have already been loaded on the client. So it doesn't fetch scripts dynamically. This means we can use RequireJS to fetch Angular modules from the server dynamically and then use Angular's DI to inject them in our application.
Angular was designed for single-page applications. You have a layout template with an area (the view) that changes with the URL. Since the page is never reloaded, when the view is changed only the document fragment part of the URL is changed. This means the views are still bookmarkable (and Back/Forward buttons work). We can map URLs (actually document fragments) to views.
phoneApp.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/phones', {
templateUrl: new XWiki.Document('ListView', 'Phone').getURL('get'),
controller: 'PhoneListCtrl'
}).when('/phones/:phoneId', {
templateUrl: new XWiki.Document('DetailView', 'Phone').getURL('get'),
controller: 'PhoneDetailCtrl'
}).otherwise({
redirectTo: '/phones'
});
}]);The '/phone' part actually translates to xwiki/bin/view/Space/Page#/phones. A small issue is that the usage of '$' (in $routeProvider) can lead to problems if the JavaScript code is parsed for Velocity. An important limitation is that you can have only one view currently displayed (this may change in Angular 2.x). So we can't have sibling or nested views. The view is marked with the ng-view empty attribute:
<!-- Here you have UI elements common to all views (this is the layout template). -->
<div ng-view>
<!-- The content displayed here depends on the current route (URL) -->
<!-- We can't have a view nested here! -->
</div>
<!-- We can't have a sibling view here! -->Views are defined by 'templates' that are evaluated on the client. The syntax used is HTML with custom ng* attributes and a handle-bars {{value}}-like syntax. We can write the Angular templates in wiki pages but the handle-bars notation clearly prevents us from using the wiki syntax. We have to use the HTML macro:
{{html}}
<ul class="phones">
<li ng-repeat="phone in phones | filter:query | orderBy:fieldToOrderBy:reverse"
class="thumbnail phone-listing">
<a href="#/phones/{{phone.id}}" class="thumb">
<img ng-src="{{phone.imageUrl}}"/>
</a>
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
<p>{{phone.snippet}}</p>
</li>
</ul>
{{/html}}In a template we can display or interact with data from the scope. The scope is very similar to the Velocity context. Think how we can put all kinds of objects on the Velocity context and then access them from the Velocity templates. One difference is that in Angular we can have nested scopes which inherit from one another. Everything that is put on the scope is considered to be the model. So the model can be any type of data, from primitive types to complex objects. The JavaScript code that puts data on the scope is the controller. Each view has a controller for instance that decides what data is displayed by that view.
myApp.controller('todoCtrl', ['$scope', function($scope) {
$scope.todos = [
{
text: 'Learn the XWiki API',
done: true
}, {
text: 'Create an XWiki application',
done: false
}
];
}]);There is a two-way binding between the model and the view. Angular listens to DOM events and updates the model whenever needed. Changes to the model done from within the Angular code (e.g. from the controller) are reflected automatically in the view. Angular detects which objects have been modified using what is called 'dirty-checking'. This aproach has some performance limitations when the model is big (lots of complex objects) but the Angular team has some fixes planed for 2.x.
Angular offers a nice mechanism to control how various data types are displayed, called filters. This is in some ways similar to our property displayers.
<dd>{{phone.connectivity.gps | checkmark}}</dd>myApp.filter('checkmark', function() {
return function(input) {
return input ? '\u2713' : '\u2718';
};
});Another mechanism called services allows us to easily fetch JSON data from the server.
Marius Dumitru Florea
Vincent Massol
Ludovic Dubost