Amber Doctor
Senior Software Engineer
AngularJS is a client side framework created by Google
Organize your python with web2py
and your JavaScript / HTML with AngularJS
var demoAngular = angular.module('demoAngular', []);
demoAngular.controller('demoAngularCtrl', function($scope) {
$scope.message = 'Hello World from Angular!';
});
<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).
{{ }}
{{ }}
demoAngular.config(function($interpolateProvider) {
$interpolateProvider.startSymbol('{[{');
$interpolateProvider.endSymbol('}]}');
});
{[{ }]}
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>
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
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
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.
$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.
<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.
<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.
<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.
<ul>
<li ng-repeat='recipe in recipeList | orderBy:"name"'
ng-class='{selected:selectedRecipe === recipeList.indexOf(recipe), hover:true}'>{[{recipe.name}]}</li>
</ul>
<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>
$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.
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.
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.
$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.
$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.
<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.
<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
$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.
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 --
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))
$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.
<pre ng-show="showJson">{[{recipeList | json}]}</pre>
By Amber Doctor