ng-click, scopes and nested directives in AngularJS - javascript

I have these nested directives the very inner of which has an 'X' sign, which is, when clicked, is supposed to delete an item (classic problem). Basically, the whole thing is a menu.
I have added an ng-click to the 'X' sign/button of the item, but i am very confused on how to link this whole thing back to the controller, so that i can call a deleteItem() function and actually remove the item. I also want to have scope for the sidebar-item separated (the original version of this code is slightly more elaborated and has conditional statements)
Here's what i have so far
The full working - i.e. not really working - version can be found in this jsfiddle
Here's the relevant part of HTML:
<div ng-app="demoApp">
<div ng-controller="sidebarController">
<div sidebar>
<div sidebar-item ng-repeat="item in items" item="item"></div>
</div>
<button ng-click="addItem();">Add Item</button>
</div>
</div>
And here's the JavaScript:
var demoApp = angular.module('demoApp', []);
demoApp.controller("sidebarController", ["$scope", function($scope) {
$scope.items = [
];
$scope.itemId = 1;
$scope.addItem = function() {
var inx = $scope.itemId++;
$scope.items.push( { id: inx, title: "Item " + inx, subTitle: "Extra content " + inx } );
};
$scope.deleteItem = function(item) {
console.log("Delete this!");
console.log(item);
};
}]);
demoApp.directive("sidebar", function() {
return {
restrict: "A",
transclude: true,
template: '<div><div ng-transclude></div></div>',
controller: ["$scope", function($scope) {
this.deleteItem = function(item) {
$scope.deleteItem(item);
$scope.$apply();
};
}]
};
});
demoApp.directive("sidebarItem", function() {
return {
restrict: "A",
scope: {
item: "="
},
require: "^sidebar",
template: '<div><span>{{ item.title }}</span><br /><span>{{ item.subTitle }}</span><br /><span ng-click="deleteItem(item)">X</span></div>',
};
});
Im sure the answer is simple, but I just cant find it.

If you want to use a required controller function you need to inject it into the link function
in sidebar-item add
link : function (scope, element, attrs, sidebar) {
scope.deleteItem = function (item) {
sidebar.deleteItem(item);
}
}

Related

Proper "Angular" way to pass behavior to directive?

When looking for information regarding Angular directives and passing behavior to directives, I get ended up being pointed in the direction of method binding on an isolate scope, i.e.
scope: {
something: '&'
}
The documentation for this functionality is a bit confusing, and I don't think it'll end up doing what I want.
I ended up coming up with this snippet (simplified for brevity), that works by passing a scope function in HomeCtrl, and the directive does it's work and calls the function. (Just incase it matters, the real code passes back a promise from the directive).
angular.module('app', []);
angular.module('app')
.directive('passingFunction',
function() {
var changeFn,
bump = function() {
console.log('bump() called');
internalValue++;
(changeFn || Function.prototype)(internalValue);
},
internalValue = 42;
return {
template: '<button ng-click="bump()">Click me!</button>',
scope: {
onChange: '<'
},
link: function(scope, element, attrs) {
if (angular.isFunction(scope.onChange)) {
changeFn = scope.onChange;
}
scope.bump = bump;
}
};
})
.controller('HomeCtrl',
function($scope) {
$scope.receive = function(value) {
console.log('receive() called');
$scope.receivedData = value;
};
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.4/angular.min.js"></script>
<div ng-app="app" ng-controller="HomeCtrl">
<passing-function on-change="receive"></passing-function>
<p>Data from directive: {{receivedData}}</p>
</div>
Is this a proper "Angular" way of achieving this? This seems to work.
What you need is to pass the function to the directive. I'll make a very small example.
On controller:
$scope.thisFn = thisFn(data) { console.log(data); };
In html:
<my-directive passed-fn="thisFn()"></my-directive>
On directive:
.directive('myDirective', [
() => {
return {
restrict: 'E',
scope: {
passFn: '&'
},
template: '<div id="myDiv" ng-click="passFn(data)"></div>',
link: (scope) => {
scope.data = "test";
}
}
}]);

Get clicked ng-repeat item in parent controller

This is my code right now:
var app = angular.module("MyApp", []);
app.controller("ctrl", function($scope) {
$scope.slides = [{
title: "Slide 1"
},{
title: "Slide 2"
}];
$scope.clicked = {title: "undefined slide"};
});
app.directive("scroller", function() {
return {
template: "<div slide ng-click='click()' ng-repeat='slide in slides'></div>"
};
});
app.directive("slide", function() {
return {
template: "{{slide.title}}",
controller: function($scope) {
// controller
$scope.click = function() {
// Tell parent I was clicked
}
},
link: function(scope, element, attrs) {
// link
}
};
});
I want the clicked in the main controller to be the slide which was clicked.
$scope.clicked = $scope.slide
obviously does not work because of the repeat scope.
Two way databinding does not work, because this would create a seconde scope for the element and hence the template of the slide directive would stop working.
I know that
$scope.$parent.clicked = $scope.slide;
would work, but I wanted to ask for a better solution, because I don't like accessing the parent scope like this.
Can someone please help me with this? Maybe broadcast the clicked item?
Thanks in advance.
You can use 'require' option of the directive. This option is used to specify that your controller requires another one to work properly.
Your code should look like this:
var app = angular.module("MyApp", []);
app.controller("ctrl", function($scope) {
$scope.slides = [{
title: "Slide 1"
},{
title: "Slide 2"
}];
$scope.clicked = {title: "undefined slide"};
this.slideClicked = function(slide){
$scope.clicked = slide;
};
});
app.directive("scroller", function() {
return {
template: "<div>{{clicked.title}}</div><div slide ng-click='click(slide)' ng-repeat='slide in slides'></div>",
controller: 'ctrl'
};
});
app.directive("slide", function() {
return {
template: "{{slide.title}}",
require: '^scroller',
controller: function($scope) {
// controller
$scope.click = function(slide) {
$scope.containerController.slideClicked(slide);
}
},
link: function(scope, element, attrs, controllers) {
scope.containerController = controllers;
// link
}
};
});
Here is https://jsfiddle.net/60zbpvau/1/
You can also store the information in a property of a property of $scope. When the $scope is created for the child controls, its properties are shallow copied.

Disable item in angularjs directive with expression from parent controller

I have a list of items which are a custom directive and each of those items has a remove button. Now I want to disable this remove button when there is only one item left in my list, but for some reason it doesn't work as expected.
I've made a plunker example where you an watch this behavior.
I guess there is something wrong with the canRemove: '&' part of my directive. But I don't know how to get it working.
View:
<body ng-controller="MainCtrl as vm">
<div ng-repeat="item in vm.items">
<my-directive item="item"
canRemove="vm.items.length != 1"
remove="vm.remove(item)">
</my-directive>
</div>
</body>
Controller:
app.controller('MainCtrl', function($scope) {
var vm = this;
vm.items = [
{
number: 1
} , {
number: 2
}
];
vm.remove = function(item) {
vm.items.splice(vm.items.indexOf(item), 1);
}
});
Directive:
app.directive('myDirective', function() {
return {
restrict: 'EA',
scope: {
item: '=',
canRemove: '&',
remove: '&'
},
controller: function() {
var vm = this;
vm.onRemove = function() {
vm.remove({ item: vm.item });
};
},
controllerAs: 'vm',
bindToController: true,
template: '<button ng-disabled="!vm.canRemove" ng-click="vm.onRemove()">' +
' Remove {{ vm.item.number }}' +
'</button>'
}
});
PS: Since I'm pretty new to angular is the way I'm handling the removing of the items a good practice? Or should I use broadcast and on instead?
First of all attribute should look like can-remove:
<my-directive item="item" can-remove="vm.items.length > 1" remove="vm.remove(item)"></my-directive>
Then in scope configuration you need to use = binding instead of &:
scope: {
item: '=',
canRemove: '=',
remove: '&'
},
Demo: http://plnkr.co/edit/DlZafON6HEdoyhzvwNIh?p=preview

AngularJs Directive with dynamic Controller and template

I want to create a directive that has dynamic view with dynamic controller. the controller and the template view is coming from the server.
The Directive
var DirectivesModule = angular.module('BPM.Directives', []);
(function () {
'use strict';
angular
.module('BPM.Directives')
.directive('bpmCompletedTask', bpmCompletedTask);
bpmCompletedTask.$inject = ['$window'];
function bpmCompletedTask ($window) {
// Usage:
// <bpmCompletedTask></bpmCompletedTask>
// Creates:
//
var directive = {
link: link,
restrict: 'E',
scope: {
type: '=',
taskdata: '=',
controllername:'#'
},
template: '<div ng-include="getContentUrl()"></div>',
controller: '#',
name: 'controllername'
};
return directive;
function link(scope, element, attrs) {
scope.getContentUrl = function () {
return '/app/views/TasksViews/' + scope.type + '.html';
}
scope.getControllerName = function ()
{
console.warn("Controller Name is " + scope.type);
return scope.type;
}
}
}
})();
Here how I'm trying to use the directive
<div ng-controller="WorkflowHistoryController as vm">
<h2>Workflow History</h2>
<h3>{{Id}}</h3>
<div ng-repeat="workflowStep in CompletedWorkflowSteps">
<bpm-completed-task controllername="workflowStep.WorkflowTaskType.DataMessageViewViewName" taskdata="workflowStep.WorkflowTaskOutcome.TaskOutcome" type="workflowStep.WorkflowTaskType.DataMessageViewViewName">
</bpm-completed-task>
</div>
</div>
The problem now is when the directive gets the controller name it get it as literal string not as a parameter.
Is it doable ?
if it's not doable, What is the best solution to create dynamic views with its controllers and display them dynamically inside ng-repeat?
Thanks,
Update 20 Jan I just updated my code in case if some one interested in it. All the Credit goes to #Meligy.
The First Directive:
(function () {
'use strict';
angular
.module('BPM.Directives')
.directive('bpmCompletedTask', bpmCompletedTask);
bpmCompletedTask.$inject = ['$compile', '$parse'];
function bpmCompletedTask ($compile, $parse) {
var directive = {
link: function (scope, elem, attrs) {
console.warn('in the first directive - before if');
if (!elem.attr('bpm-completed-task-inner'))
{
console.warn('in the first directive');
var name = $parse(elem.attr('controllername'))(scope);
console.warn('Controller Name : ' + name);
elem = elem.removeAttr('bpm-completed-task');
elem.attr('controllernameinner', name);
elem.attr('bpm-completed-task-inner', '');
$compile(elem)(scope);
}
},
restrict: 'A',
};
return directive;
}
})();
The Second Directive
angular
.module('BPM.Directives')
.directive('bpmCompletedTaskInner',['$compile', '$parse',
function ($window, $compile, $parse) {
console.warn('in the second directive');
return {
link: function (scope, elem, attrs) {
console.warn('in the second directive');
scope.getContentUrl = function () {
return '/app/views/TasksViews/' + scope.type + '.html';
}
},
restrict: 'A',
scope: {
type: '=',
taskdata: '=',
controllernameinner: '#'
},
template: '<div ng-include="getContentUrl()"></div>',
controller: '#',
name: 'controllernameinner'
};
}]);
The Html
<div ng-repeat="workflowStep in CompletedWorkflowSteps">
<div bpm-completed-task controllername="workflowStep.WorkflowTaskType.DataMessageViewViewName" taskdata="workflowStep.WorkflowTaskOutcome.TaskOutcome"
type="workflowStep.WorkflowTaskType.DataMessageViewViewName">
</div>
</div>
Update:
I got it working, but it's really ugly. Check:
http://jsfiddle.net/p6Hb4/13/
Your example has a lot of moving pieces, so this one is simple, but does what you want.
Basically you need a wrapper directive that takes the JS object and converts into a string property, then you can use هى your directive for everything else (template, scope, etc).
.
Update 2:
Code Inline:
var app = angular.module('myApp', []).
directive('communicatorInner', ["$parse", "$compile",
function($parse, $compile) {
return {
restrict: 'A',
template: "<input type='text' ng-model='message'/><input type='button' value='Send Message' ng-click='sendMsg()'><br/>",
scope: {
message: '='
},
controller: '#'
};
}
]).
directive('communicator', ['$compile', '$parse',
function($compile, $parse) {
return {
restrict: 'E',
link: function(scope, elem) {
if (!elem.attr('communicator-inner')) {
var name = $parse(elem.attr('controller-name'))(scope);
elem = elem.removeAttr('controller-name')
elem.attr('communicator-inner', name);
$compile(elem)(scope);
}
}
};
}
]).
controller("PhoneCtrl", function($scope) {
$scope.sendMsg = function() {
alert($scope.message + " : sending message via Phone Ctrl");
}
}).
controller("LandlineCtrl", function($scope) {
$scope.sendMsg = function() {
alert($scope.message + " : sending message via Land Line Ctrl ");
}
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular.min.js"></script>
<div ng-app="myApp">
<div ng-init="test = {p: 'PhoneCtrl', l: 'LandlineCtrl' }">
<communicator controller-name="test.p" message="'test1'"></communicator>
<communicator controller-name="test.l"></communicator>
</div>
</div>
.
Original (irrelevant now but can help other related issues)
Yes, it should work.
A test with Angular 1.3:
http://jsfiddle.net/p6Hb4/9/
Things to check:
Is the controller defined and added to the module? It will not work
If the controller is just a global function it won't work. It has to be added via the <myModule>.controller("<controllerName>", <functiion>) API
Does ng-controller work? Just adding it to the template
Similarly, does using ng-controller directly outside of the directive work?

Rendering AngularJS directives from array of directive names in a parent directive or controller

I'm attempting to dynamically render directives based on a configuration array of directive names. Is this possible in angular? I also want these rendered directives to live within a single parent dom element rather than each getting a new wrapper (as you would with ng-repeat)
http://jsfiddle.net/7Waxv/
var myApp = angular.module('myApp', []);
myApp.directive('one', function() {
return {
restrict: 'A',
template: '<div>Directive one</div>'
}
});
myApp.directive('two', function() {
return {
restrict: 'A',
template: '<div>Directive two</div>'
}
});
function MyCtrl($scope) {
$scope.directives = ['one', 'two'];
}
<div ng-controller="MyCtrl">
<div ng-repeat="directive in directives">
<div {{directive}}></div>
</div>
</div>
EDIT:
Since posting this, I've also tried:
.directive('parentDirective', function () {
return {
restrict: 'A',
replace: true,
link: function (scope, element) {
scope.directives = ['one', 'two'];
for (var i = 0; i < scope.directives.length; i++) {
element.prepend('<div ' + scope.directives[i] + '></div>')
}
}
};
});
<div parent-directive></div>
With this, the templates from the prepended directives are not rendered.
Here what I came up with (took a long time)... The solution is pretty versatile though, you can modify $scope.directives array at will and the directives will be fabricated dynamically. You can also point to any particular property in the current scope to retrieve the directive list from.
Demo link
app.js
var myApp = angular.module('myApp', []);
myApp.directive('one', function() {
return {
restrict: 'E',
replace: true,
template: '<div>Directive one</div>'
}
});
myApp.directive('two', function() {
return {
restrict: 'E',
replace: true,
template: '<div>Directive two</div>'
}
});
myApp.directive('dynamic', function ($compile, $parse) {
return {
restrict: 'A',
replace: true,
link: function (scope, element, attr) {
attr.$observe('dynamic', function(val) {
element.html('');
var directives = $parse(val)(scope);
angular.forEach(directives, function(directive) {
element.append($compile(directive)(scope));
});
});
}
};
});
function MyCtrl($scope) {
$scope.directives = ['<one/>', '<two/>'];
$scope.add = function(directive) {
$scope.directives.push(directive);
}
}
index.html
<div ng-controller="MyCtrl">
<div dynamic="{{directives}}"></div>
<button ng-click="add('<one/>')">Add One</button>
<button ng-click="add('<two/>')">Add One</button>
</div>
So the second attempt would have worked had I used $compile on the prepended directives like so:
.directive('parentDirective', function($compile)
....
element.prepend($compile('<div ' + scope.directives[i] + '"></div>')(scope));

Categories