There is no doubt that jQuery has become very popular among developers because of its simplicity. Let's try to combine the simplicity of jQuery and the power of Dart in a real example. For demonstration purposes, we created the js_proxy
package to help the Dart code to communicate with jQuery. It is available on the pub manager at https://pub.dartlang.org/packages/js_proxy. This package is layered on dart:js
and has a library of the same name and sole class JProxy
. An instance of the JProxy
class can be created via the generative constructor where we can specify the optional reference on the proxied JsObject
:
JProxy([this._object]);
We can create an instance of JProxy
with a named constructor and provide the name of the JavaScript object accessible through the dart:js
context as follows:
JProxy.fromContext(String name) { _object = js.context[name]; }
The JProxy
instance keeps the reference on the proxied JsObject
class and makes all the manipulation on it, as shown in the following code:
js.JsObject _object; js.JsObject get object => _object;
We can use JProxy
to create a reference to jQuery via the context from the dart:js
library as follows:
var jquery = new JProxy.fromContext('jQuery'),
Another very popular way is to use the dollar sign as a shortcut to the jQuery variable as shown in the following code:
var $ = new JProxy.fromContext('jQuery'),
Bear in mind that the original jQuery
and $
variables from JavaScript are functions, so our variables reference to the JsFunction
class. From now, jQuery lovers who moved to Dart have a chance to use both the syntax to work with selectors via parentheses.
Usually, jQuery sends a request to select HTML elements based on IDs, classes, types, attributes, and values of their attributes or their combination, and then performs some action on the results. We can use the basic syntax to pass the search criteria in the jQuery
or $
function to select the HTML elements:
$(selector)
As mentioned in Chapter 3, Object Creation, Dart has a syntactic sugar method, call
, that helps us to emulate a function and we can use the call
method in the jQuery syntax. Dart knows nothing about the number of arguments passing through the function, so we use the fixed number of optional arguments in the call
method. Through this method, we invoke the proxied function (because jquery
and $
are functions) and returns results within JProxy
:
dynamic call([arg0 = null, arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null, arg6 = null, arg7 = null, arg8 = null, arg9 = null]) { var args = []; if (arg0 != null) args.add(arg0); if (arg1 != null) args.add(arg1); if (arg2 != null) args.add(arg2); if (arg3 != null) args.add(arg3); if (arg4 != null) args.add(arg4); if (arg5 != null) args.add(arg5); if (arg6 != null) args.add(arg6); if (arg7 != null) args.add(arg7); if (arg8 != null) args.add(arg8); if (arg9 != null) args.add(arg9); return _proxify((_object as js.JsFunction).apply(args)); }
The JProxy
class is a proxy to other classes, so it marks with the @proxy
annotation. We override noSuchMethod
intentionally to call the proxied methods and properties of jQuery when the methods or properties of the proxy are invoked. The logic flow in noSuchMethod
is pretty straightforward. It invokes callMethod
of the proxied JsObject
when we invoke the method on proxy, or returns a value of property of the proxied object if we call the corresponding operation on proxy. The code is as follows:
@override dynamic noSuchMethod(Invocation invocation) { if (invocation.isMethod) { return _proxify(_object.callMethod( symbolAsString(invocation.memberName), _jsify(invocation.positionalArguments))); } else if (invocation.isGetter) { return _proxify(_object[symbolAsString(invocation.memberName)]); } else if (invocation.isSetter) { throw new Exception('The setter feature was not implemented yet.'), } return super.noSuchMethod(invocation); }
As you might remember, all Map or Iterable arguments must be converted to JsObject
with the help of the jsify
method. In our case, we call the _jsify
method to check and convert passed arguments aligned with a called function, as shown in the following code:
List _jsify(List params) {
List res = [];
params.forEach((item) {
if (item is Map || item is List) {
res.add(new js.JsObject.jsify(item));
} else {
res.add(item);
}
});
return res;
}
Before return
, the result must be passed through the _proxify
function as follows:
dynamic _proxify(value) { return value is js.JsObject ? new JProxy(value) : value; }
This function wraps all JsObject
classes within a JProxy
class and passes other values as it is.
Now create the jquery
project, open the pubspec.yaml
file, and add js_proxy
to the dependencies. Open the jquery.html
file and make the following changes:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>jQuery</title> <link rel="stylesheet" href="jquery.css"> </head> <body> <h1>Jquery</h1> <p>I'm a paragraph</p> <p>Click on me to hide</p> <button>Click me</button> <div class="container"> <div class="box"></div> </div> </body> <script src="//code.jquery.com/jquery-1.11.0.min.js"></script> <script type="application/dart" src="jquery.dart"></script> <script src="packages/browser/dart.js"></script> </html>
This project aims to demonstrate that:
In general, you can copy the JavaScript code, paste it in the Dart code, and probably make slightly small changes.
It's time to add js_proxy
in our code. Open jquery.dart
and make the following changes:
import 'dart:html'; import 'package:js_proxy/js_proxy.dart'; /** * Shortcut for jQuery. */ var $ = new JProxy.fromContext('jQuery'), /** * Shortcut for browser console object. */ var console = window.console; main() { printVersion(); } /** * jQuery code: * * var ver = $().jquery; * console.log("jQuery version is " + ver); * * JS_Proxy based analog: */ printVersion() { var ver = $().jquery; console.log("jQuery version is " + ver); }
You should be familiar with jQuery and console shortcuts by now. The call to jQuery with empty parentheses returns JProxy
and contains JsObject
with reference to jQuery from JavaScript. The jQuery object has a jquery
property that contains the current version number, so we reach this one via noSuchMethod
of JProxy
. Run the application, and you will see the following result in the console:
jQuery version is 1.11.1
Let's move on and perform some actions on the selected HTML elements.
The syntax of jQuery is based on selecting the HTML elements and it also performs some actions on them:
$(selector).action();
Let's select a button on the HTML page and fire the click
event as shown in the following code:
/** * jQuery code: * * $("button").click(function(){ * alert('You click on button'), * }); * * JS_Proxy based analog: */ events() { // We remove 'function' and add 'event' here $("button").click((event) { // Call method 'alert' of 'window' window.alert('You click on button'), }); }
All we need to do here is just remove the function
keyword, because anonymous functions on Dart do not use it, and then add the event
parameter. This is because this argument is required in the Dart version of the event listener. The code calls jQuery to find all the HTML button elements to add the click
event listener to each of them. So when we click on any button, a specified alert message will be displayed. On running the application, you will see the following message:
The jQuery supports animation out of the box, so it sounds very tempting to use it in Dart. Let's take a look at the following code snippet:
/** * jQuery code: * * $("p").click(function() { * this.hide("slow",function(){ * alert("The paragraph is now hidden"); * }); * }); * $(".box").click(function(){ * var box = this; * startAnimation(); * function startAnimation(){ * box.animate({height:300},"slow"); * box.animate({width:300},"slow"); * box.css("background-color","blue"); * box.animate({height:100},"slow"); * box.animate({width:100},"slow",startAnimation); * } * }); * * JS_Proxy based analog: */ effects() { $("p").click((event) { $(event['target']).hide("slow",(){ window.alert("The paragraph is now hidden"); }); }); $(".box").click((event) { var box = $(event['target']); startAnimation() { box.animate({'height':300},"slow"); box.animate({'width':300},"slow"); box.css("background-color","blue"); box.animate({'height':100},"slow"); box.animate({'width':100},"slow",startAnimation); }; startAnimation(); }); }
This code finds all the paragraphs on the web page to add a click
event listener to each one. The JavaScript code uses the this
keyword as a reference to the selected paragraph to start the hiding animation. The this
keyword has a different notion on JavaScript and Dart, so we cannot use it directly in anonymous functions on Dart. The target
property of event
keeps the reference to the clicked element and presents JsObject
in Dart. We wrap the clicked element to return a JProxy
instance and use it to call the hide
method.
The jQuery is big enough and we have no space in this book to discover all its features, but you can find more examples at https://github.com/akserg/js_proxy.
Now we should talk about the performance impact of using different approaches across several modern web browsers. The algorithm must perform all the following actions:
This algorithm must be implemented in the following solutions:
JProxy
and dart:js
from Dartdart:html
We implemented this algorithm on all of them, so we have a chance to compare the results and choose the champion. The following HTML code has three buttons to run independent tests, three paragraph elements to show the results of the tests, and one DIV element used as a container. The code is as follows:
<div> <button id="run_js" onclick="run_js_test()">Run JS</button> <button id="run_jproxy">Run JProxy</button> <button id="run_dart">Run Dart</button> </div> <p id="result_js"></p> <p id="result_jproxy"></p> <p id="result_dart"></p> <div id="container"></div>
The JavaScript code based on jQuery is as follows:
function run_js_test() { var startTime = new Date(); process_js(); var diff = new Date(new Date().getTime() – startTime.getTime()).getTime(); $('#result_js').text('jQuery tooks ' + diff + ' ms to process 10000 HTML elements.'), } function process_js() { var container = $('#container'), // Create 10000 DIV elements for (var i = 0; i < 10000; i++) { $('<div>Test</div>').appendTo(container); } // Find and update classes of all DIV elements $('#container > div').css("color","red"); // Remove all DIV elements $('#container > div').remove(); }
The main code registers the click
event listeners and the call function run_dart_js_test
. The first parameter of the run_dart_js_test
function must be a function which we will investigate. The second and third parameters are used to pass the selector of the result element and test the title:
void main() { querySelector('#run_jproxy').onClick.listen((event) { run_dart_js_test(process_jproxy, '#result_jproxy', 'JProxy'), }); querySelector('#run_dart').onClick.listen((event) { run_dart_js_test(process_dart, '#result_dart', 'Dart'), }); } run_dart_js_test(Function fun, String el, String title) { var startTime = new DateTime.now(); fun(); var diff = new DateTime.now().difference(startTime); querySelector(el).text = '$title tooks ${diff.inMilliseconds} ms to process 10000 HTML elements.'; }
Here is the Dart solution based on JProxy
and dart:js
:
process_jproxy() { var container = $('#container'), // Create 10000 DIV elements for (var i = 0; i < 10000; i++) { $('<div>Test</div>').appendTo(container.object); } // Find and update classes of all DIV elements $('#container > div').css("color","red"); // Remove all DIV elements $('#container > div').remove(); }
Finally, a clear Dart solution based on dart:html
is as follows:
process_dart() { // Create 10000 DIV elements var container = querySelector('#container'), for (var i = 0; i < 10000; i++) { container.appendHtml('<div>Test</div>'), } // Find and update classes of all DIV elements querySelectorAll('#container > div').forEach((Element el) { el.style.color = 'red'; }); // Remove all DIV elements querySelectorAll('#container > div').forEach((Element el) { el.remove(); }); }
All the results are in milliseconds. Run the application and wait until the web page is fully loaded. Run each test by clicking on the appropriate button. My result of the tests on Dartium, Chrome, Firefox, and Internet Explorer are shown in the following table:
Web browser |
jQuery framework |
jQuery via JProxy |
Library dart:html |
---|---|---|---|
Dartium |
2173 |
3156 |
714 |
Chrome |
2935 |
6512 |
795 |
Firefox |
2485 |
5787 |
582 |
Internet Explorer |
12262 |
17748 |
2956 |
Now, we have the absolute champion—the Dart-based solution. Even the Dart code compiled in the JavaScript code to be executed in Chrome, Firefox, and Internet Explorer works quicker than jQuery (four to five times) and much quicker than dart:js
and JProxy
class-based solutions (four to ten times).
18.118.12.186