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

Show last authors
1 Currently XWiki is still using prototype as its JS framework of choice. Extensions can be written using jquery.
2
3 Prototype has 2 limitations for us:
4
5 * It's now dead
6 * It's not enough to build modern web UIs
7
8 Possible list of JS MVC frameworks to check out:
9
10 * AngularJS (also check [[Angular 2.0>>http://blog.angularjs.org/2014/03/angular-20.html]] which is mobile first)
11 * EmberJS
12 * DurandalJS
13 * BackboneJS
14 * KnockoutJS
15
16 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>>http://www.binpress.com/blog/2014/06/26/polymer-vs-angular/]]. Thus in a near future we'll be able to use AngularJS which Web Components which is very nice.
17
18 Discussions found on the web comparing them:
19
20 * http://eviltrout.com/2013/06/15/ember-vs-angular.html
21 * http://www.quora.com/Client-side-MVC/Is-Angular-js-or-Ember-js-the-better-choice-for-Javascript-frameworks
22 * http://readwrite.com/2014/02/06/angular-backbone-ember-best-javascript-framework-for-you
23
24 Experiments inside XWiki:
25
26 * With EmberJS: [[http:~~/~~/extensions.xwiki.org/xwiki/bin/view/Extension/AngularJSDemo>>doc:extensions:Extension.AngularJSDemo]] (source: https://github.com/xwiki-contrib/application-emberjs-todolist and mail thread with explanations: http://markmail.org/message/ihg5t5qtbp5yfe6f)
27 * With AngularJS:
28 ** https://github.com/xwiki-contrib/angular-rendering
29 ** https://github.com/xwiki-contrib/application-angular-todo
30 ** https://github.com/xwiki-contrib/application-angularjsdemo
31
32 == Github Stats ==
33
34 Angular comes out here as the clear winner, although backbone and ember should not be considered "risky" choices.
35
36 |=Framework|=Forks|=Contributors
37 |Angular|9627|916
38 |Backbone|4200|230
39 |Ember|2335|380
40 |Knockout|882|40
41 |Durandel|321|61
42
43 == AngularJS (1.x) ==
44
45 Can be loaded with RequireJS.
46
47 {{code language="JavaScript"}}
48 require.config({
49 paths: {
50 angular: '//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min',
51 'angular-route': '//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular-route.min',
52 'angular-resource': '//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular-resource.min',
53 'angular-animate': '//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular-animate.min'
54 },
55 shim: {
56 angular: {
57 exports: 'angular'
58 },
59 'angular-route': {
60 deps: ['angular'],
61 exports: 'angular'
62 },
63 'angular-resource': {
64 deps: ['angular'],
65 exports: 'angular'
66 },
67 'angular-animate': {
68 deps: ['angular'],
69 exports: 'angular'
70 }
71 }
72 });
73
74 require(['angular', 'angular-route', 'angular-resource', 'angular-animate'], function(angular) {
75 // Define a new module and specify its dependencies.
76 var myApp = angular.module('myApp', [
77 'ngRoute',
78 'myAppControllers',
79 'myAppFilters',
80 'myAppServices',
81 'myAppAnimations']);
82 ...
83 });
84 {{/code}}
85
86 We can load it also from a WebJar.
87
88 {{code language="JavaScript"}}
89 require(["$services.webjars.url('angularjs/1.2.16/angular.js')"], function() {
90 var myApp = angular.module('myApp', []);
91 ...
92 });
93 {{/code}}
94
95 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):
96
97 {{code language="html"}}
98 <div ng-app="myApp">
99 ...
100 </div>
101 {{/code}}
102
103 but it can also be lazy loaded:
104
105 {{code language="JavaScript"}}
106 angular.bootstrap($('#myAppContainer')[0], ['myApp']);
107 {{/code}}
108
109 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.
110
111 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.
112
113 {{code language="JavaScript"}}
114 phoneApp.config(['$routeProvider', function($routeProvider) {
115 $routeProvider.when('/phones', {
116 templateUrl: new XWiki.Document('ListView', 'Phone').getURL('get'),
117 controller: 'PhoneListCtrl'
118 }).when('/phones/:phoneId', {
119 templateUrl: new XWiki.Document('DetailView', 'Phone').getURL('get'),
120 controller: 'PhoneDetailCtrl'
121 }).otherwise({
122 redirectTo: '/phones'
123 });
124 }]);
125 {{/code}}
126
127 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:
128
129 {{code language="html"}}
130 <!-- Here you have UI elements common to all views (this is the layout template). -->
131 <div ng-view>
132 <!-- The content displayed here depends on the current route (URL) -->
133 <!-- We can't have a view nested here! -->
134 </div>
135 <!-- We can't have a sibling view here! -->
136 {{/code}}
137
138 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}}##>>http://handlebarsjs.com/]]-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:
139
140 {{code language="none"}}
141 {{html}}
142 <ul class="phones">
143 <li ng-repeat="phone in phones | filter:query | orderBy:fieldToOrderBy:reverse"
144 class="thumbnail phone-listing">
145 <a href="#/phones/{{phone.id}}" class="thumb">
146 <img ng-src="{{phone.imageUrl}}"/>
147 </a>
148 <a href="#/phones/{{phone.id}}">{{phone.name}}</a>
149 <p>{{phone.snippet}}</p>
150 </li>
151 </ul>
152 {{/html}}
153 {{/code}}
154
155 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.
156
157 {{code}}
158 myApp.controller('todoCtrl', ['$scope', function($scope) {
159 $scope.todos = [
160 {
161 text: 'Learn the XWiki API',
162 done: true
163 }, {
164 text: 'Create an XWiki application',
165 done: false
166 }
167 ];
168 }]);
169 {{/code}}
170
171 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.
172
173 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.
174
175 {{code language="html"}}
176 <label>Sort by:</label>
177 <select ng-model="sortColumn" ng-change="saveSortColumn()">
178 <option value="name">Alphabetical</option>
179 <option value="age">Newest</option>
180 </select>
181 {{/code}}
182
183 {{code language="JavaScript"}}
184 myApp.controller('MyAppCtrl', ['$scope', '$location', function ($scope, $location) {
185 $scope.sortColumn = $location.search().sortColumn || 'age';
186 $scope.saveSortColumn = function() {
187 $location.search('sortColumn', $scope.sortColumn);
188 };
189 }]);
190 {{/code}}
191
192 {{code language="none"}}
193 /xwiki/bin/view/Space/Page#/phones?sortColumn=name
194 {{/code}}
195
196 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).
197
198 Angular offers a nice mechanism to control how data is displayed, called **filters**. This is in some ways similar to our property displayers.
199
200 {{code language="html"}}
201 <dd>{{phone.connectivity.gps | checkmark}}</dd>
202 {{/code}}
203
204 {{code language="JavaScript"}}
205 myApp.filter('checkmark', function() {
206 return function(input) {
207 return input ? '\u2713' : '\u2718';
208 };
209 });
210 {{/code}}
211
212 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).
213
214 {{code language="JavaScript"}}
215 myApp.factory('Page', ['$resource', function($resource) {
216 // The colon ':' is URL-encoded so we must decode it otherwise Angular won't find the parameters.
217 var url = new XWiki.Document(':page', ':space', ':wiki').getRestURL().replace('/%3A', '/:');
218 return $resource(url);
219 }]);
220 ...
221 myApp.controller('MyAppCtrl', ['$scope', 'Page', function ($scope, Page) {
222 $scope.page = Page.get({
223 wiki: 'xwiki',
224 space: 'Sandbox',
225 page: 'WebHome'
226 });
227 }]);
228 {{/code}}
229
230 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.
231
232 {{code language="html"}}
233 <my-dir></my-dir>
234 <span my-dir="exp"></span>
235 <!-- directive: my-dir exp -->
236 <span class="my-dir: exp;"></span>
237 {{/code}}
238
239 We can create for instance a custom ##livetable## HTML element using directives.
240
241 {{code language="html"}}
242 <livetable xclass="Blog.BlogPostClass" />
243 {{/code}}
244
245 {{code language="JavaScript"}}
246 myApp.directive('livetable', ['liveTableResource', function(liveTableResource) {
247 return {
248 restrict: 'E',
249 templateUrl: new XWiki.Document('LiveTable', 'XWiki').getURL('get'),
250 link: function(scope, element, attributes) {
251 scope.rows = liveTableResource.query({
252 'class': attributes.xclass,
253 ...
254 });
255 }
256 };
257 }]);
258 {{/code}}
259
260 One of the strong points of Angular is the extensive support for **testing** the application.
261
262 == Related Proposals ==
263
264 * [[Twitter Bootstrap Integration>>Proposal.BootstrapIntegration]]
265 * [[Javascript Frameworks in XWiki (2007)>>Design.RationalizetheuseofjavascriptframeworksinXWiki]]

Get Connected