angularjs

10 posts

AngularJS - Insert Text at Caret Position

In AngularJS, if you want to insert text at current caret position, you can use following service. The code is written in CoffeeScript.

angular  
  .module('text-insert', [])
  .service('TextInsert', () -> 
    {       
      insert: (input, text) ->
        return if !input
        scrollPos = input.scrollTop
        pos = 0
        browser = if (input.selectionStart || input.selectionStart == '0') then 'ff' else (if document.selection then 'ie' else false)
        if browser == 'ie'
          input.focus()
          range = document.selection.createRange()
          range.moveStart('character', -input.value.length)
          pos = range.text.length
        else if browser == 'ff'
          pos = input.selectionStart
        front = (input.value).substring(0, pos)
        back = (input.value).substring(pos, input.value.length)
        input.value = front + text + back
        pos = pos + text.length
        if browser == 'ie'
          input.focus()
          range = document.selection.createRange()
          range.moveStart('character', -input.value.length)
          range.moveStart('character', pos)
          range.moveEnd('character', 0)
          range.select()
        else if browser == 'ff'
          input.selectionStart = pos
          input.selectionEnd = pos
          input.focus()
        input.scrollTop = scrollPos
        angular.element(input).trigger('input')
        ''
    }
  )

The first argument of insert method is the raw DOM node, can be input or textarea. The second argument is the text to insert. See following code about how to use it.

TextInsert.insert(angular.element('#input1')[0], 'hello')  

This AngularJS service is based on another CodePen project.

A complete example can be find in this CodePen project. If you want to use JavaScript, you can click 'View Compiled' in CodePen to see the compiled JavaScript code.

See the Pen Angular Text Insert at Caret Position by Fu Cheng (@alexcheng) on CodePen.

AngularJS - Features Toggle with Grunt Build

Background

Spring Boot back-end with AngularJS front-end.

Scenario

Our product has two versions: lite version and standard version. Some features are only available in standard version. So some UI components need to be hidden in lite version. This is controlled by build process. By passing different flags to the build process, different versions can be built. Front-end code uses the same flag to show/hide different components.

Solution

Install grunt-ng-constant and load task grunt-ng-constant in Grunt config.

npm install grunt-ng-constant --save-dev  

Then add ngconstant config in Gruntfile. In the config below, I defined two environments, development and production. All environment-related configurations are put into ENV object. distType is the type I want to specify different release versions. In development build, this value is set to standard. In production build, this value is set to grunt.option('distType'), so it's controlled by command line arguments when Grunt is invoked.

ngconstant: {  
  options: {
    space: '  ',
    wrap: 'define(["angular"], function(angular){ \n return {{ "{%= __ngModule " }}%} \n\n });',
    name: 'config'
  },
  development: {
    options: {
      dest: '<%= appConfig.build %>/scripts/config.js'
    },
    constants: {
      ENV: {
        name: 'development',
        apiEndpoint: 'http://localhost:8080/',
        distType: 'standard'
      }
    }
  },
  production: {
    options: {
      dest: '<%= appConfig.build %>/scripts/config.js'
    },
    constants: {
      ENV: {
        name: 'production',
        apiEndpoint: '/',
        distType: grunt.option('distType')
      }
    }
  }
}

Add ngconstant:production to the list of production build tasks. To build a lite version, use grunt build --distType=lite. To build a standard version, use grunt build --distType=standard.

grunt-ng-constant generates the config.js file in specified directory. Include this file using <script> or load it using RequireJS.

define(['angular', 'config'], (angular, config) ->  
  angular.module('myApp', (ENV) ->
    // Use ENV to check version
  )
)

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.

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.