Angular begginer examples
Contents |
Some of angular attribute directives:
ng-app="store"
defines root element of module “store” which can have many controllersng-controller="StoreController as store"
defines controller, but it is usually defined in routes for specific templateng-repeat="product in store.products"
iteration of this element (you can use alsong-if="$even"
, or$index
variables). Iterating over object isng-repeat="(key, value) in data"
ng-show="product.CanPurchase"
,ng-hide="expession"
,ng-disabled="mySwitch"
to show/hide/disable element. In contrast,ng-if
adds new scope or totally removes DOM element.<img ng-src="{{ product.image}}">
to prevent browser to load empty src, this is only place where directive use {{ }}ng-click=" tab = 2 "
to run a code on clickng-class="{ active: tab === 1 }"
set class to key when value is true (good to haveng-init="tab = 0"
)ng-model="review.terms"
bind value of current input element to variableng-bind="review.terms"
bind innerHTML of this element to the variable (can be expression)ng-submit="reviewCtrl.addReview(product)"
call function on submitng-include="'product-title.html'"
include templateng-view
is placeholder where routes inject templates
Angular expressions can’t have conditionals and loops, but have filters.
Note that angular expressions don’t have access to window
only scope
so no
help from parseInt()
, String()
and similar.
$scope.$watch('email', function() { $scope.test(); })
can be used to rerun
validation check.
watch first argument
(string or function) is evalued on each digest and second argument (callback
listener) is called when a returning value of first argument is changed (for
array, use length).
To use with controllerAs
syntax you need to use bind
app.controller('Ctrl', function ($scope) {
this.name = 'name';
$scope.$watch(angular.bind(function () {
return this.title
}), function (oldVal, newVal) {
});
});
Expressions in angular directives <button ng-click="foo = 5">
are parsed and
evaluated. $parse()
takes expression and returns function which takes two
arguments: context and locals (overrides).
$scope.foo = 3;
var parseFn = $parse(‘foo = 5’);
parseFn($scope);
// or
$scope.$eval(‘foo = 5’);
$scope.foo; // returns 5
You can check if some property bar
on foo
is not null:
$parse('bar.baz.quux')(foo)
will return undefined instead of throwing an
exception.
You can manually trigger digest with $scope.$apply("foo = 5")
it calls
$rootScope.$digest()
which propagates down to every child scope
link.
With ControllerAs syntax, you need to inject $scope
and can not use
expression, but just empty $scope.$apply()
. You can not call $apply
in
callbacks for ng-mouseover
ng-click
since digest is in progress. It is
usable in non angular callbacks document.getElementById('b').addEventListener
'mouseover'
.
Filters: {{ data | filter:options }}
- strings
{{ 'Some string' | date:'MM/dd/yyy @ h:mma' }}
can also becurrency, lowecase, uppercase
- iterators
ng-repeat="product in store.products | filter: search | orderBy: '-price' | limitTo: -5 "
use only products that match value of search (any property of product that match search string) order by desc price and show last 5. If we want to filter only product.name than bind input tosearch.name
so search becomes object, for exampleng-model="search.name"
.
View can use directives, filters. Controller and services can depend on some providers.
- all objects are joined using dependency
injection
for specialized objects
(controllers, directives, filters, animations) or custom service. There are 5
types of recipe for creating object with injector: Provider (is the main one,
other are syntactic sugar on top of this provider recipe), Value, Factory,
Service and Constant.
You can use DI when defining components (only controller can have
$scope
andresolve
dependency) or inrun
methods (can not inject Providers) orconfig
method (can not inject Service or Value) on module. Only module can define other components:controller
and factories:directive
,filter
,value
,factory
,service
. - all services in Angular are singletons. injector uses each recipe at most once to create the object. The injector then caches the reference for all future needs. Controllers are instantiated every time app needs it.
- use strict dependency injection
ng-app="myApp" ng-strict-di"
to raise error when we use implicit annotations (function($scope)
instead of['$scope', function($scope)]
)
Special Purpose Objects: controlles, filters, directives, animations
- controller methods can be defined on $scope or on this (so we reference them
MyController.myMethod
)
angular.module('myApp',['controllers']);
angular.module('controllers', [])
.controller('MyController', ['$http', '$scope', function($http, $scope) {
this.my_value = 2;
$scope.myMethod = function() { return 2 };
}]);
- new filter definition should return function
# filter
angular.module('myApp',['myFilters']);
angular.module('myFilters',[])
.filter('checkmark', function() {
return function(input) {
return input ? '\u2713' : '\u2718';
};
});
It can be used like {{ phone.available | checkmark }}
-
two types of directives: element directive (UI widget)
<product-title></product-title>
or attribute directive (mixins like tooltip)<div product-title></div>
.angular.module('myApp') .directive('productTitle', function(){ return { restict: 'E', // 'E' element, 'A' attribute (default), 'C' class templateUrl: 'product-title.html', controller: function() {}, controllerAs: 'panel', }; }); .directive('enter', function(scope, element, attrs){} // default is 'A' so no need to write return { link: function(){} return function(scope,element, attrs) { element.bind("mouseenter", function() { scope.$apply(attrs.enter); # this will call argument string, ie some controller method }); }; });
- directives has
transclusion()
to create new scope controllerAs: 'vm'
have some problems (but works asvm1
)
- directives has
Services for creating objects: Value, Factory, Service, Provider, Constant
- egghead learn when to use a service, factory, or provider
-
Value recipe, can’t have other dependencies
var myApp = angular.module('myApp', []); myApp.value('clientId', 'a12345654321x');
-
Factory recipe use other services, is a function and lazy initialized. It returns object
# factory angular.module('myApp',['myServices']); angular.module('myServices') .factory('sport', function() { return { title: "Kayak" } }); # factory that use ngResource provider angular.module('myServices',['ngResource']) .factory('Phone', ['$resource', function($resource) { return $resource('phones/:phoneId', { phoneId: "@id", format: 'json' }, { query: {method: 'GET', params: { phoneId: 'phones' }, isArray: true }, save: { method: 'PUT' }, create: { method: 'POST' } }); }]);
-
service can be added with
.factory
which returnsnew SomeFun(arg)
, but it’s shorter with.service
function. Service methods are defined as properties ofthis
. Service just use $injector to create new instance of service’s contructor function.function UnicornLauncher(apiToken) { this.launchedCount = 0; this.launch = function() { this.launchedCount++; } } myApp.factory('unicornLauncher', ["apiToken", function(apiToken) { return new UnicornLauncher(apiToken); }]); // or myApp.service('unicornLauncher', ["apiToken", UnicornLauncher]);
Factory has more flexibility since they can return functions which can be
new
ed.
-
Provider is broader factory, it returns object with a
$get
method that is a factory function. Provider can be configured. For provider nameunicornLauncher
angular registersunicornLauncher
andunicornLauncherProvider
injectables.myApp.provider('unicornLauncher', function UnicornLauncherProvider_name_not_important() { var useTinfoilShielding = false; this.useTinfoilShielding = function(value) { useTinfoilShielding = !!value; }; this.$get = ["apiToken", function unicornLauncherFactory(apiToken) { return new UnicornLauncher(apiToken, useTinfoilShielding); }]; }); myApp.config(["unicornLauncherProvider", function(unicornLauncherProvider) { unicornLauncherProvider.useTinfoilShielding(true); }]);
-
$resource by default
save
is POST, so we need to change for rails PUT, and addcreate
. Factories need to return object. That object is bindable in all places where we usePhone
factory. It can be used like$scope.phone = Phone.get({ phoneId: $routeParams.phoneId }); $scope.phones = Phone.query();
-
Constant recipe can create value that is available in both config and run phase. Don’t use for example DEFAULT image_url since is better to do that in backend on one place
angular.module('starter') .constant( 'CONFIG', { SERVER_URL: 'http://192.168.3.2:3001', S3UPLOAD: { BUCKET: 'duleorlovic-test1', API_URL: '/api/v1/s3_access_token', }, USER_ROLES: { ADMIN: 'admin', EDITOR: 'editor', GUEST: 'guest', }, }); .run(['CONFIG','$rootScope',function(CONFIG, $rootScope) { // in html use $root.CONFIG.USER_ROLES.ADMIN $rootScope.CONFIG = CONFIG }])
RootScope
$rootScope
has not parent and was created directly fromScope()
class (not through$.new
method). It is used for event handling$.broadcast()
(down to child scopes) and$.emit()
(up in the scope hierarchy).- you can also use for something that does not change often, like
$rootScope.user = user
. In templates you can just writeand it will find `user` at root scope (can't use
).
Share error messages
For form name myForm we can check myForm.$valid
for all fields that we put
validation with required
property on input elements (form can have
novalidate
attribute to prevent browser default behavior, or we can use
ng-required="true"
). Also fields have internal properties: $dirty
(user has
interacted), $valid
, $invalid
and$pristine
(has not interacted with field
yet). To show client side errors, you can use
ng-messages instead of dealing with
ng-show
. Some example validations are: ng-minlength=5
ng-maxlength=20
ng-required="true"
. We can share messages with ng-messages-include
, but
that somehow does not work well. There are also errors from server
(resp.errors
or resp.data.errors
). Button is disabled until form is valid.
<form name="editMenuItemForm" ng-submit="vm.update(vm.email,editMenuItemForm)">
<input type="email" name="email" ng-model="vm.menuItem.email"
ng-required="true" ng-pattern="/^.+@.+\..+$/"
ng-change="editMenuItemForm.email.$setValidity('server', true)">
<div ng-messages="editMenuItemForm.email.$error">
<div ng-message="required">This field is required</div>
<div ng-message="email">This field must be an email</div>
<div ng-message="minlength">Your field is too short</div>
<div ng-message="pattern">Must look like an email</div>
<div ng-message="server">{ { vm.serverErrors.editMenuItemForm.email.toString() }}</div>
</div>
<md-button type="submit" class="md-raised md-primary"
ng-disabled="!editMenuItemForm.$valid">Update</md-button>
</form>
This works also for nested attributes, just need to reference them with
editMenuItemForm["menu_item_options.price"]
(snake case is rails style) and
create one in controller vm.menuItem.menu_item_options_attributes = [{}] unless
vm.menuItem.id
and reference with
input(ng-model='vm.menuItem.menu_item_options_attributes[0].price'
This has problem with destroy button and serverside validation since ng-message is visible only when you focus that input (probably digest is perfomed only on submit)
On server we should respond with render json: @menu_item.errors, status:
:bad_request
so we catch model errors, for example resp.data.email
. If that
field is not on form, for example unauthorized response resp.data.errors =
['Authorized only']
than we show toast. If you need to show toast but don’t
know if resp.data
is object or array than you can use toastr.error
JSON.stringify resp.data
. It should be object and you need to know the key.
For arrays you can use two methods: s.toString()
and String(s)
.
[].toString()
works fine but will raise if instead of []
is nil. So I prefer
to use String(...)
in controllers String(null)=="null"
. Please not that
String
is window
function so it is not available in angular expressions ie
for ng-message
but there we are sure that value is not nill.
Fields that are not valid (for example we put space in name
menuItem.name == undefined
) are not sent so we need to disable button when
form is not valid or check if editMenuItemForm.$valid
before we call
menuItem.update()
. If we disable button than we need to put
ng-change="form.field.$setValidity('server', true)"
to remove server error on
change, so submit button is shown again.
# in controller
vm.update = (menuItem, editMenuItemForm) ->
menuItem.update(menuItem).then(
(menu_item_from_server) ->
$mdDialog.hide()
(resp) ->
$log.debug adminEditMenuItemController: 'update error', resp_data: resp.data
for field of resp.data
if editMenuItemForm[field]
editMenuItemForm[field].$setValidity('server', false)
else
toastr.error String resp.data[field]
vm.serverErrors =
editMenuItemForm: resp.data
)
$q service
In $q service for promise object
we can call then(successCallback, errorCallback, notifyCallback)
but also
finally(callback, notifyCallback)
for clean up.
Here is example of Customer service
# www/js/services/customerAuth.service.coffee
angular.module 'starter'
.service 'CustomerAuth', ($http, CONSTANT, $q) ->
service = this
service.sessionId = null
service.login = (customer_login) ->
$q(
(resolve, reject) ->
$http.post(
CONSTANT.SERVER_URL + '/customer/sessions.json'
customer_session: customer_login
).then(
(response) ->
resolve response
(response) ->
reject response
)
)
Tips:
- use
controller as vm
in view andvar vm = this
in controller so you don’t need to inject$scope
(we need$scope
when we want to access something in promisecatch = -> $scope.vm.profileForm = "a"
) -
in directives you need to prefix
$root
to access rootScope, for example$root.user
-
by Angular conventions, lowerCamelCase is used for factory names that won’t be new’ed. Controllers and services should be UpperCased.
Filenames are lowerCamerCase, but folders are underscored or dashed
src/app/menu_items/menuItems.jade
. Template always match controller name - johnpapa style guide
- use
var vm = this;
(or more descriptiveloginVm
). Use$scope
only for publish/subscribe events$emit
,$broadcast
and$on
. - use
factory
that returns instead ofservice
- use
- for enabling html5 links, you need to put
<base href="/">
beforeapp.css
link - sometimes you need to
rm -rf .tmp
since gulp server cache miss -
angular.copy is usefull for forms and cancel button. Note that object is disconnected, so if you store a list of all items, you need to angular.copy back to original.
Also when you receive an array from server
MyService.locations = locationsFromServer
will assign new pointer, but oldvm.locations = MyService.locations
will still point to old array. -
$log can show object with source location, for example
$log.debug adminController: 'update error', resp_data: resp.data
. It is angular service which you can disable in productionangular.module 'my-module' .config($logProvider) -> $logProvider.debugEnabled = false
- inside ui router resolve, js errors are completelly hidden
Check some awesome links
Testing
Confirm
Usage <md-button ng-really-click="vm.delete(item)"
ng-really-reject="vm.cancel(item)" ng-really-message="Are you
sure?">Remove</md-button>
# app/components/ng-really-click.directive.coffee
angular.module 'myApp'
.directive 'ngReallyClick', ->
restrict: 'A'
link: (scope, element, attrs) ->
element.bind 'click', ->
message = attrs.ngReallyMessage
if message && confirm(message)
scope.$apply attrs.ngReallyClick
else if attrs.ngReallyReject
scope.$apply attrs.ngReallyReject
Data disable with Processing…
Similar to rails data-disable-with="Processing..."
here is directive.
Angular ignores data-
from data-disable-with
.
Similar to
angular-autodisable
There was some problem with JQlite and find with [type=submit]
so use my
fork
Errors
If you get circular dependency error like Uncaught Error: [$injector:cdep]
Circular dependency found: $state <- unauthorizedInterceptor <- $http <-
$templateFactory <- $view <- $state
, than use $injector
and `$state =
$injector.get ‘$state’ like in the following example.
Redirect to login state on every unauthorized
response
# www/js/interceptors/unauthorized.interceptor.coffee
angular.module 'starter'
.factory 'unauthorizedInterceptor', ($q, $injector) ->
responseError: (rejection) ->
if rejection.status == 401
$state = $injector.get('$state')
$state.go 'login'
$q.reject rejection
# www/js/app.config.coffee
angular.module 'starter'
.config ($httpProvider) ->
$httpProvider.interceptors.unshift 'unauthorizedInterceptor'
Interceptor that will set data.error
in case there is network error
angular.module 'starter'
.factory 'connectionInterceptor', ($q, NotifyService) ->
responseError: (rejection) ->
if rejection.status <= 0 # server offline
unless rejection.data && rejection.data.error
rejection.data =
error: 'There is a problem with connection'
$q.reject rejection
UI View, Angular route
angular-route
is separated package and need to be added as module dependency
and configured. config
needs Providers (factory for service that will be
configured) for example $routeProvider
and run
or controller
needs service
for example $route
angular.module('myApp',[ 'ngRoute', 'myAppControllers' ])
.config(function($routeProvider, $locationProvider){
$routeProvider
.when('/phones/:id', {
templateUrl: 'details.html',
controller: 'PhoneDetailCtrl',
})
.when('/phones', {
templateUrl: 'list.html',
controller: 'PhoneListCtrl',
})
.otherwise({redirectTo: '/phones'});
});
It is used with directive <ng-view></ng-view>
which is replaced with given
template.
UI Router
ui-router is nice but documentation is not so simple, so here are examples:
- stateParams
can be defined with
:contactId
or{contactId}
- query params are defined like
?contactId
- navigating with
ui-sref
ui-sref="admin.menu_items({staurantId: restaurant.id})"
or with$state.go 'tab.location_ticket_details', locationTicketId: locationTicket.id
- ui-sref-active to add class on navigational links that are active
-
if you want to completely change the template with edit template, than wrap it
# show.html <ui-view> My name is </ui-view> # edit.html <form> <input name="name"> </form> # my.router.coffee angular.module 'my' .config ($stateProvider) -> $stateProvider .state 'myAccount', url: '/my-account' templateUrl: 'show.html' controller: 'MyController' controllerAs: 'vm' resolve: myAccountInitialData: (myAccountInitialData) -> myAccountInitialData() .state 'myAccount.edit', url: '/edit' templateUrl: 'edit.html' # no need to resolve here since this is inside MyController
-
When you need double click to go to nested state, you are probably coverting that state with some other state stackoverflow route resolution problem.
-
resolve initialData before new state is activated link
# src/app/my_account/myAccountInitialData.coffee # http://odetocode.com/blogs/scott/archive/2014/05/20/using-resolve-in-angularjs-routes.aspx # https://promisesaplus.com/#point-41 the promise resolution procedue, ie # onFulfilled returns promise, when it is fulfilled than fullfill parent angular.module 'menucardsAngular' .factory "myAccountInitialData", (Restaurant, $auth, $q) -> -> user = $auth.validateUser() $q.all([user]).then (results) -> restaurant = Restaurant.query({}, { id: results[0].restaurant_id }) $q.all([restaurant]).then ( results ) -> restaurant: results[0] # src/app/my_account/myAccount.controller.coffee angular.module 'menucardsAngular' .controller 'MyAccountController', (myAccountInitialData, $log) -> 'ngInject' $log.debug "MyAccountController" vm = this vm.restaurant = myAccountInitialData.restaurant return # src/app/my_account/myAccount.router.coffee angular.module 'menucardsAngular' .config ($stateProvider) -> 'ngInject' $stateProvider .state 'myAccount', url: '/my-account' templateUrl: 'app/my_account/myAccount.html' controller: 'MyAccountController' controllerAs: 'vm' resolve: myAccountInitialData: (myAccountInitialData) -> myAccountInitialData()
-
if you need to do some jQuery on state change (not only when controller loads, but when we change states inside current controller) you can listen to events on
$scope
(this scope is destroyed when controller is destroyed)$scope.$on '$stateChangeStart', (e, toState, toParams, fromState, fromParams) -> $scope.sectionName = toParams.sectionName if toState.name == 'menu' $timeout -> new scrollItem 'cart' return # this is when user lands on menu page $timeout -> if $('#cart').first() new scrollItem 'cart'
- multiple views you can not have two different active state, so it will toggle
Batarang
egghead batarang.
Just select element and in console type $scope.vm.name = 'dule';$scope.apply()
Usually I define module angular.module 'name', []
only in one place
src/app/index.module.coffee
Since some old deleted templates could remove stuff, I run with gulp clean &&
gulp serve --api localhost:3001 -l
Interval for automatic updates
Order.query({status: 'active'}, {restaurantId: user.restaurant_id}).then (orders) ->
vm.orders = orders
vm.youngerThan = new Date
# could be that order happens after previous query on server and before current time, not likely
# more likely is that order happened before query on server but after current time
# so it will show up twice, that's why we check if alredy there
$interval(
->
Order.query({status: 'active', youngerThan: vm.youngerThan}, {restaurantId: user.restaurant_id}).then (orders) ->
for order in orders.reverse
vm.orders.unshift(order) if vm.orders[0] && vm.orders[0].id != order.id
vm.youngerThan = new Date
15000)
Pagination
You can use angular-paginate-anything to show pagination links on your page. On server you can use kaminari with some before filter or clean_pagination which I prefer. It is very easy to start. For angular-rails-resource I need to use $timeout hack
$scope.$on 'pagination:loadPage', (event, status, config) ->
$timeout ->
vm.users = vm.usersObject.map (userObject) -> new User userObject
Tips
- you can have another ng-click inside ng-click (ie button inside button) and
you can stop propagation with
ng-click="vm.edit(); $event.stopPropagation();"
-
don’t know why someone would use
ng-repeat="o in vm.objects tack by o.id
since DOM is updated always, eveno.title
is changed. link only usage is that directive link function is not called until we add new items or updateId look console log. If we update title Link function will not be called although DOM will be updated. - sample app https://angular-expenses.herokuapp.com/