Can there be an AngularJS directive to manage an array of conditions? - angularjs

I'm new to AngularJS and my project at the moment has a menu that only needs to be displayed sometimes.
I therefore have:
<div class="iframe-hide"
ng-show="$state.includes('deposit.card.start')||
$state.includes('deposit.card.3ds')||
$state.includes('deposit.card.waiting')||
$state.includes('deposit.bank')||
$state.includes('deposit.x')||
$state.is('deposit.x.start')||
$state.is('deposit.y.start')||
$state.is('deposit.y.frame')">
As you can imagine, as the project grows this becomes unmanageable, so I want to look into tidying it up and creating perhaps a custom directive that will handle these conditions better.
I've been thinking of adding a custom data parameter like this:
.state("deposit.card.waiting", {
url: "/waiting",
templateUrl: "app/deposit/templates/card/waiting.html",
data: { includeMenu: true }
})
The Html instead would be:
<div class="iframe-hide" show-if-true="includeMenu">
And then a directive that will check whether includeMenu is true. I wrote it here:
export class showIfTrueDirective {
static $inject = ["$", "$rootScope"];
static $rootScope: any;
public static build($, $rootScope) {
var directive: ng.IDirective = {
link: (scope, element, attributes: any) => {
var itemToShow = attributes["showIfTrue"];
// this correctly prints "includeMenu"
// grab the data from current state?. If includeMenu == true then show element, otherwise hide element
}
};
return directive;
}
}
if I hook that up:
.directive("showIfTrue", ["$", "$rootScope", (r, s) => { return ShowIfTrueDirective.build(r,s); }])
If I manage to grab the scope data then this might work but this is my first week using Anglular and not entirely sure what I'm doing. Is there a better solution for this scenario?

I managed to solve it:
export class NgHideDirective {
static $inject = ["$rootScope"];
static $rootScope: any;
public static build($rootScope) {
var directive: ng.IDirective = {
link: (scope, element, attributes: any) => {
var itemToHide = attributes["ngHide"];
$rootScope.$on('$stateChangeStart',
(event, toState) => {
if (toState.data.hasOwnProperty(itemToHide)) {
element.hide();
} else {
element.show();
}
});
}
};
return directive;
}
}
So if we now do this on an element:
<div class="iframe" ng-hide="hideMenu">
And this on the state:
.state("deposit.x.rejected", {
url: "/rejected",
templateUrl: "app/deposit/templates/x/rejected.html",
data: { hideDepositMenu: null }
Then the div will be hidden.
However this doesn't work when page is refreshed for some reason.

Related

Angular dynamic creation of Directives

I have page setup where I want to dynamically create directives and add them to the page. I have gotten this work, but in the Directives that I'm adding are not getting compiled correctly because no interpolation is occurring and the Dependency in the constructor is not getting loaded.
Here is the code that I'm using.
page.html
<dashboard-dynamic-component ng-repeat="item in vm.parts" dashboard-part="item.part.implementationComponent" dashboard-part-id="item.id"</dashboard-dynamic-component>
dashboardDynamicComponent
function dynamicComponent($compile: ng.ICompileService, $timeout: ng.ITimeoutService) {
return {
restrict: 'E',
replace: true,
templateUrl: 'app/main/dashboard/dynamicComponent.html',
scope: {
dashboardPart: '=',
dashboardPartId: '='
},
link: (scope, element) => {
var newElement = $compile(`<${scope.dashboardPart} dashboard-part-id="${scope.dashboardPartId}" />`)(scope);
element.append(newElement);
$timeout(() => {});
}
}
}
dynamicComponent.$inject = ['$compile', '$timeout'];
angular.module('Cranalytics')
.directive('dashboardDynamicComponent', dynamicComponent);
This creating the component that I expect it to, but then in the next part is were I see the problems.
titleComponent.html - Which is what is dynamically being created above
<h3 class="title">****{{vm.title}}****</h3>
titleComponent.ts
export class TitleComponent implements ng.IDirective {
restrict = 'E';
replace = true;
templateUrl = 'app/main/dashboard/titleComponent/titleComponent.html';
scope = {
dashboardPartId: '='
};
controller = TitleController;
controllerAs = 'vm';
bindToController = true;
static create(): ng.IDirectiveFactory {
const directive = () => new TitleComponent();
return directive;
}
static instance(): ng.IDirective { return new TitleComponent(); }
}
export class TitleController {
_dashboardPartId: number;
get dashboardPartId(): number {
return this._dashboardPartId;
}
set dashboardPartId(value: number) {
this._dashboardPartId = value;
if (!value) return;
this.dataService.loadById(value)
.then((result: Models.Dashboard.dashboardPart) => {
var options = JSON.parse(result.options);
this.title = options.Title;
});
}
title: string;
static $inject = ['Cranalytics.Dashboard.dashboardPartDataService'];
constructor(private dataService: Dashboard.dashboardPartDataService) {}
}
angular.module('Cranalytics')
.directive('dashboardTitleComponent', TitleComponent.create());
So I think I'm just missing one little piece because I am getting the {{vm.title}} on the page, but the interpolation is not displaying and also in the constructor in the above code the dependency for Cranalytics.Dashboard.dashboardPartDataService is returning undefined
The problem that I had was that the property setter for dashboardPartId was being called before the constructor was being called, which resulted in the who component erroring out because this.dataService was undefined. Once I refactored the loading out of the set and checked for the dataService and added that same call into the constructor everything worked.

Angular directive receiving object as attribute using Typescript

I am developing using Angular 1.5.8 and Typescript
I have a directive which is used under the scope of another directive (and another controller of course). Let's say Directive1, Controller1 and Directive2, Controller2.
Given the Controller1 already has the user information, I would like to pass this user information to the Controller2 through the Directive2, to prevent from fetching the information again from the backend.
I am not sure if this can be done, but it would be nice if that's the case :)
Below is the code to help my explanation:
Directive1 HTML:
<div>
...
<directive2 user="{{ctrl.loggedUser}}"></directive2>
...
</div>
loggedUser is loaded in Controller1 constructor through a call to the backend.
Directive2 and Directive2Ctrl Typescript code:
class Directive2 implements ng.IDirective {
controller = "Directive2Ctrl";
controllerAs = "d2Ctrl";
bindToController = {
user: "#"
};
restrict = "E";
templateUrl = "directive2.html";
static factory(): ng.IDirectiveFactory {
const directive = () => new Directive2();
return directive;
}
}
angular
.module("app")
.controller("Directive2Ctrl", Directive2Ctrl)
.directive("directive2", Directive2.factory());
class Directive2Ctrl implements IDirective2Ctrl {
public user: User;
constructor(user: User) {
// user is undefined
}
$onInit(user: User): void {
// user is undefined
}
}
I couldn't find a way of passing the user object to the Directive2Ctrl (not even sure if it is possible).
Use "scope" property instead of "bindToController" property, and replace your "#" with "=".
Then I use an interface for my specific scope to get autocompletion.
export interface IMyDirectiveScope extends ng.IScope {
user: any;
}
export class Myirective {
public restrict: string = 'E';
public templateUrl = "/mytemplate.html";
public link: (scope: IMyDirectiveScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ngModel: ng.INgModelController) => void;
public scope = {
user: "="
};
constructor() {
var context = this;
context.link = (scope: IMyDirectiveScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ngModel: ng.INgModelController) => {
//INSERT YOUR CODE
//console.log(scope.user);
};
}
public static Factory() {
var directive = () => {
return new MyDirective();
};
return directive;
}
}
In your html, remove your curly braces.
<div>
...
<directive2 user="ctrl.loggedUser"></directive2>
...
</div>
If you want to share data between different locations in your application, just put it in a service and use DI wherever you need the data.
That is, fetch the data, store it in a service and use DI to make the data available in different locations. There is no need to pass data through bindings over several layers, much easier to use a service.
var mod = angular.module('testApp', ['ngRoute']);
mod.config(['$routeProvider',
function($routeProvider) {
$routeProvider.
when('/intern', {
template: '<div class="outer" ng-controller="InternController">{{User.firstName}} <div class="nested" ng-controller="NestedController">{{NestedUser.lastName}}<test-dir></test-dir></div></div>',
resolve: {
userResolve: function($q, $timeout, User) {
var deferred = $q.defer();
// mock server call, which returns server data
$timeout(function() {
var mockUserResp = {
firstName: 'John',
lastName: 'Rambo'
};
User.setUser(mockUserResp);
deferred.resolve();
}, 1000);
return deferred.promise;
}
}
}).
otherwise({
redirectTo: '/intern'
});
}]);
mod.factory('User', function() {
var _user = null;
return {
setUser: function(user) {
_user = user;
},
getUser: function() {
return _user;
}
}
});
mod.controller('InternController', function($scope, User) {
$scope.User = User.getUser();
});
mod.controller('NestedController', function($scope, User) {
$scope.NestedUser = User.getUser();
});
mod.directive('testDir', function(User) {
return {
restrict: 'EA',
scope: {},
template: '<div class="dir">{{firstName}} is a cool guy.</div>',
link: function(scope) {
scope.firstName = User.getUser().firstName;
}
};
});
.outer {
border: 1px solid green;
}
.nested {
border: 1px solid blue;
}
.dir {
border: 1px solid orange;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.12/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.12/angular-route.min.js"></script>
<div ng-app="testApp">
<div ng-view></div>
</div>
It should be like this. But if it's still not working for you, you can create a simple plunker and I will fix it there. Cheers!
<div>
...
<directive2 user="ctrl.loggedUser"></directive2>
...
</div>
`
class Directive2 implements ng.IDirective {
controller = "Directive2Ctrl";
controllerAs = "d2Ctrl";
scope = {},
bindToController = {
user: "="
};
restrict = "E";
templateUrl = "directive2.html";
static factory(): ng.IDirectiveFactory {
return () => new Directive2();
}
}
angular
.module("app")
.controller("Directive2Ctrl", Directive2Ctrl)
.directive("directive2", Directive2.factory());
class Directive2Ctrl implements IDirective2Ctrl {
public user: User;
$onInit(user: User): void {
// user should be available
console.log(this.user);
}
}
Sorry, I wonder if you still need to set scope = {}
To do what I asked in the first place, the scope needs to be correctly used.
Here is another question which is nicely explained which uses the scope correctly:
How can I define my controller using TypeScript?

stateChangeStart event in Angular doesn't fire when refreshing page

I have a custom directive:
export class XHideDirective {
static $inject = ["$rootScope"];
static $rootScope: any;
public static build($rootScope) {
var directive: ng.IDirective = {
link: (scope, element, attributes: any) => {
var itemToHide = attributes["xHide"];
$rootScope.$on("$stateChangeStart",
(event, toState) => {
if (toState.data && toState.data.hasOwnProperty(itemToHide)) {
element.hide();
} else {
element.show();
}
});
}
};
return directive;
}
}
And what that does, is when a state has it, it'll hide all elements on the page with that directive set to that value.
.state("deposit.x.successful", {
url: "/successful/:transactionId",
controller: "DepositResultController",
templateUrl: "deposit/templates/x/successful.html",
data: { hideDepositMenu: null }
})
.state("deposit.x.pending", {
url: "/pending",
templateUrl: "deposit/templates/x/pending.html",
data: { hideDepositMenu: null }
})
.state("deposit.x.rejected", {
url: "/rejected",
templateUrl: "deposit/templates/x/rejected.html",
data: { hideDepositMenu: null }
This all works very well except in the case when I don't transition to that page naturally but I get forwarded there (either a server redirect) or if I refresh the page with Ctrl+F5. Then the "stateChangeStart" event doesn't get hit.
The directive is registered like this:
module Utils {
angular.module("Utils", [])
.directive("xHide", ["$rootScope", (r) => { return XHideDirective.build(r); }]);
}
How do I get the state in those cases?
I found this very similar issue with no solution
$stateChangeStart don't work when user refresh browser
Did you try to put this listener in a .run section ?
$rootScope.$on("$stateChangeStart",
(event, toState) => {
if (toState.data && toState.data.hasOwnProperty(itemToHide)) {
element.hide();
} else {
element.show();
}
});
I think I solved it by doing this:
export class XHideDirective {
static $inject = ["$rootScope", "$timeout"];
static $rootScope: any;
public static build($rootScope, $timeout) {
var directive: ng.IDirective = {
controller: DepositController,
link: (scope, element, attributes: any) => {
var itemToHide = attributes["xHide"];
$timeout(() => {
if (scope.hideMenu && scope.hideMenu.hasOwnProperty(itemToHide)) {
element.hide();
} else {
element.show();
}
}, 0);
$rootScope.$on("$stateChangeStart",
(event, toState) => {
if (toState.data && toState.data.hasOwnProperty(itemToHide)) {
element.hide();
} else {
element.show();
}
});
}
};
return directive;
}
}
and then inside the controller:
module Deposit {
export class DepositController extends Utils.BaseController {
constructor(public $state) {
this.$scope.hideMenu = this.$state.$current.data;
}
}
}
No idea if it's the optimal solution but it seems to work well so far.
Hope it helps someone.

Add a class when scrolled to the element

I'd like to add a class to the element, when it's in view a.k.a scrolled to, but I have no clue where to start.
Let's say I want to add it to <div></div> block and when it's scrolled in to it, the class will be added and when it's scrolled out of it, the class will be removed.
I would provide some html mark up to apply it on, but I think demonstration on <div></div> tags is satisfactionaly to work with.
ui-scroll module adds a class when you scroll past an item. You could fork it and adjust to your needs.
You might already know; there are also many jQuery plugins for this and it's pretty easy to wrap this kind of plugin with a directive:
angular.module('testApp', [])
.directive('onScrollAddClass', function() {
return {
link: function(scope, elm, attr) {
init(elm, attr.onScrollAddClass);
function init(elm, cls) {
elm.viewportChecker({
classToAdd: cls,
repeat: true
});
}
}
}
});
Here's the demo for above code.
I solved it this way on one of my websites.
angular.module("Directives")
.directive("rjOnScrollTo", [
"scroll", "$window", "$rootScope",
function(scroll, $window, $rootScope) {
function link($scope, $element) {
var offset = 100;
var pageLoaded = false;
var uniqueId = "rjOnScrollTo_" + $element.get(0).id;
var onScroll = function() {
if (pageLoaded && $window.scrollTop() + $window.height() - offset >= $element.offset().top) {
$element.addClass($scope.className);
scroll.removeCallback($window, uniqueId);
}
};
scroll.addCallback($window, uniqueId, onScroll);
$scope.$on("$destroy", function() {
scroll.removeCallback($window, uniqueId);
});
$scope.$on("$routeChangeSuccess", function() {
pageLoaded = false;
});
$scope.$on("pageLoaded", function() {
pageLoaded = true;
onScroll();
});
}
return {
link: link,
restrict: "A",
scope: {
"className": "#rjOnScrollTo"
}
};
}]);
Use like so:
<div data-rj-on-scroll-to="my-class"></div>
Edit:
You might want to pass the offset parameter in rather than hard-code it.
The service that loads the data from the back-end via AJAX fires the "pageLoaded" event. It was important in my case to wait for the page content to be fully loaded otherwise the div would be immediately in view and the class would be added straight away.

Load Angular Directive Template Async

I want to be able to load the directive's template from a promise. e.g.
template: templateRepo.get('myTemplate')
templateRepo.get returns a promise, that when resolved has the content of the template in a string.
Any ideas?
You could load your html inside your directive apply it to your element and compile.
.directive('myDirective', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
//Some arbitrary promise.
fetchHtml()
.then(function(result){
element.html(result);
$compile(element.contents())(scope);
}, function(error){
});
}
}
});
This is really interesting question with several answers of different complexity. As others have already suggested, you can put loading image inside directive and when template is loaded it'll be replaced.
Seeing as you want more generic loading indicator solution that should be suitable for other things, I propose to:
Create generic service to control indicator with.
Manually load template inside link function, show indicator on request send and hide on response.
Here's very simplified example you can start with:
<button ng-click="more()">more</button>
<div test="item" ng-repeat="item in items"></div>
.throbber {
position: absolute;
top: calc(50% - 16px);
left: calc(50% - 16px);
}
angular
.module("app", [])
.run(function ($rootScope) {
$rootScope.items = ["One", "Two"];
$rootScope.more = function () {
$rootScope.items.push(Math.random());
};
})
.factory("throbber", function () {
var visible = false;
var throbber = document.createElement("img");
throbber.src = "http://upload.wikimedia.org/wikipedia/en/2/29/Throbber-Loadinfo-292929-ffffff.gif";
throbber.classList.add("throbber");
function show () {
document.body.appendChild(throbber);
}
function hide () {
document.body.removeChild(throbber);
}
return {
show: show,
hide: hide
};
})
.directive("test", function ($templateCache, $timeout, $compile, $q, throbber) {
var template = "<div>{{text}}</div>";
var templateUrl = "templateUrl";
return {
link: function (scope, el, attr) {
var tmpl = $templateCache.get(templateUrl);
if (!tmpl) {
throbber.show();
tmpl = $timeout(function () {
return template;
}, 1000);
}
$q.when(tmpl).then(function (value) {
$templateCache.put(templateUrl, value);
el.html(value);
$compile(el.contents())(scope);
throbber.hide();
});
},
scope: {
text: "=test"
}
};
});
JSBin example.
In live code you'll have to replace $timeout with $http.get(templateUrl), I've used the former to illustrate async loading.
How template loading works in my example:
Check if there's our template in $templateCache.
If no, fetch it from URL and show indicator.
Manually put template inside element and [$compile][2] it.
Hide indicator.
If you wonder what $templateCache is, read the docs. AngularJS uses it with templateUrl by default, so I did the same.
Template loading can probably be moved to decorator, but I lack relevant experience here. This would separate concerns even further, since directives don't need to know about indicator, and get rid of boilerplate code.
I've also added ng-repeat and run stuff to demonstrate that template doesn't trigger indicator if it was already loaded.
What I would do is to add an ng-include in my directive to selectively load what I need
Check this demo from angular page. It may help:
http://docs.angularjs.org/api/ng.directive:ngInclude
````
/**
* async load template
* eg :
* <div class="ui-header">
* {{data.name}}
* <ng-transclude></ng-transclude>
* </div>
*/
Spa.Service.factory("RequireTpl", [
'$q',
'$templateCache',
'DataRequest',
'TplConfig',
function(
$q,
$templateCache,
DataRequest,
TplConfig
) {
function getTemplate(tplName) {
var name = TplConfig[tplName];
var tpl = "";
if(!name) {
return $q.reject(tpl);
} else {
tpl = $templateCache.get(name) || "";
}
if(!!tpl) {
return $q.resolve(tpl);
}
//加载还未获得的模板
return new $q(function(resolve, reject) {
DataRequest.get({
url : "/template/",
action : "components",
responseType : "text",
components : name
}).success(function(tpl) {
$templateCache.put(name, tpl);
resolve(tpl);
}).error(function() {
reject(null);
});
});
}
return getTemplate;
}]);
/**
* usage:
* <component template="table" data="info">
* <span>{{info.name}}{{name}}</span>
* </component>
*/
Spa.Directive.directive("component", [
"$compile",
"RequireTpl",
function(
$compile,
RequireTpl
) {
var directive = {
restrict : 'E',
scope : {
data : '='
},
transclude : true,
link: function ($scope, element, attrs, $controller, $transclude) {
var linkFn = $compile(element.contents());
element.empty();
var tpl = attrs.template || "";
RequireTpl(tpl)
.then(function(rs) {
var tplElem = angular.element(rs);
element.replaceWith(tplElem);
$transclude(function(clone, transcludedScope) {
if(clone.length) {
tplElem.find("ng-transclude").replaceWith(clone);
linkFn($scope);
} else {
transcludedScope.$destroy()
}
$compile(tplElem.contents())($scope);
}, null, "");
})
.catch(function() {
element.remove();
console.log("%c component tpl isn't exist : " + tpl, "color:red")
});
}
};
return directive;
}]);
````

Resources