Why is 'this' different things in two different functions? - angularjs

I have a problem where this isn't what I expect it to be, but I can't figure out why after hours of ripping my hair out. From what I understand this should be the same this in both functions, but it isn't. This causes my custom select to not update the label when you select something else in the dropdown because getCurrentOption always returns undefined since it tries to access .currentOption from the wrong this object.
Can someone explain what is happening here? And how to make it so that I pass the same this object to the two functions?
co-select.html:
<div class="co-select-form-control">
<label class="co-inset-label" ng-bind="label"></label>
<ul class="list-reset" ng-class="{'co-select': !label, 'co-select-labeled': label}">
<li ng-click="co.toggleSelect(this)" class="co-select-option clickable">
<span ng-bind="co.getCurrentOption(this) || default"></span>
<ul ng-show="co.isSelectToggled(this)" class="list-reset bg-light co-select-dropdown">
<li ng-repeat="option in list" ng-if="option !== co.getCurrentOption(this)"
ng-click="co.selectOption(this, option)" ng-bind="option" class="co-select-option"></li>
</ul>
<span class="co-select-icon">
<i class="icon icon-keyboard-arrow-{{co.isSelectToggled(this) ? 'up' : 'down'}}"></i>
</span>
</li>
</ul>
</div>
co-select directive:
coForms.directive('coSelect', [function() {
return {
restrict: 'E',
scope: {
default: '=',
list: '=',
label: '#'
},
controller: 'CoFormsCtrl',
controllerAs: 'co',
templateUrl: 'app/views/components/co-forms/co-select.html',
link: function(scope, element, attrs) {
}
};
}]);
The controller:
coForms.controller('CoFormsCtrl', [function() {
var coForms = this;
/* View functions */
coForms.toggleSelect = function(select) {
select.isToggled = !select.isToggled;
};
coForms.isSelectToggled = function(select) {
return select.isToggled ? true : false;
};
coForms.selectOption = function(select, option) {
select.currentOption = option;
console.log(select);
};
coForms.getCurrentOption = function(select) {
console.log(select);
return select.currentOption;
};
}]);
The console.log from within coForms.getCurrentOption shows this to be:
While the console.log from within coForms.selectOption shows this to be:
How I use this directive:
<co-select list="['option 1', 'option 2', 'option 3']" default="'option 1'"></co-select>

In Angular expressions this is a special word and refers to the scope on which the expression is being evaluated. Since ng-repeat creates a new scope, scope inside and outside it will be different.
Using this in expressions can be rarely required, and this case isn't an exception.

That's because ng-repeat creates a new scope (if i'm correct, it extends the parent scope), your selectOption is called inside ng-repeat so this represents that scope.
You shouldn't be using controller functions this way (passing this as parameter). You should make a variable "select" in your scope and use that.
This should work (see this plunkr: http://plnkr.co/edit/Javqd1zoKbubHEUD2Ea9?p=preview ) :
coForms.directive('coSelect', [function() {
return {
restrict: 'E',
scope: {
default: '=',
list: '=',
label: '#'
},
controller: 'CoFormsCtrl',
controllerAs: 'co',
templateUrl: 'app/views/components/co-forms/co-select.html',
link: function(scope, element, attrs) {
}
};
}]);
coForms.controller('CoFormsCtrl', ['$scope', function($scope) {
var coFormsCtrl = this;
coFormsCtrl.select={
isToggled: true,
currentOption:$scope.default
};
/* View functions */
coFormsCtrl.toggleSelect = function() {
coFormsCtrl.select.isToggled = !coFormsCtrl.select.isToggled;
};
coFormsCtrl.isSelectToggled = function() {
return coFormsCtrl.select.isToggled ? true : false;
};
coFormsCtrl.selectOption = function(option) {
coFormsCtrl.select.currentOption = option;
console.log(coFormsCtrl.select);
};
coFormsCtrl.getCurrentOption = function() {
console.log(coFormsCtrl.select);
return coFormsCtrl.select.currentOption;
};
}]);
And the template:
<div class="co-select-form-control">
<label class="co-inset-label" ng-bind="label"></label>
<ul class="list-reset" ng-class="{'co-select': !label, 'co-select-labeled': label}">
<li ng-click="co.toggleSelect()" class="co-select-option clickable">
<span ng-bind="co.getCurrentOption() || default"></span>
<ul ng-show="co.isSelectToggled()" class="list-reset bg-light co-select-dropdown">
<li ng-repeat="option in list" ng-if="option !== co.getCurrentOption()"
ng-click="co.selectOption(option)" ng-bind="option" class="co-select-option"></li>
</ul>
<span class="co-select-icon">
<i class="icon icon-keyboard-arrow-{{co.isSelectToggled() ? 'up' : 'down'}}"></i>
</span>
</li>
</ul>
</div>

Related

Function is always returning undefined

I have a problem where getCurrentOption is always returning undefined, and I can't for the love of coding figure out why. Because I have 2 other functions (coForms.toggleSelect and coForms.isSelectToggled) identical in functionality and they work fine.
For some reason select.currentOption is set correctly in coForms.selectOption but when I try and fetch the value in select.getCurrentOption it's not there anymore.. Is it losing a reference or something somewhere? I'm not too familiar with the controller as syntax nor hooking it up to a directive like this.
What is happening?
CoFormsCtrl:
var coForms = this;
coForms.toggleSelect = function(select) {
select.isToggled = !select.isToggled;
};
coForms.isSelectToggled = function(select) {
return select.isToggled ? true : false;
};
coForms.selectOption = function(select, option) {
select.currentOption = option;
};
/* This is always returning undefined atm, still search for the cause */
coForms.getCurrentOption = function(select) {
return select.currentOption;
};
Directive:
coForms.directive('coSelect', [function() {
return {
restrict: 'E',
scope: {
default: '=',
list: '=',
label: '#'
},
controller: 'CoFormsCtrl',
controllerAs: 'co',
templateUrl: 'app/views/components/co-forms/co-select.html',
link: function(scope, element, attrs) {
}
};
}]);
Directive template:
<div class="co-select-form-control">
<ul class="list-reset co-select">
<li ng-click="co.toggleSelect(this)" class="co-select-option clickable">
<span ng-bind="co.getCurrentOption(this) || default"></span>
<ul ng-show="co.isSelectToggled(this)" class="list-reset bg-light co-select-dropdown">
<li ng-repeat="option in list" ng-if="option !== co.getCurrentOption(this)"
ng-click="co.selectOption(this, option)" ng-bind="option" class="co-select-option"></li>
</ul>
</li>
</ul>
<span class="co-select-icon">
<i class="icon icon-keyboard-arrow-{{co.isSelectToggled(this) ? 'up' : 'down'}}"></i>
</span>
</div>
Then I just use it like this:
<co-select list="video.config.upload.status" default="video.config.upload.status[0]"></co-select>
Change this:
/* This is always returning undefined atm, still search for the cause */
coForms.getCurrentOption = function(select) {
return select.currentOption;
};
To this:
coForms.getCurrentOption = function(select) {
return select.currentOption || coForms.default;
};
In your directive, you have bound the default attribute of the directive's HTML to the default property of your controllers scope.
coForms.directive('coSelect', [function() {
return {
restrict: 'E',
scope: {
default: '=',
list: '=',
label: '#'
},
controller: 'CoFormsCtrl',
controllerAs: 'co',
templateUrl: 'app/views/components/co-forms/co-select.html',
link: function(scope, element, attrs) {
}
};
}]);

Dynamic Controller and templateUrl inject in an Angular Directive

I have troubles to find a solution to make a fully dynamic directive. I use the angular-gridster library to make an overview of tiles for a dashboard page. I want to dynamicly load some specific tiles by a flexible directive.
<div gridster="vm.model.gridsterOptions">
<ul>
<li gridster-item="tile.tileParams.gridParams" ng-repeat="tile in vm.model.dashboards.tiles">
<div class="box" ng-controller="tileGrid_TileController">
<eg-tile template-url={{tile.tileEngine.tempUrl}}
controller-name={{tile.tileEngine.tileCtrl}}>
</eg-tile>
</div>
</li>
</ul>
</div>
I have created the egTile directive :
(function () {
function implementation() {
return {
restrict: 'E',
transclude: true,
scope: true,
controller: '#',
bindToController:true,
controllerAs: 'vm',
name: 'controllerName',
templateUrl: function (elem, attrs) {
return attrs.templateUrl || 'app/module/tileGrid/view/templates/empty.html';
}
};
}
var declaration = [implementation];
angular.module('app.tileGrid').directive('egTile', declaration);
}());
This directive will work if I use a fixed string in the egTile directive like
<eg-tile template-url="string_url" controller-name= "string_ctrl"></eg-tile>
but I want to dynamicly select the controller and templateUrl.
I already tried to use the $parse and $observe service but without succes.
Is this even possible to make the directive so flexible ?
Thanks in advance
I found a solution for my problem.....
I used 2 extra directives that will provide the controller-string and the templateUrl-string for the "flexible directive" egTile.
One for creating the controller-string :
(function () {
function implementation($compile, $parse) {
return {
restrict: 'A',
scope: true,
terminal: true,
priority: 99999,
link: function (scope, elem) {
var name = $parse(elem.attr('eg-parse-controller'))(scope);
elem.removeAttr('eg-parse-controller');
elem.attr('controller-name', name);
$compile(elem)(scope);
}
};
}
var declaration = ['$compile', '$parse', implementation];
angular.module('app').directive('egParseController', declaration);
}());
And one for creating the template-string:
(function () {
function implementation($compile, $parse) {
return {
restrict: 'A',
scope: true,
terminal: true,
priority: 99998,
link: function (scope, elem) {
var name = $parse(elem.attr('eg-parse-template'))(scope);
elem.removeAttr('eg-parse-template');
elem.attr('template-url', name);
$compile(elem)(scope);
}
};
}
var declaration = ['$compile', '$parse', implementation];
angular.module('app').directive('egParseTemplate', declaration);
}());
Than I can use it as :
<div gridster="vm.model.gridsterOptions">
<ul>
<li gridster-item="tile.tileParams.gridParams" ng-repeat="tile in vm.model.dashboards.tiles" ng-controller="tileGrid_TileController">
<eg-tile tile="tile"
eg-parse-template=tile.tileEngine.tempUrl
eg-parse-controller='tile.tileEngine.tileCtrl'>
</eg-tile>
</li>
</ul>
with the directive definition :
(function () {
function implementation($parse) {
return {
restrict: 'E',
transclude: true,
scope: {
tile : '='
},
controller: '#',
bindToController:true,
controllerAs: 'vm',
name: 'controllerName',
templateUrl: 'app/module/tileGrid/view/tileTemplate.html',
link: function(scope, element, attrs) {
scope.getContentUrl = function() {
return attrs.templateUrl || 'app/module/tileGrid/view/templates/empty.html';
}
}
};
}
var declaration = ['$parse' , implementation];
angular.module('app.tileGrid').directive('egTile', declaration);
}());
With a tileTemplate.html :
<div class="box">
<div class="box-header">
<h3>{{ vm.tile.tileParams.title }}</h3>
<div class="box-header-btns pull-right">
<a title="Remove widget" ng-click="vm.removeTile(tile)">
<i class="fa fa-trash"></i>
</a>
</div>
</div>
<div class="box-content">
<div ng-include="getContentUrl()"></div>
</div>
</div>
With this aproach I have fully access to the tile that I passed to the egTile Directive in the dynamic loaded controllers and dynamic loaded views.
All remarks are welcome.

angular scope variable not updating inside directive

I am using angular ng-simplepaginator and i had implemented custom directive for selectbox.
directive
.directive('sel',['onChangeService', function(onChangeService) {
return {
template: '<select class="span2" ng-model="itemsPerPage" ng-options="obj as obj for obj in itemsPerPageList" data-style="btn-primary" bs-select></select>',
restrict: 'E',
scope: {
itemsPerPage: '=',
totalitems: '=totalitems'
},
link: function (scope, elem, attrs) {
scope.itemsPerPageList = ['All','10','20','30','40'];
scope.itemsPerPage = scope.itemsPerPageList[1];
},
controller: function ($scope, $element) {
$scope.$watch('itemsPerPage', function () {
onChangeService.changePager($scope, $scope.itemsPerPage, $scope.totalitems.length);
});
}
};
}]);
service
tellerApp.factory('onChangeService', function ($rootScope, Pagination) {
return {
changePager: function($scope,itemsPerPage,totalLength) {
$scope.pagination = Pagination.getNew(itemsPerPage);
$scope.pagination.numPages = Math.ceil(totalLength/$scope.pagination.perPage);
}
}
});
I am able to calling service inside directive, but the page values not updating.
for example, when i choose 20 items, the values are not updating. (i.e pagination.numPages, pagination.page values are not updating)
<ul class="pagination">
<li ng-class="pagination.firstPageDisabled()">First</li>
<li>«</li>
<li ng-repeat= "n in pagination.range(pagination.numPages, pagination.page, pagination.page+pagination.gap)" ng-class="{active: n == pagination.page}">
{{n + 1}}
</li>
<li>»</li>
<li ng-class="pagination.lastPageDisabled()">Last</li>
</ul>

Angular dynamic polymorphic directives

I'm a beginner to Angular but am poking into some slightly more advanced corners to ensure it has the features I need.
Specifically I need to:
render a sequence of widgets of different types with each implemented as an independent Angular directive
the widget type is determined from the data, not by markup
widgets are each defined in a separate file
set the scope of the directive to the data for that widget instance
I think I have solved the requirement described below and implemented at http://jsfiddle.net/cUTt4/5/
Questions:
Is this correct, best practice, and reasonably fast?
Any improvements I should add?
It would be better if the widget directives had no explicit reference { item : '=' } to obtain their isolated scope, but their sub-scopes should be built by the renderform directive. How do I do that?
My solution:
HTML
(Note the Angular templates are in script here due to limitations of jsfiddle)
<div ng-app="myApp">
<script type="text/ng-template" id="widget-type-a">
<div>
<label>{{ item.label}} </label>
<input type="text" ng-model="item.val" >
</div>
</script>
<script type="text/ng-template" id="widget-type-b">
<div>
<label>{{ item.label}}</label>
<input type="text" ng-model="item.val" >
</div>
</script>
<div ng-controller="FormCtrl">
<renderform></renderform>
</div>
</div>
main.js :
var app = angular.module('myApp', []);
function FormCtrl($scope) {
items = [
{
type: 'widget-type-a',
label : 'Widget A instance 1',
val: 1
},
{
type: 'widget-type-b',
label : 'Widget B instance 1',
val : 2
},
{
type: 'widget-type-a',
label : 'Widget A instance 2',
val : 3
}
];
$scope.items = items
}
app.directive('renderform', function($compile) {
function linkFn(scope, element) {
var item,
itemIdx,
templStr = '',
newParent,
data,
newEl;
newParent = angular.element('<div></div>')
for(itemIdx in scope.items) {
item = items[itemIdx];
templStr += '<div ' + item.type + ' item="items[' + itemIdx + ']"></div>';
}
newEl = angular.element(templStr);
$compile(newEl)(scope);
element.replaceWith(newEl);
}
return {
restrict: 'E',
link:linkFn
};
});
app.directive('widgetTypeA', function() {
return {
restrict: 'A',
templateUrl: 'widget-type-a',
scope: { item: '=' }
};
});
app.directive('widgetTypeB', function() {
return {
restrict: 'A',
templateUrl: 'widget-type-b',
scope: { item: '='}
};
});
sorry fast answer, not tested :
<div data-ng-repeat="item in items">
<div data-ng-switch data-on="item.type">
<div data-ng-switch-when="widget-type-a" data-widget-type-a="item"></div>
<div data-ng-switch-when="widget-type-b" data-widget-type-b="item"></div>
</div>
</div>
If this is what you're looking for, please improve this answer.
I have been thinking about this problem for some time now and, although the ng-switch option work for simple cases, it introduces quite a maintenance overhead.
I've come up with a solution which allows for a single point of maintenance. Consider the following:
var app = angular.module('poly', []);
app.controller('AppController', function ($scope) {
$scope.items = [
{directive: 'odd-numbers'},
{directive: 'even-numbers'},
{directive: 'odd-numbers'}
];
});
app.directive('component', function ($compile) {
return {
restrict: 'E',
controller: function () {
},
controllerAs: 'component_ctrl',
link: function (scope, element, attrs) {
var child_directive = scope.$eval(attrs.directive);
var child_element = $compile('<' + child_directive + ' data="data"></' + child_directive + '>')(scope);
element.append(child_element);
}
}
});
app.directive('oddNumbers', function ($interval) {
return {
restrict: 'E',
link: function (scope) {
scope.number = 0;
$interval(function () {
scope.number += 2;
}, 1000);
},
template: '<h1>{{ number }}</h1>'
}
});
app.directive('evenNumbers', function ($interval) {
return {
restrict: 'E',
link: function (scope) {
scope.number = 1;
$interval(function () {
scope.number += 2;
}, 1000);
},
template: '<h1>{{ number }}</h1>'
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<section ng-app="poly" ng-controller="AppController">
<div ng-repeat="item in items">
<component directive="item.directive" data="item.data"></component>
</div>
</section>
This allows for the components to be specified in the controller in an ad hoc way and the repeater not having to delegate responsibility via a switch.
NB I didn't implement how the data is passed between components

dynamically adding directives in ng-repeat

I am trying to dynamically add different directives in an ng-repeat however the output is not being interpreted as directives.
I've added a simple example here: http://plnkr.co/edit/6pREpoqvmcnJJWzhZZKq
Controller:
$scope.colors = [{name:"red"}, {name: "blue"}, {name:"yellow"}];
Directive:
app.directive("red", function () {
return {
restrict: 'C',
template: "RED directive"
}
});
Html:
<ul>
<li ng-repeat="color in colors">
<span class="{{color.name}}"></span>
</li>
</ul>
How do I make angular pick up the directive specified in the class that is output via ng-repeat?
I know this is an old question, but google brought me here, and I didn't like the answers here... They seemed really complicated for something that should be simple. So I created this directive:
***** NEW CONTENT *****
I've since made this directive more generic, supporting a parsed (the typical angular value) "attributes" attribute.
/**
* Author: Eric Ferreira <http://stackoverflow.com/users/2954747/eric-ferreira> ©2016
*
* This directive takes an attribute object or string and adds it to the element
* before compilation is done. It doesn't remove any attributes, so all
* pre-added attributes will remain.
*
* #param {Object<String, String>?} attributes - object of attributes and values
*/
.directive('attributes', function attributesDirective($compile, $parse) {
'use strict';
return {
priority: 999,
terminal: true,
restrict: 'A',
compile: function attributesCompile() {
return function attributesLink($scope, element, attributes) {
function parseAttr(key, value) {
function convertToDashes(match) {
return match[0] + '-' + match[1].toLowerCase();
}
attributes.$set(key.replace(/([a-z][A-Z])/g, convertToDashes), value !== undefined && value !== null ? value : '');
}
var passedAttributes = $parse(attributes.attributes)($scope);
if (passedAttributes !== null && passedAttributes !== undefined) {
if (typeof passedAttributes === 'object') {
for (var subkey in passedAttributes) {
parseAttr(subkey, passedAttributes[subkey]);
}
} else if (typeof passedAttributes === 'string') {
parseAttr(passedAttributes, null);
}
}
$compile(element, null, 999)($scope);
};
}
};
});
For the OP's use case, you could do:
<li ng-repeat="color in colors">
<span attributes="{'class': color.name}"></span>
</li>
Or to use it as an attribute directive:
<li ng-repeat="color in colors">
<span attributes="color.name"></span>
</li>
***** END NEW CONTENT ******
/**
* Author: Eric Ferreira <http://stackoverflow.com/users/2954747/eric-ferreira> ©2015
*
* This directive will simply take a string directive name and do a simple compilation.
* For anything more complex, more work is needed.
*/
angular.module('attributes', [])
.directive('directive', function($compile, $interpolate) {
return {
template: '',
link: function($scope, element, attributes) {
element.append($compile('<div ' + attributes.directive + '></div>')($scope));
}
};
})
;
For the specific case in this question, one can just rewrite the directive a bit to make it apply the directive to a span by class, as so:
angular.module('attributes', [])
.directive('directive', function($compile, $interpolate) {
return {
template: '',
link: function($scope, element, attributes) {
element.replaceWith($compile('<span class=\"' + attributes.directive + '\"></span>')($scope));
}
};
})
;
Then you can use this anywhere and select a directive by name dynamically. Use it like so:
<li ng-repeat="color in colors">
<span directive="{{color.name}}"></span>
</li>
I purposely kept this directive simple and straightforward. You may (and probably will) have to reword it to fit your needs.
i faced with the same problem in one of my projects and you can see how i solve this problem on jsfiddle
HTML:
<div class="page-wrapper" ng-controller="mainCtrl">
<div class="page">
<h3>Page</h3>
<ul>
<li ng-repeat="widget in widgets"><div proxy="widget" proxy-value="{{widget}}"></div></li>
</ul>
</div>
JS:
var app = angular.module('app',[]);
app.controller('mainCtrl', ['$scope', '$q', 'widgetAPI', function($scope, $q, widgetAPI) {
$scope.widgets = [];
widgetAPI.get().then(
function(data) {
$scope.widgets = data;
},
function(err) {
console.log("error", err);
}
);}])
.service('widgetAPI', ['$q', function($q) {
var api = {};
api.get = function() {
//here will be $http in real app
return $q.when(
[
{
component: 'wgtitle',
title: "Hello world",
color: '#DB1F1F',
backgroundColor: '#c1c1c1',
fontSize: '32px'
},
{
component: 'wgimage',
src: "http://cs425622.vk.me/v425622209/79c5/JgEUtAic8QA.jpg",
width: '100px'
},
{
component: 'wgimage',
src: "http://cs425622.vk.me/v425622209/79cf/S5F71ZMh8d0.jpg",
width: '400px'
}
]
);
};
return api;}])
.directive('proxy', ['$parse', '$injector', '$compile', function ($parse, $injector, $compile) {
return {
replace: true,
link: function (scope, element, attrs) {
var nameGetter = $parse(attrs.proxy);
var name = nameGetter(scope);
var value = undefined;
if (attrs.proxyValue) {
var valueGetter = $parse(attrs.proxyValue);
value = valueGetter(scope);
}
var directive = $injector.get(name.component + 'Directive')[0];
if (value !== undefined) {
attrs[name.component] = value;
}
var a = $compile(directive.template)(scope);
element.replaceWith(a);
}
}}])
.directive('wgtitle', function() {
return {
restrict: 'A',
scope: true,
replace: true,
template: '<h1 style="color:{{widget.color}}; font-size:{{widget.fontSize}}; background:{{widget.backgroundColor}}" >{{widget.title}}</h1>',
link: function(scope, element, attrs) {
}
}})
.directive('wgimage', function() {
return {
restrict: 'A',
scope: true,
replace: true,
template: '<img style="width:{{widget.width}}" src="{{widget.src}}"/>',
link: function(scope, element, attrs) {
}
}});
I hope it will usefull.
I don't think you'll be able to just assign the directive as a class name - you would need to run this through $compile again, which would be heading down the path towards recursion errors.
One possible solution is outlined at: AngularJS - how to have a directive with a dynamic sub-directive
If it works for your use case, you can use templates instead:
<div ng-repeat='template in inner' ng-include='template'></div>

Resources