javascript

28 posts

AngularJS - Simple Collapse Directive

Collapse is a common control used in web pages. Users can click to expand or collapse it. Bootstrap has a simple declarative way to create collapse. But Bootstrap's collapse doesn't work if the markup is generated dynamically using AngularJS, because it relies on element id to match target element. For example, following code doesn't work because id is generated dynamically using {{dynamic}}Collapse.

<a class="btn btn-primary" data-toggle="collapse" href="#{{dynamic}}Collapse" aria-expanded="false" aria-controls="{{dynamic}}Collapse">  
Link with href  
</a>  
<div class="collapse" id="{{dynamic}}Collapse">

</div>  

angular-ui collapse directive seems to be a good solution, but it requires new variable in the scope object. So I created a simple directive with jQuery to solve this issue.

angular.module('DemoApp', [])  
.controller('DemoCtrl', ['$scope', function($scope) {
  $scope.colors = ['Red', 'Green', 'Blue'];
}])
.directive('collapseToggler', function(){
  return {
    restrict: 'A'
    link: function(scope, elem, attrs) {
      elem.on('click', function() {
        $(this).siblings('.collapse').toggleClass('in');
      });
    }
  };
})

collapseToggler directive is applied to the toggler. When clicked, it finds the siblings with CSS class collapse and toggle CSS class in which controls display of the target element.

The limitation of this solution is that it requires the target element to be as the sibling of the toggle element. But most of the times this is the desired DOM structure.

Below is an example of how to use it.

<body ng-controller="DemoCtrl">  
  <div ng-repeat="color in colors">
    <div collapse-toggler class="toggler">What's the color?</div>
    <div class="collapse">
    {{color}}
    </div>
  </div>
</body>

See live example:

See the Pen Simple Collapse Directive by Fu Cheng (@alexcheng) on CodePen.

AngularJS - Restrict Access to Routes by Checking User Login

In web application, it's common that some pages are only available to logged-in user. This post shows how to restrict certain AngularJS routes to logged-in users. ui-router supports resolve property to check whether a route is resolved.

First we need a function returns promise to check whether a user is logged-in or not.

In the code above, create a deferred object using $q's$q.defer(). If a user object already exists, the deferred is resolved immediately. If not, a HTTP request is sent to server to get current logged-in user's information. If server returns user object, resolve the deferred, otherwise reject the deferred and redirect user to login page.

Then use checkLoggedOut function in ui-router state definition as below.

$stateProvider.state('upload',
  url: '/restricted'
  templateUrl: 'restricted.html'
  controller: 'DemoCtrl'
  resolve:
    loggedin: checkLoggedOut
)

Add checkLoggedOut to all states restricted to logged-in users.

AngularJS - Handle Session Timeout in JavaScript

If you have a web page which updates itself using Ajax background refresh tasks, when the user's session is timed-out, the response of the refreshing Ajax request will be a 302 redirect to log-in page. But the Ajax request may not be able to handle that and simply fails. The user may not see the updated results. In this case, web page should detect the session timeout and redirect the user to login page.

When using AngularJS's $http service for Ajax request, it's very simple to handle session timeout. All you need to do is to add a $http interceptor and handle the response. See CoffeeScript code below.

angular.module('myModule', [])  
  .factory('sessionTimeoutInterceptor', [() ->
    response: (response) ->
      if angular.isString(response.data) && response.data.indexOf('Log in') != -1
        window.location.reload()
      response
  ])
  .config(['$httpProvider', ($httpProvider) ->
    $httpProvider.interceptors.push('sessionTimeoutInterceptor')
  ])

In the code above, an interceptor sessionTimeoutInterceptor is added. This interceptor will check Ajax response. If response is a string and it contains certain text, e.g. Log in, then this means the session has timed-out. In this case, just refresh the page. The user will be redirected to log in page.

AngularJS and Rails 4

After you have created a Rails 4 project and want to use AngularJS for the front-end development, this post can provide some tips.

Use Bower

It's a common practice to use Bower to manage front-end dependencies. Bower should also be used in Rails development. After Bower is installed, create .bowerrc in project root directory to specify directory to put dependencies.

{
  "directory": "vendor/assets/components"
}

Then update config/application.rb file to include Bower components. Add following line to your application configuration.

config.assets.paths << Rails.root.join('vendor', 'assets', 'components')  

Then you can add dependencies using bower.json or bower install angular --save.

To use those dependencies, add the path to app/assets/javascripts/application.js, like below.

//= require angular/angular
//= require angular-resource/angular-resource
//= require angular-ui-router/release/angular-ui-router

AngularJS should be loaded successfully now.

AngularJS Templates

When using AngularJS, it's common to have HTML partial templates. These templates are loaded using templateUrl or template. If using templateUrl, these template files can be put into public directory.

If you don't want to put template files into public directory, template can also be loaded using JavaScript. Template files are put into Rails views directory as partials. In the main view, e.g. index.html.erb, render partials as below. Template's file name is _mycomp.html.

<script type="text/ng-template" id="mycomp.html">  
  <%= render partial: "mycomp" %>
</script>  

Then in AngularJS, use mycomp.html as templateUrl.

RequireJS text can also be used. Use text!mycomp.html to load template as a variable, then use as value of template in AngularJS.

AngularJS - Fix "Referencing DOM nodes in Angular expressions is disallowed"

When using AngularJS, sometimes you may see this error "Referencing DOM nodes in Angular expressions is disallowed". This may be caused by returning a jQuery expression in your scope functions.

For example, following code will have this issue.

angular.module('test', [])  
    .controller('thing', ['$scope', function ($scope) {
        $scope.action = function() {
            return $("#hello").text("World");
        };
    }]);

This is common when using CoffeeScript, because CoffeeScript adds a return statement by default.

For example, when using CoffeeScript, it's common to have code like this:

$scope.action = () ->
  $('#hello').text('world')

The code above will have this issue. A simple fix for this is:

$scope.action = () ->
  $('#hello').text('world')
  ''

AngularJS - Simple Input Text Count

With AngularJS's two-way data binding, it's very easy to count the characters while user is typing. Usually this will need to use JavaScript to watch keyup event on input or textarea elements. But with AngularJS, it's very simple. No JavaScript is required.

As code shown below, use ng-model to bind textarea to model message. Once message is changed by user input, {% raw %}{{ message.length }}{% endraw %} will display characters count.

<div>{% raw %}{{ message.length }}{% endraw %} of 120</div>  
<textarea ng-model="message" cols="30" rows="10"></textarea>  

See this JSFiddle for the code.

Introduction to iframe shim

iframe shim is an old technique which emerges from old days when browser plugins were popular. When a brower plugin is added to the browser window, it opens a hole in the browser window. If you try to show any other content in the plugin's area, the content will be hidden behind the plugin and won't show up.

To show content above plugin window, you need to create an iframe shim. iframe element can display above plugin window. So it can be used as a layer between plugin window and actual content. The iframe's size is the same as the actual content, but with a lower z-index value.

A typical usage of iframe shim has following structure:

<div class="iframeshim-container">  
  <div class='content-container'>
    <!-- Acutal content -->
  </div>
  <iframe class="shim-iframe" frameborder="0" scrolling="no"></iframe>
</div>

Certain CSS is required to make sure the iframe is the same as actual content and displays below the actual content. The z-index of iframe is a very low value -10000.

.iframeshim-container {
  z-index: 100;
  position: relative;
}

.iframeshim-container .content-container { 
  margin: 0;
  padding: 0;
  overflow: hidden;
  width: 100%;
  height: 100%;
}

.iframeshim-container .shim-iframe {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: -10000;
}

Please note, the HTML structure of iframe shim is very important. Make sure it uses the same structure as the HTML markup shown above. For example,

<div class="iframeshim-container">  
  <div class='content-container'>
    <p>Sample content</p>
  </div>
  <iframe class="shim-iframe" frameborder="0" scrolling="no"></iframe>
</div>

Use with Google Earth web plugin

In the code below, <div> with id google-earth takes up the whole space of parent element. Without iframe shim, <p> with text Hello world won't appear. After using iframe shim, the text will show above the plugin window.

<div>  
  <div id="google-earth">
  </div>
  <div class="iframeshim-container">
    <div class='content-container'>
      <p>Hello world</p>
    </div>
    <iframe class="shim-iframe" frameborder="0" scrolling="no"></iframe>
  </div>
</div>

jQuery plugin

I created a simple jQuery plugin jquery-iframeshim. This plugin can be used to wrap any HTML element into an iframe shim. For example,

$('#banner1').iframeShim({
  classNames: 'shim1'
});

AngularJS - Use $q for Asynchronous Operations

AngularJS's $q is a promise/deferred implementation.
It's a nice tool to implement asynchronous operations. Below is a small example to show how to use $q.

HTML file

<div ng-app>  
    <div ng-controller="DeferredCtrl">
        <label>Delay:</label>
        <input ng-model="delay"></input>
        <button ng-click="go()">Go</button>
        <input value="{{result}}"></input>
        <div ng-show="error" style="color:red;">{{ error }}</div>
    </div>
</div>  

Controller

function DeferredCtrl($scope, $q) {  
    $scope.delay = 3000;
    $scope.result = '';
    $scope.go = function() {
        var deferred = $q.defer();
        window.setTimeout(function() {
            var success = Math.random() > 0.5;
            if (success) {
                deferred.resolve('done');
            }
            else {
                deferred.reject('fail');
            }
        }, $scope.delay);
        $scope.error = '';
        deferred.promise.then(function(result) {
            $scope.result = result;
        }, function(error) {
            $scope.error = error;
        });
    }
}

In the controller, use $q.defer() to create a deferred object. This deferred object is used by client of asynchronous operation.
Once the asynchronous operation is done, use deferred object's resolve function to return the result.
If the asynchronous operation failed, use deferred object's reject function to return the error.

promise property of deferred object can be used to handle result of the asynchronous operation. Use promise object's then(successCallback, errorCallback, notifyCallback) to handle the result.

AngularJS - Delay Controller Initialization for Asynchronous Operations

In an AngularJS web application, a controller may require some data for initialization,
but this data needs to be retrieved from server. To meet this requirement, one solution is that after controller is initialized,
use $http service to get the data and returns a deferred. But this will change the usage of data to asynchronous operations,
which may not be possible for all scenarios. Developers usually prefer to synchronous operations. Another solution is to delay initialization of controller
until data is retrieved from server successfully.

To do this, we need to use a run service to start retrieval of the data.

angular.module('services', [])  
  .run(['$rootScope', 'dataService', ($rootScope, dataService) ->
    dataLoaded = false
    $rootScope.dataReady = false

    dataService.loadData().then(() ->
      dataLoaded = true
    )

    $rootScope.$watch(() ->
      dataLoaded
    , (ready) ->
      $rootScope.dataReady = ready
    )
  ])

After data is loaded, dataReady in $rootScope will be changed to true.

Then in controller,

angular.module('controllers', ['services'])  
  .controller('MyCtrl', ['$scope', ($scope) ->

    $scope.$watch('dataReady', (ready) ->
      init() if ready
    )

    init = () ->
      console.log('Init')
  ])

Controller watches change of dataReady and start initialization after its value changed to true.

Source code is also available in GitHub Gist.

Use resolve

If this controller is used as a route, then resolve property of $routeProvider can be used.

angular.module('myModule', [])  
  .config(['$routeProvider', 'dataService', ($routeProvider, dataService) ->
    $routeProvider
     .when('/path',
        templateUrl: 'tmpl.html'
        controller: 'MyController'
        resolve:
          data: () ->
            dataService.loadData()
  ])

ui-router also supports resolve property.

Gridlines for Google Maps

View GitHub repo

This is a small JavaScript file to create latitude/longitude gridlines over Google Maps.

The original version was made by Bill Chadwick and then other contributors continued to improve it. Refer to the file's comment for more information.

I made some improvements and made this library available in Bower.

  • Fix JSHint errors.
  • Fix the issue that this library causes map to freeze in Chrome/Safari on Mac.

Install

Use bower install google-maps-gridlines to install using Bower.

How to use

To show gridlines on Google map, use code like this

var mapOptions = {  
    center: new google.maps.LatLng(-42, 174),
    zoom: 5
};
var map = new google.maps.Map(document.getElementById("map-canvas"), mapOptions);  
var gridlines = new Graticule(map);  

Sample Page

To view example usage, check out test/simple-map.html page. You can start a simple HTTP server using python -m SimpleHTTPServer and view this sample page.