by Madincea Vasile <vmadincea@gmail.com>
Super heroic javascript framework!
It is very helpful indeed if the framework guides developers through the entire journey of building an app: from designing the UI, through writing the business logic, to testing.
— The Zen of Angular
Angular specifics
Pros
Cons
Two-way data binding
Pros
Cons
Project with a complex business model that needs to update a lot of things in the UI
Feature based structure 3 levels deep
/bower_components
/build
/node_modules
/src
/test
Gruntfile.coffee
bower.json
package.json
/app
/assets
/less
/third_party
index.html
/layout
/shared
/sidebar
/topbar
/components
/models
/feature1
feature1.coffee
feature1.html
feature1.less
/feature2
app.coffee
State machine manager with routing as bonus
https://github.com/angular-ui/ui-router
ui-router vs ng-router
# app.coffee
angular
.module 'app'
.config ($stateProvider, $urlRouterProvider, $httpProvider) ->
$httpProvider.interceptors.push 'sessionInjector'
$urlRouterProvider.otherwise "/default_state"
$stateProvider
.state "abstract_state",
abstract: true
views:
"":
templateUrl: "app/layout/layout.html"
controller: "LayoutController as layoutCtrl"
"topbar@abstract_state":
templateUrl: "app/shared/topbar/topbar.html"
controller: "TopbarController as topbarCtrl"
"sidebar@abstract_state":
templateUrl: "app/shared/sidebar/sidebar.html"
controller: "SidebarController as sidebarCtrl"
.state "abstract_state.permission_denied",
url: "/permission_denied"
templateUrl: "app/permission_denied/permission_denied.html"
.state "abstract_state.feature1",
url: "/feature1"
views:
"":
templateUrl: "app/feature1/feature1.html"
controller: "Feature1Controller as feature1Ctrl"
"toolbar@abstract_state":
templateUrl: "app/feature1/toolbar.html"
controller: "ToolbarController as toolbarCtrl"
"navigation@abstract_state":
templateUrl: "app/feature1/navigation.html"
controller: "NavigationController as navigationCtrl"
.state "abstract_state.abstract_state2",
abstract: true
views:
"":
templateUrl: "app/feature2/abstract_state2.html"
controller: "AbstractState2Controller as abstractState2Ctrl"
resolve:
initialData: (DataService) ->
DataService.asyncFetch()
.state "abstract_state.abstract_state2.feature2",
url: "/feature2"
views:
"":
templateUrl: "app/feature2/feature2.html"
controller: "Feature2Controller as featureCtrl"
"toolbar@edc":
templateUrl: "app/feature2/toolbar.html"
controller: "ToolbarController as toolbarCtrl".run ($rootScope, $state, CheckAccess) ->
$rootScope.$on '$stateChangeStart', (e, toState, toParams, fromState, fromParams) ->
# check permission to enter any state in the app
if toState.name isnt 'abstract_state.permission_denied'
denyAccess = CheckAccess.denyAccess
if denyAccess
e.preventDefault()
$state.go "abstract_state.permission_denied"
# check valid id for entering a certain state
if toState.name is 'abstract_state.feature1'
#stoping the router to go any further on the expected path
e.preventDefault()
CheckAccess.asyncIsValid(toParams.id).then (isValid) ->
if isValid
# notify: false will not emmit stateChangeStart event in order to avoid infinite loop
$state.go("abstract_state.feature1", toParams, {notify: false}).then () ->
#but we still need stateChangeSuccess to let the router start the rendering
$rootScope.$broadcast('$stateChangeSuccess', toState, toParams, fromState, fromParams)
else
$state.go "abstract_state.default"
Move logic from templates and controllers to models
Single source of truth
Use controllerAs syntax everywhere
# app.coffee
.state "abstract_state.feature1",
url: "/feature1"
views:
"":
templateUrl: "app/feature1/feature1.html"
controller: "Feature1Controller as feature1Ctrl"<div>
{{feature1Ctrl.name}}
</div>angular
.module 'app.feature1', ['app.core']
.controller "Feature1Controller", (SomeService) ->
@name = "Madincea Vasile"
Custom directive to replace ng-switch
.directive 'widgetDispatcher', ($compile) ->
restrict: 'E'
scope:
widget: '='
link: (scope, el, attr) ->
widgetType = angular.lowercase(scope.widget.type)
el.html("""
<#{widgetType}-widget ng-model="getSetValue"
ng-model-options="{getterSetter: true}">
</#{widgetType}-widget>""")
$compile(el.contents())(scope)
return Spy directive: log two-way binding behaviour
.directive 'spyNgModel', ($log) ->
require: 'ngModel'
link: (scope, element, attr, ngModel) ->
ngModel.$parsers.push (viewValue) ->
$log.info 'From view to model:'
$log.info viewValue
viewValue
ngModel.$formatters.push (modelValue) ->
$log.info 'From model to view:'
$log.info modelValue
modelValue
returnAvoid ui-router infinite loops
# check valid id for entering a certain state
if toState.name is 'abstract_state.feature1'
#stoping the router to go any further on the expected path
e.preventDefault()
CheckAccess.asyncIsValid(toParams.id).then (isValid) ->
if isValid
# notify: false will not emmit stateChangeStart event in order to avoid infinite loop
$state.go("abstract_state.feature1", toParams, {notify: false}).then () ->
#but we still need stateChangeSuccess to let the router start the rendering
$rootScope.$broadcast('$stateChangeSuccess', toState, toParams, fromState, fromParams)
else
$state.go "abstract_state.default"Custom disableClickIf directive
.directive "disableClickIf", ($parse, $rootScope) ->
priority: 100
restrict: 'A'
compile: ($element, attr) ->
fn = $parse(attr.disableClickIf)
return {
pre: (scope, element) ->
element.on 'click', (event) ->
callback = ->
if fn(scope, $event: event)
# prevents ng-click to be executed
event.stopImmediatePropagation()
# prevents href
event.preventDefault()
return false
return
if $rootScope.$$phase
scope.$evalAsync callback
else
scope.$apply callback
return
return
}<a disable-click-if="!sidebarCtrl.isValid()"
ui-sref="abstract_state.feature1">
</a>