AngularJS: show progress of a form by counting the number of disabled fields

Let say we have a survey with many questions. Some questions are enabled and some disabled at first and this keeps changing based on what is selected or entered. At any given time we want to know the count of all fields in a form, how many of them are disabled and how many still required. The number of all and required fields is easy as AngularJS keeps an array of all form elements. Disabled elements are something else.

Counting disabled fields can be a challenge if many radio buttons are used, as every radio button is counted as one element.

Lets take this code for example:

 <!doctype html>
 <html>
    <head>    
        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js"></script>
    </head>
    <body>
    
    <form>
     <input type="radio" name="typeComputer2" disabled> 1<br>
     <input type="radio" name="typeComputer2" disabled> 2<br>
     <input type="radio" name="typeComputer2" disabled> 3<br>
    </form>           
    <div id="res"></div>

    <script>
      var bla = $(':disabled').length;
      $('#res').text("Number of disabled elements: " + bla);
    </script>
    </body>
 </html>

Will produce this:

The number of disabled elements is 3. But in fact this is the same group of radio buttons and could be counted as one element (only one radio button can be selected at once).

Lets look at the solution using AngularJS.

We have this HTML document with 3 form elements.

  • First one is 3 radio buttons that are disabled and not required until the checkbox is selected
  • Second is a required radio button
  • Third is a required input field
<html ng-app>
  <head>
     <script src="http://code.angularjs.org/1.1.0/angular.min.js"></script>
     <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js"></script>
     <script src="controller.js"></script>
  </head>
  <body>           
    <div ng-controller="ctrlRead">

      <form name="survey">
        <input type="radio" name="n1" value="1" ng-model="s.el1" 
                        ng-required="s.el2" ng-disabled="!s.el2"> 1<br>
        <input type="radio" name="n1" value="2" ng-model="s.el1" 
                        ng-required="s.el2" ng-disabled="!s.el2"> 2<br>
        <input type="radio" name="n1" value="3" ng-model="s.el1" 
                        ng-required="s.el2" ng-disabled="!s.el2"> 3<br><br>

        <input type="checkbox" name="n2" ng-model="s.el2" ng-required="true"> a<br><br>

        <input type="text" name="n3" ng-model="s.el3" ng-required="true">
      </form>           

      <div id="res"> 
        Disabled: {{eDisabled}} <br>
        All: {{eAll}}  <br>
        Required: {{eRequired}} <br>
        Done in %: {{eDonePercent}} <br>
        Valid: {{survey.$valid}}
      </div>

    </div>
  </body>
</html>

Now we need to write our controller.js

function ctrlRead($scope, $timeout) {

  //a variable to store our survey data
  var s = {}; 
  $scope.s = s;

  //watch for the sur variable to change
  $scope.$watch("s", function () {
     //timeout to fire events 100ms later 
     //for DOM to register the changes
     $timeout($scope.countPercentageDone, 100);
  }, true);

  $scope.countPercentageDone = function() {      
    var requiredTrue=0;
    var requiredFalse=0;
    var disabledElementNames = new Array();    
 
    if ($scope.survey) { 
      //this traverses the form elements stored in form name
      $.each($scope.survey, function(index, value) {
        //survey holds other elements ... 
        //only form elements have $error object
        if ($scope.survey[index].$error) { 
          if ($scope.survey[index].$error.required == true) {
            requiredTrue++;
          } else {
            requiredFalse++;
            //check if the form element is disabled
            if ($("[name="+$scope.survey[index].$name+"]:disabled")) {
              //if it is and it is not just element filled in 
              //element also become disabled once filled in!!
              if ($("[name="+$scope.survey[index].$name+"]:disabled").length != 0) { 
                disabledElementNames.push($scope.survey[index].$name);
              }
            }
          }
        }
      });
    }

    $scope.eDisabled = disabledElementNames.length;
    $scope.eAll = requiredTrue+requiredFalse;
    $scope.eRequired = requiredTrue;
    $scope.eDonePercent = 100-((requiredTrue*100)/(requiredTrue+requiredFalse-disabledElementNames.length));
  }

};

To summarise the code:

  • We traverse all form elements in the $scope.survey array.
  • Because this array contains other elements as well as form elements, we have to look only at those that have an $error object defined.
  • In all elements that are not required we look for those that have lenght > 0.
    We do this because elements that we fill in also become disabled by AngularJS.

This example is also available in jsfiddle.

For the progress bar we could use bootstrap:

<div class="progress">      <div class="bar" style="width: {{eDonePercent}}%"></div> </div>

or any of the solutions I wrote about here to support older browsers.

A big thanks to Tine for all the suggestions and help.

Bootstrap progress bar in 100 images for older browsers

These are assumptions taken here:

  • Bootstrap progress bars are nice
  • They do not work in IE 6-9 (I really wonder why 24% of web users don't upgrade their browsers):
    "Progress bars use CSS3 gradients, transitions, and animations to achieve all their effects. These features are not supported in IE7-9 or older versions of Firefox."
  • I needed a progress bar for a web form that would work in all browsers
  • I'm calculating how much of a web form is completed with AngularJS and change the progress bar on he fly.

Let's start.

1. Create 100 progress bars using a php loop and a Bootstrap progress bar code:

 <!doctype html>
 <html>
    <head>    
        <link rel="stylesheet" href="lib/bootstrap/css/bootstrap.min.css">
        <script src="lib/bootstrap/js/bootstrap.min.js"></script>
    </head>
    <body>

        <div id="progress_bar" style="width: 500px; margin: 10px auto 0 auto">
            <?php
            for ($i=0; $i<101; $i++) { 
                echo '  <div class="progress" style="margin: 2px">
                            <div class="bar" style="width: '.$i.'%"></div>
                        </div>';
            }                    
            ?>
        </div>
    </body>
 </html>

It is possible to use other colours and stripes, etc ... but I'll use simplest progress bar. Keep in mind that my outer div is 500px wide as this is the size of a progress bar I wanted.

2. Crop the progress bars from a web page produces by the above code

The simplest way is to use Awesome screenshot extension for Chrome, Safari or Firefox. It is also good to cut down the image size with e.g. re-saving it for the web so it would be smaller in size (not resolution).

I ended up with this image (original size can be downloaded here):


This can be already the end of the story. The only thing wee need is to use this image as a background of a div and slide up or down based on the percentage we want to show.

3. Show the progress bar

3.1 Using CSS background-position and AngularJS binding

 <div style="float: left;">Progress: </div>
 <div style="width: 500px; height: 22px; float: left;
                  background-image:url('progressbar_small.png'); 
                  background-position:0px -{{Math.round(22*(percentage))}}px;">
 </div>

Each progress bar is 22px high and I multiply it with the percentage I calculate based on how much of the form is filled in. I also use the Math JS in binding for multiplication (maybe there's a better way but I'm not aware of any). To do this I had to define the Math object in my $scope this way $scope.Math = window.Math; 

For some reason the above did not work in IE 8 (which I really care about). Although it apparently works on some web sites. So i decided to slice up the above image in 100 images.

3.2 Using individual images

This is not the optimum solution as for every change there is an Ajax get call to retrieve a desired image. We did this only once in the above example.

This procedure was also explained in a previous post.

First I sliced up the image with ImageMagick using a crop flag (the second command should be in one line) using a bash one line script:

 mk@here/$ ls
 progressbar_small.png

 mk@here/$ for((i=0; i<101; i++)) ; do n=$[i*22]; \ 
          convert progressbar_small.png -crop 500x22+0+$n +repage progressbar${i}.png; \
          done

 mk@here/$ ls
 progressbar0.png    progressbar32.png  progressbar56.png  progressbar7.png
 ...
 progressbar2.png    progressbar53.png  progressbar77.png  progressbar_small.png

The -crop flag slices up images 500px wide and 22px high and starts at 0px on the X axis and goes down 22*i where i runs from 0 to 100. The end results are 100 images.

And finally the HTML bit:

 <div style="float: left;">Progress: </div>
 <div style="width: 500px; height: 22px; float: left;">
     <img src=progressbar{{Math.round(22*(percentage))}}.png">
 </div>

All  images are available in this zip file.

I hope this can help someone.

Any other suggestions greatly appreciated.

A big thank you to Tine for all the suggestions and help.

Slice up an image in equal slices with ImageMagick or a progress bar in 100 images

Let's assume we need a progress bar in 101 images where each shows a particular percentage form 0 to 100. How to create such image will be explained in the next post (not ready yet at the time of writing). I already prepared the image here.

The easiest way to slice up an image is in a command line using ImageMagick's a crop flag. The first command shows the image file we are going to slice. The second command (which should be in one line) is a simple a bash for loop and in each iteration we create a new slice. The third command just lists files that we created.

 mk@here/$ ls
 progressbar_small.png

 mk@here/$ for((i=0; i<101; i++)) ; do n=$[i*22]; \ 
           convert progressbar_small.png -crop 500x22+0+$n +repage progressbar${i}.png; \
           done

 mk@here/$ ls
 progressbar0.png    progressbar32.png  progressbar56.png  progressbar7.png
 ...
 progressbar2.png    progressbar53.png  progressbar77.png  progressbar_small.png

The -crop flag slices up images 500px wide and 22px high and starts at 0px on the X axis and goes down 22*i where i runs from 0 to 100. The end results are 100 images.

All images are available in this zip file.

Bash - looping or capturing a multiple line command output

I tried to loop a command which returns a multi-line result in a bash script. The problem was that the quoted command returned ONE line instead of each line separately. So I ended with one line consisted of multi-line result while I was expecting that the quotes would preserve the spacing. Without quotes I got each "word" in a new line while I was expecting everything in one line (without quotes should replace multiple blanks, tabs and newlines with a single space). Here is the "faulty" script:

#for line in `ls -l`
#for line in $(ls -l)
for line in "$(ls -l)" 
do
    echo $line"XXX"    
done

Not quite sure if this is due to OS X (or BSD) environment but I got around it by piping the command to the while loop:

(ls -l) | while read line
do
     echo $line"xxx"
done

or

while read line; do
    echo $line"xxx"
done < <(ls -l)

PHP - get favicon from an URL

I recently commented on Alan's blog about getting favicon from and URL. The simplest way is getting a domain of an URL and adding "favicon.ico" at the end. The problem arises if:

  • the favicon is not on the root on the host name
  • it has an uncommon name
  • it is not in MS ICO format which (nowadays PNG is very common).

Finding the <link rel="icon"> or <link rel="shortcut icon"> in the DOM of an external URL is hardly possible in Javascript for security reasons (look at the XMLHttpRequest for possibilities). One way of doing it is to access external URLs through proxy.

The simpler way is using some server side scripting language such us PHP and a DOM (HTML) parser. I just finished the script and it worked on a few given URLs (with keeping in mind that it could be improved). I deliberately first look at the DOM of a document (see the first bullet above) as for example a personal web page  http://osebje.famnit.upr.si/~mkljun/ might have a different favicon as the main http://osebje.famnit.upr.si/ server page. Here's the code:

<?php
function getFavicon ($url) {
    $file_headers = @get_headers($url);
    $found = FALSE;
    // 1. CHECK THE DOM FOR THE <link> TAG
    // check if the url exists - if the header returned is not 404
    if($file_headers[0] != 'HTTP/1.1 404 Not Found') {
        $dom = new DOMDocument();
        $dom->strictErrorChecking = FALSE;
        @$dom->loadHTMLfile($url);  //@ to discard all the warnings of malformed htmls
        if (!$dom) {
            $error[]='Error parsing the DOM of the file';
        } else {
            $domxml = simplexml_import_dom($dom);
            //check for the historical rel="shortcut icon"
            if ($domxml->xpath('//link[@rel="shortcut icon"]')) {
                $path = $domxml->xpath('//link[@rel="shortcut icon"]');
                $faviconURL = $path[0]['href'];
                $found == TRUE;
                return $faviconURL;
            //check for the HTML5 rel="icon"
            } else if ($domxml->xpath('//link[@rel="icon"]')) {
                $path = $domxml->xpath('//link[@rel="icon"]');
                $faviconURL = $path[0]['href'];
                $found == TRUE;
                return $faviconURL;
            } else {
                $error[]="The URL does not contain a favicon <link> tag.";
            }
        }

        // 2. CHECK DIRECTLY FOR favicon.ico OR favicon.png FILE
        // the two seem to be most common
        if ($found == FALSE) {
            $parse = parse_url($url);
            $favicon_headers = @get_headers("http://".$parse['host']."/favicon.ico");
            if($favicon_headers[0] != 'HTTP/1.1 404 Not Found') {
                $faviconURL = "/favicon.ico";
                $found == TRUE;
                return $faviconURL;
            }
            $favicon_headers = @get_headers("http://".$parse['host']."/favicon.png");
            if($favicon_headers[0] != 'HTTP/1.1 404 Not Found') {
                $faviconURL = "/favicon.png";
                $found == TRUE;
                return $faviconURL;
            }
            if ($found == FALSE) {
                $error[]= "Files favicon.ico and .png do not exist on the server's root."
            }
        }
    // if the URL does not exists ...
    } else {
        $error[]="URL does not exist";
    }

    if ($found == FALSE && isset($error) ) {
        return $error;
    }
}

// URL in one line 
$tempurl = 'http://stackoverflow.com/questions/1732348/regex-match-open-tags
-except-xhtml-self-contained-tags/1732454#1732454';
$result = getFavicon ($tempurl);
echo $result;
?>

However, the script is very slow and parsing badly structured DOMs returns a bucketful of warnings. Hence the @ before $dom->loadHTMLfile($url).

Although the slowness of the script can be accounted to waiting for server to respond, I wondered if computing times could be improved (see the measured times below).

Another way of finding the appropriate <link> tag is to read the file line by line (assuming the link tag is in one line). I know, I know ... but the <link rel="icon"> is at the beginning of the file and we could exit the loop when we find it. Here's the solution echoing the result (note that here is just the changed if sentence from the above function):

    //check if the url exists
    if($file_headers[0] != 'HTTP/1.1 404 Not Found') {
        //open the pointer to the file 
        $handle = @fopen($url, "r");
        //while the file is not end of file
        while (!feof($handle)) {
            //read next line
            $buffer = fgets($handle, 4096);
            if (strstr($buffer, '<link')) {
                if (strstr($buffer, 'icon')) {
                    $doc=new DOMDocument();
                    $doc->loadHTML('<html><head>'.$buffer.'</head><body></body></html>');
                    $domxml=simplexml_import_dom($doc); 
                    $path=$domxml->xpath('//link');
                    $faviconURL = $path[0]['href'];
                    $found == TRUE;
                    echo $faviconURL;
                    //exit the loop
                    break;
                }
            }
        }
    } 

This version was a bit faster (see user and system times below). I also thought why not giving the regular expressions a try. I know, I know ... regular expression are not meant to parse HTML. But as we know what we are looking for ...

    //check if the url exists
    if($file_headers[0] != 'HTTP/1.1 404 Not Found') {
        $handle = @fopen($url, "r");
        while (!feof($handle)) {
            $buffer = fgets($handle, 4096);
            if (strstr($buffer, '<link')) {
                if (strstr($buffer, 'icon')) {
                    preg_match_all('/href=["\']([^"\']*)["\']/i',$buffer, $array);
                    echo print_r($array);
                    break;
                }
            }
        }
    }

The third solution is comparable to the second. However, the response time from the server was quicker?!? Albeit still slow ... maybe I'm missing something ... but have no time at the moment ... Also, the # of tries I tested each script (around 20) is low to draw any conclusion.

Here are some measured times of running these scripts:

mkljun@pim:~$ time php getFavicon.php 
http://cdn.sstatic.net/stackoverflow/img/favicon.ico
real    0m26.881s
user    0m0.048s
sys    0m0.036s
mkljun@pim:~$ time php getFavicon.php 
http://cdn.sstatic.net/stackoverflow/img/favicon.ico
real    0m21.531s
user    0m0.052s
sys    0m0.028s
mkljun@pim:~$ time php getFavicon.php 
http://cdn.sstatic.net/stackoverflow/img/favicon.ico
real    0m31.562s
user    0m0.052s
sys    0m0.024s
mkljun@pim:~$ time php getFavicon2.php 
http://cdn.sstatic.net/stackoverflow/img/favicon.ico
real    0m26.080s
user    0m0.044s
sys    0m0.008s
mkljun@pim:~$ time php getFavicon2.php 
http://cdn.sstatic.net/stackoverflow/img/favicon.ico
real    0m25.918s
user    0m0.024s
sys    0m0.028s
mkljun@pim:~$ time php getFavicon2.php 
http://cdn.sstatic.net/stackoverflow/img/favicon.ico
real    0m25.984s
user    0m0.032s
sys    0m0.020s
mkljun@pim:~$ time php getFavicon3.php 
            [0] => http://cdn.sstatic.net/stackoverflow/img/favicon.ico
real    0m20.954s
user    0m0.028s
sys    0m0.024s
mkljun@pim:~$ time php getFavicon3.php 
            [0] => http://cdn.sstatic.net/stackoverflow/img/favicon.ico
real    0m26.077s
user    0m0.032s
sys    0m0.020s
mkljun@pim:~$ time php getFavicon3.php 
            [0] => http://cdn.sstatic.net/stackoverflow/img/favicon.ico
real    0m20.884s
user    0m0.028s
sys    0m0.028s