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