web

15 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 - 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'
});

CoffeeScript Notes and Tips

CoffeeScript is a popular language used by many frontend developers, I collected some useful notes and tips.

Find first/last element in array

CoffeeScript's destructuring assignment can be used to easily get array's first and last element.

[first, ..., last] = 'a,b,c'.split(',')
[first, ...] = 'a,b,c'.split(',')
[..., last] = 'a,b,c'.split(',')

Conditional(Tenary) operator

CoffeeScript has no conditional(tenary) operator ?:, use following instead:

valid = if length > 0 then true else false  

and it will be compiled to JavaScript like this:

var valid;

valid = length > 0 ? true : false;  

Loop with condition

Given an array, it's very easy to loop each element and process it, like below:

process(item) for item in items  

If you want to only process certain elements, you can use following code:

process(item) for item in items when isValid(item)  

Bind this using =>

Using CoffeeScript's fat arrow (=>), we don't need to use another
variable to capture this any more.

elem.addEventListener('onclick', () =>  
  @itemSelected()
false)  

Return value

In CoffeeScript, function always return its final value, so you don't need to use return explicitly in last statement.

myFunc = () ->  
  val = 'Hello'
  process(val)
  val

is the same as

myFunc = () ->  
  val = 'Hello'
  process(val)
  return val

Iterate object properties

To iterate objects in CoffeeScript, use for ... of, like below:

for key,value of hash  
  process(key, value)

If you want to iterate only object's own properties, use for own ... of, like below:

for own key,value of hash  
  process(key, value)

and it's compiled to following JavaScript code with hasOwnProperty check.

var key, value,  
  __hasProp = {}.hasOwnProperty;

for (key in hash) {  
  if (!__hasProp.call(hash, key)) continue;
  value = hash[key];
  process(key, value);
}

Use Grunt to build web applications

Web application development is getting much more complicated now. A typical web application may have a lot of JavaScript and CSS files. New techniques like CSS pre-processors and CoffeeScript are also popular. This means that a build process needs to be applied before a web application can be published. Some typical tasks need to be done, including:

  • Compress JavaScript files
  • Generate CSS files
  • Compile CoffeeScript files

Usually we use different tools to run different tasks. Grunt is a Node.js based tool that automates these tasks. Grunt is easy to install, just follow this guide. Typically, you just need to install Node.js and nmp, then type following command to install Grunt CLI.

npm install -g grunt-cli  

After Grunt is installed, you need to create a package.json in the root directory of your web application. In this file, you declare the dependencies of your web application. In the sample application, CoffeeScript, LESS and RequireJS are used, so Grunt plugins to run these tasks are included.

{
  "name": "myapp",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-coffee": "~0.7.0",
    "grunt-contrib-less": "~0.5.1",
    "grunt-contrib-requirejs": "~0.4.0"
  }
}

Then you need to add a Gruntfile.js or Gruntfile.coffee to configure the tasks to build your applications.

module.exports = function(grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    coffee: {
      compile: {
        files : {
          'public/js/main.js' : 'views/main.coffee',
        }
      }
    },
    requirejs: {
      compile: {
        options: {
          baseUrl: "public/js",
          out: "public/js/main-min.js",
          mainConfigFile : 'public/js/main.js',
          optimize: 'uglify2',
          name : 'lib/almond',
          include : ['main']
        }
      }
    },
    less : {
      production: {
        options: {
          paths: ["public/css"],
          yuicompress: true
        },
        files: {
          "public/css/style.css": "views/style.less"
        }
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-coffee');
  grunt.loadNpmTasks('grunt-contrib-requirejs');
  grunt.loadNpmTasks('grunt-contrib-less');

  grunt.registerTask('default', ['coffee', 'requirejs', 'less']);

};

In the above Gruntfile.js, three tasks are configured. The first task is to compile CoffeeScript files. The second task is to use RequireJS to build and compress JavaScript files. The last task is to compile LESS files. You can use grunt coffee, grunt requirejs or grunt less to run these three tasks. Or use grunt to run all three tasks.

Grunt team has developed a lot of plugins for popular tasks. There are also a lot of plugins contributed by the community. All these plugins can be found at here. These plugins are Node.js modules.

Install Grunt on Vagrant VMs

If you are going to install Grunt on Vagrant VMs, make sure you edit Vagrantfile to add following settings:

config.vm.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/v-root", "1"]

More details can be found at here.

地图说微博 - 新浪微博应用

 之前开发了一个基于新浪微博的应用“地图说微博”,后来又做了一些改进,现在来推广一下。欢迎有新浪微博账号的朋友测试使用,觉得不错的话,可以帮忙推广一下。

主要功能:

1)发布地图到微博上。很多时候可能需要在微博上分享一个位置,比如去过的不错的饭馆之类的,通过“发布地图到微博”这个功能就可以完成。有两种方式来选择地图:第一种是输入关键词搜索位置,第二种是直接浏览地图并选择。

2)地图消息墙。这个有点类似游戏的性质。在地图上随便选择一个地方,然后留下你想说的话。

3)在地图上显示微博上的最近更新的消息。

4)在地图上查看你的关注对象和粉丝的位置。

主流网站前端代码分析》系列之序

 博客很久没有更新了,主要是由于有些时候发现确实没什么值得在博客上面来写的,很多就直接写了一条微博。另外自己也花了比较多的时间在文章的写作上面。不过现在打算花点时间用边学边写的态度来认真研究一下主流网站的前端代码,并进行一下详细的分析,找出其中值得学习的地方,也从我个人的观点出发找出一些不足。

分析的目标网站:国内外比较知名的网站

分析的内容:前端代码,包括HTML,JavaScript和CSS,主要以JavaScript为主

分析的方式:查看网站网页的源代码

分析的目标:找出网站上代码中值得学习的地方并记录下来。

具体到每个网站,所包含的内容可能会不一样,写作的方式可能也比较随意。不过目的是为了学习。

Ajax应用开发最佳实践

   本文已经首发于InfoQ中文站,版权所有,原文为《Ajax应用开发:实践者指南》(http://www.infoq.com/cn/articles/ajax-guide),如需转载,请务必附带本声明,谢谢。

    InfoQ中文站是一个面向中高端技术人员的在线独立社区,为Java、.NET、 Ruby、SOA、敏捷、架构等领域提供及时而有深度的资讯、高端技术大会如QCon、免费迷你书下载如《架构师》等。