ict.ken.be

Delivering solid user friendly software solutions since the dawn of time.

Building Apps with Angular and Breeze - Notes

Categories: Angularjs WebApi

by John Papa

Links:

SPA

  • Reach, Rich, Reduced Round Tripping
  • Navigation, History, Deep Linking, Persisting State, Most Load On Initial Response, Progressive Loading When Needed

Techniques

  • Dependency Injection
  • Routing
  • MV*
  • Data Binding
  • Publisher/Subscriber
  • DOM Templates
  • Views

Value

  • Reduces the plumbing
  • Handles the monotony
  • Spend more time on what matters
  • Focus on the user stories

Solution Structure

  • Server Models
  • Data Access (Entity Framework)
  • Web Server
  • HTML/CSS/Javascript + ASP.NET WebAPI
  • Modules
  • Startup Code
  • Twitter Bootstrap
  • Angular
  • Services

Unblock file: use alt + enter
Build: ctrl + shift + B

Hot Towel

  • AngularJS.Animate
  • AngularJS.Core
  • AngularJS.Route
  • AngularJS.Sanitize
  • FontAwesome
  • HotTowel.Angular
  • jQuery
  • Moment.js
  • Spin.js
  • toastr
  • Twitter.Bootstrap

Scripts OR vendor ?
app
app/config.js OR startup ?
app/config.exceptionhandler.js
app/config.route.js
app/layout/shell.html
app/services/datacontext.js
app/services/directives.js OR subfolders ?
index.html
ie10mobile.css
initial splash page
minify and bundle before production deploy

var app = angular.module('app', ['ngRoute']):
app.Value('config', config);
app.config(['$logProvider', function ($logProvider) ... ]) //runs before app.run
app.run(['$route', function ($route) {}]);

app.config(function ($provide) { $provide.decorator('$exceptionHandler', ['$delegate', 'config', 'logger', extendExceptionHandler]);});

app.constant('routes', getRoutes());

<div data-ng-include="'/app/layout/shell.html'"></div>
<div data-ng-view class="shuffle-animation"></div>

Angular v1.2 supports controller as: <section id="dashboard view" class="mainbar" data-ng-controller="dashboard as vm">

<section data-ng-controller="sessions as vm">
<div data-ng-repeat="s in vm.sessions" data-ng-click="vm.gotoSession(s)">
<div data-cc-widget-header title="{{vm.title}}"></div>

Directives can be A-Attribute, E-Element, C-Class, M-Comment

<img data-ng-src="..." />

GetDataPartials (as projection) -> map it into an existing entity -> extend this entity
metadataStore.registerEntityTypeCtor('Person', Person);
function Person() {
this.isSpeaker = false;
}

Breeze caches data locally in memory.
if (_areSessionsLoaded() && !forceRemote) {
sessions = _getAllLocal(entityNames.session, orderBy);
return $q.when(sessions); //transform it into a promise
}

function _areSessionsLoaded(value) {
...
}
function _areItemsLoaded(key, value) {
if (value == undefined) {
return storeMeta.isLoaded[key]; //get
}
return storeMeta.isLoaded[key] = vlaue;
}

Entity types are not necessary real resources. (extendMetadata)
Eg. Speakers, Attendees all users but different entities.

Make more responsive: class="hidden-phone"

Sharing Data: creating object graphs on the client

Routing:
$locationChangeStart
$routeChangeStart
$routeChangeSuccess
$routeChangeError
definition.resolve = angular.extend(definition.resolve || {}, { prime: prime });

Filtering:
<input data-ng-model="vm.search">
<div data-ng-repeat="s in vm.speakers | filter:vm.search">

<input ng-model="search.$">
<input ng-model="search.name">
<div data-ng-repeat="friends in friends | filter:search:strict">

vm.search = function($event) {
if($event.keyCode == config.keyCodes.esc) {
vm.speakerSearch = '';
}
applyFilter();
}

function speakerFilter(speaker) {
var isMatch = vm.speakerSearch
? common.textContains(speaker.fullName, searchText)
:true;
return isMatch;
}

Breeze & UI-Bootstrap:
<pagination boundry-links="true"
on-select-page="vm.pageChanged(page)"
total-items="vm.attendeeFilteredCount"
items-per-page="vm.paging.maxPagesToShow"
class="pagination-small"
previous-text=Prev"
next-text="Next"
first-text="First"
last-text="Last"
</pagination>

.executeLocally()

Dashboard:

  • Promises, Responsive Layout, Just the counts, Local and Remote Queries

.take(0).inlineCount()
_getAllLocal(...)

<th><a data-ng-click="vm.content.setSort('track')" ...
<tr data-ng-repeat="t in vm.content.tracks | orderBy:vm.content.predicate:vm.content.reverse">

ES5's.reduce() funciton will help create the mapped object
sessions.reduce (function (accum, session) {
var trackname = session.track.name;
var trackId = session.track.id;
if (accum[trackId-1]) accum[trackId-1].count++;
else accum[trackId-1] = { track: trackName, count: 1 }
}, []);

Animations (ngAnimate):

hide ngHide .ng-hide-add (start) .ng-hide-add-active (end)
show ngShow .ng-hide-remove .ng-hide-remove-active

Splash page in index.html
-> move to shell.html
-> turn off in activateController().then(...)

ngRepeat, ngInclude, nglf, ngView
enter .ng-enter .ng-enter-active
leave .ng-leave .ng-leave-active
move .ng-move .ng-move-active (only for ng-repeat)

http://www.yearofmoo.com (keyframe animations)
http://jpapa.me/yomanimations

DataContext will grow -> Repositories

http://johnpapa.net/protoctor

function gotoSpeaker(speaker) {
if (speaker && speaker.id) {
$location.path('/speaker/' + speaker.id);

}
}

Object.defineProperty(vm, 'canSave', {
get: canSave
});
function canSave() { return !vm.isSaving; }

function goBack() { $window.history.back(); }

function onDestroy() {
$scope.$on('$destroy', function() { datacontext.cancel(); });
}

manager.hasChangesChanged.subscribe(function(eventArgs){
... eventArgs.hasChanges ...
common.$broadcast(events.hasChangesChanged, data);
});

<select data-ng-options="t.name for t in vm.tracks"
data-ng-model="vm.session.track">
</select>

function createNullo(entityName) {
var initialValues = {
name: ' [Select a ' + entityName.toLowerCase() + ']'
}:
return manager.createEntity(
entityName, initialValues, breeze.EntityState.Unchanged);
);
}

Object.defineProperty(TimeSlot.prototype, 'name', {
get : function() {
var start = this.start;
var value = ((start - nulloDate) == 0) ?
' [Select a timeslot]' :
(start && moment.utc(start).isValid()) ?
moment.utc(start).format('ddd hh:mm a') : ' [Unknown]';
return value;
}
});

Validation on the Server for Data Integrity always
Validation on the Client for User Experience

View Level: Should you copy business rules to HTML?
vs
Model Level: Should the model drive the UI?

Separation of how you calculate the validation error from how you display it.

breeze.directives.validation.js
Install-Package Breeze.Angular.Directives

<input data-ng-model="vm.session.title" placeholder="Session title" data-z-validate />

//tell breeze not to validate when we attach a newly created entity to any manager
new breeze.ValidationOptions({ validateOnAttach: false }).setAsDefault();

app.config(['zDirectivesConfigProvider', function(cfg) {
cfg.zValidateTemplate = '<span class="invalid"><i class="icon-warning-sign"></i>%error%</span>';
}])

Saving State - How do we recover quickly?

Instal-Package angular.breeze.storagewip (ngzWip)

zStorage.init(manager);

function listenForStorageEvents() {
$rootScope.$on(config.events.storage.storeChanged, function ...
... config.events.storage.wipChanged ...
... config.events.storage.error ...
}

var exportData = entityManager.exportEntities();
$window.localStorage.setItem(key, exportData);

var importData = $window.localStorage.getItem(storageKey);
entityManager.importEntities(importedData);

eg. after loading lookups in prime method, we could call zStorage.save();
but also after query and saves that are successful

http://caniuse.com/#search=localstorage

Storing work in progress (zStorageWip)

zStorageWip.init(manager);

manager.entityChanged.subscribe(function(changeArgs) {
if (changeArgs.entityAction == breeze.EnityAction.propertyChanged) {
common.$broadcast(events.entitiesChanged, changeArgs);
}
});

function autoStoreWip(immediate) {
common.debouncedThrottle(controllerId, storeWipEntity, 1000, immediate);
}

function onEveryChange() {
$scope.$on(config.events.entitiesChanged, function(event,data) { autoStoreWip(); });
}


//Forget certain changes by removing them from the entity's originalValues
//This function becomes unnecessary if Breeze decides that unmapped properties are not recorded in originalValues
//We remove the properties we do not want to track
function interceptPropertyChange(changeArgs) {
var changedProp = changeArgs.args.propertyName;
if (changedProp === 'isPartial' || changedProp === 'isSpeaker') {
delete changeArgs.entity.entityAspect.originalValues[changedProp];
}
}

var importedEntity = this.zStorageWip.loadWipEntity(wipEntityKey);
if (importedEntity) {
importedEntity.entityAspect.validateEntity();
return $.when({ entity:importedEntity, key:wipEntityKey });
}

<li data-cc-wip wip="vm.wip" routes="vm.routes" changed-event="{{vm.wipChangedEvent}}" class="nlightblue"></li>

$scope.wipExists = function() { return !!$scope.wip.length; } //double bang will turn number into boolean

<div class="widget-content" data-ng-include="'/app/wip/widget.html'"> //double, single quote

<div class="widget-content referrer" data-ng-switch="!!vm.wip.length">
<div data-ng-switch-when="false">No work in progress is found</div>
<table data-ng-switch-when="true">
...
... {{item.date | date: 'MM/dd/yyyyy @ h:mma'}}
</table>
</div>

more: http://jpape.me/htmlstore - html5 web storage indexeddb and filesystem by craig shoemaker