Angular Best Practice Survey
by Madincea Vasile <vmadincea@gmail.com>
Agenda
- What's this about?
- Angular introduction
- Angular for our project
- Project structure
- ui-router
- Fat models, slim everything else
- Models as single source of truth
- ng-repeat
- Directives
- Other small improvements
What's this about?
What's this about?
- a few Angular best practices applied on a CMED Angular project for 6 months (first iteration)
- not to just enumerate some ng best practices, but to challenge them
- all following statements in the presentation may and should be put under the microscope
Angular introduction
Angular introduction
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 introduction
-
Angular 1.x best for:
- prototyping
- complex projects with heavy business logic where one change in the model can affect multiple parts of the UI
- The docs seem to target more the prototyping part
- For other targets, prepare to dig deeper...
Angular introduction
Angular specifics
- two way data binding: view <--> controller
- $scope: javascript object to support change detection
- controllers: attach context to $scope
- directives: web components several years earlier
- modules: organize your logic structure
- services: make angular aware of your models
- dependency injection: ask for dependencies, don't look for them
- testing
Angular introduction
Pros
- two way data binding: the good parts
- directives
- templates: if you don't pollute them with too much logic
- dependency injection
- testing
Angular introduction
Cons
- two way data binding: the bad parts
-
Angular 2.0: total different beast
- backwards incompatibility
- support for web components
- Typescript
- R.I.P. : controller, scope, modules and two-way data binding
Angular introduction
Two-way data binding
Pros
- less code (no boilerplate code for manipulating the DOM)
- let's you focus more on your application
- let's you rethink the app around the model
Cons
- reduces app's performance
Angular for our project
Angular for our project
Project with a complex business model that needs to update a lot of things in the UI
- Angular as the javasript framework to help productivity
- Coffeescript to help us with javascript bad parts
- Grunt for automation (Yeoman generator legacy)
- Focus on cleaner, readable and maintainable code
- Only make improvements that don't break angular compatibility
Project structure
Project structure
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
ui-router
ui-router
State machine manager with routing as bonus
https://github.com/angular-ui/ui-router
ui-router
- nested, parallel and multiple named views
- rethink the views as states
- router abstraction
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"


ui-router
- define a layout for the app (abstract views)
- resolve:
- block access to state until resources are available (promises are resolved)
- nice when starting, to ditch in the end because of UX issues and performance
- let the rendering start and when resources are available start populating the view
- control transitions between states
- check for permission to enter state or redirect
- load/prepare resources
.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"Fat models, slim everything else
Fat models, slim everything else
- controllers should just map the model data to the view
- from templates it's much harder to move the logic because angular makes it stupidly easy to do many things directly in templates
- ng-if, ng-switch, ng-show, ng-disabled, ng-class...
- very good for prototyping, might become a pain to maintain as the project grows
Move logic from templates and controllers to models
Models
Models
- you only modify the model and everything related in the view will update accordingly
- view-model binding
- angular two way binding: view-controller
- reference binding: controller-model
- almost no events used to notify changes
- no spaghetti code
- lines of code reduced considerably
- big attention to losing reference: use angular.copy
Single source of truth
ng-repeat
ng-repeat
- use trackBy
- avoid filters in templates
- use filters in controllers and send the processed data to the template
- in case of expensive rendering of elements, replace filters with ng-show mechanism
- you render the whole data first anyway
- don't destroy the DOM elements, just not show them
Directives
Directives
- wraps-up js code, html and css in one component
- used as:
- as a reusable component: isolated scope
- inline component: no isolated scope
- helps organize template html code
- DOM manipulation should only be done from directives
Other small improvements
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>Angular Best Practive Survey
By vmadincea
Angular Best Practive Survey
- 981