Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Web Application development

About

Course Goals

  • Build responsive web site which looks good on any device
  • Understand responsive tools
  • Get to know responsive design techniques
  • Build Single Page Application

Challenges of Mobile web development

  • Many different devices with many different browsers and local changes to some of the browsers.
  • Size difference, resolution difference, aspect ratio difference.
  • Network speed difference. 30 Mbps at home vs. 3G (0.4-4 Mbps) vs. 4G (5-50 Mbps)
  • Lack of connectivity: Off-line

About the course

  • HTML

  • CSS

  • Bootstrap

  • JavaScript

  • AngularJS

  • Build front-end to MetaCPAN or to some other web site with public JSON API

  • Communicating with the server

  • Controllers

  • Dependency Injection

  • Directives

  • Filters

  • Services

  • Testing AngularJS based application

Editors

  • Brackets
  • Atom
  • VS Code

Backend

Proxy and Mojolicious Ajax server

plackup

use strict;
use warnings;

use Plack::Builder;
use Plack::App::File;
use Plack::App::Proxy;

use Plack::App::Directory;
my $app = Plack::App::Directory->new({ root => '.' })->to_app;

builder {
    mount "/openweathermap" => Plack::App::Proxy->new(remote => "http://api.openweathermap.org/")->to_app;
    mount "/imdb" => Plack::App::Proxy->new(remote => "http://www.imdb.com/")->to_app;
    mount '/' => $app;
};

HTML and CSS

Emulate devices

In Chrome show the page as if it was on a device.

  • Right click, inspect element, select device

Browserstack screenshots

Mobile or Responsive?

HTML elements

<p> </p>

  • p

  • h1, h2, ... h6

  • ul, ol, li

  • table - tr, th, td

  • div

  • span

  • html - head, body

  • hed - title

Attributes:

  • a href

  • img src

  • id

  • class

HTML form elements

<form method="POST" action="/some/path">
<input type="radio" name="grade">10</input>
<input type="text" name="firstname">

<textarea name="text"></textarea>
<select name="color">
  <option></option>
  <option>Red</option>
  <option>Green</option>
  <option>Blue</option>
</select>
<input type="submit" value="Send">
</form>

Bare HTML

<html>
<head>
  <title>Bare HTML</title>
</head>
<body>
<p>
</p>
<div id="wrapper">
    <div class="first">
      <h2>What is Lorem Ipsum?</h2>
      <p><strong>Lorem Ipsum</strong> is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.</p>
    </div>
    <div class="second">
      <h2>Why do we use it?</h2>
      <p>It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).</p>
    </div><br>

  <div class="first">
    <h2>Where does it come from?</h2>
    <p>Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.</p><p>The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.</p>
  </div>

  <div class="second">
    <h2>Where can I get some?</h2>
    <p>There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.</p>
  </div>
</div>

</body>
</html>

HTML and CSS

<html>
<head>
  <title>HTML + CSS</title>
  <style>
  #wrapper {
      overflow: hidden;
      background-color: lightblue;
  }
  .first {
      width: 50%;
      float:left;
      background-color: red;
  }
  .second {
      overflow: hidden;
      background-color: green;
  }
  </style>
</head>
<body>
<p>
</p>
<div id="wrapper">
    <div class="first">
      <h2>What is Lorem Ipsum?</h2>
      <p><strong>Lorem Ipsum</strong> is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.</p>
    </div>
    <div class="second">
      <h2>Why do we use it?</h2>
      <p>It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).</p>
    </div><br>

  <div class="first">
    <h2>Where does it come from?</h2>
    <p>Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.</p><p>The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.</p>
  </div>

  <div class="second">
    <h2>Where can I get some?</h2>
    <p>There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.</p>
  </div>
</div>

</body>
</html>

Viewport and Media Query for body

  • viewport
  • @media
  • screen
  • min-width
  • max-width
<html>
<head>
  <title>Viewport + Media query for body</title>
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <style>
  body {
      background-color: lightblue;
  }

  @media screen and (min-width: 700px) {
     body {
         background-color: blue;
     }
  }

  @media screen and (min-width: 1200px) {
     body {
         background-color: red;
     }
  }
</style>
</head>
<body>
</body>
</html>

Viewport and Media Query

<html>
<head>
  <title>Viewport + Media query</title>
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <style>
  #wrapper {
      background-color: lightblue;
  }
  .first {
      background-color: red;
  }
  .second {
      background-color: green;
  }
  
  @media screen and (min-width: 700px) {
    #wrapper {
      overflow: hidden;
    }
  
    .first {
      width: 50%;
      float:left;
    }
    .second {
      overflow: hidden;
    }
  }
</style>
</head>
<body>
<p>
</p>
<div id="wrapper">
    <div class="first">
      <h2>What is Lorem Ipsum?</h2>
      <p><strong>Lorem Ipsum</strong> is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.</p>
    </div>
    <div class="second">
      <h2>Why do we use it?</h2>
      <p>It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).</p>
    </div><br>

  <div class="first">
    <h2>Where does it come from?</h2>
    <p>Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.</p><p>The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.</p>
  </div>

  <div class="second">
    <h2>Where can I get some?</h2>
    <p>There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.</p>
  </div>
</div>

</body>
</html>

Pixels (px), em, rem

  • px
  • em
  • rem

Units: px (pixels)

Fixed size on a given screen, If used with Media Queries we have to manually calculate and update each value.

<html>
<head>
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <title>Text units - px</title>
  <link href="page_px.css" rel='stylesheet' />
</head>
<body>
<h1>My Title</h1>
<p>This is the paragraph of our page.</p>
<div class="separator">
</div>

</body>
</html>

html {
   font-size: 10px;
}

.separator {
    width: 100%;
    height: 100px;
    background-color: blue;
}

h1 {
   font-size: 30px;
}

p {
   font-size: 20px;
}

@media screen and (min-width: 800px) {
    .separator {
        height: 150px;
    }
    h1 {
        font-size: 45px;
        text-align: center;
    }
    p {
        font-size: 30px;
        text-align: center;
   }
}

Units: em (The size of Letter M)

Relative to the font-size of the element.

<html>
<head>
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <title>Text units - px</title>
  <link href="page_em.css" rel='stylesheet' />
</head>
<body>
<h1>My Title</h1>
<p>This is the paragraph of our page.</p>
<div class="separator">
</div>

</body>
</html>

html {
   font-size: 10px;
}

.separator {
    width: 100%;
    height: 10em;
    background-color: blue;
}

h1 {
   font-size: 3em;
}

p {
   font-size: 2em;
   /*border: 1em solid red;*/
}

@media screen and (min-width: 800px) {
    html {
        font-size: 15px;
    }
    h1 {
        text-align: center;
    }
    p {
        text-align: center;
   }
}

The problem will be if we add another css directive insied the 'p' directive. eg. 'border 1em solid red'. In this case the 1em will be relative to the font size of the 'p' element and not the font-size of the 'html' element. So within the same 'p' element '2emp' of the 'font-size' and '1em' of the 'border' will be equal.

Units: rem (root em)

Relative to the font-size of the html element.

<html>
<head>
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <title>Text units - px</title>
  <link href="page_rem.css" rel='stylesheet' />
</head>
<body>
<h1>My Title</h1>
<p>This is the paragraph of our page.</p>
<div class="separator">
</div>

</body>
</html>

html {
   font-size: 10px;
}

.separator {
    width: 100%;
    height: 10rem;
    background-color: blue;
}

h1 {
   font-size: 3rem;
}

p {
   font-size: 2rem;
   /*border: 1rem solid red;*/
}

@media screen and (min-width: 800px) {
    html {
        font-size: 15px;
    }
    h1 {
        text-align: center;
    }
    p {
        text-align: center;
   }
}

Viewport width and height

  • vw

  • vh

  • vw = viewport width 100vw is 100% of the viewport

  • vh = viewport height 100vh is 100% of the viewport

Exercise: Responsive HTML

Given an HTML page as in this example with a bunch of images, resize the images to be small and of the same size when the screen is small, but larger as the screen grows. Have at least 2 breaking points.

<html>
<head>
  <title>Thumbnails</title>
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
</head>
<body>
 <ul id="images">
     <li class="image">
         <ul>
               <li class="img-elem"><img src="https://i.ytimg.com/vi/rrWYiPkLW60/maxresdefault.jpg" /></li>
               <li class="text-elem">Tiger</li>
         </ul>
     </li>
     <li class="image">
         <ul>
               <li class="img-elem"><img src="http://img.xooimage.com/files110/3/6/9/cam3-49f169f.jpg" /></li>
               <li class="text-elem">Cameleon</li>
         </ul>
     </li>
     <li class="image">
         <ul>
               <li class="img-elem"><img src="http://kids.nationalgeographic.com/content/dam/kids/photos/animals/Reptiles/H-P/komodo-dragon-head-on.jpg" /></li>
               <li class="text-elem">Komodo Dragon</li>
         </ul>
     </li>
     <li class="image">
         <ul>
               <li class="img-elem"><img src="https://s-media-cache-ak0.pinimg.com/236x/aa/5a/e3/aa5ae3f068e8453615f46b093dc02491.jpg" /></li>
               <li class="text-elem">Hippo</li>
         </ul>
     </li>
     <li class="image">
         <ul>
               <li class="img-elem"><img src="http://d3lp4xedbqa8a5.cloudfront.net/s3/digital-cougar-assets/AusGeo/2014/08/08/49110/Leafy-Sea-Dragon---Rapid-Bay-Smaller_WEB.jpg" /></li>
               <li class="text-elem">Sea Dragon</li>
         </ul>
     </li>
</ul>
</body>
</html>


CSS

CSS - embed in HTML file

<style>
</style>

CSS - include from external file

<link href="css/style.css" rel="stylesheet">

CSS syntax

  • In CSS syntax looks like this:
selector {
   attribute: value;
}

CSS attributes

background-color: red;
color: #AFDE37;
text-align: center;
font-family: verdana;
font-size: 40px;

CSS selectors

html-element {
}

#id {
}

.class {
}

HTML/CSS frameworks

AngularJS

What is AngularJS

MVC Model-View-Controller and Angular (MVW or MV*)

  • Model (the data)
  • View (the HTML)
  • Controller (the code combining the two)

Learning curve

missing image: feelings_about_angularjs_over_time.png

source

Major Parts of AngularJS

Model

  • JavaScript data structures

View

  • Template compiled to a view
  • Expressions {{ something }}
  • Directives (ng-app, ng-repeat)

Whatever

  • Modules
  • Controllers
  • Data binding
  • Scope
  • Filters (date, orderBy) {{ something | filter }}
  • Services ($http, $log, $q)

Getting Started AngularJS

  • angular.min.js
  • {{ }}
  • ng-app
<script src="angular.min.js"></script>
<div ng-app>
  Hello {{ "World" }}
</div>
  • Loading angular.js
  • Add ng-app
  • Add an AngularJS expression

Loading

https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js
angular.min.js

AngularJS

Simple AngularJS expression

  • {{ }}
  • ng-app
<script src="angular.min.js"></script>
<div ng-app>
  Hello Angular {{ 19 + 23 }}
</div>

AngularJS allows a very limited subset of JavaScript expressions.

Variables in AngularJS expressions

<script src="angular.min.js"></script>
<div ng-app>
  {{ x = 23; y= 19; x + y }}
</div>

We don't declare them with var as in plain JavaScript because they are actually attributes of the $scope object Angular maintains for us.

Separate variable assignment and usage into two expressions

<script src="angular.min.js"></script>
<div ng-app>
  <div>
  Result
  {{ x + y }}
  </div>
  <div>
    Assignment:
    {{ x = 23; y= 19 }}
  </div>
  <div>
    Result
    {{ x + y }}
  </div>
</div>
Result 42
Assignment: 19
Result 42

Separate variable assignment and usage into two expressions - fixed

<script src="angular.min.js"></script>
<div ng-app>
  <div>
  Result
  {{ x + y }}
  </div>
  <div>
    Assignment:
    {{ x = 23; y= 19; null}}
  </div>
  <div>
    Result
    {{ x + y }}
  </div>
</div>
Result 42
Assignment:
Result 42

Minimal Hello User: Binding with ng-model

  • ng-model
<script src="angular.min.js"></script>
<div ng-app>
  <input ng-model="name">
  <h1>Hello, {{name}}</h1>
</div>

Full Hello User

Adding all the HTML5 fancy things.

<!DOCTYPE html>
<html ng-app>
  <head>
    <meta charset="utf-8">
    <meta name="viewport"
        content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>Hello User</title>
    <script src="angular.min.js"></script>
  </head>
  <body>
      <input ng-model="name" type="text" placeholder="Your name please">
      <h1>Hello, {{name}}</h1>
  </body>
</html>

AngularJS controller with output

  • ng-controller

  • module

  • $scope

  • Separate JavaScript from the HTML

  • ng-controller

  • angular.module(name, [dependencies])

  • .controller

  • $scope


<script src="angular.min.js"></script>
<script src="hello_world_controller.js"></script>
<div ng-app="HelloWorldApp">
    <div ng-controller="HelloWorldController">
        <h1>{{greeting}}</h1>
    </div>
</div>
angular.module('HelloWorldApp', [])
   .controller('HelloWorldController', ['$scope', function($scope) {
       $scope.greeting = "Hello World";
}]);

AngularJS - Dependency Injection

  • "Dependency injection"

<script src="angular.min.js"></script>
<script src="hello_world_controller_dependency_injection.js"></script>
<div ng-app="HelloWorldApp">
    <div ng-controller="HelloWorldController">
        <h1>{{greeting}}</h1>
    </div>
</div>
angular.module('HelloWorldApp', [])
   .controller('HelloWorldController', function($scope) {
       $scope.greeting = "Hello World";
});

Angular controller with binding

  • ng-model
  • ng-click
<script src="angular.min.js"></script>
<script src="hello_user_controller.js"></script>
<div ng-app="HelloUserApp">
    <div ng-controller="HelloUserController">
        <input ng-model="name"><button ng-click="NameChange()">Update</button>
        <h1>greeting: {{greeting}}</h1>
        <h2>name: {{name}}</h2>
    </div>
</div>
angular.module('HelloUserApp', [])
      .controller('HelloUserController', ['$scope', function($scope) {
          $scope.NameChange = function () {
              $scope.greeting = "Hello " + $scope.name;
          };
      }]);

Add numbers using AngularJS

<script src="angular.min.js"></script>
<div ng-app>
  <input ng-model="a">
  <input ng-model="b">
  <h1>{{ a + b }}</h1>
</div>

Let's try this...

Add numbers in controller

  • ng-keyup
<script src="angular.min.js"></script>
<script src="add_numbers_controller.js"></script>
<div ng-app="AddNumbersApp">
    <div ng-controller="AddNumbersController">
        <input ng-model="a" ng-keyup="AddNumbers()">
        <input ng-model="b" ng-keyup="AddNumbers()">
        <h1>{{ sum }}</h1>
    </div>
</div>
angular.module('AddNumbersApp', [])
    .controller('AddNumbersController', ['$scope', function($scope) {
        $scope.AddNumbers = function() {
            var a = Number($scope.a || 0);
            var b = Number($scope.b || 0);
            $scope.sum = a+b;
        }
}]);

Add numbers using function call

<script src="angular.min.js"></script>
<script src="add_numbers_function.js"></script>
<div ng-app="AddNumbersApp">
    <div ng-controller="AddNumbersController">
        <input ng-model="a">
        <input ng-model="b">
        <h1>{{ AddNumbers() }}</h1>
    </div>
</div>

angular.module('AddNumbersApp', [])
    .controller('AddNumbersController', ['$scope', function($scope) {
        $scope.AddNumbers = function() {
            var a = Number($scope.a || 0);
            var b = Number($scope.b || 0);
            return a+b;
        }
}]);

Add numbers using HTML5

  • input
  • number
<script src="angular.min.js"></script>
<div ng-app>
  <input ng-model="a" type="number">
  <input ng-model="b" type="number">
  <h1>{{ a + b }}</h1>
</div>

Add numbers ng-init

  • ng-init
<script src="angular.min.js"></script>
<div ng-app ng-init="a = 19; b = 23;">
  <input ng-model="a" type="number">
  <input ng-model="b" type="number">
  <h1>{{ a + b }}</h1>
</div>

Exercise: In memory counter

  1. Create an application that has a button called 'Increment' and shows a number.
  2. Every time the button is clicked the number increases by 1.
  3. Implement it in a single HTML file without a 'script' tag.
  4. Add another button called 'Decrement'.
  5. When the user clicks that button the counter is decremented.
  6. Still have this in a single HTML file without a 'script' tag.
  7. Change the application now moving everything to a controller.
  8. Both the initialization and the code of increment and decrement.
  9. Disallow negative numbers: If the user clicks on 'decrement' while the counter
  10. is at 0, display a message in a 'div' element that this operation is not allowed.

Solution: In memory counter

  • ng-init
  • ng-click
<script src="angular.min.js"></script>
<div ng-app>
    <button ng-init="counter = 0" ng-click="counter = counter + 1">Increment</button>
    {{counter}}
</div>
counter += 1    would not work

Solution: In memory counter with decrement

  • ng-init
  • ng-click
<script src="angular.min.js"></script>
<div ng-app>
    <div ng-init="counter = 0"></div>
    <button ng-click="counter = counter + 1">Increment</button>
    <button ng-click="counter = counter - 1">Decrement</button>
    {{counter}}
</div>

Solution: In memory counter with controller

<script src="angular.min.js"></script>
<script>
angular.module("CounterApp", [])
    .controller("CounterController", ['$scope', function($scope) {
        $scope.counter = 0;
        $scope.increment = function() {
            $scope.message = "";
            $scope.counter++;
        };
        $scope.decrement = function() {
            if ($scope.counter > 0) {
                $scope.counter--;
            } else {
                $scope.message = "We cannot go below 0";
            }
        };
}])
</script>
<div ng-app="CounterApp">
    <div ng-controller="CounterController">
        <button ng-click="increment()">Increment</button>
        <button ng-click="decrement()">Decrement</button>
        <div>{{counter}}</div>
        <div>{{message}}</div>
    </div>
</div>

Exercise: Calculator

Create a simple calculator in AngularJS. Two input boxed to accept two numbers and 'select' element with the 4 basic operators. As the user changes the operator or changes the values, display the result of the calculation.

Solution: Calculator

<script src= "angular.min.js"></script>
<script src= "calculator.js"></script>

<div ng-app="CalculatorApp" ng-controller="CalculatorController">
  <p><input type="number" ng-model="a"></p>
  <p><input type="number" ng-model="b"></p>
  <p><select ng-model="operator">
        <option>+</option>
        <option>*</option>
        <option>-</option>
        <option>/</option>
     </select></p>
  <p>{{ result() }}</p>
 </div>
angular.module('CalculatorApp', [])
    .controller('CalculatorController', ['$scope', function($scope) {
        $scope.result = function() {
            if ($scope.operator === '+') {
                return $scope.a + $scope.b;
            }
            if ($scope.operator === '-') {
                return $scope.a - $scope.b;
            }
            if ($scope.operator === '*') {
                return $scope.a * $scope.b;
            }
            if ($scope.operator === '/') {
                return $scope.a / $scope.b;
            }
        };
    }]);

AngularJS - Services

Services

  • $scope
  • $log
  • $interval
  • $timeout
  • $messages
  • $filter
  • $http
  • $resource

Console log

  • console.log
angular.module('DemoApp', [])
    .controller('DemoController', function() {
        console.debug("Some debug");
        console.info("Some info");
        console.log("Some log");
        console.warn("Some warning");
        console.error("Some error");
    });

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <script src="angular.min.js"></script>
  <script src="console_log.js"></script>
</head>
<body ng-app="DemoApp" ng-controller="DemoController">
 <h1>Open the console!</h1>
 
</body>
</html>

Logging with $log

  • $log
angular.module('DemoApp', [])
    .controller('DemoController', ['$log', function($log) {
        $log.debug("Some debug");
        $log.info("Some info");
        $log.log("Some log");
        $log.warn("Some warning");
        $log.error("Some error");
    }]);
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <script src="angular.min.js"></script>
  <script src="logging.js"></script>
</head>
<body ng-app="DemoApp" ng-controller="DemoController">
 <h1>Open the console!</h1>
 
</body>
</html>

Turn off logging with $logProvider

  • config
  • $logProvider
angular.module('DemoApp', [])
.config(['$logProvider', function($logProvider) {
    $logProvider.debugEnabled(false); // turns off the calls to $log.debug, but not the others
}])
.controller('DemoController', ['$log', function($log) {
    $log.debug("Some debug");
    $log.info("Some info");
    $log.log("Some log");
    $log.warn("Some warning");
    $log.error("Some error");
}]);
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <script src="angular.min.js"></script>
  <script src="logging_off.js"></script>
</head>
<body ng-app="DemoApp" ng-controller="DemoController">
 <h1>Main Title</h1>
 
</body>
</html>

Showing the correct line number

$log.debug = console.debug.bind(console);

Dependency Injection

angular.module('DemoApp', [])
    .controller('DemoController', ['$scope', '$log', function($scope, $log) {
        console.log($log);
        console.log($scope);
    }]);

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <script src="angular.min.js"></script>
  <script src="dependency_injection.js"></script>
</head>
<body ng-app="DemoApp" ng-controller="DemoController">
 <h1>Open the console!</h1>
 
</body>
</html>

The order of $scope and $timeout in the function call (dependency injection) must match the order in the list before.

Automatic counter with $timeout

  • $timeout
  • setTimeout
<script src="angular.min.js"></script>
<script>
angular.module('CounterApp', [])
    .controller('CounterController', ['$scope', '$timeout', function($scope, $timeout) {
        $scope.counter = 0;
        var updateCounter = function() {
            $scope.counter++;
            $timeout(updateCounter, 1000);
        };
        updateCounter();
    }]);
</script>
<div ng-app="CounterApp">
   <div ng-controller="CounterController">
   {{counter}}
   </div>
</div>

$timeout

Automatic counter with stop button

<script src="angular.min.js"></script>
<script>
angular.module('CounterApp', [])
    .controller('CounterController', ['$scope', '$timeout', function($scope, $timeout) {
        var timer;
        $scope.counter = 0;
        $scope.stopCounter = function() {
            $timeout.cancel(timer);
        };
        var updateCounter = function() {
            $scope.counter++;
            timer = $timeout(updateCounter, 1000);
        };
        updateCounter();
    }]);
</script>
<div ng-app="CounterApp">
   <div ng-controller="CounterController">
   {{counter}}
   <button ng-click="stopCounter()">Stop</button>
   </div>
</div>

Automatic counter with stop and start buttons

<script src="angular.min.js"></script>
<script>
angular.module('CounterApp', [])
    .controller('CounterController', ['$scope', '$timeout', function($scope, $timeout) {
        var timer;
        $scope.counter = 0;
        $scope.stopCounter = function() {
            $timeout.cancel(timer);
            timer = undefined;
        };
        $scope.startCounter = function() {
            if (timer === undefined) {
                updateCounter();
            }
        };
        var updateCounter = function() {
            $scope.counter++;
            timer = $timeout(updateCounter, 1000);
        };
        updateCounter();
    }]);
</script>
<div ng-app="CounterApp">
   <div ng-controller="CounterController">
   {{counter}}
   <button ng-click="stopCounter()">Stop</button>
   <button ng-click="startCounter()">Start</button>
   </div>
</div>

Simple pages

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <script src="../angular/angular.min.js"></script>
  <title>Simple Pages</title>
</head>
<body ng-app>

<div>
  <div>
      <button ng-click="page='first'">First</button>
      <button ng-click="page='second'">Second</button>
  </div>
  
  
  <div ng-init="page='first'"></div>
  
  <div ng-show="page === 'first'">
  <h2>First</h2>
  </div>
  
  <div ng-show="page === 'second'">
  <h2>Second</h2>
  </div>
 
</div>

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

Simple pages with controller

  • ng-show
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <script src="../angular/angular.min.js"></script>
  <script src="simple_pages_controller.js"></script>
  <title>Simple Pages</title>
</head>
<body ng-app="DemoApp" ng-controller="DemoController">
<div>
  <div>
      <button ng-click="goto('first')">First</button>
      <button ng-click="goto('second')">Second</button>
  </div>
  
  <div ng-show="page === 'first'">
  <h2>First</h2>
  </div>
  
  <div ng-show="page === 'second'">
  <h2>Second</h2>
  </div>
 
</div>

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

angular.module('DemoApp', [])
.controller('DemoController', ['$scope', function($scope) {

    $scope.page='first';

    $scope.goto = function(name) {
        console.log("Switching from " + $scope.page + " to " + name);
        $scope.page = name;
    }
}]);

HTML form elements with AngularJS

  • ng-change
  • ng-show
  • select
  • option
  • checkbox
<script src= "../angular/angular.min.js"></script>
<script src= "form.js"></script>

<div ng-app="DemoApp" ng-controller="DemoController">
  <form name="myForm">
      <div>Name: <input ng-model="name"></div>
      <div>Color: 
          <select ng-model="color" ng-change="change_color()">
              <option value="blue">Blue</option>
              <option value="yellow">Yellow</option>
              <option value="green">Green</option>
              <option>White</option>
          </select>
      </div>
      <div>
          <ul>
             <li><input type="checkbox" ng-model="vehicle.bike">Bike</li>
             <li><input type="checkbox" ng-model="vehicle.moped">Moped</li>
             <li><input type="checkbox" ng-model="vehicle.car">Car</li>
          </ul>
      </div>
  </form>
  <hr>
   <div>Name: {{name}}</div>
   <div>Color: {{ color }} My Color: {{ my_color }}</div>
   <div>White is <span ng-show="white">on</span> <span ng-show="! white">off</span></div>
   <div>Vehicles:
      <span ng-show="vehicle.bike">Bike</span>
      <span ng-show="vehicle.moped">Moped</span>
      <span ng-show="vehicle.car">Car</span>
   </div>

</div>
angular.module('DemoApp', [])
.controller('DemoController', ['$scope', function($scope) {

    function set_color() {
        $scope.my_color = $scope.color;
        $scope.white = ($scope.color === 'White');
    }
    $scope.color = "yellow";
    $scope.change_color = set_color;
    set_color();

}]);

Input validation with $messages

  • $messages

Show what happens if we don't add the dependency and/or we don't include the code of the dependency!

<script src= "../angular/angular.min.js"></script>
<script src= "../angular/angular-messages.min.js"></script>
<script src= "form_messages.js"></script>

<div ng-app="DemoApp" ng-controller="DemoController">
  <form name="myForm">
      <div>Name: <input ng-model="name" name="name" required minlength="5" ></div>
      <div class="error" ng-messages="myForm.name.$error">
          <div ng-message="required">The name is required</div>
          <div ng-message="minlength">The name must be at least 5 characters long.</div>
      </div>
  </form>
  <hr>
   <div>Name: {{name}}</div>
</div>

<style>
.error {
    background: #FF6D6D;
}
</style>
angular.module('DemoApp', ['ngMessages'])
.controller('DemoController', ['$scope', function($scope) {

}]);

TODO with AngularJS

  • ng-repeat
  • ng-click
<script src="angular.min.js"></script>
<script src="todo1.js"></script>
<div ng-app="todoApp" ng-controller="todoController">
    <input ng-model="title"><button ng-click="add()">Add</button>
    <ul>
        <li ng-repeat="t in tasks">{{ t }}</li>
    </ul>
</div>
angular.module('todoApp', [])
    .controller('todoController', ['$scope', function($scope) {
        $scope.tasks = [];
        $scope.add = function() {
            $scope.tasks.push($scope.title);
        }
    }]);

  • ENTER does not work
  • Reload removes data

TODO with AngularJS (ENTER to submit and delete item)

  • ng-submit
<script src="angular.min.js"></script>
<script src="todo2.js"></script>
<div ng-app="todoApp" ng-controller="todoController">
    <form ng-submit="add()">
    <input ng-model="title"><button>Add</button>
    </form>
    <ul>
        <li ng-repeat="t in tasks track by $index">{{ t }}
             <button ng-click="delete()">x</button></li>
    </ul>
</div>
angular.module('todoApp', [])
    .controller('todoController', ['$scope', function($scope) {
        $scope.tasks = [];
        $scope.add = function() {
            $scope.tasks.push($scope.title);
        }
        $scope.delete = function() {
            $scope.tasks.splice(this.$index, 1);
        }
    }]);

TODO with AngularJS (localStorage)

  • localStorage.setItem
  • localStorage.getItem
  • JSON.parse
  • JSON.stringify
<script src="angular.min.js"></script>
<script src="todo3.js"></script>
<div ng-app="todoApp" ng-controller="todoController">
    <form ng-submit="add()">
    <input ng-model="title"><button>Add</button>
    </form>
    <ul>
        <li ng-repeat="t in tasks track by $index">{{ t }}
             <button ng-click="delete()">x</button></li>
    </ul>
</div>

angular.module('todoApp', [])
    .controller('todoController', ['$scope', function($scope) {
        var load = function() {
            var raw_data = localStorage.getItem('todo');
            if (raw_data === null) {
                $scope.tasks = [];
            } else {
                $scope.tasks = JSON.parse(raw_data);
            }
        };
        var save = function() {
            localStorage.setItem('todo', JSON.stringify($scope.tasks));
        }

        $scope.add = function() {
            $scope.tasks.push($scope.title);
            save();
        }
        $scope.delete = function() {
            $scope.tasks.splice(this.$index, 1);
            save();
        }

        load();
    }]);

Exercise: Automatic Counter with $interval

  • $interval
  • setInterval

Implement an automatic counter using the $interval service

The $interval service of Angular will run its callback every N miliseconds so we don't have to re-schedule it every time, on the other hand we will have to call the cancel method to stop it when the user clicks on the stop button.

Exercise: TODO

Extend the TODO applications by the following:

  • Add Input validation to the TODO, require the text to be at least 3 characters long.
  • Save the timestamp of the creation of the item.
  • Allow the setting of a due-date for the item.
  • Allow the user to set priority level 1-5 using a 'select' element.
  • Allow the user to type in some longer text in a 'textarea' and save that too.
  • Allow editing the item.

Solution: Automatic Counter with $interval

  • $interval
  • setInterval
<script src="angular.min.js"></script>
<script>
angular.module('CounterApp', [])
    .controller('CounterController', ['$scope', '$interval', function($scope, $interval) {
        var timer;
        $scope.counter = 0;
        $scope.stopCounter = function() {
            $interval.cancel(timer);
            timer = undefined;
        };
        $scope.startCounter = function() {
            if (timer === undefined) {
                console.log('start');
                timer = $interval(updateCounter, 1000);
            }
        };
        var updateCounter = function() {
            console.log('update');
            $scope.counter++;
        };
        $scope.startCounter();
    }]);
</script>
<div ng-app="CounterApp">
   <div ng-controller="CounterController">
   {{counter}}
   <button ng-click="stopCounter()">Stop</button>
   <button ng-click="startCounter()">Start</button>
   </div>
</div>

AngularJS - Routing

Routing

Simple routing

  • ng-view

  • $route

  • $routeProvider

  • template - inline template

  • templateUrl - URL of template file on server

  • templateUrl - id of embedded template inside the Controller

<script src="angular.min.js"></script>
<script src="angular-route.min.js"></script>
<script src="simple_routing.js"></script>

<div ng-app="DemoApp" ng-controller="DemoController">


<h1>{{title}}</h1>
<a href="#">home</a>
<a href="#first">first</a>
<a href="#second">second</a>
<a href="#third">third</a>
 
<div ng-view></div>





<script type="text/ng-template" id="third.html">
<h2>Third Page</h2>
From the main HTML page.
</script>


</div>

angular.module("DemoApp", ['ngRoute'])
.controller("DemoController", ['$scope', function($scope) {
    $scope.title = "Simple Router Example";
}])
.config(['$routeProvider', function($routeProvider) {
    $routeProvider.
        when('/first', {
            template: '<h2>First Page</h2> from the template in the code',
        }).
        when('/second', {
            templateUrl: 'second.html',
        }).
        when('/third', {
            templateUrl: 'third.html',
        }).
        otherwise({
            redirectTo: '/'
        });
}]);
<h2>Second Page</h2>
From the template on the disk.

Routing from code

  • $location
<script src="angular.min.js"></script>
<script src="angular-route.min.js"></script>
<script src="routing_from_code.js"></script>

<div ng-app="DemoApp" ng-controller="DemoController">

<h1>{{title}}</h1>
<button ng-click="goto('/')">home</button>
<button ng-click="goto('first')">first</button>
 
<div ng-view></div>

</div>
angular.module("DemoApp", ['ngRoute'])
.controller("DemoController", ['$scope', '$location', function($scope, $location) {
    $scope.title = "Routing from code";
    $scope.goto = function(page) {
        console.log(page);
        $location.path(page);
    };
}])
.config(['$routeProvider', function($routeProvider) {
    $routeProvider.
        when('/first', {
            template: '<h2>First Page</h2> from the template in the code',
        }).
        otherwise({
            redirectTo: '/'
        });
}]);

Two Angular controllers on the same page

<script src="../angular/angular.min.js"></script>
<script src="two.js"></script>

<div ng-app="DemoApp">
    <div ng-controller="OneController">
        {{name}} <input ng-model="title"> {{title}}
    </div>
    <div ng-controller="TwoController">
        {{name}} <input ng-model="title"> {{title}}
    </div>
</div>
angular.module('DemoApp', [])
    .controller('OneController', ['$scope', function($scope) {
        $scope.name  = 'One';
    }])
    .controller('TwoController', ['$scope', function($scope) {
        $scope.name  = 'Two';
    }]);

Two Angular Apps on the same page

Multiple apps

Routing with controller

<script src="angular.min.js"></script>
<script src="angular-route.min.js"></script>
<script src="routing_controller.js"></script>

<div ng-app="DemoApp" ng-controller="DemoController">

<a href="#/">home</a>
<a href="#first">first</a>
<a href="#second">second</a>
 
<div ng-view></div>

</div>

angular.module("DemoApp", ['ngRoute']);

angular.module("DemoApp")
.controller("DemoController", ['$scope', function($scope) {
    console.log('Demo controller');
    $scope.title = "Demo";
}]);

angular.module("DemoApp")
.controller("HomeController", ['$scope', function($scope) {
    console.log('Home controller');
    $scope.title = "Home";
}]);

angular.module("DemoApp")
.controller("FirstController", ['$scope', function($scope) {
    console.log('First controller');
    $scope.title = "First";
}]);

angular.module("DemoApp")
.controller("SecondController", ['$scope', function($scope) {
    console.log('Second controller');
    $scope.title = "Second";
}]);

angular.module("DemoApp")
.config(['$routeProvider', function($routeProvider) {
    $routeProvider.
        when('/', {
            template: 'Home page - {{title}}',
            controller: 'HomeController'
        }).
        when('/first', {
            template: 'First page - {{title}}',
            controller: 'FirstController'
        }).
        when('/second', {
            template: 'Second page - {{title}}',
            controller: 'SecondController'
        }).
        otherwise({
            redirectTo: '/'
        });
}]);

Routing with parameters

<script src="angular.min.js"></script>
<script src="angular-route.min.js"></script>
<script src="routing_params.js"></script>

<div ng-app="DemoApp" ng-controller="DemoController">

<a href="#/">home</a>
<a href="#/item/42">42</a>
<a href="#/person/Foo/Bar">person</a>
 
<div ng-view></div>

</div>


angular.module("DemoApp", ['ngRoute'])
.controller("DemoController", ['$scope', function($scope) {
    console.log('Demo controller');
    $scope.title = "Demo";
}]);

angular.module("DemoApp")
.controller("HomeController", ['$scope', function($scope) {
    console.log('Home controller');
    $scope.title = "Home";
}]);

angular.module("DemoApp")
.controller("FirstController", ['$scope', '$routeParams', function($scope, $routeParams) {
    console.log('First controller');
    console.log($routeParams);
    $scope.params = $routeParams;
    $scope.title = "First";
}]);

angular.module("DemoApp")
.controller("SecondController", ['$scope', '$routeParams', function($scope, $routeParams) {
    console.log('Second controller');
    console.log($routeParams);
    $scope.params = $routeParams;
    $scope.title = "Second";
}]);

angular.module("DemoApp")
.config(['$routeProvider', function($routeProvider) {
    $routeProvider.
        when('/', {
            template: 'Home page - {{title}}',
            controller: 'HomeController'
        }).
        when('/item/:id', {
            template: 'First page params: {{params}}',
            controller: 'FirstController'
        }).
        when('/person/:fname/:lname', {
            template: 'Second page params: {{params}}',
            controller: 'SecondController'
        }).
        otherwise({
            redirectTo: '/'
        });
}]);


Exercise: Routing

  • Change the TODO application so you will have a main page listing all the items with links to /item/ID and those pages showing the details and have a link to /edit/ID that will allow the user to edit details about the item.

Bootstrap

Start with Bootstrap

  1. Download Bootstrap.
  2. Unzip the file.
  3. ... or use the CDN
  4. Create HTML file using the resources provied by Bootstrap.
  5. ... or a simple skeleton.

HTML5 Skeleton

  • DOCTYPE
  • viewport
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <title>HTML5 skeleton</title>
</head>
<body>

<a href="http://code-maven.com/html-skeleton">HTML5 skeleton</a>

</body>
</html>

Bootstrap Skeleton

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <link href="css/bootstrap.min.css" rel="stylesheet">

  <script src="js/jquery.js"></script>
  <script src="js/bootstrap.min.js"></script>
  <title>Bootstrap HTML skeleton</title>
</head>
<body>


<a href="http://code-maven.com/bootstrap-skeleton">Bootstrap skeleton</a>

<p>
<a href="http://getbootstrap.com/">Get Bootstrap</a> - download or use the CDN.
</p>
</body>
</html>

Bootstrap HTML Tags

  • h1
  • mark
  • del
  • s
  • ins
  • u
  • small
  • em
  • strong
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <link href="css/bootstrap.min.css" rel="stylesheet">

  <script src="js/jquery.js"></script>
  <script src="js/bootstrap.min.js"></script>
  <title>Bootstrap HTML tags</title>
</head>
<body>
   <h1>Main Title (h1) <small>small print</small></h1>
   <h2>Sub title (h2) <small>small print</small></h2>
   <h3>Sub sub title (h3) <small>small print</small></h3>
   <h4>Sub sub sub title (h4) <small>small print</small></h4>
   <h5>Sub sub sub title (h5) <small>small print</small></h5>
   <h6>Sub sub sub title (h6) <small>really small print</small></h6>
<hr>

<h2><a href="http://getbootstrap.com/css/#type">Typography</a></h2>
<p>
We can use <mark>'mark'</mark> to highlight text.
</p>
<p>
We can visually <del>delete text</del> using 'del' or we can mark it 
as just <s>strike through</s> using 's'. The difference is semantic.
</p>
<p>
We can visually <ins>insert text</ins> using 'ins', or we can mark it
as <u>underline</u> using 'u' without the semantic value.
</p>
<p>
<small>small</small> and <strong>strong</strong> and <em>em for italicized</em>.


</body>
</html>


Bootstrap Grid

Bootstrap has two grids:

  • Responsive
  • Fluid

They both help us to divide the screen into 12 equal sized columns, or several blocks of columns that add up to 12.

Fluid Container, rows and columns

  • link

  • container

  • container-fluid

  • row

  • col-md-1

  • container will leave a large margin

  • container-fluid is full-page

  • rows need to be divided into 12 columns (or fewer that add up to 12)

  • col-md-1, col-md-2 ..

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <link href="css/bootstrap.min.css" rel="stylesheet">
  <style>
  .col-md-1 {
     background-color: red;
   }
  .col-md-2 {
     background-color: lightblue;
   }
  .col-md-3 {
     background-color: blue;
   }
  .col-md-7 {
     background-color: yellow;
   }
  .col-md-10 {
     background-color: green;
   }
  </style>
  <title>Bootstrap grid with fluid container</title>
</head>
<body>
<div class="container-fluid">
  <div class="row">
      <div class="col-md-1">Left 1</div>
      <div class="col-md-10">Center 10</div>
      <div class="col-md-1">Right 1</div>
  </div>

  <div class="row">
      <div class="col-md-2">Left 2</div>
      <div class="col-md-7">Center 2</div>
      <div class="col-md-3">Right 3</div>
  </div>
</div>

</body>
</html>

Buttons

  • btn
  • Size: btn-lg btn-sm btn-xs
  • Color: btn-default btn-primary btn-success btn-info btn-warning btn-danger btn-link
  • Mix the two
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
  <link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
  <div class="row">
      <div class="col-md-12">
        <button>Plain button</button>
        <button class="btn">btn button</button>

        <h3>Button Size</h3>
        <button class="btn btn-lg">Click</button>
        <button class="btn btn-sm">Click</button>
        <button class="btn btn-xs">Click</button>

        <h3>Button level (?) option (?) color (?)</h3>
        <button class="btn btn-default">btn btn-default</button>
        <button class="btn btn-primary">btn btn-primary</button>
        <button class="btn btn-success">btn btn-success</button>
        <button class="btn btn-info">btn btn-info</button>
        <button class="btn btn-warning">btn btn-warning</button>
        <button class="btn btn-danger">btn btn-danger</button>
        <button class="btn btn-link">btn btn-link</button>

        <h3>Mix</h3>
        <button class="btn btn-xs btn-danger">btn btn-xs btn-danger</button>
        <button class="btn btn-lg btn-success">btn btn-lg btn-success</button>


        <h2>Link</h3>
        <div><a href="/">regular</a></div>
        <div><a href="/" class="btn">btn</a></div>
        <div><a href="/" class="btn btn-primary">btn btn-primary</a></div>
        <div><a href="/" class="btn btn-success btn-lg">btn btn-success btn-lg</a></div>
 


      </div>
  </div>
</div>

</body>
</html>

Glyphicons

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
  <link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
  <div class="row">
      <div class="col-md-12">

    glyphicon-search: <span class="glyphicon glyphicon-search" aria-hidden="true"></span><br>


    <div>
    btn-default btn-lg:
    <button type="button" class="btn btn-default btn-lg" aria-label="Left Align">
        <span class="glyphicon glyphicon-align-left" aria-hidden="true"></span>
    </button>
    </div>

    <div>
    btn-default:
    <button type="button" class="btn btn-default" aria-label="Right Align">
        <span class="glyphicon glyphicon-align-right" aria-hidden="true"></span>
    </button>

    <button type="button" class="btn btn-default" aria-label="Center Align">
        <span class="glyphicon glyphicon-align-center" aria-hidden="true"></span>
    </button>
    </div>

    <div>
    btn-default btn-xs:
    <button type="button" class="btn btn-default btn-xs" aria-label="Justify Align">
        <span class="glyphicon glyphicon-align-justify" aria-hidden="true"></span>
    </button>
    </div>

    <p>

    <button type="button" class="btn btn-default btn-lg">
       <span class="glyphicon glyphicon-star" aria-hidden="true"></span> Star
    </button>

      </div>
  </div>
</div>

</body>
</html>

Menu or navigation bar

  • The 'Toggle navigation' is the button that is show on mobile devices opening the menu. (JavaScript required)
  • For the drop-down to work we need to include js/bootstrap.min.js that requires JQuery.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
  <link href="css/bootstrap.min.css" rel="stylesheet">
  <script src="../handlebars/jquery.js"></script>
  <script src="js/bootstrap.min.js"></script>
</head>
<body>
<div class="container-fluid">
  <div class="row">
      <div class="col-md-12">
      <nav class="navbar navbar-default">
        <div class="container-fluid">
          <!-- Brand and toggle get grouped for better mobile display -->
          <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
              <span class="sr-only">Toggle navigation</span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="#">Brand</a>
          </div>

          <!-- Collect the nav links, forms, and other content for toggling -->
          <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
            <ul class="nav navbar-nav">
              <li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li>
              <li><a href="#">Link</a></li>
              <li class="dropdown">
                <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
                <ul class="dropdown-menu">
                  <li><a href="#">Action</a></li>
                  <li><a href="#">Another action</a></li>
                  <li><a href="#">Something else here</a></li>
                  <li role="separator" class="divider"></li>
                  <li><a href="#">Separated link</a></li>
                  <li role="separator" class="divider"></li>
                  <li><a href="#">One more separated link</a></li>
                </ul>
              </li>
            </ul>
            <form class="navbar-form navbar-left" role="search">
              <div class="form-group">
                <input type="text" class="form-control" placeholder="Search">
              </div>
              <button type="submit" class="btn btn-default">Submit</button>
            </form>
            <ul class="nav navbar-nav navbar-right">
              <li><a href="#">Link</a></li>
              <li class="dropdown">
                <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
                <ul class="dropdown-menu">
                  <li><a href="#">Action</a></li>
                  <li><a href="#">Another action</a></li>
                  <li><a href="#">Something else here</a></li>
                  <li role="separator" class="divider"></li>
                  <li><a href="#">Separated link</a></li>
                </ul>
              </li>
            </ul>
          </div><!-- /.navbar-collapse -->
        </div><!-- /.container-fluid -->
      </nav>

      </div>
  </div>
</div>

</body>
</html>

Bootstrap tables

  • table
  • table-striped
  • table-hover

tables

<!DOCTYPE html>
<html lang="en">
  <head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <link href="css/bootstrap.min.css" rel="stylesheet" />
  <title>Tables</title>
  </head>
  <body>
    <div class="container-fluid">

      <div class="row">
        <div class="col-xs-1"></div>
        <div class="col-xs-10">
        <table>
           <thead>
              <tr><th>Name</th><th>Years</th></tr>
           </thead>
           <tbody>
              <tr><td>Thomas Edison</td><td>1847-1931</td></tr>
              <tr><td>Benjamin Franklin</td><td>1706-1790</td></tr>
              <tr><td>Alexander Graham Bell</td><td>1847-1922</td></tr>
              <tr><td>Nikola Tesla</td><td>1856-1943</td></tr>
              <tr><td>George Washington Carver</td><td>-1943</td></tr>
              <tr><td>Leonardo da Vinci</td><td>1452-1519</td></tr>
           </tbody>
        </table>
        </div>
        <div class="col-xs-1"></div>
      </div>

      <hr>

      <div class="row">
        <div class="col-xs-1"></div>
        <div class="col-xs-10">
          <table>
             <tr><td>class="table"</td></tr>
             <tr><td>class="table table-striped"</td></tr>
             <tr><td>class="table table-hover"</td></tr>
          </table>
        </div>
        <div class="col-xs-1"></div>
      </div>

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

Bootstrap form elements

forms

<!DOCTYPE html>
<html lang="en">
  <head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <link href="css/bootstrap.min.css" rel="stylesheet" />
  <title>Forms</title>
  </head>
  <body>
    <div class="container-fluid">

      <div class="row">
        <div class="col-xs-1"></div>
        <div class="col-xs-10">
        <h1>Form</h1>
        <form>
            <div class="form-group">
               <label for="name">Full Name</label>
               <input type="text" class="form-control" id="name" placeholder="Name">
            </div>
            <div class="form-group">
               <label for="email">Email address</label>
               <input type="email" class="form-control" id="email" placeholder="Email">
            </div>

            <div class="form-group">
                <label>
                   Gender:
                </label>
                <div class="radio-inline">
                  <label>
                    <input type="radio" name="gender" id="gender_ms" value="ms"> Ms
                  </label>
                </div>
                <div class="radio-inline">
                  <label>
                    <input type="radio" name="gender" id="gender_mr" value="mr"> Mr
                  </label>
                </div>
                <div class="radio-inline">
                  <label>
                    <input type="radio" name="gender" id="gender_mx" value="mx"> Mx
                  </label>
                </div>
                <div class="radio-inline">
                  <label>
                    <input type="radio" name="gender" id="gender_alien" value="alien" disabled> Alien
                  </label>
                </div>
            </div>

            <div class="form-group">
                <label for="language" class="col-sm-2 control-label">Language:</label>

                <select id="language" class="form-control">
                  <option value=""></option>
                  <option value="perl">Perl</option>
                  <option value="python">Python</option>
                  <option value="php">PHP</option>
                  <option value="ruby">Ruby</option>
                  <option value="javascript">JavaScript</option>
                </select>
            </div>

            <div class="checkbox">
              <label>
                <input type="checkbox" id="agree" value=""> Agree
              </label>
            </div>
            <button type="submit" class="btn btn-default">Update</button>
        </form>

        </div>
        <div class="col-xs-1"></div>
      </div>

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


Lead Paragraph

<!DOCTYPE html>
<html lang="en">
  <head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <link href="css/bootstrap.min.css" rel="stylesheet" />
  <title>Lead Paragraph</title>
  </head>
  <body>
    <div class="container-fluid">

      <div class="row">
        <div class="col-xs-2"></div>
        <div class="col-xs-7">
        <p class="lead">Using class="lead" makes the letter bigger.</p>
        <p class="lead">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
        <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
        </div>
        <div class="col-xs-3"></div>
      </div>
      <hr>

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

Bootstrap Grid is Responsive

<html>
  <head>
  <title>Responsive Grid</title>
  <meta name="viewport" content="width=device-width" />
  <link href="css/bootstrap.min.css" rel="stylesheet" />

  <style>
  .left {
      background-color: red;
  }
  .main {
      background-color: lightblue;
  }
  .right {
      background-color: lightgreen;
  }
  .main-left {
      background-color: lightblue;
  }
  .main-right {
      background-color: blue;
  }
  </style>
  </head>
  <body>
    <div class="container-fluid">
      <!-- Stack the columns on mobile by making one full-width and the other half-width -->
      <div class="row">
        <div class="col-xs-12 col-md-8 main">.col-xs-12 .col-md-8</div>
        <div class="col-xs-6 col-md-4 right">.col-xs-6 .col-md-4</div>
      </div>
      <hr>

      <!-- Columns start at 50% wide on mobile and bump up to 33.3% wide on desktop -->
      <div class="row">
        <div class="col-xs-12 col-sm-6 col-md-4 left">.col-xs-12 .col-sm-6 .col-md-4</div>
        <div class="col-xs-12 col-sm-6 col-md-4 main">.col-xs-12 .col-sm-6 .col-md-4</div>
        <div class="col-xs-12 col-sm-6 col-md-4 right">.col-xs-12 .col-sm-6 .col-md-4</div>
      </div>
      <hr>
       
      <!-- Columns start at 50% wide on mobile and bump up to 33.3% wide on desktop -->
      <div class="row">
        <div class="col-xs-6 col-md-4 left">.col-xs-6 .col-md-4</div>
        <div class="col-xs-6 col-md-4 main">.col-xs-6 .col-md-4</div>
        <div class="col-xs-6 col-md-4 right">.col-xs-6 .col-md-4</div>
      </div>
      <hr>
      
      <!-- Columns are always 50% wide, on mobile and desktop -->
      <div class="row">
        <div class="col-xs-6 left">.col-xs-6</div>
        <div class="col-xs-6 right">.col-xs-6</div>
      </div>
      <hr>

      <div class="row">
        <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3 left">.col-xs-12 .col-sm-6 .col-md-4 col-lg-3</div>
        <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3 main-left">.col-xs-12 .col-sm-6 .col-md-4 col-lg-3</div>
        <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3 main-right">.col-xs-12 .col-sm-6 .col-md-4 col-lg-3</div>
        <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3 right">.col-xs-12 .col-sm-6 .col-md-4 col-lg-3</div>
      </div>
      <hr>


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

Bootstrap Grid can be hidden

<html>
  <head>
  <title>Responsive Grid</title>
  <meta name="viewport" content="width=device-width" />
  <link href="css/bootstrap.min.css" rel="stylesheet" />

  <style>
  .left {
      background-color: red;
  }
  .main {
      background-color: lightblue;
  }
  .right {
      background-color: lightgreen;
  }
  </style>
  </head>
  <body>
    <div class="container-fluid">

      <div class="row">
        <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3 left">xs-12 sm-6 md-4 lg-3</div>
        <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3 visible-md-block visible-lg-block main-left">xs-12 sm-6 md-4 lg-3 visible-md visible-lg</div>
        <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3 visible-sm-block visible-md-block visible-lg-block main-right">xs-12 sm-6 md-4 lg-3 visible-sm visible-md visible-lg</div>
        <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3 right">xs-12 sm-6 md-4 lg-3</div>
      </div>
      <hr>
    </div>
  </body>
</html>


Bootstrap Grid hide the side when small

<html>
  <head>
  <title>Hide side</title>
  <meta name="viewport" content="width=device-width" />
  <link href="css/bootstrap.min.css" rel="stylesheet" />

  <style>
  .left {
      background-color: red;
  }
  .main {
      background-color: lightblue;
  }
  .right {
      background-color: lightgreen;
  }
  </style>
  </head>
  <body>
    <div class="container-fluid">

      <div class="row">
        <div class="col-xs-12 col-md-9 main">.col-xs-12 .col-md-9</div>
        <div class="col-md-3 visible-md-block visible-lg-block right">.col-md-3</div>
      </div>
      <hr>

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

Exercises: Bootstrap

Take the previous examples (e.g. the TODO app) and change it to use Bootstrap.

  • Convert the form elements
  • Replace the error style with an 'alert alert-warning' class
  • Add a Glyphicon to the save button and replace the 'x' for delete by another Glyphicon

AngularJS - Design

Design

Bootstrap Angular

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <link href="../bootstrap/css/bootstrap.min.css" rel="stylesheet">
  <script src="angular.min.js"></script>
  <script src="ui-bootstrap-tpls-1.3.3.min.js"></script>

  <script>
   angular.module('DemoApp', [])
   .controller('DemoController', function($scope) {
       $scope.message = "Hello World";
   });
  
  </script>

</head>
<body ng-app="DemoApp" ng-controller="DemoController">

<div class="container-fluid">
  <div class="row">
      <div class="col-md-1"></div>
      <div class="col-md-10"></div>
      <div class="col-md-1"></div>
  </div>

  <div class="row">
      <div class="col-md-2"></div>
      <div class="col-md-7">
        <h1>Main Title</h1>
        <h2>Sub title</h2>
        {{message}}
      </div>
      <div class="col-md-3"></div>
  </div>
</div>

</body>
</html>

AngularJS resources

Other resources

AngularJS - Ajax - Building a Single Page Application

What are Single-page web applications?

  • Gmail?
  • Smooth, no page reload
  • "Live" - updates even without interaction
  • Back/Forward buttons work properly
  • History is being recorded
  • [Example](http://127.0.0.1:5000/examples/d2/angular_v2.html" %}

Access data on server

Mojolicious Backend

back-end

$ cd examples/mojo_ajax
$ morbo app.pl
use Mojolicious::Lite;
use Mojo::JSON qw(decode_json);
use FindBin;
use DBI;
use DBIx::Simple;

get '/' => sub {
    my $c = shift;
    $c->stash( urls => [
        [ '/api/v1/greeting' => 'GET no CORS'],
        [ '/v1'              => 'HTML demo page'],
        [ '/api/v2/greeting' => 'GET woth CORS'],
        [ '/v2'              => 'HTML demo page'],

        [ '/api/v2/reverse'  => 'GET with ?text=TEXT'],

        [ '/api/v2/items'    => 'GET returning list of items'],
        [ '/api/v2/item'     => 'POST expecting {text => TEXT}'],
        [ '/api/v2/item/ID'  => 'DELETE where ID is the id number of an item'],
    ]);
    $c->render( template => 'index' );
};

get '/v1' => sub {
    my $c = shift;
    $c->render( template => 'greeting', version => 'v1' );
};

get '/v2' => sub {
    my $c = shift;
    $c->render( template => 'greeting', version => 'v2' );
};


get '/api/v1/greeting' => {
    json => {
        text => 'Hello World from Mojolicious'
    }
};

under sub {
    my $c = shift;

    # 'Access-Control-Allow-Origin' => '*';
    $c->res->headers->access_control_allow_origin('*');

    # 'Access-Control-Allow-Headers' => 'Content-Type';
    $c->res->headers->append('Access-Control-Allow-Headers' => 'Content-Type');

    # 'Access-Control-Allow-Methods' => 'POST';
    $c->res->headers->append('Access-Control-Allow-Methods' => 'GET, POST, OPTIONS, DELETE');
};

get '/api/v2/greeting' => sub {
    my $c = shift;
    $c->render( json => {
        text => 'Hello World from Mojolicious with CORS enabled'
    });
};

get '/api/v2/reverse' => sub {
    my $c = shift;
    $c->render( json => {
        text => scalar reverse $c->param('str'),
    });
};

options '/api/v2/item' => sub {
    my $c = shift;
    $c->render( json => {nothing => ''} );
};
options '/api/v2/item/:id' => {
    json => {nothing => ''}
};

post '/api/v2/item' => sub {
    my $c = shift;
    my $data = decode_json $c->req->body;
    
    get_db()->query('INSERT INTO items (text, date) VALUES (?, ?)', $data->{text}, time);
    $c->render( json => {
        text => $data->{text},
    });
};

del '/api/v2/item/:id' => sub {
    my $c = shift;
    my $id = $c->param('id');
    $c->app->log->debug("delete $id");
    my $res = get_db()->query('DELETE FROM items WHERE id=?', $id)->rows;
    $c->app->log->debug("deleted $id: $res");
    if ($res) {
        $c->render( json => {
            ok => 1
        });
    } else {
        #$c->reply->not_found;
        $c->render( json => {
            text => 'No such item',
                },
            status => 404,
        )
    }
};


get '/api/v2/items' => sub {
    my $c = shift;
    my @items = get_db()->query('SELECT * FROM items')->hashes;
    $c->render( json => {
        items => \@items,
    });
};

sub get_db {
    my $dbfile = "$FindBin::Bin/items.db";
    if (not -e $dbfile) {
        my $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile","","");
        $dbh->do(q{CREATE TABLE items (
            id      INTEGER PRIMARY KEY,
            text    VARCHAR(255),
            date    VARCHAR(10)
        )
        });
    }
    return DBIx::Simple->connect("dbi:SQLite:dbname=$dbfile","","", {
        RaiseError => 1,
        PrintError => 0,
        AutoCommit => 1,
    });
}

app->secrets(['My very secret passphrase.']);
app->start;

__DATA__

@@ index.html.ep

<html>
 <head>
   <title>Back-end for building Single Page Applications</title>
 </head>
<body>
<h1>Back-end for building Single Page Application</h1>

<table>
  <tr><th>URL</th><th>Explanation</th></tr>
% for my $u (@$urls) {
  <tr><td><a href="<%= $u->[0] %>"><%= $u->[0] %></a></td><td><%= $u->[1] %></td></tr>
% } 
</table>
</body>
</html>

@@ greeting.html.ep
 
<html ng-app="DemoApp" ng-controller="DemoController">
  <head>
      <script src="angular.min.js"></script>
      <title><%= $version %></title>

<script>
angular.module('DemoApp', [])
.controller('DemoController', ['$scope', '$http', function($scope, $http) {
    $scope.title = 'Static Title';
    $http({
        method: 'GET',
        url: '/api/<%= $version %>/greeting'
    }).then(function(response) {
        console.log('success', response);
        $scope.greeting = response.data.text;
    }, function(response) {
        console.log('failure', response);
    });
}]);
</script>

  </head>
<body>
<h1>{{title}}</h1>
<h2>{{greeting}}</h2>
</body>
</html>

$http GET request

  • $http
  • GET
  • CORS
  • Access-Control-Allow-Origin
<script src="../angular/angular.min.js"></script>
<script src="angular_v1_greeting.js"></script>

<div ng-app="DemoApp" ng-controller="DemoController">
        {{greeting}}
        <hr>
    <div ng-show="error">Error! Check the console.</div>
</div>
angular.module("DemoApp", [])
.controller("DemoController", ['$scope', '$http', function($scope, $http) {
    $http.get('http://127.0.0.1:3000/api/v1/greeting').then(
        function(response) {
            console.log(response);
            $scope.greeting = response.data["text"];
            $scope.error = false;
        },
        function(response) {
            console.log("error");
            console.log(response);
            $scope.error = true;
        }
    );
}]);

CORS - Cross-Origin Resource Sharing

$http GET request with CORS enabled

  • Access-Control-Allow-Origin
<script src="../angular/angular.min.js"></script>
<script src="angular_v2_greeting.js"></script>
<div ng-app="DemoApp" ng-controller="DemoController">
        {{greeting}}
        <hr>
    <div ng-show="error">Error! Check the console.</div>
</div>
angular.module("DemoApp", [])
.controller("DemoController", ['$scope', '$http', function($scope, $http) {
    $http.get('http://127.0.0.1:3000/api/v2/greeting').then(
        function(response) {
            console.log(response);
            $scope.greeting = response.data["text"];
            $scope.error = false;
        },
        function(response) {
            console.log("error");
            console.log(response);
            $scope.error = true;
        }
    );
}]);

$http GET request with data

  • GET
  • encodeURIComponent
<script src="../angular/angular.min.js"></script>
<script src="angular_v2_reverse.js"></script>

<div ng-app="DemoApp" ng-controller="DemoController">
    <input ng-model="str"> <button ng-click="reverse()">Reverse</button>
    Reversed: {{reversed}}
    <div ng-show="error">Error! Check the console.</div>
</div>
angular.module("DemoApp", [])
.controller("DemoController", ['$scope', '$http', function($scope, $http) {
    $scope.reverse = function() {
        $http.get('http://127.0.0.1:3000/api/v2/reverse?str='
               + encodeURIComponent($scope.str)).then(
            function(response) {
                console.log(response.data);
                $scope.reversed = response.data["text"];
            },
            function(response) {
                console.log("error");
            }
        );
    }
}]);

$http POST, OPTIONS requests

  • POST
  • OPTIONS

remove examples/mojo_ajax/items.db

$ sqlite3 examples/mojo_ajax/items.db
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <script src="../angular/angular.min.js"></script>
  <script src="angular_v2_add_item.js"></script>
  <link rel="stylesheet" href="style.css" type="text/css" media="print, projection, screen" />
  <title>List items</title>
</head>
<body>

<script>
</script>
<div ng-app="DemoApp" ng-controller="DemoController">
    <input ng-model="text"><button ng-click="add_item()">Add</button>

    <div id="error" ng-show="error">{{error}}</div>
</div>
</body>
</html>

angular.module("DemoApp", [])
.controller("DemoController", ['$scope', '$http', function($scope, $http) {
    $scope.add_item = function() {
        $scope.error = '';
        $http.post('http://127.0.0.1:3000/api/v2/item', {text: $scope.text} ).then(
            function(response) {
                console.log(response);
                $scope.result = response.data;
                //console.log($scope.items);
            }, function(response) {
                console.log("error", response);
                $scope.error = response.data.text;
            }
        );
        $scope.text = '';
    }
    $scope.error = '';
}]);



$http list items

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <script src="../angular/angular.min.js"></script>
  <script src="angular_v2_list_items.js"></script>
  <link rel="stylesheet" href="style.css" type="text/css" media="print, projection, screen" />
  <title>List items</title>
</head>
<body>

<script>
</script>
<div ng-app="DemoApp" ng-controller="DemoController">
    <input ng-model="text"><button ng-click="add_item()">Add</button>

    <table id="items-table" class="tablesorter">
        <thead>
        <tr><th>Item</th><th>Date</th></tr>
        </thead>
        <tbody>
        <tr ng-repeat="i in data.items track by $index"><td>{{ i.text }}</td><td class="date" sort="{{ i.date }}">{{ i.date }}</td></tr>
        </tbody>
    </table>
    <div id="error" ng-show="error">{{error}}</div>
</div>
</body>
</html>


angular.module("DemoApp", [])
.controller("DemoController", ['$scope', '$http', function($scope, $http) {
    var error = function(response) {
        console.log("error", response);
        $scope.error = response.data.text;
    }
    $scope.add_item = function() {
        $scope.error = '';
        $http.post('http://127.0.0.1:3000/api/v2/item', {text: $scope.text} ).then(
            function(response) {
                console.log(response);
                $scope.result = response.data;
                //console.log($scope.items);
                $scope.get_items();
            }, error
        );
        $scope.text = '';
    }
    $scope.get_items = function() {
        $scope.error = '';
        $http.get('http://127.0.0.1:3000/api/v2/items').then(
            function(response) {
                //console.log(response);
                $scope.data = response.data;
                console.log($scope.data);
            }, error
        );
    }
    $scope.error = '';
    $scope.get_items();
}]);

$http DELETE request

  • DEL
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <script src="../angular/angular.min.js"></script>
  <script src="angular_v2.js"></script>
  <link rel="stylesheet" href="style.css" type="text/css" media="print, projection, screen" />
  <title>List items</title>
</head>
<body>

<script>
</script>
<div ng-app="DemoApp" ng-controller="DemoController">
    <input ng-model="text"><button ng-click="add_item()">Add</button>

    <table id="items-table" class="tablesorter">
        <thead>
        <tr><th>Item</th><th>Date</th><th>X</th></tr>
        </thead>
        <tbody>
        <tr ng-repeat="i in data.items track by $index"><td>{{ i.text }}</td><td class="date" sort="{{ i.date }}">{{ i.date }}</td><td><button class="delete" ng-click="delete(i.id)">x</button></td></tr>
        </tbody>
    </table>
    <div>
    <button class="delete" ng-click="delete(3)">Delete ID 3</button>
    </div>
    <div id="error" ng-show="error">{{error}}</div>
</div>
</body>
</html>

angular.module("DemoApp", [])
.controller("DemoController", ['$scope', '$http', function($scope, $http) {
    var error = function(response) {
        console.log("error", response);
        $scope.error = response.data.text;
    }
    $scope.add_item = function() {
        $scope.error = '';
        $http.post('http://127.0.0.1:3000/api/v2/item', {text: $scope.text} ).then(
            function(response) {
                console.log(response);
                $scope.result = response.data;
                //console.log($scope.items);
                $scope.get_items();
            }, error
        );
        $scope.text = '';
    }
    $scope.get_items = function() {
        $scope.error = '';
        $http.get('http://127.0.0.1:3000/api/v2/items').then(
            function(response) {
                //console.log(response);
                $scope.data = response.data;
                console.log($scope.data);
            }, error
        );
    }
    $scope.delete = function(id) {
        console.log(id);
        $scope.error = '';

        $http.delete('http://127.0.0.1:3000/api/v2/item/' + id).then(
            function(response) {
                console.log(response);
                $scope.get_items();
            }, error
        );
    }
    $scope.error = '';
    $scope.get_items();
}]);

Using ngResource

  • ngResource

ngResource

<script src="angular.min.js"></script>
<script src="angular-resource.min.js"></script>
<script src="resource_v2_get.js"></script>

<div ng-app="DemoApp" ng-controller="DemoController">
{{res.text}}
</div>

angular.module("DemoApp", ["ngResource"])
.controller("DemoController", ['$scope', '$resource', function($scope, $resource) {
    var Greeting = $resource('http://127.0.0.1:3000/api/v2/greeting');
    $scope.res = Greeting.get();
}]);

ngResource error handling (no CORS)

<script src="angular.min.js"></script>
<script src="angular-resource.min.js"></script>
<script src="resource_v1_get.js"></script>

<div ng-app="DemoApp" ng-controller="DemoController">
{{res.text}}
<hr>
<div ng-show="error">Error! Check the console.</div>
</div>

angular.module("DemoApp", ["ngResource"])
.controller("DemoController", ['$scope', '$resource', function($scope, $resource) {
    var Greeting = $resource('http://127.0.0.1:3000/api/v1/greeting');
    $scope.res = Greeting.get( function(){}, function(resp) {
        console.log('error');
        console.log(resp);
        $scope.error = true;
    } );
}]);


ngResource GET with param

<script src="angular.min.js"></script>
<script src="angular-resource.min.js"></script>
<script src="resource_v2_reverse.js"></script>

<div ng-app="DemoApp" ng-controller="DemoController">
<input ng-model="str"> <button ng-click="reverse()">Reverse</button>
    Reversed: {{res.text}}
<hr>
<div ng-show="error">Error! Check the console.</div>
</div>


angular.module("DemoApp", ["ngResource"])
.controller("DemoController", ['$scope', '$resource', function($scope, $resource) {
    var Greeting = $resource('http://127.0.0.1:3000/api/v2/reverse');

    $scope.reverse = function() {
        $scope.res = Greeting.get({ str: encodeURIComponent($scope.str) },
            function(){ }, 
            function(resp) {
                console.log('error');
                console.log(resp);
                $scope.error = true;
            }
        );
        console.log($scope.res);
    };
}]);



Public APIs with Cross-origin Resource Sharing (CORS) enabled

<!DOCTYPE html>
<html>
<head>
    <title>CORS - Cross-Origin Resource Sharing</title>
    <meta charset="utf-8">
    <meta name="viewport"
      content="width=device-width, initial-scale=1.0, user-scalable=yes">

    <script src="../angular/angular.min.js"></script>
    <script src="cors.js"></script>
    <link href="cors.css" rel="stylesheet">
</head>
<body>

<div ng-app="CORSApp" ng-controller="CORSController">
    <select ng-model="site" ng-change="clear()" ng-options="s as s.name for s in sites">
    <select>
    <button ng-click="try()">Try</button>
    URL: {{site.url}}
    <hr>
    Result: {{ data }}
    <div ng-show="error" id="error">Failed</div>
</div>
</body>
</html>
angular.module('CORSApp', [])
    .controller('CORSController', function($scope, $http) {
        //var url = 'https://en.wikipedia.org/w/api.php?action=query&titles=Main%20Page&prop=revisions&rvprop=content&format=json';
        //var url = 'https://api.smartsheet.com/2.0/sheets';
        //var url = 'http://public-api.wordpress.com/rest/v1/sites';

        $scope.sites = [
            {
                url: "https://api.github.com",
                name: "GitHub"
            },
            {
                url: "http://api.metacpan.org/v0/release/_search?size=10",
                name: "MetaCPAN v0"
            },
            {
                url: 'http://api.openweathermap.org/data/2.5/weather?q=Orlando',
                name: 'OpenWeatherMap'
            }
        ]

        $scope.clear = function() {
            console.log('clear');
            $scope.data = '';
            $scope.error = 0;
        }
        $scope.try = function() {
            $http.get($scope.site.url).then(
                function(response) {
                    console.log(response);
                    $scope.data = response.data;
                },
                function(response) {
                    console.log("error");
                    console.log(response);
                    $scope.error = 1;
                }
            );
        }
    });
#error {
    color: red;
}

MetaCPAN API

New API

Old API

Exercise: Implement a new interface for MetaCPAN

  • one example
  • source with some queries.
  • Recent module listing
  • Allow the user to select the number of items to be listed.
  • Remember the selection between reloads.
  • Page to see the documentation of a module
  • Remember the modules the user has visited and allow the user to see the history
  • ...

AngularJS - Filters

Filters in HTML view

Change the way 'value' is displayed (similar to 'map' in Perl, Python, Ruby, and JavaScript)

  • {{ some_value | FILTER }}
  • {{ some_value | FILTER:param }}

Reduce the elements of value (similar to 'grep' in Perl, 'select' in Ruby, or 'filter' in Python and JavaScript)

  • ng-repeat="v in some_array | filter:FILTER"

Filters in JavaScript Controller

Change the way 'value' is displayed ('map')

  • $scope.new_value = $filter('FILTER')($scope.some_value)
  • $scope.new_value = $filter('FILTER')($scope.some_value, param)

Reduce the elements of value ('grep', 'select', 'filter')

  • $scope.new_array = $filter('filter')($scope.some_array, FILTER)
  • $scope.new_array = $scope.some_array.filter(FILTER)

Filter date

<!DOCTYPE html>
<html>
<head>
    <title>Try</title>
    <meta charset="utf-8">
    <meta name="viewport"
      content="width=device-width, initial-scale=1.0, user-scalable=yes">

    <script src="../angular/angular.min.js"></script>
<script>
angular.module("DemoApp", []).
   controller('DemoController', function($scope) {
       $scope.now = new Date();
   })
</script>

</head>
<body>
<h1>Date</h1>
<div ng-app="DemoApp" ng-controller="DemoController">
    <table>
        <tr><td>date</td><td>{{ now | date }}</td></tr>
        <tr><td>date:'medium'</td><td>{{ now | date:'medium' }}</td></tr>
        <tr><td>date:'short'</td><td>{{ now | date:'short' }}</td></tr>
        <tr><td>date:'fullDate'</td><td>{{ now | date:'fullDate' }}</td></tr>
        <tr><td>date:'longDate'</td><td>{{ now | date:'longDate' }}</td></tr>
        <tr><td>date:'mediumDate'</td><td>{{ now | date:'mediumDate' }}</td></tr>
        <tr><td>date:'shortDate'</td><td>{{ now | date:'shortDate' }}</td></tr>
        <tr><td>date:'mediumTime'</td><td>{{ now | date:'mediumTime' }}</td></tr>
        <tr><td>date:'shortTime'</td><td>{{ now | date:'shortTime' }}</td></tr>
        <tr><td>date:'yyyy-MM-dd HH:mm:ss'</td>
            <td>{{ now | date:'yyyy-MM-dd HH:mm:ss' }}</td></tr>
    </table>

    <style>
     td {padding-right: 30px;}
     td { border-bottom: 1px solid; }
    </style>    
</div>

</body>
</html>

Filter number

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <script src="../angular/angular.min.js"></script>
 
  <script>
   angular.module('DemoApp', [])
   .controller('DemoController', ['$scope', function($scope) {
        $scope.price = 1234.56789;
   }]);
  </script>
 
</head>
<body ng-app="DemoApp" ng-controller="DemoController">
<h1>number filter used in HTML</h1>

<input ng-model="price">

<table>
<tr><td>price             </td><td>{{price}}            </td></tr>
<tr><td>price | number    </td><td>{{price | number}}   </td></tr>
<tr><td>price | number:0  </td><td>{{price | number:0}} </td></tr>
<tr><td>price | number:4  </td><td>{{price | number:4}} </td></tr>
<tr><td>price | number:-1 </td><td>{{price | number:-1}}</td></tr>
<tr><td>price | number:-2 </td><td>{{price | number:-2}}</td></tr>
</table>

</body>
</html>

Filter by case-insensitive substring

<!DOCTYPE html>
<html>
<head>
    <title>Try</title>
    <meta charset="utf-8">
    <meta name="viewport"
      content="width=device-width, initial-scale=1.0, user-scalable=yes">

    <script src="../angular/angular.min.js"></script>
<script>
angular.module("DemoApp", []).
   controller('DemoController', function($scope) {
       $scope.names = [ "Foo", "Bar", "perl", "properly", "Perla" ];
   })
</script>

</head>
<body>
<h1>Filter by case-insensitive substring</h1>
<div ng-app="DemoApp" ng-controller="DemoController">
    <h3>names (unfiltered)</h3>
    {{ names }}

    <h3>names | filter:'perl'</h3>
    {{ names | filter:'perl' }}
    <h3>ng-repeat="n in names"</h3>
    <ul>
        <li ng-repeat="n in names">{{ n }}</li>
    </ul>
    <h3>ng-repeat="n in names | filter:'perl'"</h3>
    <ul>
        <li ng-repeat="n in names | filter:'perl'">{{ n }}</li>
    </ul>

    <h3>ng-repeat="n in names | filter:'!perl'"</h3>
    <ul>
        <li ng-repeat="n in names | filter:'!perl'">{{ n }}</li>
    </ul>
</div>

</body>
</html>

Filter by attributes

<!DOCTYPE html>
<html>
<head>
    <title>Try</title>
    <meta charset="utf-8">
    <meta name="viewport"
       content="width=device-width, initial-scale=1.0, user-scalable=yes">

    <script src="../angular/angular.min.js"></script>
<script>
angular.module("DemoApp", []).
   controller('DemoController', function($scope) {
       $scope.people = [
           {
               name: 'Foo',
               email: 'foo@example.com',
               employed: true
           },
           {
               name: 'Bar',
               phone: 123,
               employed: false
           },
           {
               name: 'Perl',
               address: 'Home',
               employed: true
           }
       ];
   })
</script>

</head>
<body>
<h1>Try</h1>
<div ng-app="DemoApp" ng-controller="DemoController">
    <h2>filter by matching attribute key-value pair</h2>
    <h3>people</h3>
    <table>
        <tr class="ok-{{p.employed}}" ng-repeat="p in people">
            <td>{{ p.name }}</td>
            <td>{{ p.email }}</td>
            <td>{{ p.phone }}</td>
            <td>{{ p.address }}</td>
        </tr>
    </table>
    <h3>ng-repeat="p in people | filter:{employed: true}"</h3>
    <table>
        <tr ng-repeat="p in people | filter:{employed: true}">
            <td>{{ p.name }}</td>
            <td>{{ p.email }}</td>
            <td>{{ p.phone }}</td>
            <td>{{ p.address }}</td>
         </tr>
    </table>

    <h3>ng-repeat="p in people | filter:{name: 'Foo'}"</h3>
    <table>
        <tr ng-repeat="p in people | filter:{name: 'Foo'}">
            <td>{{ p.name }}</td>
            <td>{{ p.email }}</td>
            <td>{{ p.phone }}</td>
            <td>{{ p.address }}</td>
         </tr>
    </table>
</div>

<style>
    td {padding-right: 30px;}
    td { border-bottom: 1px solid; }
    .ok-true { background-color: #00DD00;}
    .ok-false { background-color: #DD0000;}
</style>

</body>
</html>

Filter table

<html>
<head>
  <script src="angular.min.js"></script>
  <script src="filter_table.js"></script>
</head>
<body ng-app="DemoApp" ng-controller="DemoController">

<table>
  <thead>
     <tr>
         <td><input ng-model="by_value.name"></td>
         <td><input ng-model="by_value.email"></td>
         <td><select ng-model="by_value.country" ng-options="c for c in countries"></select></td>
<!--
         <td><select ng-model="by_value.country">
             <option value=""></option>
             <option ng-repeat="c in countries" value="c">{{c}}</option>
             </select></td>
-->
     </tr>
  </thead>
  <tbody>
    <tr ng-repeat="p in people | filter:by_value">
       <td>{{p.name}}</td>
       <td>{{p.email}}</td>
       <td>{{p.country}}</td>
    </tr>
  </tbody>
</table>
</body>
</html>

angular.module('DemoApp', []);

angular.module('DemoApp')
.controller('DemoController', ['$scope', function($scope) {
  $scope.people = [
      {
          name: 'Foo',
          email: 'foo@company.com',
          country: 'Switzeland'
      },
      {
          name: 'Bar',
          email: 'bar@manage.com',
          country: 'Switzeland'
      },
      {
          name: 'Qux',
          email: 'qux@example.com',
          country: 'France',
      },
      {
          name: 'Zorg',
          email: 'z@example.com',
          country: 'Peru',
      }
  ];

  $scope.countries = [''];
  var countries = {};
  $scope.people.forEach(function(p) {
     if (! countries[p.country]) {
         $scope.countries.push(p.country);
         countries[p.country] = 1;
     }
  });
}]);

Filter by calling a function

<!DOCTYPE html>
<html>
<head>
    <title>Try</title>
    <meta charset="utf-8">
    <meta name="viewport"
      content="width=device-width, initial-scale=1.0, user-scalable=yes">

    <script src="../angular/angular.min.js"></script>
<script>
angular.module("DemoApp", []).
   controller('DemoController', function($scope) {
       $scope.words = ['Foo', 'Bar', 'Longword', 'See']

       $scope.long = function (s) {
           return s.length > 4;
       };

       $scope.longer_than = function (n) {
           return function(s) {
                console.log(s);
               return s.length > n;
           }
       };

       $scope.grep = function (r) {
           return function(str) {
               var p = new RegExp(r)
               return p.exec(str);
           }
       }
   })
</script>

</head>
<body>
<h1>Filter using function</h1>
<div ng-app="DemoApp" ng-controller="DemoController">
    <input ng-model="price" type="number"><br>
  <table border="1">
    <tr><td><pre>words</pre></td>
        <td>{{ words }}</td></tr>
    <tr><td><pre>words | filter:long</pre></td>
        <td>{{ words | filter:long }}</td></tr>
    <tr><td><pre>words | filter:longer_than(4)</pre></td>
        <td>{{ words | filter:longer_than(4) }}</td></tr>
    <tr><td><pre>words | filter:grep('(.)\\1')</pre></td>
        <td>{{ words | filter:grep('(.)\\1') }}</td></tr>
</div>

</body>
</html>

New (crazy) filter

<!DOCTYPE html>
<html>
<head>
    <title>Crazy Case</title>
    <meta charset="utf-8">
    <meta name="viewport"
      content="width=device-width, initial-scale=1.0, user-scalable=yes">

    <script src="../angular/angular.min.js"></script>
<script>
angular.module("DemoApp", ['FilterApp']).
   controller('DemoController', function($scope) {
   });
 angular.module('FilterApp', [])
    .filter('crazycase', function(){
        return function(input) {
            if (input === undefined) {
                return '';
            }
            var crazy = '';
            for(i=0; i < input.length; i++) {
                if (i % 2) {
                    crazy += input[i].toUpperCase();
                } else {
                    crazy += input[i].toLowerCase();
                }
            }
            return crazy;
        }
    })
</script>

</head>
<body>
<h1>Crazy Case</h1>
<div ng-app="DemoApp" ng-controller="DemoController">
    <input ng-model="text" type="text" placeholder="Type in some text">
    <div>{{ text }}</div>
    <div>{{ text | crazycase }}</div>
</div>

</body>
</html>

Filter number in JavaScript Controller

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <script src="../angular/angular.min.js"></script>
 
  <script>
   angular.module('DemoApp', [])
   .controller('DemoController', ['$scope', '$filter', function($scope, $filter) {
        $scope.price = 1234.56789;
        $scope.price_number = $filter('number')($scope.price);
        $scope.price_number0 = $filter('number')($scope.price, 0);
        $scope.price_number4 = $filter('number')($scope.price, 4);
        $scope.price_number_1 = $filter('number')($scope.price, -1);
        $scope.price_number_2 = $filter('number')($scope.price, -2);
   }]);
  </script>
 
</head>
<body ng-app="DemoApp" ng-controller="DemoController">
<h1>number filter used in JavaScript</h1>

<input ng-model="price">

<table>
<tr><td>price             </td><td>{{price}}          </td></tr>
<tr><td>price | number    </td><td>{{price_number}}   </td></tr>
<tr><td>price | number:0  </td><td>{{price_number0}}   </td></tr>
<tr><td>price | number:4  </td><td>{{price_number4}}   </td></tr>
<tr><td>price | number:-1 </td><td>{{price_number_1}}   </td></tr>
<tr><td>price | number:-2 </td><td>{{price_number_2}}   </td></tr>
</table>

</body>
</html>

Exercise: display clock and stopper

  • Create an application that will display the current time on the html page and will constantly update it every second.
  • Add a stop and a start button. When 'stop' is clicked time is frozen. When 'start' is clicked display starts to show current time.
  • Add a stopper to the page with 3 buttons. Start, Stop, and 'Reset'.
  • Add a countdown to the page: An input box wher the user can enter a number. A 'Start' button to start the countdown. Display some message at the end of the countdown.

AngularJS - Directives

Directives

  • ng-app
  • ng-controller
  • ng-model
  • ng-click
  • ng-view
  • ng-repeat
  • ...

We can also create our own directives. But What for?

Directives are abstrctions of the View, the HTML in our application.

Example: List people

<script src="angular.min.js"></script>
<script src="list_people.js"></script>
<div ng-app="DemoApp" ng-controller="DemoController">

  <div class="person" ng-repeat="p in people">
     <div>{{p.name}}</div>
     <div>{{p.email}}</div>
  </div>

</div>

<style>
  .person {
    background: #EEE;
    margin: 20px;
  }
</style

angular.module('DemoApp', [])
.controller('DemoController', ['$scope', function($scope) {
    $scope.people = [
        {
            name: 'Foo',
            email: 'foo@company.com'
        },
        {
            name: 'Bar',
            email: 'bar@company.com'
        },
        {
            name: 'Qux',
            email: 'qux@company.com'
        }
    ];

}]);

Example: List people with directive

  • directive
  • scope
  • replace
  • template
<script src="angular.min.js"></script>
<script src="list_people_directive.js"></script>
<div ng-app="DemoApp" ng-controller="DemoController">

   <div my-person person="p" ng-repeat="p in people"></div>

</div>

<style>
  .person {
    background: #EEE;
    margin: 20px;
  }
</style


angular.module('DemoApp', []);

angular.module('DemoApp')
.controller('DemoController', ['$scope', function($scope) {
    $scope.people = [
        {
            name: 'Foo',
            email: 'foo@company.com'
        },
        {
            name: 'Bar',
            email: 'bar@company.com'
        },
        {
            name: 'Qux',
            email: 'qux@company.com'
        }
    ];

}]);

angular.module('DemoApp')
    .directive('myPerson', function () {
    return {
        scope: {
            person: '='
        },
        replace: true,
        template: '<div class="person"><div>{{person.name}}</div> <div>{{person.email}}</div></div>'
    }
});

Example: List people with directive and templateUrl

  • templateUrl
<script src="angular.min.js"></script>
<script src="list_people_directive_url.js"></script>
<div ng-app="DemoApp" ng-controller="DemoController">

   <div my-person person="p" ng-repeat="p in people"></div>

</div>

<style>
  .person {
    background: #EEE;
    margin: 20px;
  }
</style

angular.module('DemoApp', []);

angular.module('DemoApp')
.controller('DemoController', ['$scope', function($scope) {
    $scope.people = [
        {
            name: 'Foo',
            email: 'foo@company.com'
        },
        {
            name: 'Bar',
            email: 'bar@company.com'
        },
        {
            name: 'Qux',
            email: 'qux@company.com'
        }
    ];

}]);

angular.module('DemoApp')
    .directive('myPerson', function () {
    return {
        scope: {
            person: '='
        },
        replace: true,
        templateUrl: 'person.html'
    }
});

Hello World directive

<!DOCTYPE html>
<html>
<head>
    <title>Hello World Directive</title>
    <meta charset="utf-8">
    <meta name="viewport"
       content="width=device-width, initial-scale=1.0, user-scalable=yes">

    <script src="../angular/angular.min.js"></script>
    <script src="hello_world_directive.js"></script>
</head>
<body>
<h1>Hello World!</h1>
<div ng-app="HelloApp" ng-controller="HelloController">
  <div hello-world></div>
  <div hello:world></div>
  <div hello_world></div>
  <div data-hello-world></div>
  <div x-hello-world></div>
  <hello-world></hello-world>
</div>

</body>
</html>
angular.module('HelloApp', ['HelloDirective'])
   .controller('HelloController', function() {
});

angular.module('HelloDirective', [])
    .directive('helloWorld', function () {
    return {
             template: "Hello World!"
    }
});

Create new directive

<!DOCTYPE html>
<html>
<head>
    <title>My Directive</title>
    <meta charset="utf-8">
    <meta name="viewport"
      content="width=device-width, initial-scale=1.0, user-scalable=yes">

    <script src="../angular/angular.min.js"></script>
    <script src="demo_directive.js"></script>
    <script src="demo_directive_definition.js"></script>
</head>
<body>

<h1>Demo</h1>
<div ng-app="DemoApp">
    <div ng-controller="DemoController">
      <div my-demo></div>
      <div my:demo></div>
      <div my_demo></div>
      <div data-my-demo></div>
      <div x-my-demo></div>
      <hr>
      <div><my-demo></div>
      <div><my-demo></my-demo></div>
    </div>
</div>

</body>
</html>
angular.module('DemoApp', ['DemoDirective'])
    .controller('DemoController', ['$scope', function($scope) {
        $scope.language = {
            name: 'Perl',
        };
    }]);

angular.module('DemoDirective', [])
    .directive('myDemo', function() {
        return {
            template: 'Name: {{language.name}}'
        };
    });

Weather App

Weather App background

<html>
<head>
<title>Open WeatherMap</title>
</head>
<body>
<ul>
  <li><a href="http://api.openweathermap.org/data/2.5/forecast?q=Orlando,us&mode=json&cnt=2appid=">no appid</a></li>
  <li><a href="http://api.openweathermap.org/data/2.5/forecast?q=Orlando,us&mode=json&cnt=2&appid=a452877e758e5881d0d9ab3fcc406fbe">with appid</a></li>
  <li><a href="/openweathermap/data/2.5/forecast?q=Orlando,us&mode=json&cnt=2&appid=a452877e758e5881d0d9ab3fcc406fbe">proxy with appid</a></li>
</ul>
</body>
</html>

Weather App - steps

  1. Sekeleton
  2. Include dependenices, Add dependencies to App
  3. Add routing
  4. Update form to have ng-click and ng-model , set the url to include the name, if there was a name
  5. In the controller Grab the value from
  6. The temratures are in Kelvin. Add buttons so the users can select if they want to see the temratures in Kelvin, Fahrenheit, or Celsius. Round the numbers to 1 digit after the decimal point. C = K - 273.15; F = kelvin * 9/5 - 459.67;
  7. Allow the user to see 1, 3, 5 days. Show buttons that will set this value. (The measurement are currently every 3 hours)

Weather App Skeleton

<!DOCTYPE html>
<html lang="en" ng-app="WeatherApp" ng-controller="WeatherController">
<head>
  <title>{{title}}</title>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <meta http-equiv="X-UA-Compatible" content="IE-Edge">
  <script src="../../angular/angular.min.js"></script>
  <script src="../../angular/angular-route.min.js"></script>
  <script src="../../angular/angular-resource.min.js"></script>
  <link  href="../../bootstrap/css/bootstrap.min.css" rel="stylesheet">
  <script src="app.js"></script>
</head>
<body>
<div class="container-fluid">
    <div class="row">
        <div class="col-xs-12">
            <nav class="navbar navbar-default">
       <!-- <div class="container-fluid">-->
          <!-- Brand and toggle get grouped for better mobile display -->
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                        <span class="sr-only">Toggle navigation</span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    <a class="navbar-brand" href="#">Weather Forcast</a>
                </div>

                <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                    <form class="navbar-form navbar-left" role="search" ng-submit="search()">
                        <div class="form-group">
                          <input type="text" class="form-control" placeholder="Search" ng-model="city">
                        </div>
                        <button type="submit" class="btn btn-default">Submit</button>
                    </form>
                </div>
            </nav>
      </div>
  </div>

  <div class="row">
     <div class="col-xs-12">
        <h1>{{title}}</h1>
        <div ng-view></div>
     </div>
  </div>
</div>
</body>
</html>

var api_key = 'a452877e758e5881d0d9ab3fcc406fbe';

angular.module('WeatherFilters', [])
.filter('kelvin2celsius', function($filter) {
    return function(kelvin) {
        var celsius = kelvin - 273.15;
        return $filter('number')(celsius, 1);
    };
})
.filter('kelvin2fahrenheit', function() {
    return function(kelvin) {
        var fahrenheit = kelvin * 9/5 - 459.67;
        return $filter('number')(fahrenheit, 1);
    };
})
.filter('convert_temp', function($filter) {
    return function(kelvin, scale) {
        if (scale === 'C') {
            var celsius = kelvin - 273.15;
            return $filter('number')(celsius, 1);
        }
        if (scale === 'F') {
            var fahrenheit = kelvin * 9/5 - 459.67;
            return $filter('number')(fahrenheit, 1);
        }
        return kelvin;
    };
})
.filter('ts2date', function() {
    return function(ts) {
        return new Date(ts * 1000 );
    }
});

angular.module('WeatherApp', ['ngRoute', 'ngResource', 'WeatherFilters'])
.controller('WeatherController', ['$scope', '$location', function($scope, $location) {
    $scope.title = "Weather Forecast";
    $scope.search = function() {
        console.log($scope.scale);
        console.log('search', $scope.city);
        $location.path("/city/" + $scope.city);
    };
}])
.config(['$routeProvider', function($routeProvider) {
    $routeProvider.
        when('/', {
            templateUrl: 'main.html',
            controller: 'mainController'
        }).
        when('/city/:name', {
            templateUrl: 'city.html',
            controller: 'cityController'
        }).
        otherwise({
            redirectTo: '/'
        });
}]);


angular.module('WeatherApp')
.controller('mainController', [function () {}])
.controller('cityController', ['$scope', '$routeParams', '$resource', function ($scope, $routeParams, $resource) {
    //console.log('city', $routeParams.name);
    $scope.city = $routeParams.name;
//<span ng-init="scale = 'K'"></span>
    $scope.scale = 'K';
    $scope.days  = '1';

    $scope.$watch('days', function(new_value, old_value) { 
        console.log('new value', new_value);
        $scope.update(new_value);
    });

    $scope.update = function(days) {
        $scope.weatherAPI = $resource('/openweathermap/data/2.5/forecast/daily', {
            }, { });
    //    $scope.weatherAPI = $resource('http://api.openweathermap.org/data/2.5/forecast/daily', {
    //        callback: "JSON_CALLBACK" }, { get: { method: "JSONP" } });
    
        $scope.weatherResults = $scope.weatherAPI.get( {
            q: $scope.city,
            mode: 'json',
            cnt: days,
            appid: api_key
        } );
        console.log($scope.weatherResults);
    };

}]);
City: {{city}}

<div>
Scale:
<button class="btn btn-default" ng-class="{'disabled btn-primary': scale === 'K'}" ng-click="scale = 'K'">K</button>
<button class="btn btn-default" ng-class="{'disabled btn-primary': scale === 'C'}" ng-click="scale = 'C'">C</button>
<button class="btn btn-default" ng-class="{'disabled btn-primary': scale === 'F'}" ng-click="scale = 'F'">F</button>
</div>

<div>
Days:
<button class="btn btn-default" ng-class="{'disabled btn-primary': days === '1'}" ng-click="days = '1'">1</button>
<button class="btn btn-default" ng-class="{'disabled btn-primary': days === '3'}" ng-click="days = '3'">3</button>
<button class="btn btn-default" ng-class="{'disabled btn-primary': days === '7'}" ng-click="days = '7'">7</button>
<button class="btn btn-default" ng-class="{'disabled btn-primary': days === '10'}" ng-click="days = '10'">10</button>
</div>

<table class="table table-striped">
   <tr>
     <th>Date</th>
     <th>Temprature</th>
     <th>Weather</th>
   </tr>
   <tr ng-repeat="d in weatherResults.list">
   <td>{{d.dt | ts2date | date:'yyyy-MM-dd' }}</td>
   <td>{{d.temp.day | convert_temp:scale }} {{ scale }} </td>
   <td>{{d.weather[0].description}}</td>
   </tr>
</table>
main page

TODO

v3 - todo

v3: file:///Users/gabor/work/D2-Ajax/client/v3.html

v3 - back-end

package D2::Ajax;
use Dancer2;
use MongoDB ();
use JSON::MaybeXS;
use DateTime::Tiny;
sub DateTime::Tiny::TO_JSON { shift->as_string };

our $VERSION = '0.1';

sub _mongodb {
    my ($collection) = @_;

    my $client = MongoDB::MongoClient->new(host => 'localhost', port => 27017);
    $client->dt_type( 'DateTime::Tiny' );
    my $db   = $client->get_database( config->{app}{mongodb} );
    return $db->get_collection($collection);
}

hook before => sub {
    if (request->path =~ m{^/api/}) {
        header 'Content-Type' => 'application/json';
    }
    if (request->path =~ m{^/api/v[23]/}) {
        header 'Access-Control-Allow-Origin' => '*';
        header 'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS, DELETE';
    }
};

get '/' => sub {
    template 'index';
};

get '/api/v1/greeting' => sub {
    return to_json { text => 'Hello World' };
};

get '/v1' => sub {
    return template 'v1';
};

get '/api/v2/greeting' => sub {
    return to_json { text => 'Hello World' };
};

get '/api/v2/reverse' => sub {
    my $text = param('str') // '';
    my $rev = reverse $text;
    return to_json { text => $rev };
};

post '/api/v2/item' => sub {
    my $text = param('text') // '';
    $text =~ s/^\s+|\s+$//g;
    if ($text eq '') {
        return to_json { error => 'No text provided' };
    }

    my $items = _mongodb('items');
    my $obj = $items->insert({
        text => $text,
        date => DateTime::Tiny->now,
    });
    my $json = JSON::MaybeXS->new;
    $json->convert_blessed(1);
    return $json->encode( { ok => 1, text => $text, id => $obj->to_string } );
};

get '/api/v2/items' => sub {
    my $items = _mongodb('items');
    my @data =  $items->find->all;
    my $json = JSON::MaybeXS->new;
    $json->convert_blessed(1);
    return $json->encode( { items =>  \@data } );
};

del '/api/v2/item/:id' => sub {
    my $id = param('id');

    my $items = _mongodb('items');
    $items->remove({ _id => MongoDB::OID->new($id) });

    my $json = JSON::MaybeXS->new;
    return to_json { ok  => 1 };
};

get '/api/v2/item/:id' => sub {
    my $id = param('id');

    my $items = _mongodb('items');
    my $data = $items->find_one({ _id => MongoDB::OID->new($id) });
    my $json = JSON::MaybeXS->new;
    $json->convert_blessed(1);
    return $json->encode( { item =>  $data } );
};


options '/api/v2/item/:id' => sub {
    return '';
};

######################### v3

post '/api/v3/item' => sub {
    my $text = param('text') // '';
    $text =~ s/^\s+|\s+$//g;
    if ($text eq '') {
        return to_json { error => 'No text provided' };
    }
    my $details = param('details') // '';
    $details =~ s/^\s+|\s+$//g;
    my $id = param('id');
    my $items = _mongodb('items');

    my $obj;
    #debug($text);
    #debug($details);
    if ($id) {
        $obj = MongoDB::OID->new($id);
        $items->update({ _id => $obj }, {
            '$set' => {
                text => $text,
                details => $details,
            }
        });
    } else {
        $obj = $items->insert({
            text => $text,
            date => DateTime::Tiny->now,
        });
    }

    my $data = $items->find_one({ _id => $obj });
    my $json = JSON::MaybeXS->new;
    $json->convert_blessed(1);
    return $json->encode( { ok => 1, item => $data } );
};

get '/api/v3/items' => sub {
    my $items = _mongodb('items');
    my @data =  $items->find->all;
    my $json = JSON::MaybeXS->new;
    $json->convert_blessed(1);
    return $json->encode( { items =>  \@data } );
};

del '/api/v3/item/:id' => sub {
    my $id = param('id');

    my $items = _mongodb('items');
    $items->remove({ _id => MongoDB::OID->new($id) });

    my $json = JSON::MaybeXS->new;
    return to_json { ok  => 1 };
};

get '/api/v3/item/:id' => sub {
    my $id = param('id');

    my $items = _mongodb('items');
    my $data = $items->find_one({ _id => MongoDB::OID->new($id) });
    my $json = JSON::MaybeXS->new;
    $json->convert_blessed(1);
    return $json->encode( { item =>  $data } );
};


options '/api/v3/item/:id' => sub {
    return '';
};

true;

v3 - html and templates

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">

  <title>D2::Ajax - v3</title>
  <script type="text/javascript" src="../public/javascripts/jquery.js"></script>
  <script type="text/javascript" src="../public/javascripts/jquery.tablesorter.min.js"></script>
  <script src="../public/javascripts/handlebars.min.js"></script>
  <script src="v3.js"></script>

  <link rel="stylesheet" href="../public/themes/blue/style.css" type="text/css" media="print, projection, screen" />

  <script id="show-items-template" type="text/x-handlebars-template">
    <div id="msg"></div>

    <form>
      <input name="text" id="text">
      <input type="submit" id="add-item" value="Add item">
    </form>

    {{#if data.items}}
        <table id="items-table" class="tablesorter">
          <thead>
          <tr><th>Item</th><th>Date</th><th>X</th></tr>
          </thead>
          <tbody>
          {{#each data.items}}
             <tr><td><a href="#id/{{ _id.$oid }}">{{ text }}</a></td><td class="date" sort="{{ date }}">{{ date }}</td><td><button class="delete" data-id="{{ _id.$oid }}">x</a></td></tr>
          {{/each}}
          </tbody>
        </table>
    {{/if}}
  </script>

  <script id="show-item-template" type="text/x-handlebars-template">
      <form id="update_form">
          <input name="text" id="text" value="{{ item.text }}"><br>
          <input name="id" id="id" type="hidden" value="{{ item._id.$oid }}">
          {{ item.date }}<br>
          <textarea name="details" id="details" cols="50" rows="10">{{item.details}}</textarea><br>
          <input type="submit" id="update-item" value="Update item">

      </form>
  </script>
</head>
<body>
<a href="#">home</a>

<div id="content"></div>

</body>
</html>

v3 - javascript

var items;

function routing(route) {
    if (! route) {
        route = '#';
    }
    console.log('route: ' + route);
    if (route === '#') {
        show_items();
        return;
    }
    var m = /^#id\/(\w+)$/.exec(route);
    if (m) {
        console.log(m[1]);
        get_item(m[1]);
    }
}

function delete_item() {
    var id = $(this).attr('data-id');
    jQuery.ajax({
        url: 'http://127.0.0.1:5000/api/v3/item/' + id,
        type: 'DELETE',
        success: function(data) {
            var j;
            for ( j = 0; j < items["items"].length; j++) {
                if (items["items"][j]["_id"]["$oid"] === id) {
                    items["items"].splice(j, 1)
                    break;
                }
            }
            show_items();
        }
    });
}


function get_items() {
    jQuery.get('http://127.0.0.1:5000/api/v3/items', function(data) {
        items = data;
        show_items();
    });
}

function get_item(id) {
    jQuery.get('http://127.0.0.1:5000/api/v3/item/' + id , function(data) {
        console.log(data);
        _display('show-item-template', data);
        $("#update-item").click(update_item);

    });
}


function show_items() {
    if (items === undefined) {
        get_items()
        return;
    }

    var i;
    console.log(items);
    _display('show-items-template', { data: items });


    var cfg = {
        textExtraction: function(node) {
            var $node = $(node);
            var sort = $node.attr("sort");
            if (!sort) { return $node.text(); }
            if ($node.hasClass("date")) {
                return (new Date(sort)).getTime();
            } else {
                return sort;
            }
        }
    };
    $("#items-table").tablesorter(cfg);
    $(".delete").click(delete_item);
    $("#add-item").click(add_item);
}

function _display(template_name, data) {
    var source   = $('#' + template_name).html();
    var template = Handlebars.compile(source);
    var html    = template(data);

    $("#content").html(html);
    return;
}

function update_item() {
    var i;
    var fields = {
        'id' : $("#id").val(),
        'text' : $("#text").val(),
        'details' : $("#details").val()
    };
    jQuery.post('http://127.0.0.1:5000/api/v3/item', fields , function(data) {
        console.log(data);
        if (data["error"]) {
            $("#msg").html('Error: ' + data["error"]);
        }
        if (data["ok"]) {
            for (i=0; i < items["items"].length; i++) {
                if (items["items"][i]['_id']['$oid'] === fields['id']) {
                    items["items"][i]['text'] = fields['text'];
                    break;
                }
            }
            window.location.hash = '#';
        }

    });
    return false;
}

function add_item() {
    var text = $("#text").val();
    jQuery.post('http://127.0.0.1:5000/api/v3/item', { text: text } , function(data) {
        console.log(data);
        if (data["error"]) {
            $("#msg").html('Error: ' + data["error"]);
        }
        if (data["ok"]) {
            items["items"].push( data["item"] );
            show_items();
            $("#msg").html('Item "' + data["item"]["text"] + '" added');
        }

    });
    return false;
};

$(document).ready(function() {
    $(window).bind('hashchange', function () {
		routing(window.location.hash);
	});
    routing(window.location.hash);
});

Building a Single-page application

Source Code

Technology

Setting up Vagrant

  • Install Vagrant
  • Install VirtualBox
  • vagrant init szabgab/pde
  • Edit Vagrantfile adding the following lines:
  • config.vm.network "forwarded_port", guest: 5000, host:5000
  • config.vm.network "forwarded_port", guest: 3000, host:3000
  • vagrant up (first time this will download 800Mb file)
  • article
  • vagrant ssh
  • /vagrant is mapped to your directory on the host
  • sudo apt-get install tree

Install Perl and Dancer2

  • cpanm Dancer2
  • cpanm MongoDB

Install Node.JS

Dancer single file

#!/usr/bin/env perl
use Dancer2;
 
get '/' => sub {
    return 'Hello World';
};

dance;

$ perl singlefile.pl
>> Dancer2 v0.161000 server 1443 listening on http://0.0.0.0:3000
$ plackup -r singlefile.pl
Watching ./lib singlefile.pl for file updates.
HTTP::Server::PSGI: Accepting connections at http://0:5000/
  • -p port listen on port
  • -R dir watch dir as well

Create Dancer Skeleton

  • Create Skeleton
dancer2 -a D2::Ajax

Creates directory D2-Ajax

Dancer Directory layout

$ tree
.
├── MANIFEST
├── MANIFEST.SKIP
├── Makefile.PL
├── bin
│   └── app.psgi
├── config.yml
├── cpanfile
├── environments
│   ├── development.yml
│   └── production.yml
├── lib
│   └── D2
│       └── Ajax.pm
├── public
│   ├── 404.html
│   ├── 500.html
│   ├── css
│   │   ├── error.css
│   │   └── style.css
│   ├── dispatch.cgi
│   ├── dispatch.fcgi
│   ├── favicon.ico
│   ├── images
│   │   ├── perldancer-bg.jpg
│   │   └── perldancer.jpg
│   └── javascripts
│       └── jquery.js
├── t
│   ├── 001_base.t
│   └── 002_index_route.t
└── views
    ├── index.tt
    └── layouts
        └── main.tt

Dancer script and module

#!/usr/bin/env perl

use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../lib";

use D2::Ajax;
D2::Ajax->to_app;
package D2::Ajax;
use Dancer2;

our $VERSION = '0.1';

get '/' => sub {
    template 'index';
};

true;

Run Dancer

  • plackup -R lib bin/app.psgi
  • http://127.0.0.1:5000/

Clean-up Dancer Skeleton

  • Put "Dancer example" in views/index.tt
  • empty or remove public/css/style.css
  • remove footer from views/layouts/main.tt
Hello World!
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-type" content="text/html; charset=<% settings.charset %>" />
<title>D2::Ajax</title>
<link rel="stylesheet" href="<% request.uri_base %>/css/style.css" />

<!-- Grab jQuery from a CDN, fall back to local if necessary -->
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<script type="text/javascript">/* <![CDATA[ */
    !window.jQuery && document.write('<script type="text/javascript" src="<% request.uri_base %>/javascripts/jquery.js"><\/script>')
/* ]]> */</script>

</head>
<body>
<% content %>
<div id="footer">
Powered by <a href="http://perldancer.org/">Dancer2</a> <% dancer_version %>
</div>
</body>
</html>

Run tests

perl Makefile.PL
make
make test

Makefile.PL

use strict;
use warnings;
use ExtUtils::MakeMaker;

# Normalize version strings like 6.30_02 to 6.3002,
# so that we can do numerical comparisons on it.
my $eumm_version = $ExtUtils::MakeMaker::VERSION;
$eumm_version =~ s/_//;

WriteMakefile(
    NAME                => 'D2::Ajax',
    AUTHOR              => q{YOUR NAME <youremail@example.com>},
    VERSION_FROM        => 'lib/D2/Ajax.pm',
    ABSTRACT            => 'YOUR APPLICATION ABSTRACT',
    ($eumm_version >= 6.3001
      ? ('LICENSE'=> 'perl')
      : ()),
    PL_FILES            => {},
    PREREQ_PM => {
        'Test::More' => 0,
        'YAML'       => 0,
        'Dancer2'     => 0.161000,
    },
    dist                => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', },
    clean               => { FILES => 'D2-Ajax-*' },
);

Dancer Test scripts

use strict;
use warnings;

use Test::More tests => 1;
use_ok 'D2::Ajax';
use strict;
use warnings;

use D2::Ajax;
use Test::More tests => 2;
use Plack::Test;
use HTTP::Request::Common;

my $app = D2::Ajax->to_app;
is( ref $app, 'CODE', 'Got app' );

my $test = Plack::Test->create($app);
my $res  = $test->request( GET '/' );

ok( $res->is_success, '[GET /] successful' );

Dancer Config file

# This is the main configuration file of your Dancer2 app
# env-related settings should go to environments/$env.yml
# all the settings in this file will be loaded at Dancer's startup.

# Your application's name
appname: "D2::Ajax"

# The default layout to use for your application (located in
# views/layouts/main.tt)
layout: "main"

# when the charset is set to UTF-8 Dancer2 will handle for you
# all the magic of encoding and decoding. You should not care
# about unicode within your app when this setting is set (recommended).
charset: "UTF-8"

# template engine
# simple: default and very basic template engine
# template_toolkit: TT

template: "simple"

# template: "template_toolkit"
# engines:
#   template:
#     template_toolkit:
#       start_tag: '<%'
#       end_tag:   '%>'

HTML forms

<form method="POST" action="/register">

<input type="text" placeholder="Your name" name="name"><br>
<input type="password" name="password" placeholder="Your password"><br>
<input type="hidden" name="secret" value="42"><br>

<label>Chocolate</label><input type="checkbox" name="icecream" value="chocolate"><br>
<label>Vanilla</label><input type="checkbox" name="icecream" value="vanilla"><br>

<hr>

<label>Car <input type="radio" name="rb" value="car">
<label>Motor <input type="radio" name="rb" value="motor">
<hr>
<textarea name="ta" rows="10" cols="80">
</textarea>


<input type="submit" value="Send">

</form>

Log form parameters

#!/usr/bin/env perl
use Dancer2;
use Data::Dumper qw(Dumper);
 
get '/' => sub {
    template 'form';
};

post '/register' => sub {
    debug Dumper scalar params();
    debug Dumper scalar params('query');
    debug Dumper scalar params('body');
    return 'Submitted';
};
 
dance;
        

Run this as plackup -r form.pl or as perl form.pl.

Accessing parameters in Dancer

  • param('name');
  • params->{name};
  • %params = params; $params{name};
  • $params = params; $params->{name};

Work both with GET and POST; Mixing values from QUERY_STRING and request body.

  • For POST requests also: params('body')
  • For GET values see: params('query')
  • For route parameters: params('route')

Logging in Dancer

debug "hello";
debug param('name');

use Data::Dumper qw(Dumper);
debug Dumpter scalar params;

Passing values to the template

#!/usr/bin/env perl
use Dancer2;
use Data::Dumper qw(Dumper);

set engines => {
    template => {
        template_toolkit => {
            start_tag  => '<%',
            end_tag    =>  '%>',
        }
    }
};
set template =>  "template_toolkit";
# order of the two 'set' calls matters!
 
get '/' => sub {
    template 'demo', {
        name => 'Perl Maven',
        grades => [67, 78, 93],
        friends => [
           {
             name => 'Foo',
           },
           {
             name => 'Bar',
           }
        ],
        details => {
           name     => 'Perl Maven',
           birthday => '2012.06.02',
           email    => 'admin@example.com',
        }
    };
};

dance;
        

Template::Toolkit

{% embed include file="src/examples/form/views/demo.tt)

Exercise: Create reverse echo page

Create a page that shows a single input box and a button. When the user clicks on the button the form is submitted and the server sends back the same string and the reversed verson of the same string.

Input: hello

Output: hello olleh

Exercise: Implement a TODO list

The main form has an input box and a button. When the user types in some text and clicks on the button, the back-end stores in in a JSON file. (Dancer provides functions to_json and from_json. We can use Path::Tiny () and then Path::Tiny::path() with slurp_utf8 and spew_utf8 to read a file and to write it out.

When saving the item we need to save the text the user typed in and an id number. (We can use the time function of Time::HiRes. On the response page list the items we have in the file.

Next to each item add a button with the word "delete" on it. When clicking on that button, delete the item from the JSON file on the server and show the list again.

First Ajax example

  • Ajax - Asynchronous JavaScript and XML
  • ... but we usually send JSON

Steps

  • Route serving JSON
  • (Test this route)
  • HTML page with JQuery to send AJAX request and handle response
  • Route serving the HTML page with the AJAX request

Add route returning JSON

  • lib/D2/Ajax.pm
get '/' => sub {
    template 'index';
};
get '/api/v1/greeting' => sub {
    header 'Content-Type' => 'application/json';
    return to_json { text => 'Hello World' };
};
  • http://127.0.0.1:5000/api/v1/greeting
  • curl http://127.0.0.1:5000/api/v1/greeting
{"text":"Hello World"}
  • curl -I http://127.0.0.1:5000/api/v1/greeting
HTTP/1.0 200 OK
Date: Mon, 24 Aug 2015 14:21:11 GMT
Server: HTTP::Server::PSGI
Server: Perl Dancer2 0.161000
Content-Type: application/json
Content-Length: 22

Test route returning JSON

  • t/v1.t
use strict;
use warnings;

use D2::Ajax;
use Test::More tests => 1;
use Plack::Test;
use HTTP::Request::Common;

subtest v1_greeting => sub {
    plan tests => 3;

    my $app = D2::Ajax->to_app;

    my $test = Plack::Test->create($app);
    my $res  = $test->request( GET '/api/v1/greeting' );

    ok $res->is_success, '[GET /] successful';
    is $res->content, '{"text":"Hello World"}';
    is $res->header('Content-Type'), 'application/json';
};

Page with AJAX request

  • jQuery.get
<div id="msg"></div>

<script>
$(document).ready(function() {
    jQuery.get('/api/v1/greeting', function(data) {
        console.log(data);
        $("#msg").html(data["text"]);
    });
});
</script>

Route serving template with AJAX request

lib/D2/Ajax.pm

get '/v1' => sub {
    return template 'v1';
};

Try v1

Next step: Stand alone client

Create client code that could be served from another server or even just as a file on our local machine.

Stand-alone AJAX client

URL includes the hostname

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">

  <title>D2::Ajax - v1</title>
  <script type="text/javascript" src="../public/javascripts/jquery.js"></script>
</head>
<body>

<div id="msg"></div>

<script>
$(document).ready(function() {
    jQuery.get('http://127.0.0.1:5000/api/v1/greeting', function(data) {
        console.log(data);
        $("#msg").html(data["text"]);
    });
});
</script>

</body>
</html>

Try: [v1](file:///Users/gabor/work/D2-Ajax/client/v1.html" %}

HTTP Access Control (CORS)

XMLHttpRequest cannot load http://127.0.0.1:5000/api/v1/greeting.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'null' is therefore not allowed access.

lib/D2/Ajax.pm

header 'Access-Control-Allow-Origin' => '*';

Let's create v2 of the API

get '/api/v2/greeting' => sub {
    header 'Access-Control-Allow-Origin' => '*';
    header 'Content-Type' => 'application/json';
    return to_json { text => 'Hello World' };
};

Try: [v2](file:///Users/gabor/work/D2-Ajax/client/v2.html" %}

Testing the v1 and v2 API calls

Copy t/v1.t to t/v2.t and add two lines testing the Access-Control-Allow-Origin header and the lack of it.

subtest v1_greeting => sub {
    my $res  = $test->request( GET '/api/v1/greeting' );
    ...
    is $res->header('Access-Control-Allow-Origin'), undef;
}
subtest v2_greeting => sub {
    my $res  = $test->request( GET '/api/v2/greeting' );
    ...
    is $res->header('Access-Control-Allow-Origin'), '*';
}
perl Makefile.PL
make
make test

Proxy

use strict;
use warnings;

use Dancer2;
use LWP::UserAgent;
use HTTP::Request;
use URI::Escape qw(uri_unescape);

get '/' => sub {
    return q{<form method="POST"> <input name="q"><input type="submit" value="Send"></form>};
};

post '/' => sub {
    my $raw_url = substr request->{body}, 2;
    my $ua = LWP::UserAgent->new;
    debug $raw_url;
    my $url = uri_unescape $raw_url;
    debug $url;
    my $request = HTTP::Request->new(GET => $url);
    my $response = $ua->request($request);
    return $response->content;
};

dance;

Next step: Create reverse echo

Send data to server that will echo it back. After reversing the string.

Reverse echo with AJAX and Dancer

  • lib/D2/Ajax.pm
get '/api/v2/reverse' => sub {
    header 'Access-Control-Allow-Origin' => '*';
    header 'Content-Type' => 'application/json';
    my $text = param('str');
    my $rev = reverse $text;
    return to_json { text => $rev };
};

Test reverse echo in Dancer

  • t/v2.t
subtest v2_reverse => sub {
    plan tests => 6;

    my $app = D2::Ajax->to_app;

    my $test = Plack::Test->create($app);
    my $res  = $test->request( GET '/api/v2/reverse?str=Hello world' );

    ok $res->is_success, '[GET /] successful';
    is $res->content, '{"text":"dlrow olleH"}';
    is $res->header('Content-Type'), 'application/json';
    is $res->header('Access-Control-Allow-Origin'), '*';

    my $res2  = $test->request( GET '/api/v2/reverse?str=' );
    is $res2->content, '{"text":""}';

    my $res3  = $test->request( GET '/api/v2/reverse' );
    is $res3->content, '{"text":""}';
};

Client side of AJAX string reverse

<form>
<input name="str" id="str">
<input type="submit" id="reverse" value="Reverse">
</form>


<script>
    $("#reverse").click(function() {
        var str = $("#str").val();
        jQuery.get('http://127.0.0.1:5000/api/v2/reverse?str=' + encodeURIComponent(str) , function(data) {
            console.log(data);
            $("#msg").html(data["text"]);
        });
       return false;
    });
</script>

Try: [v2](file:///Users/gabor/work/D2-Ajax/client/v2.html" %}

Refactoring Dancer app - use the before hook

header 'Content-Type' => 'application/json';
header 'Access-Control-Allow-Origin' => '*';
hook before => sub {
    if (request->path =~ m{^/api/}) {
        header 'Content-Type' => 'application/json';
    }
    if (request->path =~ m{^/api/v2/}) {
        header 'Access-Control-Allow-Origin' => '*';
    }
};

Silencing the noisy tests

make test
PERL_DL_NONLAZY=1 "/Users/gabor/perl5/perlbrew/perls/perl-5.20.1_WITH_THREADS/bin/perl" "-MExtUtils::Command::MM" "-MTest::Harness" "-e" "undef *Test::Harness::Switches; test_harness(0, 'blib/lib', 'blib/arch')" t/*.t
t/001_base.t ......... ok
t/002_index_route.t .. 1/2 [D2::Ajax:97653] core @2015-05-28 13:18:51> looking for get / in /Users/gabor/perl5/perlbrew/perls/perl-5.20.1_WITH_THREADS/lib/site_perl/5.20.1/Dancer2/Core/App.pm l. 1171
[D2::Ajax:97653] core @2015-05-28 13:18:51> Entering hook core.app.before_request in (eval 52) l. 1
[D2::Ajax:97653] core @2015-05-28 13:18:51> Entering hook core.app.after_request in (eval 52) l. 1
t/002_index_route.t .. ok
t/v1.t ............... [D2::Ajax:97654] core @2015-05-28 13:18:51> looking for get /api/v1/greeting in /Users/gabor/perl5/perlbrew/perls/perl-5.20.1_WITH_THREADS/lib/site_perl/5.20.1/Dancer2/Core/App.pm l. 1171
[D2::Ajax:97654] core @2015-05-28 13:18:51> Entering hook core.app.before_request in (eval 52) l. 1
[D2::Ajax:97654] core @2015-05-28 13:18:51> Entering hook core.app.after_request in (eval 52) l. 1
t/v1.t ............... ok
t/v2.t ............... [D2::Ajax:97655] core @2015-05-28 13:18:52> looking for get /api/v2/greeting in /Users/gabor/perl5/perlbrew/perls/perl-5.20.1_WITH_THREADS/lib/site_perl/5.20.1/Dancer2/Core/App.pm l. 1171
[D2::Ajax:97655] core @2015-05-28 13:18:52> Entering hook core.app.before_request in (eval 52) l. 1
[D2::Ajax:97655] core @2015-05-28 13:18:52> Entering hook core.app.after_request in (eval 52) l. 1
t/v2.t ............... 1/2 [D2::Ajax:97655] core @2015-05-28 13:18:52> looking for get /api/v2/reverse in /Users/gabor/perl5/perlbrew/perls/perl-5.20.1_WITH_THREADS/lib/site_perl/5.20.1/Dancer2/Core/App.pm l. 1171
[D2::Ajax:97655] core @2015-05-28 13:18:52> Entering hook core.app.before_request in (eval 52) l. 1
[D2::Ajax:97655] core @2015-05-28 13:18:52> Entering hook core.app.after_request in (eval 52) l. 1
[D2::Ajax:97655] core @2015-05-28 13:18:52> looking for get /api/v2/reverse in /Users/gabor/perl5/perlbrew/perls/perl-5.20.1_WITH_THREADS/lib/site_perl/5.20.1/Dancer2/Core/App.pm l. 1171
[D2::Ajax:97655] core @2015-05-28 13:18:52> Entering hook core.app.before_request in (eval 52) l. 1
[D2::Ajax:97655] core @2015-05-28 13:18:52> Entering hook core.app.after_request in (eval 52) l. 1
[D2::Ajax:97655] core @2015-05-28 13:18:52> looking for get /api/v2/reverse in /Users/gabor/perl5/perlbrew/perls/perl-5.20.1_WITH_THREADS/lib/site_perl/5.20.1/Dancer2/Core/App.pm l. 1171
[D2::Ajax:97655] core @2015-05-28 13:18:52> Entering hook core.app.before_request in (eval 52) l. 1
Use of uninitialized value $text in reverse at /Users/gabor/work/D2-Ajax/blib/lib/D2/Ajax.pm line 33.
[D2::Ajax:97655] core @2015-05-28 13:18:52> Entering hook core.app.after_request in (eval 52) l. 1
t/v2.t ............... ok
All tests successful.
Files=4, Tests=6,  2 wallclock secs ( 0.04 usr  0.02 sys +  1.87 cusr  0.24 csys =  2.17 CPU)
Result: PASS
BEGIN {
    $ENV{DANCER_ENVIRONMENT} = 'test';
}
PERL_DL_NONLAZY=1 "/Users/gabor/perl5/perlbrew/perls/perl-5.20.1_WITH_THREADS/bin/perl" "-MExtUtils::Command::MM" "-MTest::Harness" "-e" "undef *Test::Harness::Switches; test_harness(0, 'blib/lib', 'blib/arch')" t/*.t
t/001_base.t ......... ok
t/002_index_route.t .. ok
t/v1.t ............... ok
t/v2.t ............... 1/2 Use of uninitialized value $text in reverse at /Users/gabor/work/D2-Ajax/blib/lib/D2/Ajax.pm line 33.
t/v2.t ............... ok
All tests successful.
Files=4, Tests=6,  2 wallclock secs ( 0.04 usr  0.02 sys +  1.72 cusr  0.15 csys =  1.93 CPU)
Result: PASS

Next step: TODO list

  • Back-end: Save item in the back-end in MongoDB
  • Form that accepts text sends to the server
  • Back-end: Fetch list of all item
  • List items received from the back-end
  • Delete item

Add item to MongoDB (POST route)

use MongoDB ();

post '/api/v2/item' => sub {
    my $text = param('text') // '';
    $text =~ s/^\s+|\s+$//g;
    if ($text eq '') {
        return to_json { error => 'No text provided' };
    }

    my $client = MongoDB::MongoClient->new(host => 'localhost', port => 27017);
    my $db   = $client->get_database( 'd2-ajax' );
    my $items = $db->get_collection('items');
    $items->insert({
        text => $text,
    });
    return to_json { ok => 1, text => $text };
};

Test adding item to MongoDB (POST route)

use JSON::MaybeXS qw(decode_json);

subtest v2_items => sub {
    plan tests => 4;

    my $app = D2::Ajax->to_app;

    my $test = Plack::Test->create($app);

    my $res  = $test->request( POST '/api/v2/item', {text => 'First Thing to do' } );
    ok $res->is_success, '[POST /] successful';
    is_deeply decode_json($res->content), { ok => 1, text  => 'First Thing to do' };
    is $res->header('Content-Type'), 'application/json';
    is $res->header('Access-Control-Allow-Origin'), '*';
};

MongoDB client

$ mongo
(mongod-3.0.1) test> use d2-ajax
switched to db d2-ajax
(mongod-3.0.1) d2-ajax> db.items.find()
{
  "_id": ObjectId("55671d33a11460085a6cd701"),
  "text": "First Thing to do"
}
Fetched 1 record(s) in 2ms

Use separate database for testing

config.yml

app:
  mongodb: d2-ajax

lib/D2/Ajax.pm

my $db   = $client->get_database( config->{app}{mongodb} );

t/v2.t

my $db_name = 'd2-ajax-' . $$ . '-' . time;
diag $db_name;
D2::Ajax->config->{app}{mongodb} = $db_name;

Drop the database automatically

use MongoDB ();
my $client = MongoDB::MongoClient->new(host => 'localhost', port => 27017);
my $db   = $client->get_database( $db_name );
$db->drop;

Fetch all the items

use JSON::MaybeXS;
get '/api/v2/items' => sub {
    my $client = MongoDB::MongoClient->new(host => 'localhost', port => 27017);
    my $db   = $client->get_database( config->{app}{mongodb} );
    my $items = $db->get_collection('items');

    my @data =  $items->find->all;
    my $json = JSON::MaybeXS->new;
    $json->convert_blessed(1);
    return $json->encode( { items =>  \@data } );
};

Test: Fetch all the items

my $get1  = $test->request( GET '/api/v2/items');
my $items1 = decode_json($get1->content);
is scalar @{$items1->{items}}, 1;
is $items1->{items}[0]{text}, 'First Thing to do';

Add more tests

my $res2  = $test->request( POST '/api/v2/item', { text => '' } );
is $res2->content, '{"error":"No text provided"}';

my $res3  = $test->request( POST '/api/v2/item' );
is $res3->content, '{"error":"No text provided"}';
my $get3  = $test->request( GET '/api/v2/items');
my $items3 = decode_json($get3->content);
is scalar @{$items3->{items}}, 1;
is $items3->{items}[0]{text}, 'First Thing to do';
my $res4  = $test->request( POST '/api/v2/item', { text => '  one more  ' });
is_deeply decode_json($res4->content), { ok => 1, text => 'one more' };

my $get4  = $test->request( GET '/api/v2/items');
my $items4 = decode_json($get4->content);
is scalar @{$items4->{items}}, 2;
is $items4->{items}[0]{text}, 'First Thing to do';
is $items4->{items}[1]{text}, 'one more';

Add and retrieve elements using jQuery and AJAX

<hr>

<form>
<input name="text" id="text">
<input type="submit" id="add-item" value="Add item">
</form>

<div id="items"></div>
$(document).ready(function() {
    ...
    show_items();
    ...
})
function show_items() {
    jQuery.get('http://127.0.0.1:5000/api/v2/items', function(data) {
        var i, html;
        html  = '<ul>';
        console.log(data);
        for (i = 0; i < data["items"].length; i++) {
            html += '<li>' + data["items"][i]["text"] + '</li>';
        }
        html += '</ul>';
        $("#items").html(html);
    });
}

Data structure in Perl

{
  'items' => [
    {
      '_id' => {
        '$oid' => '556d6735a11460452f6e7601'
      },
      'text' => 'First Thing to do'
    },
    {
      '_id' => {
        '$oid' => '556d6735a11460452f6e7602'
      },
      'text' => 'one more'
    }
  ]
}

Add item

$("#add-item").click(function() {
   var text = $("#text").val();
   jQuery.post('http://127.0.0.1:5000/api/v2/item', { text: text } , function(data) {
       console.log(data);
       if (data["error"]) {
           $("#msg").html('Error: ' + data["error"]);
       }
       if (data["ok"]) {
           $("#msg").html('Item ' + data["text"] + ' added');
       }
       show_items();

   });
  return false;
});

Setting up Travis-CI

{% embed include file="src/examples/snippets/travis.yml)

Code refactoring: _mongodb

sub _mongodb {
    my ($collection) = @_;

    my $client = MongoDB::MongoClient->new(host => 'localhost', port => 27017);
    my $db   = $client->get_database( config->{app}{mongodb} );
    return $db->get_collection($collection);
}
my $items = _mongodb('items');

Deleting item: Dancer, MongoDB backend

del '/api/v2/item/:id' => sub {
    my $id = param('id');

    my $items = _mongodb('items');
    $items->remove({ _id => MongoDB::OID->new($id) });

    my $json = JSON::MaybeXS->new;
    return to_json { ok  => 1 };
};

Test Deleting item backend

my @items = ("One 1", "Two 2", "Three 3");
foreach my $it (@items) {
    my $res = $test->request( POST '/api/v2/item', { text => $it });
    is_deeply decode_json($res->content), { ok => 1, text => $it };
}
my $get5  = $test->request( GET '/api/v2/items');
my $items5 = decode_json($get5->content);
is scalar @{$items5->{items}}, 5;

my $del3  = $test->request( DELETE '/api/v2/item/' . $items5->{items}[3]{'_id'}{'$oid'} );
is $del3->content, '{"ok":1}';

my $get6  = $test->request( GET '/api/v2/items');
my $items6 = decode_json($get6->content);
is scalar @{$items6->{items}}, 4;
is_deeply $items5->{items}[0], $items6->{items}[0];
is_deeply $items5->{items}[1], $items6->{items}[1];
is_deeply $items5->{items}[2], $items6->{items}[2];
is_deeply $items5->{items}[4], $items6->{items}[3];
is_deeply $items5->{items}[5], $items6->{items}[4];

Deleting item: client side

function show_items() {
    jQuery.get('http://127.0.0.1:5000/api/v2/items', function(data) {
        var i, html;
        html  = '<ul>';
        console.log(data);
        for (i = 0; i < data["items"].length; i++) {
            html += '<li>';
            html += data["items"][i]["text"];
            html += '<button class="delete" data-id="' + data["items"][i]["_id"]["$oid"] + '">x</a>';
            html += '</li>';
        }
        html += '</ul>';
        $("#items").html(html);
        $(".delete").click(delete_item);
    });
}

Add click-event handler to every element with "delete" class.

$(".delete").click(delete_item);
function delete_item() {
    var id = $(this).attr('data-id');
    jQuery.ajax({
        url: 'http://127.0.0.1:5000/api/v2/item/' + id,
        type: 'DELETE',
        success: function(data) {
            show_items();
        }
    });
}

Access-Control-Allow-Methods

Look at the JavaScript console.

XMLHttpRequest cannot load http://127.0.0.1:5000/api/v2/item/556db39fa114604bac0757d1.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'null' is therefore not allowed access. The response had HTTP status code 404.
OPTIONS http://127.0.0.1:5000/api/v2/item/556db39fa114604bac0757d1

The command-line console of Dancer.

looking for options /api/v2/item/556db39fa114604bac0757d1

Add header: Access-Control-Allow-Methods

Add to the before hook of lib/D2/Ajax.pm:

header 'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS, DELETE';

Add to lib/D2/Ajax.pm

options '/api/v2/item/:id' => sub {
    return '';
};

Test it:

my $options  = $test->request( OPTIONS '/api/v2/item/anything' );
ok $options->is_success, '[POST /] successful';
is $options->header('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS, DELETE';

Replace manual HTML generation by the use of Handlebars

html  = '<ul>';
for (i = 0; i < data["items"].length; i++) {
    html += '<li>';
    html += data["items"][i]["text"];
    html += '<button class="delete" data-id="' +  data["items"][i]["_id"]["$oid"]  + '">x</a>';
    html += '</li>';
}
html += '</ul>';
<script id="show-items-template" type="text/x-handlebars-template">
<ul>
{{#each data.items}}
    <li>{{ text }} <button class="delete" data-id="{{ _id.$oid }}">x</a></li>
{{/each}}
</ul>
</script>

Handlebars code

<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/3.0.3/handlebars.min.js"></script>
    var source   = document.getElementById('show-items-template').innerHTML;
    var template = Handlebars.compile(source);
    var html    = template({ data: data });

Add timestamp to item (back-end)

use DateTime::Tiny;
date => DateTime::Tiny->now,
post '/api/v2/item' => sub {
        my $text = param('text') // '';
        $text =~ s/^\s+|\s+$//g;
        if ($text eq '') {
            return to_json { error => 'No text provided' };
        }
     
        my $items = _mongodb('items');
        $items->insert({
            text => $text,
            date => DateTime::Tiny->now,
        });
        return to_json { ok => 1, text => $text };
};

mongo client

$ mongo
test> use d2-ajax
d2-ajax> db.items.find()
{
  "_id": ObjectId("557593b5a114607aa9188b91"),
  "date": ISODate("2015-06-08T16:08:05Z"),
  "text": "new item"
}
Fetched 1 record(s) in 3ms

Add timestamp to item (back-end TO_JSON)

Route exception: encountered object '2015-06-08T16:08:05',
but neither allow_blessed, convert_blessed nor allow_tags settings are enabled
(or TO_JSON/FREEZE method missing

Run the tests

$ perl Makefile.PL
$ make
$ make test

t/v2.t ............... 1/4 [D2::Ajax:31818] error @2015-06-08 16:16:58>
Route exception: encountered object '2015-06-08T16:16:57', but neither allow_blessed,
convert_blessed nor allow_tags settings are enabled (or TO_JSON/FREEZE method missing)
at /Users/gabor/work/D2-Ajax/blib/lib/D2/Ajax.pm line 71.
in /Users/gabor/perl5/perlbrew/perls/perl-5.22.0_WITH_THREADS/lib/site_perl/5.22.0/Return/MultiLevel.pm l. 36
malformed JSON string, neither tag, array, object, number, string or atom,
at character offset 0 (before "<!DOCTYPE html PUBLI...") at t/v2.t line 67.
# Child (v2_items) exited without calling finalize()
$ prove -vl t/v2.t

ok 1 - v2_greeting
ok 2 - v2_reverse
    # {
    #   'items' => [
    #     {
    #       '_id' => {
    #         '$oid' => '5575974aa114607c78523711'
    #       },
    #       'date' => 'DateTime',
    #       'text' => 'First Thing to do'
    #     }
    #   ]
    # }
ok 3 - v2_items
ok 4 - no warnings
ok
All tests successful.
Files=1, Tests=4,  2 wallclock secs ( 0.04 usr  0.01 sys +  1.45 cusr  0.12 csys =  1.62 CPU)
Result: PASS

Tell MongoDB to use DateTime::Tiny

$client->dt_type( 'DateTime::Tiny' );
$ prove -vl t/v2.t

ok 1 - v2_greeting
ok 2 - v2_reverse
    # {
    #   'items' => [
    #     {
    #       '_id' => {
    #         '$oid' => '557598d0a114607c945d5021'
    #       },
    #       'date' => 'DateTime::Tiny',
    #       'text' => 'First Thing to do'
    #     }
    #   ]
    # }
ok 3 - v2_items
ok 4 - no warnings
ok
All tests successful.
Files=1, Tests=4,  3 wallclock secs ( 0.05 usr  0.01 sys +  1.52 cusr  0.15 csys =  1.73 CPU)
Result: PASS

Monkey patching DateTime::Tiny

sub DateTime::Tiny::TO_JSON { shift->as_string };
$ prove -vl t/v2.t

ok 1 - v2_greeting
ok 2 - v2_reverse
    # {
    #   'items' => [
    #     {
    #       '_id' => {
    #         '$oid' => '557599a1a114607cad788481'
    #       },
    #       'date' => '2015-06-08T16:33:21',
    #       'text' => 'First Thing to do'
    #     }
    #   ]
    # }
ok 3 - v2_items
ok 4 - no warnings
ok
All tests successful.
Files=1, Tests=4,  2 wallclock secs ( 0.04 usr  0.00 sys +  1.52 cusr  0.14 csys =  1.70 CPU)
Result: PASS

Testing

like $items1->{items}[0]{date}, qr/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d$/;

Add timestamp to item (front-end)

<script id="show-items-template" type="text/x-handlebars-template">
  <ul>
  {{#each data.items}}
      <li>{{ text }} {{ date }} <button class="delete" data-id="{{ _id.$oid }}">x</a></li>
  {{/each}}
  </ul>
</script>

jQuery Tablesorter

<script id="show-items-template" type="text/x-handlebars-template">
<table id="items-table" class="tablesorter">
    <thead>
    <tr><th>Item</th><th>Date</th><th>X</th></tr>
    </thead>
    <tbody>
{{#each data.items}}
    <tr><td>{{ text }}</td><td>{{ date }}</td><td><button class="delete" data-id="{{ _id.$oid }}">x</a></td></tr>
{{/each}}
    </tbody>
</table>
</script>
<script type="text/javascript" src="../public/javascripts/jquery.tablesorter.min.js"></script>
<table id="items-table" class="tablesorter">
$("#items-table").tablesorter();

Add Tablesorter themes

Copy from GitHub

<link rel="stylesheet" href="../public/themes/blue/style.css" type="text/css" media="print, projection, screen" />

Add Tablesorter Date column

Add to the template:

class="date" sort="{{ date }}"
var cfg = {
    textExtraction: function(node) {
        var $node = $(node);
        var sort = $node.attr("sort");
        if (!sort) { return $node.text(); }
        if ($node.hasClass("date")) {
            return (new Date(sort)).getTime();
        } else {
           return sort;
       }
   }
};
$("#items-table").tablesorter(cfg);

Handlebars

Getting started with Handlebars

Handlebars

<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/3.0.3/handlebars.min.js"></script>


<script src="handlebars.min.js"></script>

Handlebars Template

Hello `{{first_name}}` {{last_name}}
<script id="text-template" type="text/x-handlebars-template">
   Hello <b>{{first_name}}</b> {{last_name}}
</script>

Handlebars process

var source   = document.getElementById('text-template').innerHTML;
var template = Handlebars.compile(source);
var context = {first_name: fname, last_name: lname};
var html    = template(context);

jQuery

var source   = $('#text-template').html();

Handlebars inject into DOM

Inject HTML into DOM

Plain JavaScript

document.getElementById('result').innerHTML = html;

jQuery

$("#result").html(html);

Greeting in JavaScript - html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
  <title>Hello World</title>
</head>
<body>

First name: <input id="first_name">
Last name: <input id="last_name">
<button id="say">Say hi!</button>

<hr>
<div id="result"></div>


<script src="pure_js_greeting.js"></script>
</body>
</html>

Greeting in JavaScript - js

function say_hi() {
    var fname = document.getElementById('first_name').value;
    var lname = document.getElementById('last_name').value;

    var html = 'Hello <b>' + fname + '</b> ' + lname;

    document.getElementById('result').innerHTML = html;
}

document.getElementById('say').addEventListener('click', say_hi);

Greeting with Habdlebars - html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
  <title>Hello World - Handlebars</title>

  <script src="handlebars.min.js"></script>
  <script id="text-template" type="text/x-handlebars-template">
    Hello <b>{{first_name}}</b> {{last_name}}
  </script>

</head>
<body>

First name: <input id="first_name">
Last name: <input id="last_name">
<button id="say">Say hi!</button>

<hr>
<div id="result"></div>

<script src="handlebars_greeting.js"></script>
</body>
</html>

Greeting with Habdlebars - js

function say_hi() {
    var fname = document.getElementById('first_name').value;
    var lname = document.getElementById('last_name').value;

    var source   = document.getElementById('text-template').innerHTML;
    var template = Handlebars.compile(source);
    var context = {first_name: fname, last_name: lname};
    var html    = template(context);

    document.getElementById('result').innerHTML = html;
}

document.getElementById('say').addEventListener('click', say_hi);

Greeting with Habdlebars and jQuery - html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
  <title>Hello World - Handlebars - jQuery</title>

  <script src="jquery.js"></script>
  <script src="handlebars.min.js"></script>
  <script src="handlebars_greeting_jquery.js"></script>
  <script id="text-template" type="text/x-handlebars-template">
    Hello <b>{{first_name}}</b> {{last_name}}
  </script>

</head>
<body>

First name: <input id="first_name">
Last name: <input id="last_name">
<button id="say">Say hi!</button>

<hr>
<div id="result"></div>

</body>
</html>

Greeting with Habdlebars and jQuery - js

function say_hi() {
    var fname = $('#first_name').val();
    var lname = $('#last_name').val();

    var source   = $('#text-template').html();
    var template = Handlebars.compile(source);
    var context = {first_name: fname, last_name: lname};
    var html    = template(context);
    $('#result').html(html);
}

$(document).ready(function() {
    $('#say').click(say_hi);
});

Handlebars data

var data = {
   "title" : "Code Maven",
   "description" : "Coding is fun!",
   "address" : {
        "street" : "Main str."
   },
   "names" : [ "Foo", "Bar"],
   "articles" : [
       {
          "author": "Foo",
          "title" : "Handling user events in JavaScript",
          "url"   : "http://code-maven.com/handling-events-in-javascript",
          "desc"  : "This is the first article"
       },
       {
          "author": "Bar",
          "title" : "On-load counter with JavaScript and local storage",
          "url"   : "http://code-maven.com/on-load-counter-with-javascript-and-local-storage"
      },
       {
          "author": "Foo",
          "url"   : "http://code-maven.com/"
       }
   ],
   "people" : [
       {
           "name" : "Foo",
           "age" : 42
       },
       {
            "name" : "Bar",
            "age"  : 23
       }
   ]
};

Handlebars process

$(document).ready(function() {
    var source   = $('#text-template').html();
    var template = Handlebars.compile(source);
    var html    = template(data);
    $('#text').html(html);
});

Handlebars object and array

{{title}}
{{address.street}}
{{names.[0]}} - {{names.[1]}}
{{articles.1.title}}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">

  <script src="jquery.js"></script>
  <script src="handlebars.min.js"></script>
  <script src="handlebars_data.js"></script>
  <script src="handlebars_process.js"></script>

  <script id="text-template" type="text/x-handlebars-template">
    {{title}}<br>
    {{address.street}}<br>
    {{names.[0]}} - {{names.[1]}}<br>
    {{articles.1.title}}<br>
  </script>
</head>
<body>
<div id="text"></div>
</body>
</html>

Handlebars each

  • each
{{#each articles}}
   ...
{{/each}}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">

  <script src="jquery.js"></script>
  <script src="handlebars.min.js"></script>
  <script src="handlebars_data.js"></script>
  <script src="handlebars_process.js"></script>

  <script id="text-template" type="text/x-handlebars-template">
    <h2>{{title}}</h2>
    <h3>{{description}}</h3>
    <ul>
    {{#each articles}}
        <li><a href="{{url}}">{{title}}</a></li>
    {{/each}}
    </ul>
  </script>
</head>
<body>
<div id="text"></div>
</body>
</html>

Handlebars if

  • if
{{#if desc}}
   {{desc}}
{{else}}
   No description
{{/if}}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">

  <script src="jquery.js"></script>
  <script src="handlebars.min.js"></script>
  <script src="handlebars_data.js"></script>
  <script src="handlebars_process.js"></script>

  <script id="text-template" type="text/x-handlebars-template">
    <h2>{{title}}</h2>
    <h3>{{description}}</h3>
    <ul>
    {{#each articles}}
    <li><a href="{{url}}">{{title}}</a>
        {{#if desc}}
           <br>{{desc}}
        {{else}}
           <br>No description
        {{/if}}
    </li>
    {{/each}}
    </ul>
  </script>
</head>
<body>
<div id="text"></div>
</body>
</html>

Handlebars parent context

{{../../description}}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">

  <script src="jquery.js"></script>
  <script src="handlebars.min.js"></script>
  <script src="handlebars_data.js"></script>
  <script src="handlebars_process.js"></script>

  <script id="text-template" type="text/x-handlebars-template">
    <h2>{{title}}</h2>
    <h3>{{description}}</h3>
    <ul>
    {{#each articles}}
    <li><a href="{{url}}">{{title}}</a>
        {{#if desc}}
           <br>{{desc}}
        {{else}}
           <br>{{../../description}}
        {{/if}}
    </li>
    {{/each}}
    </ul>
  </script>
</head>
<body>
<div id="text"></div>
</body>
</html>

Handlebars static helpers - html

{{greeting}}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">

  <script src="jquery.js"></script>
  <script src="handlebars.min.js"></script>
  <script src="handlebars_helper_static.js"></script>
  <script src="handlebars_data.js"></script>
  <script src="handlebars_process.js"></script>

  <script id="text-template" type="text/x-handlebars-template">
      <h3>{{greeting}}</h3>
  </script>
</head>
<body>
<div id="text"></div>
</body>
</html>

Handlebars static helpers - js

  • SafeString
Handlebars.registerHelper('greeting', function() {
    return new Handlebars.SafeString( '<i>Hello World at ' + new Date + '</i>' );
});

Use SafeString here, if you want to make sure HTML tags are NOT escaped by Handlebars

Handlebars link helper - html

  • this
{{link this}}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">

  <script src="jquery.js"></script>
  <script src="handlebars.min.js"></script>
  <script src="handlebars_helper_link.js"></script>
  <script src="handlebars_data.js"></script>
  <script src="handlebars_process.js"></script>

  <script id="text-template" type="text/x-handlebars-template">
      <h2>{{title}}</h2>
      <h3>{{description}}</h3>
      <ul>
      {{#each articles}}
        <li>{{link this}}</li>
      {{/each}}
      </ul>
  </script>
</head>
<body>
<div id="text"></div>
</body>
</html>

Handlebars link helper

Handlebars.registerHelper('link', function(obj) {
    var url  = obj.url;
    var title = obj.title;
    if (title == undefined) {
        title = url;
    }
    return new Handlebars.SafeString( '<a href="' + url + '">' + title + '</a>' );
});

Handlebars if_eq - html

  • if_eq
  • iff

Works:

{{#if name}}
{{/if}}

Does not work:

{{#if name === 'Foo'}}
{{/if}}

So we will implement

{{#if_eq name === 'Foo'}}
{{/if_eq}}

Error: if_eq doesn't match if

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">

  <script src="jquery.js"></script>
  <script src="handlebars.min.js"></script>
  <script src="handlebars_if_eq.js"></script>
  <script src="handlebars_data.js"></script>
  <script src="handlebars_process.js"></script>

  <script id="text-template" type="text/x-handlebars-template">
      <h2>{{title}}</h2>
      <h3>{{description}}</h3>
      <ul>
      {{#each articles}}
        {{#if_eq author 'Foo'}}
           <li>{{author}} - {{url}}</li>
        {{/if_eq}}
      {{/each}}
      </ul>
  </script>
</head>
<body>
<div id="text"></div>
</body>
</html>

Handlebars if_eq

Handlebars.registerHelper('if_eq', function(a, b, opts) {
    if (a === b) {
        return opts.fn(this);
    } else {
        return opts.inverse(this);
    }
});

Handlebars conditionals - html

  • iff
{{#iff name '===' 'Foo'}}
{{#iff answer '>' 30}}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">

  <script src="jquery.js"></script>
  <script src="handlebars.min.js"></script>
  <script src="handlebars_helper_conditionals.js"></script>
  <script src="handlebars_data.js"></script>
  <script src="handlebars_process.js"></script>

  <script id="text-template" type="text/x-handlebars-template">
      {{#each people}}
         {{#iff name '===' 'Foo'}}
            name: {{name}} - {{age}}<br>
         {{/iff}}

         {{#iff age '>' 30}}
            age &gt; 30 for {{name}} - {{age}}<br>
         {{/iff}}
      {{/each}}
  </script>
</head>
<body>
<div id="text"></div>
</body>
</html>

Handlebars conditionals

Handlebars.registerHelper('iff', function(a, operator, b, opts) {
    var bool = false;
    switch(operator) {
       case '===':
           bool = a === b;
           break;
       case '>':
           bool = a > b;
           break;
       case '<':
           bool = a < b;
           break;
       default:
           throw "Unknown operator " + operator;
    }

    if (bool) {
        return opts.fn(this);
    } else {
        return opts.inverse(this);
    }
});

Handlebars helpers

  • Helpers
  • [Helpers Comparision](http://assemble.io/helpers/helpers-comparison.html" %}

Page with back and forward buttons (html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">

  <script src="jquery.js"></script>
  <script src="handlebars.min.js"></script>
  <script src="handlebars_data.js"></script>
  <script src="pages.js"></script>

  <script id="home-template" type="text/x-handlebars-template">
      <h1>Home</h1>
  </script>

  <script id="foo-template" type="text/x-handlebars-template">
      <h1>Foo</h1>
  </script>
  <script id="bar-template" type="text/x-handlebars-template">
      <h1>Bar</h1>
  </script>
  <script id="zorg-template" type="text/x-handlebars-template">
      <h1>Zorg</h1>
  </script>
</head>
<body>
<style>
li {
    display: inline;
    list-style-type: none;
    padding-right: 20px;
}
</style>
<ul>
    <li><a href="#">Home</a></li>
    <li><a href="#foo">Foo</a></li>
    <li><a href="#bar">Bar</a></li>
    <li><a href="#zorg">Zorg</a></li>
</ul>
<div id="text"></div>
</body>
</html>

Page with back and forward buttons (js)

  • window.location.hash
  • hashchange
function show(tmpl) {
    var template_name = '#' + tmpl + '-template';
    var source   = $(template_name).html();
    var template = Handlebars.compile(source);
    var html    = template(data);
    $('#text').html(html);
}

function routing(route) {
    if (! route) {
        route = '#';
    }
    console.log(route);
    if (route === '#') {
        show('home');
        return;
    }
    var m = /^#(foo|bar|zorg)$/.exec(route);
    if (m) {
        console.log(m[1]);
        show(m[1])
    }
}

$(document).ready(function() {
    $(window).bind('hashchange', function () {
        routing(window.location.hash);
    });
    routing(window.location.hash);
});