Two way binding in custom directive with ng-repeat - angularjs

I basically have a list of items that are stored as an array of objects. Some of these items are "children" of others. This is denoted by a "parentid" setting in each object. This only means that I want to display children as a nested list within the parent.
So, I start by iterating through the array and find one with no parent. When I find it, I call a directive that takes that ID and then again iterates through the main array and finds any item whose parentID is identical to the one in question and displays it. There can be multiple levels of children so that the directive is called recursively.
This works fine and displays all of the children, but I want to be able to select any one of these items and see that ID or object at the top level. This is not working. I pass a variable into each directive with two way binding but no parent is ever updated. I understand the primitive problem and I am passing an object but that is not working. I think that I need to do something with the repeats, but I am not sure what. I do not think transclusion is the issue.
In the demo below, I need to display the updated var3 variable whenever I click on an item.
Initial HTML
<div>{{var3}}</div>
<div>
<my-directive var1="item1"
var2="item2"
var3="item3">
</my-directive>
</div>
Directive
(function ()
{
'use strict';
angular
.module('app.core')
.directive('myDirective', myDirective);
function structureContent($templateRequest, $compile)
{ return {
restrict : 'E',
scope:{
var1:'=',
var2:'#',
var3:'='
},
controller : ["$scope", "$element", "$attrs",
function($scope, $element, $attrs) {
}
],
link : function(scope, element, attrs){
scope.$watch('worksheet',
function(){
$templateRequest("myTemplate.html").then(function(html){
var template = angular.element(html);
element.html(template);
$compile(template)(scope);
});
}, true
);
}
}
};
})();
Directive HTML
<div>
<div ng-click="var3=thisItem.itemid;">(Display item) </div>
// var3 is what I want to be able to pull out at the top level
</div>
<div ng-repeat="thisObj in myArray | orderBy:'order' track by thisObj.itemid"
ng-switch on="thisObj.type_id"
ng-if="thisObj.content.parentid==thisItem.itemid"
class="subSection" >
<div ng-switch-when="1">
<my-directive var1="{{var1}}"
var2="var2"
var3="var3">
</my-directive>
</div>
</div>

Related

isolate scope in directive undefined

I'm having trouble creating a directive, since the isolate scope does not get passed from the html and gives me undefined in the directives controller.
lmLoop definition
angular.module('loops.directives')
.directive('lmLoop', function() {
return {
restrict: 'E',
templateUrl: 'modules/loops/partials/loop.tpl.html',
scope: {
loop: "=",
},
controller: ['$scope', '$log',
function($scope, $log) {
console.log($scope.loop); // undefined
}
]
}
});
modules/loops/partials/loop.tpl.html
<div class="loop-container">
Testing: {{ loop.message }} <!-- empty -->
</div>
How Im trying to use my directive
<div class="loops-container">
<lm-loop ng-repeat="loop in loops" loop="loop"/>
</div>
This renders the view correctly, meaning that it repeats the right amount of times, once for each item in the array, but it does not pass the variable "loop" and the div stays empty. But when I do the following it renders correctly and displays the message
<ul>
<li ng-repeat="loop in loops">
{{ loop.message }} <!-- WORKS!!! -->
</li>
</ul>
The directive and the ul are both inside another directive. That directive gets the loops by a service like this:
controller: function($scope) {
loopService.getLoops($scope.group._id, function(loops) {
$scope.loops = loops;
}, function(err) {
$log.error(err);
});
... Other code ...
}
I don't know whether the asynchronous call to the service gets the data after the
lmLoop directive gets instantiated and therefore makes the data undefined? Or maybe (probably) I'm doing something else wrong?
try to add priority: 1001 in your directive
Good explanations here ng-repeat in combination with custom directive

generating a unique id and class for variable number of directives

Here's a jsbin illustrating the issue, but I will explain it also below with code. I am making a custom directive in AngularJs that will be inserted once for every entry in the database, which I iterate over using ng-repeat as you see here.
<div ng-controller="DemoCtrl as demo">
<h1> Hello {{ name }}
<div class="some-list" ng-repeat="customer in customers">
<div id="not-unique" class="not-unique" my-dumb-graphic datajson=customer></div>
I need to make a unique class or id
</div>
</div>
In case you didn't notice, the directive tucked into that code is my-dumb-graph, with the corresponding myDumbGraphic name in the code below. In order to insert the graphic, which I will do in the link function of the directive below, I need to be able to select a unique id or class in the html above, and I will need to be able to select it from within the link function in the directive, so somehow need to reference the id from the html in the js. You can see in the jsbin that inside the link function of the directive, the id is not yet unique (i.e. the dynamic part hasn't been computed yet), even though it's eventually unique by the time it's rendered to the dom.
<div id="not-unique" class="not-unique" my-dumb-graphic datajson=customer></div>
Rest of the code
var app = angular.module('jsbin', ['ngRoute'])
.config(function ($routeProvider) {
'use strict';
var routeConfig = {
controller: 'DemoCtrll'
};
$routeProvider
.when('/', routeConfig)
.otherwise({
redirectTo: '/'
});
});
app.controller('DemoCtrl', function($scope, $routeParams) {
$scope.name = "World";
$scope.customers = [
{
name: 'David',
street: '1234 Anywhere St.'
},
{
name: 'Tina',
street: '1800 Crest St.'
},
{
name: 'Michelle',
street: '890 Main St.'
}
];
});
app.directive('myDumbGraphic', function () {
return {
restrict: 'A',
scope: {
datajson: '='
},
link: function (scope, elem, attrs) {
...code to insert my dumb graphic...need to select unique id or class or both...need to select unique id from here
}
};
});
Update
As several people have suggested, there are multiple ways to create a dynamic id for a div, however, the dynamic part of the div id won't have computed by the time I need it in the link function. For example, If, following suggestion of other answer, I set id in the html to this <div id="id_{{::id}}" then the dynamic part of it won't have computed by the time I need it to inside the link function, although if I inspect the dom after it's rendered it has computed. In the link function, I can access the div through the elem like this "#"+elem[0].id; and log statements show that at that time it hasn't computed- this is what log statements show for "#"+elem[0].id; ----> #id_{{::id}}
The directive link function has the corresponding element pass in as a parameter, so you already have the selector:
link: function(scope, element, attrs) {
}
Alternatively, you could take leverage your $scope.customers array keys as unique ids and set view elements, pass the id into the directive, and use that as a selector (this assumes you jQuery loaded):
<h1> Hello {{ name }} </h1>
<!-- use customers array index -->
<div class="some-list" ng-repeat="(customerId, customer) in customers">
<!-- append customer id to this element id so it is unique -->
<div id="directive-element-selector-{{customerId}}" class="not-unique" my-dumb-graphic datajson=customer customer-array-id=customerId></div>
<div id="inner-selector-{{customerId}">
I need to make a unique class or id
</div>
</div>
</div>
directive:
app.directive('myDumbGraphic', function () {
return {
restrict: 'A',
scope: {
datajson: '=',
customerArrayId: '='
},
link: function (scope, elem, attrs) {
$('#directive-element-selector-' + customerArrayId) // jQuery selector, this should equal elem pass in as link function param
$('#inner-selector-' + customerArrayId) // inner selector
}
};
});
You can use the $scope $id property which will be unique for each directive and scope in your system, so you can do something like
<div class="myclass_{{::$id}}"></div>
or something like that
Note:
the :: is for one time binding

Creating a Reusable Component in AngularJS

I am new to Stackoverflow. I'm also new to AngularJS. I apologize if I'm not using this correctly. Still, I'm trying to create a UI control with AngularJS. My UI control will look like this:
+---------------------+
| |
+---------------------+
Yup. A textbox, which has special features. I plan on putting it on pages with buttons. My thought is as a developer, I would want to type something like this:
<ul class="list-inline" ng-controller="entryController">
<li><my-text-box data="enteredData" /></li>
<li><button class="btn btn-info" ng-click="enterClick();">Enter</button></li>
</ul>
Please note, I do not want buttons in my control. I also want the enteredData to be available on the scope associated with child controls. In other words, when enterClick is called, I want to be able to read the value via $scope.enteredData. In an attempt to create my-text-box, I've built the following directive:
myApp.directive('myTextBox', [function() {
return {
restrict:'E',
scope: {
entered: '='
},
templateUrl: '/templates/myTextBox.html'
};
}]);
myApp.controller('myTextBoxController', ['$scope', function($scope) {
$scope.doSomething = function($item) {
$scope.entered = $item.name;
// Need to somehow call to parent scope here.
};
}]);
myTextBox.html
<div ng-controller="myTextBoxController">
<input type="text" class="form-control" ng-model="query" placeholder="Please enter..." />
</div>
entryController.js
myApp.controller('entryController', ['$scope', function($scope) {
$scope.enteredData = '';
$scope.enterClick = function() {
alert($scope.enteredData);
};
});
Right now, I have two issues.
When enterClick in entryController is called, $scope.enteredData is empty.
When doSomething in myTextBoxController is called, I do not know how to communicate to entryController that something happened.
I feel like I've setup my directive correctly. I'm not sure what I'm doing wrong. Can someone please point me in the correct direction.
Three suggestions for you.
1) You really shouldn't create a directive with a template that references a controller defined elsewhere. It makes the directive impossible to test in isolation and is generally unclear. If you need to pass data into a directive from a parent scope use the isolate scope object on your directive to bind to that data (Note how the directive template doesn't have a controller) http://jsfiddle.net/p4ztunko/
myApp.directive('myTextBox', [function () {
return {
restrict: 'E',
scope: {
data: '='
},
template: '<input type="text" class="form-control" ng-model="data" placeholder="Please enter..." />'
};
}]);
myApp.controller('entryController', ['$scope', function ($scope) {
$scope.enteredData = 'Stuff';
$scope.enterClick = function () {
alert($scope.enteredData);
};
}]);
<div>
<ul class="list-inline" ng-controller="entryController">
<li>{{enteredData}}
<my-text-box data="enteredData" />
</li>
<li>
<button class="btn btn-info" ng-click="enterClick();">Enter</button>
</li>
</ul>
</div>
2) Don't obfuscate HTML when you don't need to. One of the goals of angular is to make the markup more readable, not replace standard elements with random custom elements. E.g. If you want to watch the value of the input and take action depending on what it is you could do that in the linking function (Note: still not referencing an external controller) http://jsfiddle.net/Lkz8c5jo/
myApp.directive('myTextBox', function () {
return {
restrict: 'A',
link: function(scope, element, attrs){
function doSomething (val) {
alert('you typed ' + val);
}
scope.$watch(attrs.ngModel, function (val) {
if(val == 'hello'){
doSomething(val);
}
});
}
};
});
myApp.controller('entryController', ['$scope', function ($scope) {
$scope.enteredData = 'Stuff';
$scope.enterClick = function (data) {
alert('You clicked ' + data);
};
}]);
<div>
<ul class="list-inline" ng-controller="entryController">
<li>{{enteredData}}
<input type="text" ng-model="enteredData" my-text-box />
</li>
<li>
<button class="btn btn-info" ng-click="enterClick(enteredData)">Enter</button>
</li>
</ul>
</div>
3) Pass data into controller functions from the UI instead of referencing the $scope object in the function like in ng-click="enterClick(enteredData)" It makes testing easier because you remove the $scope dependency from that method
$scope.enteredData is empty because you're not using the correct scope. The $scope in entryController is not the same $scope as in myTextBoxController. You need to specify one of these controllers on your directive, and then use that to reference the proper scope.
It seems like you should move the enterClick and corresponding button into your directive template. Then move the enter click into your text box controller and you will be able to reference $scope.enteredData.
You can notify a parent scope of a change by using $emit. (This is in reference to your comment "// Need to somehow call to parent scope here.")
Furthermore, you may have an issue of not using the proper variable. In myTextBox directive, you declare $scope.entered, yet you are effectively setting $scope.data equal to the value of enteredData in the html.

Place element outside the controller and have them still work AngularJS

How can I control variables and elements using filters that are outside the controller?
I have set up an example to better help me explain my question http://embed.plnkr.co/7E5Ls3oH0q4HuEZsewJL/
You will see I have a div that is being toggled using ng-show and then inside a search input that filters a list of names. The problem arrises when I need to take the toggled div and the search filter and put it outside the 'MainCtrl'. Is there a way that I can have these sitting outside the controller but still interacting with the content of the 'MainCtrl'?
There are several ways to communicate between components which aren't prototypically linked in your app. You could use Angular events to broadcast your search and then handle it from your controller. Or, you could even use $rootScope in both components as a global space both can use to store variables and methods. These are bad ideas - they will make your life harder down the road.
Instead, whenever you need to share information across controllers and/or directives which aren't directly linked, your first thought should to create a service.
Such a service might look like this:
app.factory('Search', function() {
var search = {
results: [],
query: '',
showDetails: false
};
return search;
});
You would inject it into both the controller and the controller or directive (I chose to create a directive) for the search box and initialize the service to a scope variable in each:
app.controller('MainCtrl', function ($scope, friendsFactory, Search) {
$scope.friends = friendsFactory.query();
$scope.search = Search;
});
app.directive('search', function(Search){
return {
restrict: 'E',
scope: {},
templateUrl: 'directive-search.html',
link: {
pre: function(scope, elem, attrs) {
scope.search = Search;
}
}
}
});
You could use that variable in your views, as the model for the search box and the filter for your ng-repeat:
Directive Template
<div ng-class="'details'" ng-show="search.showDetails">
<label for="">Search Names</label>
<input type="text" ng-model="search.query" />
</div>
Filter
<div class="content" ng-controller="MainCtrl">
<a ng-click="search.showDetails = !search.showDetails"> Click Me</a>
<div ng-repeat="friend in friends | filter:search.query | limitTo: 5">
{{friend.name}}
</div>
</div>
Plunker Demo
Note: this simple implementation allows for one individual search/results pair on the page. If you need to have more than one, you will need to further develop the service to allow for more than one instance.

Why does two way binding sometimes work without using a dot in Angular?

Consider this fiddle: Fiddle 1 When you select a date, you will notice that the text above it is not updating. This is because I had to use an object in my list, like this: Fiddle 2 (simplified).
But, on the other hand, this does work, without a dot: Fiddle 3
Could someone explain what the difference is between fiddle 1 and fiddle 3? I've read about prototypical inheritance (unerstanding scopes), but I don't understand this behavior.
Fiddle 3:
HTML:
<div ng-controller="MyCtrl">
Hello, {{name}}!
<button ng-click="visible = !visible">Toggle</button>
<div ng-show="visible">
Some content
<sample visible="visible"></sample>
</div>
</div>
Javascript:
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
$scope.name = 'Superhero';
$scope.visible = true;
}
myApp.directive("sample", function(){
return {
restrict: 'E',
template: '<span ng-click="hide()" style="cursor: pointer;">X</span>',
scope:{
visible: '='
},
link: function(scope, element, attributes){
scope.hide = function(){
console.log(scope.visible);
scope.visible = false;
}
}
}
});
If your directive creates an isolate scope (and there are no intermediate scopes), and it uses = for two-way databinding, you don't need to use object properties – i.e., you don't need a "dot" to get it to work.
In Fiddle 1 and 2, ng-repeat is creating an intermediate (child) scope that prototypically inherits from the MyCtrl scope. In this case, you need to use object properties.
The Fiddle3 makes sense since it tries to hide itself and all variables are in its own isolated scope.
It will be more comparable to Fiddle1 if you want to implement something like when click on X, trigger the Toggle button to hide sample control. Because in Fiddle1 you actually wanted to figure out how to modify the value outside the isolated scope, rather than inside the scope created by the directive.

Resources