AngularJS & Web2py



Amber Doctor

amberdoctor@gmail.com

Code Available At:

AngularJS


AngularJS is a client side framework created by Google


Organize your python with web2py

and your JavaScript / HTML with AngularJS



Angular App and Controllers

default/index.html (views)

var demoAngular = angular.module('demoAngular', []);

demoAngular.controller('demoAngularCtrl', function($scope) {
    $scope.message = 'Hello World from Angular!';
});

Script defining and mapping controller to app.

<div ng-app='demoAngular'>
    <div ng-controller='demoAngularCtrl'>    {[{ message }]} <!-- Displays message from above --></div></div>

Wrap ng-app around code that will use AngularJS.


Wrap ng-controller around code that uses controller's scope. Multiple controllers allowed (not nested).

interpolateProvider

default/index.html (views)

{{ }}

Default Angular Expression Indicator 
{{ }}

Default Web2py Expression Indicator 
demoAngular.config(function($interpolateProvider) {
        $interpolateProvider.startSymbol('{[{');
        $interpolateProvider.endSymbol('}]}');
});
Allows AngularJS and Web2py to coexist 
{[{ }]}

Remapped Angular Expression Indicator 

Angular Model & Data Binding

recipe_book.py (models)

demoAngular.controller('demoAngularCtrl', function($scope) {
        $scope.message = 'Hello World from Angular!';
        $scope.databindMessage = '{{=message}}';});
{{if 'message' in globals():}}
<h4>"message" (Web2py): {{=message}}</h4>
<div ng-app='demoAngular'>
  <div ng-controller='demoAngularCtrl'>
    <h4>"message" (Angular): {[{message}]}</h4>
    <h4>"databindMessage": {[{databindMessage}]}</h4>
    <h4>Type "yourMessage" here: <input type="text" ng-model="yourMessage" placeholder="yourMessage"></h4>
    <h4>See "yourMessage" here: {[{yourMessage}]}</h4>

Comparison of setting "message" variables server side with Web2py and client side with AngularJS.  databindMessage is a server side defined "message" that is mapped to the AngularJS model.

Data Bindings in Action

default/index.html (views)






Recipe Book Database

recipe_book.py (models)

db.define_table('recipe',
                Field('name', 'string'),
                Field('instructions', 'text'),
                format='%(name)s')
db.define_table('ingredient',
                Field('recipe_ref', 'reference recipe'),
                Field('name', 'string'),
                Field('quantity', 'float'),
                Field('unit', 'string'),
                format='%(quantity)s %(unit)s %(name)s') 



Define recipe and ingredient tables 

Populate the Database

z_fixtures.py (models)

RESET = False

if RESET:
    for table in db.tables:
        # Make sure to cascade, or this will fail
        # for tables that have FK references.
        db[table].truncate("CASCADE")
    db.commit()

def add_recipe(new_recipe):
    recipe_id = db.recipe.validate_and_insert(name=new_recipe['name'], instructions=new_recipe['instructions'])['id']
    for ingredient in new_recipe['ingredient_list']:
        db.ingredient.insert(recipe_ref=recipe_id, name=ingredient['name'], quantity=ingredient['quantity'], unit=ingredient['unit'])

def add_recipe_list(recipe_list):
    for recipe in recipe_list:
        add_recipe(recipe)

recipe_list = [{"ingredient_list":[{"name":"chocolate",
                                    "quantity":1,
                                    "unit":"bar"},
                                   {"name":"graham crackers",
                                    "quantity":1,
                                    "unit":"box"},
                                   {"name":"marshmallows",
                                    "quantity":1,
                                    "unit":"bag"}],
                "name":"Smores",
                "instructions":"Toast Marshmallow over fire.  Sandwich the toasted marshmallow and chocolate between two graham crackers."},
               --REPEAT MORE RECIPES--
               ]
if RESET:
    add_recipe_list(recipe_list)


Create some sample data

Recipe Book Index

recipes.py (controllers)

import gluon.contrib.simplejson
def index():
    rows = db(db.recipe).select().as_list()
    return dict(recipe_list=gluon.contrib.simplejson.dumps(rows))


Pull recipes from the database as a list.


Convert the recipe list to JSON.


Return JSON recipe list as the value in a python dict. 

 

Notice ordering isn't specified.  Angular can handle it.

Data Bind Recipe List

recipes/index.html (views)


$scope.recipeList = {{=XML(recipe_list)}};


Data binding the Web2py data to the Angular model.


Since the data is JSON, Web2py has to be explicitly told to render the XML that is being passed.


You can see how recipeList looks at any given time by selecting the "Show Recipe List Json" button in the app.

Recipe Book In Action

recipes/index.html (views)


Make a List Using ng-repeat

recipes/index.html (views)

<ul>
    <li ng-repeat='recipe in recipeList'>        {[{recipe.name}]}    </li>
</ul>


Here we use an ng-repeat to iterate through recipeList to print each recipe's name. 


In angular the format is similar to a python for-loop.

Angular OrderBy

recipes/index.html (views)

<ul>
    <li ng-repeat='recipe in recipeList | orderBy:"name"'>
        {[{recipe.name}]}    </li>
</ul>


By default, Angular will print the items in the list using the list's default order. 

 

To change this order, we can specify an orderBy. 


Make sure the name of the field you want to orderBy is specified in quotes.

Variable orderBy

https://docs.angularjs.org/api/ng/filter/orderBy

<th><a href="" ng-click="predicate = 'name'; reverse=!reverse">Name</a></th>
<th><a href=""ng-click="predicate = 'phone'; reverse=!reverse">Phone Number</a></th>

<tr ng-repeat="friend in friends | orderBy:predicate:reverse">
    <td>{{friend.name}}</td>
    <td>{{friend.phone}}</td>
</tr>

 

Example uses predicate to enable changing the orderBy.

Snippet from AngularJS docs showing sorting being driven by clicking table headers.

ng-class

recipes/index.html (views)

<ul>
	<li ng-repeat='recipe in recipeList | orderBy:"name"' 
	ng-class='{selected:selectedRecipe === recipeList.indexOf(recipe), hover:true}'>{[{recipe.name}]}</li>
</ul>
ng-class allows a css class to be turned on or off based on the evaluation of an expression or function that results in a boolean.

"selected" is turned on and off based on whether the selectedRecipe is equal to the index of the current item in the list.

recipeList.indexOf(recipe) is used in place of $index when orderBy or Filters are applied as $index changes when order changes

Setting a class equal to “true” turns it on all the time.

ng-click

recipes/index.html (views)

<ul>
	<li ng-repeat='recipe in recipeList | orderBy:"name"' 
	ng-click='getIngredients(recipe)' 
	ng-class='{selected:selectedRecipe === recipeList.indexOf(recipe), hover:true}'>{[{recipe.name}]}</li>
</ul>


ng-click performs the specified function or expression on click.

This example calls getIngredient() and passes recipe as an argument.

"recipe" is passed in place of $index and indexOf(recipe) is called in getIngredients as this list has an orderBy applied.

getIngredients()

recipes/index.html (views)

$scope.getIngredients = function(recipe){
         //check if the list already has ingredients - if not grab them
        index = $scope.recipeList.indexOf(recipe);
        if ($scope.recipeList[index]['ingredient_list']){
            $scope.selectedRecipe = index;
            $scope.numberOfBatches = 1;
        } else {
            //Get the data from the database via http call
            $http.get('{{=URL('recipes','get_ingredients')}}' + '.json/' + recipe.id)
            -- ABREVIATED CODE --
        };


Check if ingredients are in the model for the recipe.

If so display the selected recipe with ingredients.

If not make a http get request for the ingredients.

get_ingredients()

recipes.py (controllers)

def get_ingredients():
    recipe_id = request.args[0]
    query = db.ingredient.recipe_ref==recipe_id
    rows = db(query).select().as_list()
    return dict(ingredient_list=rows)


Format the query with as_list().


Be sure to return a dict to allow .json calls.


JSON Calls

db.py (models)


response.generic_patterns = ['*'] if request.is_local else []


Update your db.py file to remove the if clause.



response.generic_patterns = ['*']


This change allows you to make JSON calls without creating a .json view for each controller.

Recipe Book In Action

recipes/index.html (views)


Success Callback

recipes/index.html (views)

$scope.getIngredients = function(recipe){
			-- ABBREVIATED CODE -- 
    $http.get('{{=URL('recipes','get_ingredients')}}' 
                                       + '.json/' + recipe.id)
        .success(function(data, status) {
            $scope.recipeList[index]['ingredient_list'] =
                                          data.ingredient_list;
            $scope.selectedRecipe = index;
            $scope.numberOfBatches = 1;
        })
        .error(function(data, status){-ABBREVIATED CODE-});
};

http has a built in success callback.

Pass in data and status to the success function.

Data contains the returned data from Web2py.

Status has the https request status.

Error Callback

recipes/index.html (views)

$scope.getIngredients = function(recipe){
			-- ABBREVIATED CODE -- 
    $http.get('{{=URL('recipes','get_ingredients')}}' 
                                       + '.json/' + recipe.id)
        .success(function(data, status){-- ABBREVIATED CODE --})
        .error(function(data, status) {
            $scope.errorMessage = 'Recipe Detail Request Failed';
            $scope.selectedRecipe = -1;
        });
};


http has a build in error callback.

Status will contain the status of the http request.

In the error callback handle errors.

Example sets errorMessage and resets selectedRecipe.

Batch Multiplier

recipes/index.html (views)

<div ng-hide="selectedRecipe == -1">
    <h3>Selected Recipe</h3>
    <h4>{[{recipeList[selectedRecipe].name}]}</h4>
    <div>Number of Batches <input 
        type='text'  ng-model='numberOfBatches' />
    </div>
    <div><ul>
        <li ng-repeat='ingredient in recipeList[selectedRecipe].ingredient_list'>{[{ingredient.quantity*numberOfBatches}]} {[{ingredient.unit}]} {[{ingredient.name}]}</li>    </ul></div>
    <div>{[{recipeList[selectedRecipe].instructions}]}</div>
</div>


Use data model to perform client side calculations based on user input.

Recipe Book In Action

recipes/index.html (views)


Adding a Recipe

recipes/index.html (views)

<div><input type='text' ng-model='newRecipe.name'></div>
<div><input type='text' ng-model='newRecipe.instructions'></div>
<div><ul>
    <li ng-repeat='ingredient in newRecipe.ingredient_list'>
        <input type='text' ng-model='ingredient.name' />
        <input type='number'ng-model='ingredient.quantity' />
        <input type='text' ng-model='ingredient.unit' />
        <span ng-click='removeIngredient($index)'>Remove</span>
    </li>
</ul></div>
<button ng-click='addIngredient()'>Add Ingredient</button>
<button ng-click='saveRecipe()'>Save Recipe</button>


Create a form and bind it to the model.
* placeholder text removed from above example

saveRecipe()

recipes/index.html (views)

$scope.saveRecipe = function(){
    $http.put('{{=URL('recipes','add_recipe')}}', $scope.newRecipe)        .success(function(data, status) {            $scope.recipeList.push(data.newRecipe); 
            $scope.showAddRecipe = false;
            $scope.newRecipe = {'ingredient_list':[]};
        })
        .error(function(data, status) {
            $scope.errorMessage = 'Save Recipe Failed';
    });
};


http put request takes a URL and DATA as parameters.

On success, add the recipe to the recipeList and clear newRecipe.

add_recipe() -- part 1

recipes.py (controllers)

def add_recipe():
    new_recipe = 
        gluon.contrib.simplejson.loads(request.body.read())
    recipe_id =
        db.recipe.validate_and_insert(
                  name=new_recipe['name'],
                  instructions=new_recipe['instructions']
                  )['id']
    for ingredient in new_recipe['ingredient_list']:
        -- See Next Slide --


Convert the recipe from json to a python dictionary.

Use validate_and_insert() to save the recipe.

Store the recipe id to use in saving the ingredients.

add_recipe() -- part 2

recipes.py (controllers)

for ingredient in new_recipe['ingredient_list']:
    db.ingredient.validate_and_insert(
                            recipe_ref=recipe_id,
                            name=ingredient['name'],
                            quantity=ingredient['quantity'],
                            unit=ingredient['unit'])
    new_recipe = get_recipe(recipe_id)
    return gluon.contrib.simplejson.dumps(
                                      dict(newRecipe=new_recipe))


Loop through and save each of the ingredients
and call validate_and_insert().


Call get recipe to get the recipe with ids.

Return the recipe in JSON.

Show & Hide JSON

recipes/index.html (views)

$scope.toggleShowJson = function(){
            $scope.showJson = !$scope.showJson;};

<button ng-click='toggleShowJson()'>
    <span ng-show='showJson'>Hide</span>
    <span ng-hide='showJson'>Show</span>
 Recipe List JSON</button>
<pre ng-show="showJson">{[{recipeList | json}]}</pre>

ng-show (and ng-hide) binds an expression to a html element to determine the element's visibility.


Display Model as JSON

recipes/index.html (views)


<pre ng-show="showJson">{[{recipeList | json}]}</pre>


Just add "| json"
to the standard way to display the angular data.

Thank You

AngularJS and Web2py


Amber Doctor

amberdoctor@gmail.com

Code Available At:

AngularJS and Web2py

By Amber Doctor

AngularJS and Web2py

  • 10,166