How can I easily implement multiple layouts in Angular JS? - angularjs

I need to be able to specify different layouts for different routes, and most preferably I would like to be able to define layouts and other parameters in an object in route config and have them propagate on route change.

Here's the way I solved it in my current project.
Working demo to be found here
What if you could define objects in your $routeProvider.when(...) block like this:
Route definition:
$routeProvider
.when('/', {
templateUrl: 'main.html',
controller: 'MainCtrl',
resolve: {
interceptor: interceptWith('routeChangeInterceptor', {
stateClass: {
fullWidthLayout: true
}
})
}
});
And have them propagate and use it to add classes with an ng-class like interface using the stateClass objects like this?
HTML:
<body state-class="{'full-width-layout': fullWidthLayout}"> ... </body>
<div class="some-class" state-class="{'some-class': someValue}"> ... </div>
How to:
This is using an interceptWith(...) helper that simply injects a service and calls it with given parameters, but it could also be implemented using array notation like this
interceptor: ['serviceToInject', function(injectedService) { .. }];
Only this way It's DRYer. See demo fore more on this.
The service that is used to broadcast the object from the route definition:
//This interceptor is responsible for emiting an event on rootScope
.factory('routeChangeInterceptor', ['$rootScope', function($rootScope) {
var _prepare = function(state) {
$rootScope.$broadcast('data:broadcast-state', state);
};
return {
prepare: _prepare
};
}]);
The directive used to add/remove classes based on the broadcasted state event object looks like this:
//This directive receives and $parses object/classname mappings,
//and it will add or remove the defined class for every mapping that is defined.
angular.module('broadcastState')
.directive('stateClass', ['$parse', function ($parse) {
var _linkFn = function link(scope, element, attrs) {
scope.$on('data:broadcast-state', function(e, state) {
//Set defaults
state = state || {};
state.stateClass = state.stateClass || {};
var classes = $parse(attrs.stateClass)(state.stateClass);
angular.forEach(classes,function(value,className) {
if(value && typeof value === 'boolean')
{
element.addClass(className);
}
else
{
element.removeClass(className);
}
});
});
}
return {
restrict: 'A',
link: _linkFn
};
}]);
Check out the plnkr to read more.

Looks like https://github.com/angular-ui/ui-router from the Angular team is the best approach.

Try this http://angular-route-segment.com/ (A lightweight extension for AngularJS $route service which supports tree-like nested views and routes, and advanced flow handling)

Related

call a function after directive is loaded

I am working on custom directive of angular 1.6
I want to call a function after my directive is loaded. Is there a way to achieve this?
I tried link function, but it seems the link function is executed before the directive is loading.
Any help will be appreciated.
This is my directive code
<pagination total-data= noOfRecords></pagination>
app.directive("pagination", [pagination]);
function pagination() {
return {
restrict: 'EA',
templateUrl: 'template/directives/pagination.html',
scope: {
totalData: '=',
},
link: dirPaginationControlsLinkFn
};
}
Since you are using AngularJS V1.6, consider making the directive a component:
app.component("pagination", {
templateUrl: 'template/directives/pagination.html',
bindings: {
totalData: '<',
},
controller: "paginationCtrl"
})
And use the $onChanges and $onInit life-cycle hooks:
app.controller("paginationCtrl", function() {
this.$onChanges = function(changesObj) {
if (changesObj.totalData) {
console.log(changesObj.totalData.currentValue);
console.log(changesObj.totalData.previousValue);
console.log(changesObj.totalData.isFirstChange());
};
};
this.$onInit = function() {
//Code to execute after component loaded
//...
});
});
Components are directives structured in a way that they can be automatically upgraded to Angular 2+ components. They make the migration to Angular 2+ easier.
For more information, see
AngularJS Developer Guide - Components
AngularJS 1.5+ Components do not support Watchers, what is the work around?
You might add a watch to totalData in link function and do the stuff there.
scope.$watch('totalData', function () {
if (scope.totalData && scope.totalData.length > 0) { //or whatever condition is required
}
});
You can try calling your function inside $onInit() or inside $postLink() docs might help

How to inject service dependencies from templates into controllers?

I have the following template.html:
<html>
...
<my-tab></my-tab>
...
</html>
Tag <my-tab> is represented by tabTemplate.html and managed by a MyTab controller with the following constructor:
constructor(
private firstService: FirstServiceClass,
){
this.doSomething(); // this fuction uses firstService
}
FirstServiceClass - a custom class.
I have another tags like <my-tab2>, <my-tab3> etc. They are managed by MyTab2, MyTab3 controllers.
The code of these controllers is almost the same, the difference between them is a parameter in a constructor.
I need to remove copypaste.
Is it possible to specify this parameter somehow?
Use component bindings:
app.component("myTab", {
bindings: {
tab: "<"
},
controller: "myTabController",
templateURL: "myTab.html"
}
Usage:
<my-tab tab="0"></my-tab>
<my-tab tab="1"></my-tab>
class myTabController {
constructor (firstService) {
//...
}
$onInit() {
console.log(this.tab);
//...
}
}
For more information, see
AngularJS Developer Guide - Component-based Architecture
for tab="0" it's needed to use myTabController constructor(firstService). for tab="1" it's needed to use myTabController constructor(secondService). Is it possible?
Use the $injector service to inject different services:
app.component("myTab", {
bindings: {
tab: "<",
service: "#"
},
controller: "myTabController",
templateURL: "myTab.html"
}
class myTabController {
constructor ($injector) {
this.$injector = $injector;
}
$onInit() {
this.tabService = this.$injector.get(this.service);
//...
}
}
Usage:
<my-tab tab="0" service="firstService"></my-tab>
<my-tab tab="1" service="secondService"></my-tab>
For more information, see
AngularJS $injector Service API Reference - get
you can create a directive for my-tab that would be better for binding new temple for this directive and use a model controller for logic.
////This html will be inside parents controller
<div class="tabClass hide">
<my-tab></my-tab>
</div>
////////////////////
'use strict';
angular.module('demoApp').directive('myTab', function () {
return {
templateUrl: "~/MyTab.html",//Create a template for directive view named like MyTab
restrict: "E",
controller: function ($scope, $routeParams, $http, $filter, $modal ) {
//essential code
}
};
});
//and finally add directive referance to index file or where you referances other js file in your project

Angular 1.5 component attribute presence

I'm refactoring some angular directives to angular 1.5-style components.
Some of my directives have behavior that depends on a certain attribute being present, so without the attribute having a specific boolean value. With my directives, I accomplish this using the link function:
link: function(scope,elem,attrs, controller){
controller.sortable = attrs.hasOwnProperty('sortable');
},
How would I do this with the angular 1.5-style component syntax?
One thing I could do is add a binding, but then I'd need to specify the boolean value. I'd like to keep my templates as-is.
Use bindings instead of the direct reference to the DOM attribute:
angular.module('example').component('exampleComponent', {
bindings: {
sortable: '<'
},
controller: function() {
var vm = this;
var isSortable = vm.sortable;
},
templateUrl: 'your-template.html'
});
Template:
<example-component sortable="true"></example-component>
Using a one-way-binding (indicated by the '<') the value of the variable 'sortable' on the controller instance (named vm for view model here) will be a boolean true if set as shown in the example. If your sortable attribute currently contains a string in your template an '#' binding may be a suitable choice as well. The value of vm.sortable would be a string (or undefined if the attribute is not defined on the component markup) in that case as well.
Checking for the mere presence of the sortable attribute works like this:
bindings: { sortable: '#' }
// within the controller:
var isSortable = vm.sortable !== undefined;
Using bindings may work but not if you are trying to check for the existence of an attribute without value. If you don't care about the value you can just check for it's existence injecting the $element on the controller.
angular
.module('yourModule')
.component('yourComponent', {
templateUrl: 'your-component.component.html',
controller: yourComponentsController
});
function yourComponentController($element) {
var sortable = $element[0].hasAttribute("sortable");
}
There is a built-in way to do this by injecting $attrs into the controller.
JS
function MyComponentController($attrs) {
this.$onInit = function $onInit() {
this.sortable = !!$attrs.$attr.hasOwnProperty("sortable");
}
}
angular
.module("myApp", [])
.component("myComponent", {
controller: [
"$attrs",
MyComponentController
],
template: "Sortable is {{ ::$ctrl.sortable }}"
});
HTML
<my-component sortable>
</my-component>
<my-component>
</my-component>
Example
JSFiddle

angularjs: injecting async service into directive with $resource

I'm very much trying to get my head around angularJS and directives still.
I have an existing REST service that outputs JSON data as follows (formatted for readability):
{"ApplicationType":
["Max","Maya","AfterEffects","Nuke","WebClient","Other"],
"FeatureCategory":
["General","Animation","Compositing","Management","Other"],
"FeatureStatus":
["Completed","WIP","NotStarted","Cancelled","Rejected","PendingReview"],
"BugStatus":
["Solved","FixInProgress","NotStarted","Dismissed","PendingReview"]}
I then have a service (which appears to be working correctly) to retrieve that data that I wish to inject into my directive.
(function () {
'use strict';
var enumService = angular.module('enumService', ['ngResource']);
enumService.factory('Enums', ['$resource',
function ($resource) {
return $resource('/api/Enums', {}, {
query: { method: 'GET', cache: false, params: {}, isArray: false }
});
}
]); })();
My intentions are to use the data from the json response to bind to html selector 'options' for the purposes of keeping the data consistent between the code behind REST service and the angular ( ie. the json data is describing strongly typed model data from c# eg. Enum.GetNames(typeof(ApplicationType)) )
projMgrApp.directive('enumOptions', ['Enums',
function (Enums) {
return {
restrict: 'EA',
template: '<option ng-repeat="op in options">{{op}}</option>',
scope: {
key: '#'
},
controller: function($scope) { },
link: function (scope, element, attrs) {
scope.options = Enums.query(function (result) { scope.options = result[scope.key]; });
}
};
}
]);
the intended usage would be to use as follows:
<label for="Application" class="control-label col-med-3 col">Application:</label>
<select id="Application" class="form-control col-med-3 col pull-right">
<enum-options key="ApplicationType"></enum-options>
</select>
which would then produce all of the options consistent with my c# enums.
In this case it appears the directive is never being called when the tag is used.
Note. I assume the factory is working fine, as i can inject it into a separate controller and it works as anticipated.
1) I guess projMgrApp is the main module. Have you included enumService as dependency to this module?
angular.module('projMgrApp',['enumServices'])
Otherwise your main module won't be aware of the existence of your service.
2) Are you aware how the directives are declared and used. When you declare
projMgrApp.directive('EnumOptions', ['Enums', function (Enums) {}])
actually should be used in the html code as:
<enum-options></enum-options>
I m not quite sure about the name but it should start with lowercase letter like enumOptions
3) I don't know why you use this key as attribute. You don't process it at all. scope.key won't work. You either have to parse the attributes in link function (link: function(scope, element, attributes)) or create isolated scope for the directive.
Add as property of the return object the following:
scope : {
key:'#' //here depends if you want to take it as a string or you will set a scope variable.
}
After you have this , you can use it in the link function as you did (scope.key).
Edit:
Here is a working version similar (optimized no to use http calls) to what you want to achieve. Tell me if I'm missing anything.
Working example
If you get the baddir error try to rename your directive name to enumOptions according to the doc (don't forget the injection):
This error occurs when the name of a directive is not valid.
Directives must start with a lowercase character and must not contain leading or trailing whitespaces.
Thanks to Tek's suggestions I was able to get this fully functioning as intended. Which means now my angularJS + HTML select tags/directives are completely bound to my APIs enums. I know down the line that I am going to be needing to adjust add to these occasionally based on user feedback, plus I use these enums to represent strongly typed data all over the app. So it will be a big time saver and help reduce code repetition. Thanks to everyone that helped!
The service and directive that I used is copied below, in case other people starting out with Angular run into similar issues or have similar requirements.
Service:
(function () {
'use strict';
var enumService = angular.module('enumService', [])
.service('Enums', ['$http', '$q', function ($http, $q) {
return {
fetch: function () {
var defer = $q.defer();
var promise = $http.get("/api/Enums").then(function (result) {
defer.resolve(result);
});
return defer.promise;
}
}
}]);})();
Directive:
angular.module('projMgrApp').directive('enumOptions', ['Enums', function (Enums) {
return {
restrict: "EA",
scope: {
key: "#"
},
template: "<select><option ng-repeat='enum in enumIds' ng-bind='enum'></option><select>",
link: function (scope) {
Enums.fetch().then(function (result) {
scope.enumIds = result.data[scope.key];
});
}
};
}]);

AngularJS: What is the best way to bind a directive value to a service value changed via a controller?

I want to create a "Header" service to handle the title, buttons, and color of it.
The main idea is to be able to customize this header with a single line in my controllers like this:
function HomeCtrl($scope, Header) {
Header.config('Header title', 'red', {'left': 'backBtn', 'right': 'menuBtn'});
}
So I created a service (for now I'm only focussing on the title):
app.service('Header', function() {
this.config = function(title, color, buttons) {
this.title = title;
}
});
...And a directive:
app.directive('header', ['Header', function(Header) {
return {
restrict: 'E',
replace: true,
template: '<div class="header">{{title}}</div>',
controller: function($scope, $element, $attrs) {
$scope.$watch(function() { return Header.title }, function() {
$scope.title = Header.title;
});
}
};
}]);
So, this actually works but I'm wondering if there are no better way to do it.
Especially the $watch on the Header.title property. Doesn't seem really clean to me.
Any idea on how to optimize this ?
Edit: My header is not in my view. So I can't directly change the $scope value from my controller.
Edit2: Here is some of my markup
<div class="app-container">
<header></header>
<div class="content" ng-view></div>
<footer></footer>
</div>
(Not sure this piece of html will help but I don't know which part would actually...)
Thanks.
If you are using title in your view, why use scope to hold the object, rather than the service? This way you would not need a directive to update scope.header, as the binding would update it if this object changes
function HomeCtrl($scope, Header) {
$scope.header = Header.config('Header title', 'red', {'left': 'backBtn', 'right': 'menuBtn'});
}
and refer to title as
<h1>{{header.title}}</h1>
Update
Put this in a controller that encapsulates the tags to bind to the header:
$scope.$on("$routeChangeSuccess", function($currentRoute, $previousRoute) {
//assume you can set this based on your $routeParams
$scope.header = Header.config($routeParams);
});
Simple solution may be to just add to rootScope. I always do this with a few truly global variables that every controller will need, mainly user login data etc.
app.run(function($rootScope){
$rootScope.appData={
"header" : {"title" : "foo"},
"user" :{}
};
});
.. then inject $rootScope into your controllers as warranted.

Resources