In the following code, i change the object's property on clicking the 'tab' element, but the corresponding ngbind span is not getting updated. Do i have to call some function to update the view?
HTML:
<html ng-app="splx">
...
<body ng-controller="Application">
<span ng-bind="obj.val"></span>
<tabpanel selector="obj">
<div tab value="junk">junk</div>
<div tab value="super">super</div>
</tabpanel>
</body>
</html>
JS:
var cf = angular.module('splx', []);
function Application($scope) {
$scope.obj = {val: "something"};
}
cf.directive('tabpanel', function() {
return {
restrict: 'AE',
scope: {
selector: '='
},
controller: ['$scope', function($scope) {}]
};
});
cf.directive('tab', function() {
return {
require: '^tabpanel',
restrict: 'AE',
scope: true,
link: function(scope, elem, attrs) {
elem.bind('click', function() {
scope.$parent.selector.val = "newthing";
});
}
};
});
cf.directive('tab', function() {
return {
require: '^tabpanel',
restrict: 'AE',
scope: true,
link: function(scope, elem, attrs) {
elem.bind('click', function() {
scope.$apply(function () {
scope.$parent.selector.val = "newthing";
});
});
}
};
});
That works for me. Just missing a little scope.$apply in there.
Might want to have a look at https://coderwall.com/p/ngisma if you find yourself using/having trouble with '$apply already in progress'.
If you want to change the value to what you clicked on, I'd do something like this:
scope.$parent.selector.val = attrs.tab;
As opposed to:
scope.$parent.selector.val = "newthing";
And then you can change your markup to look like this:
<tabpanel selector="obj">
<div tab="junk">junk</div>
<div tab="super">super</div>
</tabpanel>
Hope that helps!
First problem: you are not binding your controller to your app.
You need cf.controller('Application', Application);.
Also you need ng-controller="Application" in HTML on a parent of that span and the tabpanel directive.
Second problem: after changing that scope variable in your click event you need to
scope.$apply() to let Angular know something changed and it needs to $digest it.
You can check out my version here.
Related
Today I encounter some really strange behaviour in AngularJS using $watch. I simplified my code to the following example:
https://jsfiddle.net/yLeLuard/
This example contains a service which will keep track of a state variable. The directives are used to bind a click event to the DOM changing the state variable through the service.
There are two problems in this example:
The first close button (with the ng-click property on it) only changes the state on the second click
The two buttons without the ng-click are not changing the state at all
main.html
<div ng-controller="main">
State: {{ api.isOpen | json }}
<div ng-click="" open>
<button>Open - Working fine</button>
</div>
<div ng-click="" close>
<button>Close - Works, but only on second click</button>
</div>
<div open>
<button>Open - Not Working</button>
</div>
<div close>
<button>Close - Not Working</button>
</div>
</div>
main.js
var myApp = angular.module('myApp', []);
myApp.controller('main', ['$scope', 'state', function($scope, state) {
$scope.api = state;
$scope.api.isOpen = false;
$scope.$watch('api.isOpen', function() {
console.log('state has changed');
});
}]);
myApp.directive('open', ['state', function(state) {
return {
restrict: 'A',
scope: {},
replace: true,
template: '<button>Open</button>',
link: function(scope, element) {
element.on('click', function() {
state.isOpen = true;
});
}
};
}]);
myApp.directive('close', ['state', function(state) {
return {
restrict: 'A',
scope: {},
replace: true,
template: '<button>Close</button>',
link: function(scope, element) {
element.on('click', function() {
state.isOpen = false;
});
}
};
}]);
myApp.service('state', function() {
return {
isOpen: null
};
});
That's because you are using a native event listener on click. This event is asynchronous and out of the Angular digest cycle, so you need to manually digest the scope.
myApp.directive('open', ['state', function(state) {
return {
restrict: 'A',
scope: {},
link: function(scope, element) {
element.on('click', function() {
scope.$apply(function() {
state.isOpen = true;
});
});
}
};
}]);
Fixed fiddle: https://jsfiddle.net/7h95ct1y/
I would suggest changing the state directly in the ng-click: ng-click="api.isOpen = true"
You should put your link function as a pre-link function, this solves the problem of the second click.
https://jsfiddle.net/2c9pv4xm/
myApp.directive('close', ['state', function(state) {
return {
restrict: 'A',
scope: {},
link:{
pre: function(scope, element) {
element.on('click', function() {
state.isOpen = false;
console.log('click', state);
});
}
}
};
}]);
As for the not working part, you're putting your directive on the div, so when you click on the div it works but not on the button. Your open directive should be on the button.
Edit: other commenters suggested that you change the state directly in the ng-click. I wouldn't recommend it, it might work in this case but if you're bound to have more than an assignation to do it's not viable.
I am trying to change "Collapse" text to "Expand" on click of h2 tag. at the same time I am applying "active" class to h2 tag. using following directive, which is working fine, but now I am clueless about how to change "collapse" text to Expand on h2 tag click
HTML
<h2 class="panel-title" toggleclass>My Tandem URL</h2>
<a class="collapse-arrow">Collapse</a>
js
.directive("toggleclass", function () {
return {
restrict: 'A',
scope: false,
link: function (scope, element, attrs) {
element.bind("click", function () {
element.toggleClass('active');
});
}
}
})
Can anyone plz help..
The
If you want to do DOM manipulation in your directive you could change the contents of the element by doing:
var link = element.find('a');
link.text(link.text() === 'Collapse' ? 'Expand' : 'Collapse')`.
Using html instead of text also works.
You have to move the link inside the h2 to have the directive see the link.
Another approach is having the link text changed via the scope, but then you need to have a template and bind the link text to the directive scope. Then you have to wrap both elements in a directive.
This may make the solution a bit too big for this simple use case...
Another suggestion on making up an directive: encapsulates html and behaivor inside: fiddle .
angular.module('myModule', [])
.directive('togglable', function () {
return {
restrict: 'EA',
template: '<div><h2 class="panel-title" ng-class="{ active: isCollapsed }" ng-click="toggle()" ng-transclude></h2><a class="collapse-arrow">{{ isCollapsed ? "Expand" : "Collapse"}}</a></div>',
transclude: true,
replace: true,
scope: false,
controller: function ($scope) {
$scope.isCollapsed = false;
$scope.toggle = function () {
$scope.isCollapsed = !$scope.isCollapsed;
};
}
};
});
Key features:
- no manual DOM manipulaton;
- uses transclude feature;
Such a directive is easy to use:
<togglable>My Tandem URL</togglable>
Depending on how your app works, maybe you can try this:
HTML:
<h2 class="panel-title" ng-class="{'active': expand}" toggleclass>My Tandem URL</h2>
<a class="collapse-arrow" ng-hide="expand">Collapse</a>
<a class="collapse-arrow" ng-show="expand">Expand</a>
JS:
angular.module('myApp', [])
.controller('myCTRL', function($scope) {
$scope.expand = false;
})
.directive("toggleclass", function () {
return {
restrict: 'A',
scope: false,
link: function (scope, element, attrs) {
element.bind('click', function() {
scope.$apply(function() {
scope.expand = !scope.expand;
});
});
}
}
});
http://jsfiddle.net/uqbc9asf/
Here's my fiddle
I basically want to be able to change the text when a button is pressed. I have tried with both $observe and $watch inside link, but I still don't manage to get it working.
Code:
(function(){
angular.module('app', [])
.directive('testDirective', function(){
return {
restrict: 'E',
scope: {
title: '#'
},
template: '<div>this is a {{ title }}</div>',
link: function(scope, element, attrs) {
//?
}
};
});
})()
You need to pass data as scope variable, you should not pass it as a string if you want to track changes.
check this fiddle, replace counter data with your desired data. Hope this helps
<div ng-controller='myctrl'>
<test-directive title="counter"></test-directive>
<hr></hr>
<button type="button" ng-click = 'onclickbutton()'>Change names</button>
</div>
(function(){
angular.module('app', [])
.controller('myctrl',function($scope){
$scope.counter = 0;
$scope.onclickbutton = function(){
$scope.counter++;
}
})
.directive('testDirective', function(){
return {
restrict: 'E',
scope: {
title: '='
},
template: '<div>this is a {{ title }}</div>',
link: function(scope, element, attrs) {
}
};
});
})();
I have created two directives and inserted the first directive into the second one. The content of template attribute works fine but the scope variable of the controller is not recognized. Please provide me solution on this
sample link: http://jsbin.com/zugeginihe/2/
You didn't provide the attribute for the second directive.
HTML
<div second-dir first-dir-scope="content">
<div first-dir first-dir-scope="content"></div>
</div>
Link demo: http://jsbin.com/jotagiwolu/2/edit
The best option would using parent directive, We could take use of require option of directive like require: '?secondDir' in firstDir
Code
var myApp = angular.module("myApp", []);
myApp.controller("myController", function($scope) {
$scope.content = "test1";
});
myApp.directive("firstDir", function() {
return {
restrict: "AE",
require: '?secondDir',
scope: {
firstDirScope: "="
},
template: "<div>first content</div>",
link: function(scope, element, attrs, secondDir) {
console.log(scope.firstDirScope, 'first');
}
};
});
myApp.directive("secondDir", function() {
return {
restrict: "AE",
scope: {
firstDirScope: "="
},
controller: function($scope) {
console.log($scope.firstDirScope, 'second');
}
};
});
Working JSBin
I'm learning AngularJS and I'm training for how to build a reusable directive.
The problem is that it works with an array with one element, but not with two or more.
The HTML tag is just: <breadcrumb></breadcrumb> which in case, render as expected. But, I need to do manually what "replace:true" would do.
The error is: parent is null.
I exhausted all Google searches looking for an example.
But my case is peculiar, because there is an <inner ng-repeat> inside <breadcrumb> which is an another inner-directive instead an normal tag, that's why replace doesn't work and I need to do manually.
There is a problem related to "dynamic template load..."
OBS: I tried to do in both "link:" and "compile:" the logic, same error...
I could make the code work, but, I'm unable to remove <inner> tag as transclude would do automatic.
Now the output is almost perfect, and I just need to remove the <inner>, but no luck until now. I tried to replace the element tag, but still no luck.
<ul class="breadcrumb">
<!-- I want to remove this <inner> and leave <li> alone! -->
<inner data-ng-repeat="_item in items track by $index" class="ng-scope">
<li class="ng-scope">1
</li>
</inner>
<!-- remove for clarity -->
</ul>
var myModule = angular.module("myModule", []);
myModule.directive('breadcrumb', function($timeout) {
"use strict";
var directiveDefinitionObject = {
template: '<ul class="breadcrumb"><inner data-ng-repeat="_item in items track by $index"></inner></ul>',
replace: true,
// transclude: true,
restrict: 'E',
scope: {},
controller: ["$scope", "$element", "$attrs", "$transclude", controller],
link: ["scope", "iElement", "iAttrs", link]
};
function link(scope, iElement, iAttrs) {
scope.addNewItem = function(new_item) {
scope._push(new_item);
}
}
function controller($scope, $element, $attrs, $transclude) {
$scope.items = [1, 2, 3, 4, 5];
$scope._push = function(item) {
$scope.items.push(item);
};
$scope._pop = function() {
$scope.items.pop();
};
$scope.is_last = function(item) {
return $scope.items.indexOf(item) == ($scope.items.length - 1);
}
}
return directiveDefinitionObject;
});
myModule.directive("inner", ["$compile",
function($compile) {
"use strict";
function getItemTemplate(index) {
return '<li>{{ _item }}</li>';
}
return {
require: "^breadcrumb",
restrict: "E",
compile: function compile(tElement, tAttrs)
{
return function postLink(scope, iElement, iAttrs)
{
iElement.html(getItemTemplate(0));
$compile(iElement.contents())(scope);
};
}
};
}
]);
You can just remove the compile function of yours in inner directive and set replace: true, because it's just mocking the default behavior of replace, as you stated. So you inner directive would become:
myModule.directive("inner", ["$compile",
function($compile) {
"use strict";
return {
replace: true
require: "^breadcrumb",
restrict: "E",
template: '<li>{{ _item }}</li>'
};
}
]);
But you have a problem that your items array is defined into your breadcrumb directive, what is wrong and will not let you make it reusable. You could bind it at scope definition with something like this:
<breadcrumb items="someItemsArrayFromParentScope"></breadcrumb>
...directive('breadcrumb', function() {
...
return {
...
scope: {
items: '='
}
}
});
This would create a two directional binding between the array from parent and internal widget scope. But going further, you might want to let the user define the inner elements of the breadcrumb, it would be as following:
myModule.directive('breadcrumb', function($timeout) {
var directiveDefinitionObject = {
template: '<ul class="breadcrumb" ng-transclude></ul>',
replace: true,
transclude: true,
restrict: 'E',
scope: {},
controller: ["$scope", "$element", "$attrs", "$transclude", controller]
};
function controller($scope, $element, $attrs, $transclude) {
$scope.addNewItem = function(new_item) {
$scope._push(new_item);
}
$scope._push = function(item) {
$scope.items.push(item);
};
$scope._pop = function() {
$scope.items.pop();
};
$scope.is_last = function(item) {
return $scope.items.indexOf(item) == ($scope.items.length - 1);
}
}
return directiveDefinitionObject;
});
myModule.directive("inner", function($compile) {
return {
require: "^breadcrumb",
restrict: "E",
template: '<li><a href="#" ng-transclude></a></li>',
replace: true,
transclude: true
};
}
);
And in your html you would come with:
<breadcrumb>
<inner ng-repeat="item in items"><i>{{item}}</i></inner>
</breacrumb>
The trick is the ng-transclude directive inside the templates. It just get the contents of the element and "move it" to inside the element marked with ng-transclude and link it against the parent scope, what is awesome, as you can have dynamic naming for the items that would be based on parent scope. The items of the ng-repeat, for example, would be defined in the parent scope, as expected. This is the preferred way, and you would even get the possibility of use different inner templates (as I did with the <i> tag). You would even be able to not use an ng-repeat and hardcode the inner elements, if it's the case:
<!-- language: lang-html -->
<breadcrumb>
<inner>First Path</inner>
<inner>Second Path: {{someParentScopeVariable}}</inner>
</breacrumb>
Here is a working Plnker.
I was able to rebuilt it.
I've learnt a lot since my first attempt.
The solution was simplified because the dynamic template is rubbish to handle, because ng-repeat does not redraw the entire array. So, I did it my own way and it was a clean solution.