The Todo-AngularJS sample demonstrates Breeze and AngularJS working together in a single page CRUD (Create/Read/Update/Delete) application.
Check out John Lantz's CodeProject article describing this sample.
The user experience is the same for this and all Todo Sample variations. The source lies within the "Samples" package which you can download here.
Todo-AngularJS runs in modern browsers such as IE9, IE10, and recent Chrome, Safari, Firefox, and WebKit browsers. Breeze does not support AngularJS apps running in older browsers that lack ECMAScript 5 property getters and setters.
Todo is the simplest full-CRUD app we could think of. The architecture is deliberately primitive and simplistic. There's only one model type (TodoItem) and only one screen. It’s all about the mechanics of manipulating data and presenting them for user review and edit.
The entire app is organized in a single .NET web application project that hosts both server-side and client-side components.
The server is a simple ASP.NET Web Application that acts both as a provider of client-side assets (HTML, CSS, and JavaScript) and as an ASP.NET Web API data service.
The folders and files devoted to these roles are outlined in blue .
The Controllers folder holds a single Breeze-styled Web API controller that sits in front of an Entity Framework “Code First” model talking to a SQL Server database.
The server is identical across all Todo sample variations.
The client-side assets reside in the folders and files outlined in red.
There are no client-side .NET dependencies: no ASP.MVC, no Razor, just pure CSS, HTML, and JavaScript.
The app folder holds the client application JavaScript.
The Content folder holds CSS files.
3rd party vendor libraries (including Breeze) are in the Scripts folder.
The index.html file is both the host page that loads the assets and the residence of the one-and-only view, written in Angularized-HTML.
We divide the client app into four functional areas:
Area | Comment |
---|---|
Layout | Index.html contains the "View" HTML with AngularJS data binding markup. It's also the application host page with css and script tags. |
Presentation Logic |
JavaScript in the controller.js exposes data and method binding points to the view. All AngularJS code lives here. Many of the methods implement CRUD actions by delegating to methods of the service layer. |
Model & Data Access |
JavaScript in the dataservice.js creates, queries, deletes, and saves entities using BreezeJS. All BreezeJS code lives here. |
Logging | The logger.js presents activity messages and errors as "toasts" popping from the lower right via the toastr.js 3rd party library. |
We assume you're acquainted with AngularJS and that the markup in index.html and the JavaScript programming model are familiar to you.
The Todo app's "View" is embedded in index.html. You'll recognize the way AngularJS markup binds buttons to actions, value attributes to data properties, CSS classes to controller properties, and repeats Todo items within a list using an <li> tag template.
The “data-ng-app” attribute references the “app” module; the “data-ng-controller” attribute references the “TodoCtrl” controller module.
Here’s an excerpt from the view showing the repeated TodoItem template within the <li>
tag:
<li data-ng-repeat="item in items | filter:itemFilter">
<!-- Readonly View -->
<div data-ng-hide="isEditing(item)">
<input type="checkbox"
data-ng-model="item.IsDone"
data-ng-class="{done: item.IsDone}" />
<label data-ng-click="editBegin(item)"
data-ng-class="{done: item.IsDone, archived: item.IsArchived}">
</label>
<a href="#" data-ng-click="deleteItem(item)">X</a>
</div>
<!-- Editing View -->
<div data-ng-show="isEditing(item)">
<form data-ng-submit="editEnd()">
<input type="text" autofocus
data-ng-model="item.Description"
data-ng-blur="editEnd()"
data-ng-mouseout="editEnd()" />
</form>
</div>
</li>
We're using the HTML “data-“ prefix for attributes that AngularJS recognizes as its directives.</p>
Every AngularJS app needs a main module. Scripts/app/main.js defines that module, “app”.
Important: the ‘app’ module definition takes a dependency on the “Breeze AngularJS Service” module.
angular.module('app', ['breeze.angular']);
That module has a “breeze” service which, when injected into any application component, will configure Breeze for this application. We’ll see it injected into the dataservice
component.
All application JavaScript files follow the Immediately Invoked Function Execution (IIFE) pattern that keeps the global window namespace free of application variable pollution. Such modules begin and end in this fashion.
(function() {
... your code here ...
})();
The Scripts/app/controller.js file defines the AngularJS controller that is responsible for the presentation of Todo
items to the user in the “Todo” view.
The controller relies on AngularJS’s dependency injection to acquire access to both AngularJS and application services.
angular.module('app').controller('TodoCtrl',
['$q', '$scope', '$timeout', 'dataservice', 'logger', controller]);
The syntax is a bit peculiar but you get used to it quickly. “DI” is an essential aspect of AngularJS programming and almost every AngularJS application follows this formula.
Let’s break it down:
angular.module('app') // get the application module from AngularJS
.controller('TodoCtrl', // name the controller
['service1', 'service2', ... // name(s) of services to inject
controller]); // the definition function is last item in the array
// The definition function w/ a parameter for each injectable named above
function controller(service1, service2, ...) {...}
Many folks prefer an anonymous definition function instead of the named
controller
definition function that we like. It’s up to you.
This controller asks AngularJS to inject a number of services:
$q
- the AngularJS promises manager for this app$scope
- a context object to which the view binds$timeout
- AngularJS equivalent of ‘setTimeout’dataservice
- the application data access servicelogger
- the application’s logging facilityThe $scope
object is the vehicle for binding the controller to the HTML in the view. The controller add properties and methods to the $scope
object. The HTML binds to these $scope
members declaratively with AngularJS “directives”.
“directives” is a fancy word for “data bindings”
A controller written in this “MVVM” style makes no references to view elements at all. The View (the HTML) only uses JavaScript expressions to bind to the $scope
.
AngularJS binding declarations are written in a kind of JavaScript syntax. But, as a matter of principle, we keep those JavaScript expressions clean and free of all decision logic. Logical expressions belong in the controller, not in the HTML.
The dataservice.js file handles the creation of new Todo objects and all interactions with the server. It's written in Breeze and almost all Breeze-related code is in this dataservice. See the "Todo Sample Dataservice" page for details.</p>
Notice the use of AngularJS dependency injection.
angular.module('app').factory('dataservice',
['$timeout', 'breeze', 'logger', dataservice]);
We’re injecting the AngularJS $timeout
service (an abstraction over JavaScript’s setTimeout
), the ‘breeze’ service, and the application’s ‘logger’ service (described below).
Inject the ‘breeze’ service is a way of accessing Breeze itself without getting it from the global (window
) namespace. But that’s not why we’re injecting it. We’re actually looking for a side-effect of that injection: the configuration of Breeze for an AngularJS application. See “Breeze AngularJS Service” to learn more.
The controller
is insulated from data access details and has no direct connection with Breeze. The dataservice
api provides it with all that it needs … and no more.
The dataservice
illustrates the “Revealing Module” module pattern by listing its api near the top of the file:
var service = {
addPropertyChangeHandler: addPropertyChangeHandler,
createTodo: createTodo,
deleteTodo: deleteTodo,
getTodos: getTodos,
hasChanges: hasChanges,
purge: purge,
reset: reset,
saveChanges: saveChanges
};
Now for a few comments about these methods.
The controller considers issuing a save whenever a property changes. It attaches a handler to the addPropertyChangeHandler
. That method invokes the handler whenever there is a change to any entity property of any entity. You don’t have to listen to every entity or every property individually. Breeze offers a “one stop shop” via the EntityManager.entityChanged
event.
createTodo
creates a new instance of a TodoItem
initialized as requested. This item is new in the cache but not yet saved to the database. By design, the command to save it comes from the controller. Note that we did not define the TodoItem
in JavaScript; Breeze figured that out from metadata.
deleteTodo
marks the TodoItem
to be deleted but does not physically delete it. By design, the command to update the database (to “save” the delete) comes from the controller.
getTodos
issues a Breeze EntityQuery
for all Todos, optionally excluding archived Todos (whose .isArchived
flag is true). The code illustrates how to construct a query based on user input.
hasChanges
indicates if the cache holds any unsaved changes. It delegates to EntityManager.hasChanges()
.
purge
and reset
are demo-only methods that reset the database to initial conditions after you’ve wrecked havoc with your trials. They post messages to the Web API controller with AngularJS’s $http
service. $http
is just fine for simple AJAX commands while we let Breeze handle data access.
saveChanges
asks Breeze to save all cached entities with pending changes. There might be one entity to save (as when you change a description or delete a Todo). There might be many entities to save (as when you click “Mark all completed”). You don’t have to keep track of which entities are “dirty”. You don’t have to differentiate between add, modify and delete operations. That’s Breeze’s job.
This application takes care of one potential ‘gotcha’: an attempt to save a second change to an entity when a save request for that entity is already “in flight”.
That can happen in an app like this which saves automatically when it detects changes. It has no Save button.
Duplicate saves are rarely acceptable. For example, you don’t want to add the same TodoItem
twice.
We guard against duplicate save by putting save requests on a queue. If there is a save request in process, the EntityManager.saveChanges
queues subsequent requests until it receives a server response for the current save. It processes the queue in order until it runs dry.
“Save Queuing” is a Breeze Labs feature and you can learn more about it here. We enabled “Save Queuing” for this application’s EntityManager
at the top of the dataservice
:
manager.enableSaveQueuing(true);
The logger.js file is an abstraction for logging messages and displaying them to the user. Internally it uses John Papa’s open source toastr library to display messages as "toasts" that float up from the bottom right of the screen.
It also relies on AngularJS’s $log
service to write a second copy of the messages to the browser console window.
You can explore Breeze + AngularJS binding with this Todos Plunker.
The following sample only works with modern browsers (IE10+, FF, Chrome).