Angular + JSData
js-data-angular - An Angular wrapper for JSData
Since JSData was originally written specifically for Angular (angular-data), some Angular-specific things had to be taken out to make JSData work with any framework. Ember and React don't have $scope
, $q
or $http
, so JSData can't use those. js-data-angular
brings back to JSData the Angular-specific stuff that was originally in Angular-data.
Here's the math: js-data + js-data-angular = angular-data 2.0
.
While JSData and its adapters provide constructor classes like DS
and DSHttpAdapter
that you have to instantiate yourself, js-data-angular
sets everything up for you. So you can just inject DS
(the result of new JSData.DS()
) and DSHttpAdapter
(the result of new DSHttpAdapter()
. You can set their defaults via DSProvider.defaults
and DSHttpAdapterProvider.defaults
.
Quick Start
bower install --save js-data-angular js-data
or npm install --save js-data-angular js-data
Load js-data-angular.js
after js-data.js
.
Create your Angular app:
angular.module('myApp', ['js-data'])
Set global configuration:
angular.module('myApp')
.config(function (DSProvider, DSHttpAdapterProvider) {
angular.extend(DSProvider.defaults, { ... });
angular.extend(DSHttpAdapterProvider.defaults, { ... });
})
Define your resources:
angular.module('myApp')
.run(function (DS) {
// DS is the result of `new JSData.DS()`
// We don't register the "User" resource
// as a service, so it can only be used
// via DS.<method>('user', ...)
// The advantage here is that this code
// is guaranteed to be executed, and you
// only ever have to inject "DS"
DS.defineResource('user');
})
.factory('Comment', function (DS) {
// This code won't execute unless you actually
// inject "Comment" somewhere in your code.
// Thanks Angular...
// Some like injecting actual Resource
// definitions, instead of just "DS"
return DS.defineResource('comment');
});
Use it:
angular.module('myApp')
.controller('commentsCtrl', function ($scope, Comment) {
Comment.findAll().then(function (comments) {
$scope.comments = comments;
});
});
Adapters
If you're not using Angular, then you have to load js-data-http.js
to get an http adapter for JSData. Users of js-data-angular
don't have to, because js-data-angular
provides a DSHttpAdapter
implementation that uses $http
.
Other adapters like DSLocalStorageAdapter
, DSFirebaseAdapter
, etc. have to be loaded separately.
Example:
npm install --save js-data js-data-firebase js-data-angular
or bower install --save js-data js-data-firebase js-data-angular
Load js-data.js
, js-data-firebase.js
and then js-data-angular.js
. File order is important!
angular.module('myApp', ['js-data'])
.config(function (DSFirebaseAdapterProvider) {
DSFirebaseAdapterProvider.defaults.basePath = 'https://myfirebaseurl.com';
})
.run(function (DS, DSFirebaseAdapter) {
// the firebase adapter was already registered
DS.adapters.firebase === DSFirebaseAdapter;
// but we want to make it the default
DS.registerAdapter('firebase', DSFirebaseAdapter, { default: true });
// Now you can interact with Firebase like
// it's a RESTful data layer. No backend required!
DS.defineResource('book');
})
.controller('booksCtrl', function ($scope, DS) {
DS.findAll('book').then(function (books) {
// all the books you have in Firebase
$scope.books = books;
});
});
js-data-angular adds a few methods to DS
:
DS#bindOne
DS#bindOne(resourceName, id, scope, expr[, cb])
Shorthand for manually watching the lastModified timestamp of an item and manually updating the item on the $scope.
// shortest version
User.bindOne(1, $scope, 'user');
// short version
store.bindOne('user', 1, $scope, 'user');
// long version
$scope.$watch(function () {
return store.lastModified('user', 1);
}, function () {
$scope.user = _this.get(resourceName, id);
});
Because DS#bindOne
uses Angular's $scope.$watch
internally, its return value is a function which can be called to remove the binding:
// create binding and save the remove-binding function for later
var functionThatRemovesBinding = User.bindOne(1, $scope, 'user');
// later, to remove the binding
functionThatRemovesBinding();
DS#bindAll
DS#bindAll(resourceName, params, scope, expr[, cb])
Shorthand for manually watching the lastModified timestamp of a collection and manually updating the collection on the $scope.
var params = {
where: {
age: {
'>': 30
}
}
};
// shortest verions
User.bindAll(params, $scope, 'users');
// short version
store.bindAll('user', params, $scope, 'users');
// long version
$scope.$watch(function () {
return store.lastModified('user');
}, function () {
$scope.users = store.filter('user', params);
});
Like with DS#bindOne
, the return value of DS#bindAll
is a function which can be called to remove the binding:
// create binding and save the remove-binding function for later
var functionThatRemovesBinding = User.bindAll({}, $scope, 'users');
// later, to remove the binding
functionThatRemovesBinding();
Using JSData with Angular Guide
Couple of things to keep in mind when using JSData with Angular:
Angular's DI is lazy
This means that when you do app.service('MyService', ...)
, MyService
doesn't actually get instantiated until the first time you try to inject MyService
into your app. This can cause a problem with JSData when you define a resource inside of app.service
or app.factory
, but you don't actually inject the resource anywhere in your app. The resource never actually gets defined. Here is a common solution:
app.service('User', function (DS) {
return DS.defineResource({...});
}).run(function (User) {}); // Make sure the User resource actually gets defined
app.service('Post', function (DS) {
return DS.defineResource({...});
}).run(function (Post) {}); // Make sure the Post resource actually gets defined
JSData lives outside of Angular's $scope lifecycle
While JSData does its best to let Angular know when it needs to trigger $digest loops to check for changes to data contained in the data store, sometimes Angular might not get the message. You might find cases where you need to manually trigger a $digest.
ngRepeat and accessing hasMany relations
Let's say you have these definitions:
var Post = DS.defineResource({
name: 'post',
hasMany: {
comment: {
localField: 'comments',
foreignKey: 'post_id'
}
}
});
var Comment = DS.defineResource({
name: 'comment',
belongsTo: {
post: {
localField: 'post',
localKey: 'post_id'
}
}
});
And the following in your template:
<div ng-repeat="comment in post.comments track by comment.id">
...
</div>
This markup will trigger an infinite digest error because the comments
array you get when you access post.comments
is an array created by the relation's getter property accessors. It will actually be a different array every time you access post.comments
, even if the comments haven't changed at all. With the way Angular checks for changes, Angular will think something has changed, so it will keep triggering the $digest loop, forever.
To avoid this when you need to access an entity's hasMany relation in a template, first attach that relation to the $scope:
Comment.bindAll({
post_id: post.id
}, $scope, 'comments');
<!-- this will be fine -->
<div ng-repeat="comment in comments track by comment.id">
...
</div>
How do I load data for my View?
There are really two ways you can deal with it:
First way: Load all required data before the View is allowed to render
.when('/report/:id', {
templateUrl: 'routes/report/report.html',
controller: 'ReportCtrl',
controllerAs: 'ReportCtrl',
resolve: {
report: function ($route, Report) {
return Report.find($route.current.params.id).then(function (report) {
return report.DSLoadRelations(['route', 'comment']);
});
}
}
});
.controller('ReportCtrl', function ($route) {
// data already loaded
this.report = $route.current.locals.report;
// relations already loaded too
this.report.comments; // [...]
this.report.route; // {...}
})
Second way: Allow View to render, lazily load the data, then View reacts to incoming data and re-renders. View must gracefully handle itself when the data hasn't arrived yet.
.when('/report/:id', {
templateUrl: 'routes/report/report.html',
controller: 'ReportCtrl',
controllerAs: 'ReportCtrl',
resolve: {
report: function ($route, Report) {
// don't return the promise, so the View won't wait
// for its resolution to render
Report.find($route.current.params.id).then(function (report) {
report.DSLoadRelations(['route', 'comment']);
});
}
}
});
.controller('ReportCtrl', function ($route, Report) {
// data isn't loaded yet, so watch for it
// "report" will show up on $scope.ReportCtrl.report
Report.bindOne($routeParams.id, $scope, 'ReportCtrl.report');
});
<div id="report-view">
<div class="report" ng-show="ReportCtrl.report">...</div>
<div class="loading-spinner" ng-hide="ReportCtrl.report">...</div>
</div>
Hopefully that gives you the idea.
Updated less than a minute ago