object oriented ngresource - angularjs dc lightning round talks 2015-02-18
TRANSCRIPT
Object-Oriented ngResource
Carl Gieringer Software Engineer, Revmetrix
https://github.com/carlgieringer/ngResourcePattern
Overview
$resource: good for CRUD
Impedance mismatch with OOD Data
Behavior
Solutions Data: transformers
Behavior: Resource.prototype
Wrap that pattern in a service
ngResource: good for CRUD
// Definition angular.module('app.books', []) .factory('Book', function($resource) { return $resource('/api/v1/books/:id', { id: '@id' }); }). // Create controller('BookCreateCtrl', function(Book) { $scope.book = new Book(); $scope.saveBook = function() { $scope.book.$save(); } });
ngResource: good for CRUD
// List $scope.books = Book.query();
// Read $scope.book = Book.get({id: bookId});
// Update $scope.book.$save();
// Delete $scope.book = new Book({id: bookId}); $scope.book.$delete();
OOD: Semantically Coherent Units of Data and Behavior
function Book(props) { this.id = props.id; this.name = toTitleCase(props.name); this.published = new Date(props.published); ... } Book.prototype = { isPublished: function() { return this.published < new Date(); } }
Resource-OO Impedance Mismatch: data
$scope.book = Book.get({id: 42}); // GET /api/v1/books/42 // { id: 42, name: "A Guide", published: 308548800 } // In controller $scope.book = Book.get({id: bookId}, function() { $scope.book.published = new Date($scope.book.published); }); // In another controller $scope.book = Book.get({id: bookId});
Resource-OO Impedance Mismatch: behavior
// In controller $scope.isPublished = function(book) { return book.published < new Date(); } // In another controller $scope.isPublished = function(book) { return book.published < new Date(); } // In yet another controller $scope.isPublished = function(book) { return book.published <= new Date(); }
Impedance Matching Data: transformers
$resource('/api/books/:id', null, { get: { transformRequest: [ transformBookForServer angular.toJson ], transformResponse: [ angular.fromJson, transformBookFromServer ] } // query, save, delete omitted });
Impedance Matching Data: transformers
function transformBookForServer(book) { book.published = book.published.valueOf(); // ... return book; } function transformBookFromServer(book) { book.published = new Date(book.published); // ... return book; }
Impedance Matching Behavior: prototype
angular.module('app.books', []) .factory('Book', function($resource) { var Book = $resource('/api/books/:id'); Book.prototype.isPublished = function() { return this.published < new Date(); }; return Book; });
Result Usage: DRY and Object-Oriented
$scope.book = Book.get({id: bookId}); <button ng-show="book.isPublished()"> Order Now! </button>
Resulting Invocation: Repetitive
angular.module('app.books', []) .factory('Book', function($resource) { return $resource('/api/v1/books/:id', null, { get: { transformRequest: [ transformBookForServer angular.toJson ], transformResponse: [ angular.fromJson, transformBookFromServer ] }, query: { isArray: true, transformRequest: [ transformBookForServer angular.toJson ], transformResponse: [ angular.fromJson, _.partialRight(_.map, transformBookFromServer) ] },
save: { method: 'POST', transformRequest: [ transformBookForServer angular.toJson ], transformResponse: [ angular.fromJson, transformBookFromServer ] }, delete: { method: 'DELETE', transformRequest: [ transformBookForServer angular.toJson ], transformResponse: [ angular.fromJson, transformBookFromServer ] } }); });
Wrap It in a Service: resoureSrv
angular.module('app.books', []) .factory('Book', function(resourceSrv) { return resourceSrv.makeResource('/api/v1/books/:id',{ responseTransformer: transformBookFromServer, requestTransformer: transformBookForServer, prototype: { isPublished: function() { return this.published < new Date(); } } }); });
Wrap It in a Service: makeResource function makeResource(url, options) { options = options || {}; var actions = { get: makeAction({ responseTransformer: options.responseTransformer }), query: makeAction({ isArray: true, responseTransformer: options.responseTransformer }), save: makeAction({ method: 'POST', responseTransformer: options.responseTransformer, requestTransformer: options.requestTransformer }), update: makeAction({ method: 'PUT', responseTransformer: options.responseTransformer, requestTransformer: options.requestTransformer }) }; var Resource = $resource(url, options.params, actions, options.options); angular.extend(Resource.prototype, options.prototype); return Resource; }
Wrap It in a Service: makeAction
function makeAction(options) { var action = {}; if (options.method) { action.method = options.method; } if (options.isArray) { action.isArray = options.isArray; } if (options.params) { action.params = options.params; } if (options.requestTransformer) { action.transformRequest = makeRequestTransformer(options.requestTransformer); } if (options.responseTransformer) { action.transformResponse = makeResponseTransformer(options.responseTransformer); } return action; }
Wrap It in a Service: makeResponseTransformer
function makeResponseTransformer(responseTransformer) { return function generatedResponseTransformer(data) { try { data = angular.fromJson(data); } catch (err) { throw new Error("API response is not JSON"); } return _.isArray(data) ? _.map(data, responseTransformer) : responseTransformer(data); }; }
Wrap It in a Service: makeResponseTransformer
function makeRequestTransformer(requestTransformer) { return function generatedRequestTransformer(data) { var copy = angular.copy(data); var transformed = requestTransformer(copy); return angular.toJson(transformed); }; }