opitzconsulting / jquery-mobile-angular-adapter

jquery mobile angular adapter
MIT License
517 stars 114 forks source link

PROJECT IS IN MAINTENANCE MODE.

NEW PROJECT angular-jqm

Newer versions of jqm will be integrated using angular-jqm and not this project.

Background: See the post in the angular mailing list: https://groups.google.com/d/msg/angular/qG8YRwVaL_g/zgKAvaArmB0J

Build Status

JQuery Mobile Angular Adapter

Description

Integration between jquery mobile and angular.js. Needed as jquery mobile enhances the pages with new elements and styles and so does angular. With this adapter, all widgets in jquery mobile can be used directly in angular, without further modifications. Furthermore, this adapter also provides special utilities useful for mobile applications.

If you are interested in how to build mobile web apps with this adapter, have a look at the german book Mobile Web-Apps mit JavaScript.

Dependencies

Examples

Reporting Issues

Usage

Note: The directive ng-app for the html element is required, as in all angular applications.

Plain

Include this adapter after angular and jquery mobile (see below).

<html ng-app>
<head>
    <title>MobileToys</title>
    <link rel="stylesheet" href="https://github.com/opitzconsulting/jquery-mobile-angular-adapter/blob/master/lib/jquery.mobile.css"/>
    <script src="https://github.com/opitzconsulting/jquery-mobile-angular-adapter/raw/master/lib/jquery.js"></script>
    <script src="https://github.com/opitzconsulting/jquery-mobile-angular-adapter/raw/master/lib/jquery.mobile.js"></script>
    <script src="https://github.com/opitzconsulting/jquery-mobile-angular-adapter/raw/master/lib/angular.js"></script>
    <script src="https://github.com/opitzconsulting/jquery-mobile-angular-adapter/raw/master/lib/jquery-mobile-angular-adapter.js"></script>
</head>

With requirejs 2.x

Create a index.xhtml file like the one below:

<html ng-app>
<head>
    <title>MobileToys</title>
    <link rel="stylesheet" href="https://github.com/opitzconsulting/jquery-mobile-angular-adapter/blob/master/lib/jquery.mobile.css"/>
    <script src="https://github.com/opitzconsulting/jquery-mobile-angular-adapter/raw/master/lib/requirejs.js" data-main="main.js"/>
</head>
<body>
   ... your jqm pages ...
</body>
</html>

And a main.js file with the following content:

require.config({
  shim:{
    'angular':{ deps:['jquery'], exports:'angular'}
  }
});
function tryHoldReady() {
  if (!tryHoldReady.executed && window.jQuery) {
    window.jQuery.holdReady(true);
    tryHoldReady.executed = true;
  }
}
tryHoldReady();
require.onResourceLoad = tryHoldReady;
require([
  "jquery",
  "jquery-mobile-angular-adapter",
  ... // your controllers, angular modules, ...
], function (jquery) {
  jquery.holdReady(false);
});

Notes:

Versioning

The adapter always takes the major and minor version of jquery mobile, e.g. if you want to use jquery mobile 1.3.x, use the adapter version 1.3.x. The patch version is assigend incrementally.

Build

Directory structure

Install the dependencies: npm install.

Build it: ./node_modules/.bin/grunt

Auto-Run tests when file change: ./node_modules/.bin/grunt dev

Navigation and routes

The adapter integrates angular routes with jquery mobile:

Default routing: basePath+$location.url()

Notes:

Restrictions:

Scopes

Every page of jquery mobile gets a separate scope. The $digest of the global scope only evaluates the currently active page, so there is no performance interaction between pages.

For communicating between the pages use the ngm-shared-controller directive (see below).

Directives, Filters and Services

Directive ngm-shared-controller

Syntax: <div ngm-shared-controller="name1:Controller1,name2:Controller2, ...">

Mobile pages are small, so often a usecase is split up into multiple pages. To share common behaviour and state between those pages, this directive allows shared controllers.

The directive will create an own scope for every given controllers and store it in the variables as name1, name2, .... If the controller is used on more than one page, the instance of the controller is shared.

Note that the shared controller have the full scope functionality, e.g. for dependecy injection or using $watch.

Event-Directives of jQuery Mobile

The following event directives are supported (see http://jquerymobile.com/demos/1.2.0/docs/api/events.html):

Usage: E.g. <a href="#" ngm-swipeleft="myFn()">

Directive ngm-if

The directive @ngm-if allows to add/remove an element to/from the dom, depending on an expression. This is especially useful at places where we cannot insert an ng-switch into the dom. E.g. jquery mobile does not allow elements between an ul and an li element.

Usage: E.g. <div ngm-if="myFlag">asdfasdf</div>

Service $history

Methods and Properties:

Service $location (extensions)

Service $waitDialog

The service $waitDialog allows the access to the jquery mobile wait dialog. It provides the following functions:

Default messages are:

Filter paged: Paging for lists

Lists can be paged in the sense that more entries can be additionally loaded. By "loading" we mean the display of a sublist of a list that is already fully loaded in JavaScript. This is useful, as the main performance problems result from DOM operations, which can be reduced with this paging mechanism.

To implement this paging mechanism, the angular filter paged was created. For displaying a page within a list, simply use:

list | paged:'pagerId':12

This returns the subarray of the given array with the currently loaded pages.

Parameters:

  1. The first parameter is required and must be unique for every usage of the paged filter. It is the property name in the scope which stores the state of pagination for this filter usage, and also contains the function loadMore and hasMore (see below).
  2. If the second parameter is a number, it is interpreted as the pageSize. If this parameter is omitted, the default page size is used. This is by default 10, and can be configured using

    module(["ng"]).value('defaultListPageSize', 123);

For filtering and sorting the paged array, you can use filter chaining with the angular filters filter and orderBy, e.g.

list | filter:{name:'someName'} | orderBy:'name' | paged:'pagerId'

To show a button that loads the next page of the list, use the following syntax:

<a href="#" ngm-if="pagerId.hasMore" ngm-vclick="pagerId.loadMore()">Load More</a>

The following example shows an example for a paged list for the data in the variable myList:

<ul data-role="listview">
    <li ng-repeat="item in list | paged:'pager1'">{{item}}</li>
    <li ngm-if="pager1.hasMore">
        <a href="#" ngm-vclick="pager1.loadMore()">Load more</a>
     </li>
</ul>

Note: pagerId.cache stores the last result that was returns for a list | paged:'pagerId' expression. This can be used to check whether the paged list is empty, .. without refiltering the list.

Notes on the integration of some jqm widgets

widget collapsible

widget checkboxradio

widget popup

widget panel

widget slider in a <select>

Integrating custom jquery mobile plugins with angular

All integration work is done using the jqmNgWidget provider.

This will automatically create angular directives for all jqm widgets that are contained in $.mobile and use the jqm widget factory (e.g. $.mobile.dialog). If you want to specify a custom handler for a jqm directive, use the following pattern:

ng.config(["jqmNgWidgetProvider", function(jqmNgWidgetProvider) {
    jqmNgWidgetProvider.widget("somePlugin", ["jqmNgWidget", function(jqmNgWidget) {
        return {
            link: function(widgetName, scope, iElement, iAttrs, ngModelCtrl) {
                jqmNgWidet.createWidget(widgetName, iElement, iAttrs);
                // Do additional binding work...
            }
        }
    }]);
}]);

See src/main/webapp/widgetAdapters.js for more examples.

Integration strategy

Jquery mobile has two kinds of markup:

Integration strategy:

  1. We have a precompile phase: This is called before the angular compiles does it's work, i.e. before $compile is called, and before directive.template and directive.templateUrl is evaluated. Here, we trigger the jqm create and pagecreate event. Before this, we instrument all stateful jqm widgets (see above), so they do not really create the jqm widget, but only add the attribute jqm-create=<widgetName> and jqm-link=<widgetname> to the corresponding element. By this, all stateless markup can be used by angular for stamping (e.g. in ng-repeat), without calling a jqm method again, so we are fast. Furthermore, we have special handlers in the precompile phase for those jqm widgets that wrap themselves into new elements (checkboxradio, slider, button, selectmenu, search input), as the angular compiler does not like this. Finally, we also mark all jqm pages with the jqm-page attribute. This is needed as jqm pages are represented as data-role=page in the dom and angular does not allow to create directives that only match pages but not other jqm widgets.

  2. We have the directive ngmPage: This creates an own scope for every page. By this, we are able to disconnect the scope of the pages that are not visible right now. This is important for optimizing performance. This creates the jqm pages by calling element.page() in the pre link phase, however without the pagecreate event. By this, we only create the page instance, but do not modify the dom (as this is not allowed in the pre link phase). Furthermore, the page jqm widget instance is already available for the other widgets, which are created in the post link phase.

  3. We have the directive ngmCreate: This will create the jqm widgets in the post link phase. For widgets that wrap themselves into new elements this needs to be called for the wrapper, that was already created in the precompile phase. This is important as the jqm widgets do more DOM transformations during creations that the angular compiler does not like (e.g. the jqm widget <input type="checkbox>" enhances the sibling <label> element and wraps that element). By calling the widget during the post link phase of the wrapper element those DOM modifications are ok with angular.

  4. We have the directive ngmLink: Here we listen for changes in the model and refresh the jqm widgets when needed and vice versa. For elements that wrap themselves into new elements this will be called on the original element (e.g. the <input> for <input type="checkbox"> elements), in contrast to the ngmCreate directive.

  5. All together: This minimizes the number DOM traversals and DOM changes

    • We use angular's stamping for stateless widget markup, i.e. we call the jqm functions only once, and let angular do the rest.
    • We do not use the jqm create event for refreshing widgets, but angular's directives. By this, we prevent unneeded executions of jquery selectors.
    • We reuse the selectors in jqm for detecting which elements should be enhanced with which jqm widgets.

Ohter possibilities not chosen:

Integration of jqm routing and angular routing: