rich object models & angular.js
DESCRIPTION
Super excited about Angular? Ready to change the world and build a super-heroic app? With directives, Angular has pretty much nailed it when it comes to interacting with the DOM. And plain-old Javascript objects as models? AWESOME! Wait, hang on. Say that you're building a app that has lots of business logic and interrelated data. For that sort of app, history shows us that a rich object model can often be the best place to put your logic and data relationships - from both a testing and ease-of-development perspective. That's the approach that frameworks like Ember advocate - but Ember forces you to extend on its own object model. In this talk I'll ask whether we can get the best of both worlds - a rich data model, whilst still using plain-old Javascript objects. We'll delve into things like: - Lazy-loading data relationships between models - Decorating loaded data with business logic - Object identity uniqueness (critical for bindings to work as expected) - Computed properties for models Attendees will leave with an understanding of how a rich object model can help them build beautiful, fast and easy-to-maintain apps.TRANSCRIPT
Rich Object Models & AngularBen Teese, Shine Technologies
Overview
Why?
Loading Data
Adding Business Logic
Advanced Stuff
Most of the apps I build are
CRUD
...it was nice
WARNING
The remainder of this presentation contains UX that some viewers may find
disturbing
Non-Recurring
Engineering
Internal Currency
External Currency Customer
Recurring Engineering
Material Cost Items
Internal Cost Items
DepartmentCost
Currency
External Cost Items
Cost
Currency
Shipsets
Details Years
Line Replaceable
Unit
Sales Price Override
SubassembliesInternal Cost Item
External Cost Items
Standard Sales Price
Spare Parts Sales Price
Customer Type Prices
Currency
Proposal
Currency Currency
Customer Type Sales Price
Currency
Purchase Price RangesSupplier
Purchase Price
Currency
DepartmentCost
Currency
Customer Type
OMG
Restangular
// GET /proposalsRestangular.all('proposals').getList().then( function(proposals) { $scope.proposals = proposals; });
Getting Stuff
or...
// GET /proposals$scope.proposals = Restangular.all('proposals').getList(). $object;
// GET /proposals/:id/cost_items $scope.proposal.getList('cost_items').then( function(costItems) { $scope.costItems = costItems; });
Getting Nested Stuff
Rich Models
angular.module('pimpMyPlane.services', ['restangular']). factory('ProposalSvc', function(Restangular) { Restangular.extendModel('proposals', function(obj) { return angular.extend(obj, {! profit: function() { return this.revenue().minus(this.cost()); }, revenue: function() { return this.price(). convertTo(this.internalCurrency); } ... }); });
return Restangular.all('proposals'); })
angular.module('pimpMyPlane.models'). factory('Proposal', function() { return { profit: function() { return this.revenue().minus(this.cost()); }, revenue: function() { return this.price(). convertTo(this.internalCurrency); }, ... }; }
A Model Mixin
angular.module('pimpMyPlane.services', ['restangular', 'pimpMyPlane.models']).factory('ProposalSvc', function(Restangular, Proposal){ Restangular.extendModel('proposals', function(obj) { return angular.extend(obj, Proposal); });
return Restangular.all('proposals'); });
Using the Mixin
angular.module('pimpMyPlane.services', ['restangular']). factory('ProposalSvc', function(Restangular) { Restangular.extendModel('proposals', function(obj) { angular.extend(obj.recurringEngineering, { ... }); angular.extend(obj.nonRecurringEngineering, { ... }); angular.extend(obj.internalCurrency, { ... }); angular.extend(obj.externalCurrency, { ... });
return angular.extend(obj, Proposal); }); ... })
What about nested models?
angular.module('pimpMyPlane.services', ['restangular', 'pimpMyPlane.models']). factory('Proposals', function(Restangular, Proposal) { Restangular.extendModel('proposals', function(obj) { return Proposal.mixInto(obj); }); ... });
Introduce mixInto()
angular.module('pimpMyPlane.models'). factory('Proposal', function( Currency, RecurringEngineering, NonRecurringEngineering ) { return { mixInto: function(obj) { RecurringEngineering.mixInto( obj.recurringEngineering ); NonRecurringEngineering.mixInto( obj.nonRecurringEngineering ); Currency.mixInto(obj.internalCurrency); Currency.mixInto(obj.externalCurrency)) return angular.extend(obj, this); }, profit: function() { return this.revenue().minus(this.cost()); }, ... }; });
Non-Recurring
Engineering
Internal Currency
External Currency Customer
Recurring Engineering
Material Cost Items
Internal Cost Items
DepartmentCost
Currency
External Cost Items
Cost
Currency
Shipsets
Details Years
Line Replaceable
Unit
Sales Price Override
SubassembliesInternal Cost Item
External Cost Items
Standard Sales Price
Spare Parts Sales Price
Customer Type Prices
Currency
Proposal
Currency Currency
Customer Type Sales Price
Currency
Purchase Price RangesSupplier
Purchase Price
Currency
DepartmentCost
Currency
Customer Type
Shazam
Identity Maps
Non-Recurring
Engineering
Internal Currency
External Currency Customer
Recurring Engineering
Material Cost Items
Internal Cost Items
DepartmentCost
Currency
External Cost Items
Cost
Currency
Shipsets
Details Years
Line Replaceable
Unit
Sales Price Override
SubassembliesInternal Cost Item
External Cost Items
Standard Sales Price
Spare Parts Sales Price
Customer Type Prices
Currency
Proposal
Currency Currency
Customer Type Sales Price
Currency
Purchase Price RangesSupplier
Purchase Price
Currency
DepartmentCost
Currency
Customer Type
Identity Map
USD
EUR
IT
“currency”:1
“currency”:2
“department”:1
......
Finance
“department”:2
......
angular.module('pimpMyPlane.models'). factory('Money', function(Currency, identityMap) { return { mixInto: function(obj) { obj.currency = identityMap( 'currency', Currency.mixInto(obj.currency) ); angular.extend(object, this); }, ... });
Mapping Nested Currencies
angular.module('pimpMyPlane.services', ['restangular', 'pimpMyPlane.models']).factory('CurrenciesSvc', function( Restangular, Currency, identityMap ) { Restangular.extendModel('currencies', function(obj){ return identityMap( 'currency', Currency.mixInto(obj) ); });
return Restangular.all('currencies'); });
Mapping RESTful Currencies
Getter Functions(Uniform Access Principle)
angular.module('pimpMyPlane.models'). factory('Proposal', function(extendWithGetters) { return { mixInto: function(obj) { ... return extendWithGetters(obj, this); }, get profit() { return this.revenue.minus(this.cost); }, get revenue() { return this.price.convertTo( this.internalCurrency ); }, ... }; });
Memoization
angular.module('pimpMyPlane.models'). factory('Proposal', function(Model) { return Model.extend({ memoize: ['revenue', 'cost'], ... get profit() { return this.revenue.minus(this.cost); }, get revenue() { return this.price.convertTo( this.internalCurrency ); }, ... }; });
<div ng-controller="ProposalCtrl"> ... <input type="number" ng-model="currency.conversionFactor" ng-change="proposal.unmemoize()"></input> ... <table> <tr> <td>Number of Aircraft</td> <td> <input type="number" min="1" ng-model="proposal.numberOfAircraft" ng-change="proposal.unmemoize()"></input> </td> </tr> </table></div>
Unmemoization
Computed Properties
angular.module('pimpMyPlane.models'). factory('Proposal', function(...) { return Model.extend({ ... computedProperties: { profit: [function() { return this.revenue.minus(this.cost); }, 'revenue', 'cost'],
cost: [function() { return this.recurringEngineering.cost.plus( this.nonRecurringEngineering.cost ); }, 'recurringEngineering.cost', 'nonRecurringEngineering.cost'] }, }); });
...needs more work
Let’s Wrap This Up
Rich Models can work
Identity Maps
Getters, Memoization
Computed properties
Shrink-wrappedBoeing 737
@benteese
Please enjoy the remainder of your flight