Paul Hammant's Blog: (almost) No JavaScript prototyping with Angular in a single source file
[ Following on from last week’s blog entry ] So what is this Angular stuff like in a real world app? We’ll I’ve gone to the demo site for SpreeCommerce’s open source shopping cart and saved a live snapshot of the page using the MAFF plugin for Firefox. SpreeCommerce is a good target for me presently, as the copyright/license for it does not preclude hacking to showcase Angular.
I checked that into GitHub as a new repository : https://github.com/paul-hammant/spreecommerce_angular. There are three commits, if you look here. The first is a checking as was saved. The second, was the result of loading the page into DreamWeaver, and reformatting. The third is the result of replacing the Web 1.0 functionality (Ruby on Rails) with Angular. If you click on the diff link in the github interface you can see what I did precisely. Generally though, I removed all other JavaScript, and changed the first row of the cart to a repeating row in the Angular style. There’s also a JSON model for the cart contents, which for the purposes of honoring this blog entry’s title, I’m going to consider that not JavaScript as such. The only real JavaScript is a wrapper for an an alert box, that’s hooked into “Continue Shopping”, and “Checkout” as they don’t actually work. The won’t work until the backend is reworked to handle a JSON version of the cart contents on a POST. Incidentally, if you’re new to this, ng:repeat=”item in cart.items” is where the magic is really hooked in.
You can play with the finished product here: https://paul-hammant.github.io/spreecommerce_angular/ (as well as look at the source in situ). The page looks like this:
The difference to the original is that the “update” button is missing as POST-redirect-GET to the server side is not longer needed to recalculate the cart size.
Angular’s advantage restated
The expressions that are possible on augmented arrays in particular, and angular ‘functions’ generally, are the stroke of genius power boost in my opinion. They are allowing me to prototype without writing JavaScript functions, yet build a highly functional client-side application. In the example above we have $sum(expression) used for a subtotal summing all the result of price times quantity for ALL the items in the model:
<td colspan="2" class="totals"> Subtotal: {{cart.items.$sum('price*qty') | currency}} </td>
For the individual item ‘row’ in the cart, $sum(..) is not used, but an expression is:
<td class="total"> {{item.price*item.qty | currency}} </td>
Miško has also added convenience functions to modify the model, for user interaction:
<a ng:click="cart.items.$remove(item)" class="delete button">Remove</a>
Not shown in this example, are $filter(expression) :
<tr ng:repeat="item in cart.items.$filter('price > 9.95')"/>
<td>..</td>
</tr>
Nor is $orderBy(field, reverseOrNot) :
<tr ng:repeat="item in cart.items.$orderBy('price', false)"/>
<td>..</td>
</tr>
Expressions can be hard coded in single-quotes, or variables passed in. In the case of ordering tables, you’d assign the order column to a variable, and have some other UI control change that (as well as ascending or descending). I’ve an example of that, and filtering here from a year ago: https://paul-hammant.github.io/StoryNavigator
Powerful stuff huh?
A better Etsy.com example.
Etsy.com (a fantastic shopping site for hand-made designer craft items) have a items within stores within the cart. It’s more of a model. I’ve made an Angular version of their cart too, but presently don’t yet have permission to push the source to GitHub (it’s copyrighted to them, not open source). Here’s the screen-shot:
The JSON source for the ‘cart’ looks like so:
scope.cart = {
stores:[
{
cartId: 138229652,
name: "GoldenSection",
items:[
{
id: "89166902",
txt: "January - photography print calendar zodiac Aquarius Capricorn winter birthday owl romantic gift for her him unisex home decor wall art love",
short: "january-photography-print-calendar",
price: 26,
shipping: 5,
qty: 1,
note: "",
img: {
url: "index_files/il_75x75.297883532.jpg",
w: 75,
h: 75
}
}
],
payment: "Paypal"
},
{
cartId: 138148701,
name: "RetroModernArt",
items:[
{
id: "69841278",
txt: "Bike Art Black Tandum Bike 8x10 Linocut art print",
short: "bike-art-black-tandum-bike-8x10-linocut",
price: 24,
shipping: 4,
qty: 1,
note: "",
img: {
url: "index_files/il_75x75.249358938.jpg",
w: 75,
h: 75
}
},
{
id: "85933033",
txt: "Valentines Day LOVE Red Block Print - LOVE art in RED Typography Woodblock 5 x 7 on white paper",
short: "valentines-day-love-red-block-print-love",
price: 12,
shipping: 2,
qty: 1,
note: "",
img: {
url: "index_files/il_75x75.297883532.jpg",
w: 75,
h: 75
}
}
],
payment: "Paypal"
}
]
};
There are some interesting things to note, that are not as prominent in the SpreeCommerce cart that I have released the source for, but still true:
- I’m mixing reference information in the cart’s JSON, alongside mutable entities like ‘quantity’.
- index_files the directory does not really hold any images, that’s just the way the MAFF plugin saved a local copy.
In the case of the former, I could do a refactoring to separate the ‘never changes’ reference data from the cart contents that does change (quantities change and lines disappear). In the end though it does not matter. The whole JSON document being pushed back to the server for a ‘checkout’ action is hardly going to hurt it. I’ve optimized the JSON document to support the user interface composition, which is what I think you should do for Angular apps.
Etsy’s Cart Size.
Remember that items are within stores that are within the cart. Here’s the use of $sum() to flatten that to a single number:
{{cart.stores.$sum('items.length')}}
Etsy’s orders have a shipping amount too.
The per store expression to calculate a total for the items obeys the standard order of operations of course:
{{store.items.$sum('price*qty + shipping') | currency}}
Etsy’s grand total:
Well this isn’t possible yet, in an expression. Just as well its not needed for the Etsy cart:
{{ cart.stores.$sum('store.$sum('price*qty + shipping')') | currency }}
Yeesh!
More Aggregate Functions
I’m really enamored by thinking of the JSON document as a live database within the web page. I was pretty good at SQL in the early 90’s and loved the composition of reports with judicious use of SQL’s GROUP BY, HAVING and UNION. If you ever meet me for an interview (ThoughtWorks or Client) and have put SQL on your resume, I’m going to see how far you can go with those.
Take a look at a tutorial for Group-By. We can used that to explore a few more array (aggregate) functions in Angular. Functions that don’t exist yet, but I hope will be implemented in due course.
scope.orders = [
{
OrderDate: "2008/11/12",
OrderPrice: 1000,
Customer: "Hansen"
},
.... etc
];
Group-by:
<tr><td>Customer</td><td>Total Order</td></tr>
<tr ng:repeat="custOrd in orders.$groupBy('Customer').$select('Customer, sum(OrderPrice) as OrderTotal')" />
<td>{{custOrd.Customer}}</td><td>{{custOrd.OrderTotal}}</td>
</tr>
Having (equivalent):
<tr><td>Customer</td><td>Total Order</td></tr>
<tr ng:repeat="custOrd in orders.$groupBy('Customer').$select('Customer, sum(OrderPrice) as OrderTotal').$filter('OrderTotal > 1000')" />
<td>{{custOrd.Customer}}</td><td>{{custOrd.OrderTotal}}</td>
</tr>
Min, Max, Average:
<tr><td>Customer</td><td>Minimum Order</td><td>Maximum Order</td><td>Average Order</td></tr>
<tr ng:repeat="custOrd in orders.$groupBy('Customer').$select('Customer, min(OrderPrice) as MinOrder, max(OrderPrice) as MaxOrder, mean(OrderPrice) as AverageOrder')" />
<td>{{custOrd.Customer}}</td><td>{{custOrd.MinOrder}}</td><td>{{custOrd.MaxOrder}}</td><td>{{custOrd.AverageOrder}}</td>
</tr>
Simpler aggregate Functions.
It is possible to consider the following …
- orders.$min(‘OrderPrice’)
- orders.$max(‘OrderPrice’)
- orders.$mean(‘OrderPrice’)
… on the whole array, or an array after being filtered:
<span>Hansen average: {{ orders.$filter("{ Customer: 'Hansen'}").$mean('OrderPrice') }} </span>
It is much less useful than the pivoted table version that Group-By gives us though.