JavaScript Framework Evaluation

Last modified by Vincent Massol on 2024/11/19 16:14

 XWiki
 Design
 Dormant
 

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

Also note that ShadowDOM (part of "Web Components") which allows creating new HTML elements) is a competitor to AngularJS's directives but they are actually compatible. Thus in a near future we'll be able to use AngularJS which Web Components which is very nice.

Discussions found on the web comparing them:

Experiments inside XWiki:

Github Stats

Angular comes out here as the clear winner, although backbone and ember should not be considered "risky" choices.

FrameworkForksContributors
Angular9627916
Backbone4200230
Ember2335380
Knockout88240
Durandel32161

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}}

Note that we can ensure strict (X)HTML5 validity by prefixing the custom Angular attributes with data- (e.g. data-ng-src). 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.

There is a simple way to store the view state in the URL. In other words we can have bookmarkable views. This is needed for instance if we want to implement the live table using Angular. The live table state (filter values, sort column, sort order) is currently saved in the URL hash (document fragment). We can achieve the same with Angular, without interfering with the route path.

<label>Sort by:</label>
<select ng-model="sortColumn" ng-change="saveSortColumn()">
  <option value="name">Alphabetical</option>
  <option value="age">Newest</option>
</select>
myApp.controller('MyAppCtrl', ['$scope', '$location', function ($scope, $location) {
  $scope.sortColumn = $location.search().sortColumn || 'age';
  $scope.saveSortColumn = function() {
    $location.search('sortColumn', $scope.sortColumn);
  };
}]);
/xwiki/bin/view/Space/Page#/phones?sortColumn=name

The declaration of behaviour directly in HTML (ng-click, ng-change) doesn't look very good though. Our practice has been to avoid in-line JavaScript code and to use behavioural CSS classes (e.g. withTip). Fortunately we can implement them using directives (e.g. a custom attribute that adds behaviour to an element).

Angular offers a nice mechanism to control how data is 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';
  };
});

We can package and expose business logic as services. Angular comes with a bunch of useful services, like $http which can be used to make AJAX requests. We can use the $resource service to wrap (query/post) REST resources or anything that generates JSON on the server (a wiki page for instance).

myApp.factory('Page', ['$resource', function($resource) {
 // The colon ':' is URL-encoded so we must decode it otherwise Angular won't find the parameters.
 var url = new XWiki.Document(':page', ':space', ':wiki').getRestURL().replace('/%3A', '/:');
 return $resource(url);
}]);
...
myApp.controller('MyAppCtrl', ['$scope', 'Page', function ($scope, Page) {
  $scope.page = Page.get({
    wiki: 'xwiki',
    space: 'Sandbox',
    page: 'WebHome'
  });
}]);

Angular offers a way to reuse UI elements (widgets) or to enhance DOM elements with reusable behaviour by packaging them as directives. Directives can be used as custom HTML elements, custom HTML attributes, CSS class as well as comments.

<my-dir></my-dir>
<span my-dir="exp"></span>
<!-- directive: my-dir exp -->
<span class="my-dir: exp;"></span>

We can create for instance a custom livetable HTML element using directives.

<livetable xclass="Blog.BlogPostClass" />
myApp.directive('livetable', ['liveTableResource', function(liveTableResource) {
 return {
    restrict: 'E',
    templateUrl: new XWiki.Document('LiveTable', 'XWiki').getURL('get'),
    link: function(scope, element, attributes) {
      scope.rows = liveTableResource.query({
       'class': attributes.xclass,
        ...
      });
    }
  };
}]);

One of the strong points of Angular is the extensive support for testing the application.

Related Proposals

{{/html}}

Get Connected