Creating a list and rendering with angular directives - javascript

I am doing a learning project for the MEAN stack, and I am really stuck at something which I need help.
On the real project, what I do is I have a Form creator were different components can be created and arranged according to the user needs, then directives similar to the used on this simplified example renders the from component by component and enable the user to populate it.
On this JSFiddle, as I mentioned, there is a simplified version where I use a similar approach to the one I want to use on my project.
My Logic is: I create a new array where all the values I input on the textbox are stored after a small processing on the format, then I have two directives that have access to a factory function where the data is stored, that loop trough all the items and render them one by one.
I can see that the factory function is working and create the set of data as I wanted to be.
Here is where the problems start:
I call the directive like this
<render-all-items></render-all-items>
the definition of this directive is
.directive('renderAllItems', function (DataServ) {
return {
restrict: 'E',
scope: {},
link: function (scope, elem, attrs) {
scope.values = DataServ.currentTemplate.getAllItems();
},
template: '<div ng-repeat="item in values">{{item}}<render-item render="item"></renderItem></div>'
};
});
This directive supposedly iterates the list of elements and render them one by one. The single Item render is working after a initial typo correction.
The output on the modal is:
[[
Item order = and item value =
]]
Item order = and item value =
And is always the same output, no matter how many items are on the array.
My main goal is easy:
I should be able to add as many items I want using the textbox and then when I press the open modal, I should be able to see the list of elements rendered in the modal dialog.
I would really appreciate guidance on where I am doing it wrong to achieve the result I want.
Thanks in advance.

You have a typo on this line:
<div><pre><render-item render="template.createTemplateItem(textBoxData)"></render-item></pre>
Should be:
<div><pre><render-item render="template.CreateTemplateItem(textBoxData)"></render-item></pre>
This segment of code:
Template.prototype.getAllItems = function () {
//take a template item object and add it to
//the template items repository
return JSON.stringify(this.items);
};
is called once by your renderAllItems directive when it links:
link: function (scope, elem, attrs) {
scope.values = DataServ.currentTemplate.getAllItems();
},
All changes to that template's items array are not reflected in the directive because you JSON.stringify'd the array.
https://jsfiddle.net/urq3gu5o/

Related

Angular directives that call methods on child directives

I am looking for advice on how to implement a hierarchical structure in Angular, where a directive (<partition>) can call a method on a child directive's controller (<property-value>).
I have put together a detailed example here:
https://jsfiddle.net/95kjjxkh/1/
As you can see, my code contains an outer directive, <partition>, which displays one or more <property-value> directives within.
The <property-value> directive offers an editing method, editItem(), which allows the user to change the value of a single entry. (To keep my example short, I simply assign a random number here, but in my production app, a modal will appear, to query the user for a new value.)
This works fine. However, in the outer directive, <partition>, I would like to add the ability to create a new, blank <property-value> directive and then immediately call its editing method so that the user can enter an initial value. If no initial value is entered, the new item would be discarded.
I have seen examples of inner directives calling methods on enclosing directives, but not the other way around.
Is there a way to do this? Alternatively, is there a better way for me to build this kind of view?
You can always use $broadcast to talk both ways. To your parent as well as to your childrens.
In your Child controller you can do the following
app.directive('propertyValue', function() {
return {
require : '^partition'
restrict: 'E',
scope: {
item: '='
},
with this you will get the parent controller in child directive's link function like this
link:function(scope,element,attrs,partitionCtrl){
partitionCtrl.getChildCtrl(element)
}
in partition controller create getChildCtrl function and with that call "propertyvalue" controller function
controller: function ($scope, ItemFactory) {
// your code
var propValueCtrl =undefined;
this.getChildCtrl =function(elem)
{
propValueCtrl = elem.controller();
}
this.callChildFunction = function()
{
propValueCtrl.Edit();// whatever is the name of function
}
call this function when needed in property link function.
Hope this helps.

AngularJs Directive: How to dynamically set attributes on ng-repeat element

I am relatively new to AngularJS. While venturing into directive creation, I can across this problem: How to dynamically add / remove attributes on the children of the directive's element when these children are dynamically added with 'ng-repeat'?
First, I thought of this solution:
template
...
a.list-group-item(ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)', ng-href='playlist/{{ playlist._id }})
...
*directive
link: function(scope, elm, attrs) {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
...
Result
It turns out, my code never enters this angular.forEach loop because listItems is empty. I suppose it's because the ng-repeat is waiting for the scope.playlists to populate with the data from a async call to a server via $resource.
temporary fix
in the directive definition, I added a boolean variable that checks for the presence of 'add' in the element's attributes: var adding = 'add' in attrs ? true : false;
And then in the template,
a.list-group-item(ng-if='adding', ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)')
a.list-group-item(ng-if='!adding', ng-repeat='playlist in playlists', ng-href='playlist/{{playlist._id }}')
While it works fine, it is obviously not DRY at all. HELP!
Instead of removing attributes, change your click handler.
Add $event to the list of arguments and conditionally use preventDefault().
<a ng-click='addToPlaylist($event,playlist)' ng-href='playlist'>CLICK ME</a>
In your controller:
$scope.addToPlaylist = function(event,playlist) {
if (!$scope.adding) return;
//otherwise
event.preventDefault();
//do add operation
};
When not adding, the function returns and the href is fetched. Otherwise the default is prevented and the click handler does the add operation.
From the Docs:
$event
Directives like ngClick and ngFocus expose a $event object within the scope of that expression. The object is an instance of a jQuery Event Object when jQuery is present or a similar jqLite object.
-- AngularJS Developer Guide -- $event
The way that you are trying to do things may not be the most Angularish (Angularist? Angularyist?) way. When using angular.element() to select child elements as you are trying to do here, you can make sure the child elements are ready as follows:
link: function(scope, elm, attrs) {
elm.ready(function() {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
});
}
However, this is unlikely to work in your situation, as #charlietfl points out below. If you want to avoid the solution you already have (which I think is better than your first attempt), you will have to reimplement your code altogether.
I would suggest defining an additional directive that communicates with its parent directive using the require property of the directive definition object. The new directive would have access to an add property of the parent (this.add in the parent directive's controller) and could be programmed to behave accordingly. The implementation of that solution is beyond the scope of this answer.
Update:
I decided to give the implementation something of a shot. The example is highly simplified, but it does what you are trying to do: alter the template of a directive based on the attributed passed to it. See the example here.
The example uses a new feature in Angular 1: components. You can read more about injectable templates and components here. Essentially, components allow you to define templates using a function with access to your element and its attributes, like so:
app.component('playlistComponent', {
// We can define out template as a function that returns a string:
template: function($element, $attrs) {
var action = 'add' in $attrs
? 'ng-click="$ctrl.addToPlaylist(playlist, track)"'
: 'ng-href="playlist/{{playlist._id}}"';
return '<a class="list-group-item" ng-repeat="playlist in playlists" ' +
action + '></a>';
},
// Components always use controllers rather than scopes
controller: ['playlistService', function(playlists) {
this.playlists = playlists;
this.addToPlaylist = function(playlist, track) {
// Some logic
};
}]
});

ngRepeat track by: How to add event hook on model change?

I have a simple ngRepeat like the following:
<some-element ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id">
<!-- stuff -->
</some-element>
arrayOfRecords is updated from a server and may contain new data.
ngRepeat's track by feature can figure out when a new element is added to the array and automatically updates the DOM without changing the existing elements. I would like to hook into that code and execute a callback function when there's new data coming in or old data is removed. Is it possible to easily do this via Angular?
From what I understand, there's a $$watchers which triggers callbacks whenever there's changes to certain variables, but I don't know how to go about hacking that. Is this the right direction?
NOTE: I know I can manually save the arrayOfRecords and compare it with the new values when I fetch them to see what changed. However, since Angular already offers a track by feature which has this logic, it would be nice if I can have Angular automatically trigger an event callback when an element is added or removed from the array. It doesn't make sense to duplicate this logic which already exists in Angular.
Probably you could create a directive and add it along with ng-repeat, so the directive when created(when item is added by ng-repeat) will emit an event and similarly when the item is destroyed it will emit another event.
A simple implementation here:
.directive('tracker', function(){
return{
restrict:'A',
link:function(scope, el, attr){
scope.$emit('ITEM_ADDED', scope.$eval(attr.tracker))
scope.$on('$destroy', function(){
scope.$emit('ITEM_REMOVED', scope.$eval(attr.tracker))
});
}
}
});
and use it as:
<some-element
ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id"
tracker="item">
and listen for these events at the parent controller for example.
Demo
Or using function binding but in a different way, without using isolate scope for that.
.directive('tracker', function() {
return {
restrict: 'A',
link: function(scope, el, attr) {
var setter = scope.$eval(attr.tracker);
if(!angular.isFunction(setter)) return;
setter({status:'ADDED', item:scope.$eval(attr.trackerItem)});
scope.$on('$destroy', function() {
setter({status:'REMOVED', item:scope.$eval(attr.trackerItem)});
})
}
}
});
Demo
The one above was specific to your question since there is no other built in way, Note that if you were to really find out the items added/removed, you could as well do it in your controller by diffing the 2 lists. You could try use lodash api like _.unique or even simple loop comparisons to find the results.
function findDif(oldList,newList){
return {added:_.uniq(newList, oldList), removed:_.uniq(oldList, newList)};
}
Demo
You can change it to:
<div ng-model="arrayOfRecords">
<some-element ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id">
<!-- stuff -->
</some-element>
</div>
The model will change as soon as arrayOfRecords will change.

angularjs: Bound data in custom direct no longer updating after using "track by" in ng-repeat

This is an addendum to this question I asked previously:
Why does my custom directive not update when I derive the output from the bounded data?
My current dilemma is that I have duplicates in data that I generate inside a custom directive used in an ng-repeat. This means I have to use "track by". This somehow breaks the binding and when I update the model, it no longer updates. If I don't use update and remove the duplicates (which for the example can be done easily but for my real problem I cannot), it works. Here is the jsfiddle of how the issue:
http://jsfiddle.net/Lwsq09d0/2/
My custom directive has this:
scope: {
data: "="
},
link: function (scope, element, attrs) {
scope.$watch(function () {
return scope.data
}, function () {
var getPages = function(extra) {
var pages = [];
pages.push('...');
for (var i = scope.data.list[0]; i <= scope.data.list[1] + extra; i++) {
pages.push(i);
}
pages.push('...');
return pages;
}
scope.pages = getPages(1);
}, true);
},
// Remove "track by $index" to see this working and make sure to remove the duplicates
// "..." pushed in to the generated data.
template: '<ul><li ng-repeat="d in pages track by $index" my-button="d"></li></ul>'
In the fiddle, I have an ng-click call a controller function to modify data.
I've seen other questions about track by breaking binding, but I haven't seen one where the ng-repeat variable is generated in the custom directive via the bound data.
Thanks for any help.
Track by is optimized not to rebuild the DOM for already created items. In your example, you are using $index as the identifier. As such, ng-repeatsees the identifier 1 (for the second element in the pages array) and decides that it does not have to rebuild the DOM. This is causing the problem that you are experiencing.
One possible solution might be to generate page Objects that have a unique id, and to track by that:
var lastID = 0;
function createPage(name){
return { name: name, id: lastID++ };
}
// ... Directive code
pages.push(createPage('...')); // Do this for everything that you push to pages array
// ... More directive code
template: '<ul><li ng-repeat="d in pages track by d.id" my-button="d.name"></li></ul>'
Your JSFiddle, updated to work: http://jsfiddle.net/uv11fe93/

Angular Directive to Directive call

If you have a directive that you're using multiple times on a page how can 1 directive communicate with another?
I'm trying to chain directives together in a parent child relationship. When directive A is clicked i want to filter Directive B to only have the children of the selected item in Directive A. In this case there may be infinite number of directives and relationships on the page.
Normally i would have Directive A call a filter method on each of it's children, and each child calls it's child to continue filtering down the hierarchy.
But i can't figure out if calling methods from 1 directive to another is possibe.
Thanks
It sounds like you are looking for a directive controller. You can use the require: parameter of a directive to pull in another directive's controller. It looks like this:
app.directive('foo', function() {
return {
restrict: 'A',
controller: function() {
this.qux = function() {
console.log("I'm from foo!");
};
},
link: function(scope, element, attrs) {
}
};
});
app.directive('bar', function() {
return {
restrict: 'A',
require: '^foo',
link: function(scope, element, attrs, foo) {
foo.qux();
}
};
});
From the angular docs, here are the symbols you can use with require and what they do.
(no prefix) - Locate the required controller on the current element.
? - Attempt to locate the required controller, or return null if not found.
^ - Locate the required controller by searching the element's parents.
?^ - Attempt to locate the required controller by searching the element's parents, or return null if not found.
Here's a jsbin of my example. http://jsbin.com/aLikEF/1/edit
Another option that may work for what you need is to have a service that each directive sets up a watch on and can manipulate. For example, directive1 may watch a property in the service and respond to changes and also setup a button that can change that property. Then, directive2 can also watch and change the service, and they will respond to one another however you set that up. If you need a jsbin of that also, just let me know.
I hope this helps!
You could try putting all of the data into a service that the directives can each reference.
Something like:
app.factory('selectedStuffService', function(){
var allItems = [];
var selectedItems = [];
function addSelectedItem(item){
selectedItems.push(item);
}
return {
allItems: allItems,
selectedItems: selectedItems,
addSelectedItem: addSelectedItem
}
}
Interactions in directive A change the values in the selectedItems array and directive B can bind to it. You can easily add other methods to the service to filter/manipulate the items as needed and any directive that uses the service should be able to update based on changes made by other directives.

Categories

Resources