Angularjs two way data binding on ng-repeat with directive - angularjs

What I am trying to achieve: trigger an angular js directive on a jquery ui event which in turn calls a method in the controller, which adds a value in an array which should appear in the view because of the two way data binding.
What does not work: the value is properly added in the array but the view is not updated
HTML
<li id="collection_{{$index+1}}" class="collection" ng-repeat="collection in column | filter:avoidUndefined" ng-controller="collectionController" ng-model="collection">
<h1 class="collection-title"> {{ collection.title }}</h1>
<ul class="bookmarks sortable droppable" droppable=".draggable" sortable=".bookmarks.sortable">
<li class="bookmark" ng-repeat="bookmark in collection.bookmarks">
<a class="bookmark-link bookmark-{{bookmark.id}}" href="{{bookmark.url}}">{{bookmark.title}}</a>
</li>
</ul>
</li>
DIRECTIVE
directives.directive('droppable',function(){
return {
link:function(scope,el,attrs){
el.droppable({
accept: attrs.droppable,
drop: function( event, ui ) {
var url = $(ui.helper).text();
scope.addBookmark(null, url, url);
$(this).removeClass('ui-state-highlight');
},
...
CONTROLLER
$scope.addBookmark = function (id, title, url){
if (id == null) {
...
}
var bookmark = {
'id': id,
'title': title,
'url': url,
'type': 'bookmark'
};
$scope.collection.bookmarks.push(bookmark);
};
$scope.collection.bookmarks is updated properly but the view stays the same. If I call the same addBookmark function directly with a normal button it works.

You forgot to wrap your drop-callback in $apply:
directives.directive('droppable',function(){
return {
link:function(scope,el,attrs){
el.droppable({
accept: attrs.droppable,
drop: function( event, ui ) {
scope.$apply(function(scope){
var url = $(ui.helper).text();
scope.addBookmark(null, url, url);
$(this).removeClass('ui-state-highlight');
});
},
...

My recommendation is to use $emit instead of calling a method of the controller directly in your directive.
Directives should be always independent components, if inside the directive there is a call to a method from a controller(outside the directive) this will create a dependency between my directive and the controller and of course this will force one not being able to exist without the other.
If I would have to apply a design principle to a directive it will be the S in SOLID, Single responsibility principle. Directives should be able to encapsulate and work independently.
I would probably try this on my directive: scope.$emit("UrlChanged", url);
Something like this:
directives.directive('droppable',function(){
return {
link:function(scope,el,attrs){
el.droppable({
accept: attrs.droppable,
drop: function( event, ui ) {
var url = $(ui.helper).text();
scope.$emit("UrlChanged", url);
$(this).removeClass('ui-state-highlight');
},
On my controller:
$scope.$on("UrlChanged", function(event, url){
... //your has changed.
});

Related

How can I use interpolation to specify element directives?

I want to create a view in angular.js where I add a dynamic set of templates, each wrapped up in a directive. The directive names correspond to some string property from a set of objects. I need a way add the directives without knowing in advance which ones will be needed.
This project uses Angular 1.5 with webpack.
Here's a boiled down version of the code:
set of objects:
$scope.items = [
{ name: "a", id: 1 },
{ name: "b", id: 2 }
]
directives:
angular.module('myAmazingModule')
.directive('aDetails', () => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./a.html")
}))
.directive('bDetails',() => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./b.html")
}));
view:
<li ng-repeat="item in items">
<div>
<{{item.name}}-details/>
</div>
</li>
so that eventually the rendered view will look like this:
<li ng-repeat="item in items">
<div>
<a-details/>
</div>
<div>
<b-details/>
</div>
</li>
How do I do this?
I do not mind other approaches, as long as I can inline the details templates, rather then separately fetching them over http.
Use ng-include:
<li ng-repeat="item in items">
<div ng-controller="myRavishingController"
ng-include="'./'+item.name+'.html'">
</div>
</li>
I want to inline it to avoid the http call.
Avoid http calls by loading templates directly into the template cache with one of two ways:
in a script tag,
or by consuming the $templateCache service directly.
For more information, see
AngularJS $templateCache Service API Reference
You can add any html with directives like this:
const el = $compile(myHtmlWithDirectives)($scope);
$element.append(el);
But usually this is not the best way, I will just give a bit more detailed answer with use of ng-include (which actully calls $compile for you):
Add templates e.g. in module.run: [You can also add templates in html, but when they are required in multiple places, i prefer add them directly]
app.module('myModule').run($templateCache => {
$templateCache.put('tplA', '<a-details></a-details>'); // or webpack require
$templateCache.put('tplB', '<b-details></b-details>');
$templateCache.put('anotherTemplate', '<input ng-model="item.x">');
})
Your model now is:
$scope.items = [
{ name: "a", template: 'tplA' },
{ name: "b", template: 'tplB' },
{ name: "c", template: 'anotherTemplate', x: 'editableField' }
]
And html:
<li ng-repeat="item in items">
<div ng-include="item.template">
</div>
</li>
In order to use dynamic directives, you can create a custom directive like I did in this plunkr:
https://plnkr.co/edit/n9c0ws?p=preview
Here is the code of the desired directive:
app.directive('myProxy', function($compile) {
return {
template: '<div>Never Shown</div>',
scope: {
type: '=',
arg1: '=',
arg2: '='
},
replace: true,
controllerAs: '$ctrl',
link: function($scope, element, attrs, controller, transcludeFn) {
var childScope = null;
$scope.disable = () => {
// remove the inside
$scope.changeView('<div></div>');
};
$scope.changeView = function(html) {
// if we already had instanciated a directive
// then remove it, will trigger all $destroy of children
// directives and remove
// the $watch bindings
if(childScope)
childScope.$destroy();
console.log(html);
// create a new scope for the new directive
childScope = $scope.$new();
element.html(html);
$compile(element.contents())(childScope);
};
$scope.disable();
},
// controller is called first
controller: function($scope) {
var refreshData = () => {
this.arg1 = $scope.arg1;
this.arg2 = $scope.arg2;
};
// if the target-directive type is changed, then we have to
// change the template
$scope.$watch('type', function() {
this.type = $scope.type;
refreshData();
var html = "<div " + this.type + " ";
html += 'data-arg1="$ctrl.arg1" ';
html += 'data-arg2="$ctrl.arg2"';
html += "></div>";
$scope.changeView(html);
});
// if one of the argument of the target-directive is changed, just change
// the value of $ctrl.argX, they will be updated via $digest
$scope.$watchGroup(['arg1', 'arg2'], function() {
refreshData();
});
}
};
});
The general idea is:
we want data-type to be able to specify the name of the directive to display
the other declared arguments will be passed to the targeted directives.
firstly in the link, we declare a function able to create a subdirective via $compile . 'link' is called after controller, so in controller you have to call it in an async way (in the $watch)
secondly, in the controller:
if the type of the directive changes, we rewrite the html to invoke the target-directive
if the other arguments are updated, we just update $ctrl.argX and angularjs will trigger $watch in the children and update the views correctly.
This implementation is OK if your target directives all share the same arguments. I didn't go further.
If you want to make a more dynamic version of it, I think you could set scope: true and have to use the attrs to find the arguments to pass to the target-directive.
Plus, you should use templates like https://www.npmjs.com/package/gulp-angular-templatecache to transform your templates in code that you can concatenate into your javascript application. It will be way faster.

Send a data to another controller in AngularJS

How I can to send array.length to another controller?
First controller: Code below
function uploader_OnAfterAddingFile(item) {
var doc = {item: {file: item.file}};
if (doc.item.file.size > 10240) {
doc.item.file.sizeShort = (Math.round((doc.item.file.size / 1024 / 1024) * 100) / 100) + 'MB';
} else {
doc.item.file.sizeShort = (Math.round((doc.item.file.size / 1024) * 100) / 100) + 'KB';
}
doc.item.showCancel = true;
if ($scope.documentStatus) {
item.formData.push({status: $scope.documentStatus});
}
if ($scope.tenderDraftId) {
item.formData.push({tenderDraftId: $scope.tenderDraftId});
}
item.getDoc = function () { return doc; };
doc.item.getUploadItem = function () { return item; };
$scope.documents.push(doc);
//I need send $scope.documents.length
}
send to this function on other controller
Second Controller:
They are in one page.
First Controller it is a component which release upload files.
Second controller it is a modal window where we have 2 input of text and element with first controller.
All I need it to now array.length of files which were upload in submit function on modal window. I tried with $rootScope but it didn`t help me.
I think what you really want to do here is to $emit or $broadcast an event. This will allow you to write less code and be able to pass this data effortlessly to anyplace in the application that you wish! Using event listeners, $on, would also provide the same effect.
Please give this article a good read to understand which option is best for your use case.
https://medium.com/#shihab1511/communication-between-controllers-in-angularjs-using-broadcast-emit-and-on-6f3ff2b0239d
TLDR:
$rootScope.$broadcast vs. $scope.$emit
You could create a custom service that stores and returns the value that you need:
see more information under the title 'Create Your Own Service'.
Or you could inject routeParams to the second controller: see more information
I came across a similar problem the other day. I would use data binding along with a $ctrl method. Here is a really good article with an example that you can replicate with your use case: http://dfsq.info/site/read/angular-components-communication Hope this helps. This form of communication makes it a lot easier to share data between two components on the same page. Article example:
Header component: input and output
.component('headerComponent', {
template: `
<h3>Header component</h3>
<a ng-class="{'btn-primary': $ctrl.view === 'list'}" ng-click="$ctrl.setView('list')">List</a>
<a ng-class="{'btn-primary': $ctrl.view === 'table'}" ng-click="$ctrl.setView('table')">Table</a>
`,
controller: function( ) {
this.setView = function(view) {
this.view = view
this.onViewChange({$event: {view: view}})
}
},
bindings: {
view: '<',
onViewChange: '&'
}
})
So it means that header component can be used something like this
<header-component view="root.view" on-view-change="root.view = $event.view"></header-component>
Main component: input
.component('mainComponent', {
template: `
<h4>Main component</h4>
Main view: {{ $ctrl.view }}
`,
bindings: {
view: '<'
}
})
Parent component
<header-component view="root.view" on-view-change="root.view = $event.view"></header-component>
<main-component view="root.view"></main-component>
I used the method explained above to pass data between two controllers to hide one component, when a button is clicked in a different component. The data that was being passed was a boolean. I would expect you would be able to do the same thing with array.length.

use ng-bind-html with URL

Currently I have a div with ng-bind-html that I use to bind to HTML that I pass in
<div ng-bind-html="renderHtml(message.text)" />
Currently message.text is HTML. I would rather like to pass in a URL and have that HTML inserted in to the div.
How can I achieve that?
Personally, i would make a custom directive to do such a thing.
I would send the url as attribute paramater, make the directive fetch the html with $http.get(), and when its loaded, append it to the html.
You can add the source as:
<div ng-bind-html="URl | safeHtml"></div>
And need to add $sce for safe binding of the url with this directive:
app.filter('safeHtml', function ($sce) {
return function (val) {
return $sce.trustAsHtml(val);
};
});
Edit:
Rather than direct binding the url, embed it in iframe and use it for safe binding:
$scope.URL = <iframe ng-src="url"></iframe>
I would do it like this:
<input ng-model="myUrl"/>
<div id="myDiv" />
And then add a watch for myUrl, and make a request for the url each time it changes:
$scope.$watch('message.text', function(newValue) {
$.ajax({ url: 'newValue', success: function(data) {
$("#myDiv").html(data);
}});
}

What is the "right" way in Angularjs of doing "master-detail" inter directive communication

I have a directive that displays a list of "master" items and when the user clicks on one of these items I want any "details" directives on the page (there could be more than one) to be updated with the details of the currently selected "master" item.
Currently I'm using id and href attributes as a way for a "details" directive to find its corresponding master directive. But my impression is that this is not the angular way, so if it's not, what would be a better solution?
I appreciate that typically when the issue of inter-communication between directives is raised then the obvious solutions are either to use require: "^master-directive" or to use a service, but in this case the directives are not in the same hierarchy and I don't think using a service is appropriate, as it would make the solution more complicated.
This is some illustrative code showing what I'm doing currently.
<div>
<master-list id="master1"></master-list>
</div>
<div>
<details-item href="#master1" ></details-item>
</div>
In the master-list directive when an item is selected I set an attribute to indicate the currently selected master item:
attrs.$set('masterListItemId',item.id);
In the details-item directive's link function I do:
if (attrs.href) {
var id = attrs.href.split('#')[1];
var masterList = angular.element(document.getElementById(id));
if (masterList) {
var ctrl = masterList.controller('masterList');
ctrl.attrs().$observe('masterListItemId',function(value) {
attrs.$set('detailItemId',value);
});
}
}
attrs.$observe('detailItemId',function(id) {
// detail id changed so refresh
});
One aspect that put me off from using a service for inter-directive communication was that it is possible (in my situation) to have multiple 'masterList' elements on the same page and if these were logically related to the same service, the service would end up managing the selection state of multiple masterList elements. If you then consider each masterList element had an associated detailItem how are the right detailItem elements updated to reflect the state of its associated masterList?
<div>
<master-list id="master1"></master-list>
</div>
<div>
<master-list id="master2"></master-list>
</div>
<div>
<details-item href="#master1" ></details-item>
</div>
<div>
<details-item href="#master2" ></details-item>
</div>
Finally I was trying to use directives, rather than using controller code (as has been sensibly suggested) as I'd really like the relationship between a masterList and its associated detailItems to be 'declared' in the html, rather than javascript, so it is obvious how the elements relate to each other by looking at the html alone.
This is particularly important as I have users that have sufficient knowledge to create a html ui using directives, but understanding javascript is a step too far.
Is there a better way of achieving the same thing that is more aligned with the angular way of doing things?
I think I would use a service for this. The service would hold the details data you care about, so it would look something like this.
In your master-list template, you might have something like a list of items:
<ul>
<li ng-repeat"item in items"><a ng-click="select(item)">{{item.name}}</a></li>
</ul>
...or similar.
Then in your directives, you would have (partial code only)
.directive('masterList',function(DetailsService) {
return {
controller: function($scope) {
$scope.select = function(item) {
DetailsService.pick(item); // or however you get and retrieve data
};
}
};
})
.directive('detailsItem',function(DetailsService) {
return {
controller: function($scope) { // you could do this in the link as well
$scope.data = DetailsService.item;
}
};
})
And then use data in your details template:
<div>Details for {{data.name}}</div>
<ul>
<li ng-repeat="detail in data.details">{{detail.description}}</li>
</ul>
Or something like that.
I would not use id or href, instead use a service to retrieve, save and pass the info.
EDIT:
Here is a jsfiddle that does it between 2 controllers but a directive would be the same idea
http://jsfiddle.net/u3u5kte7/
EDIT:
If you want to have multiple masters and details, leave the templates unchanged, but change your directive controllers and services as follows:
.directive('masterList',function(DetailsService) {
return {
controller: function($scope) {
$scope.select = function(item) {
DetailsService.pick($scope.listId,item); // or however you get and retrieve data
};
}
};
})
.directive('detailsItem',function(DetailsService) {
return {
controller: function($scope) { // you could do this in the link as well
$scope.data = DetailsService.get($scope.listId).item;
}
};
})
.factory('DetailsService',function(){
var data = {};
return {
pick: function(id,item) {
data[id] = data[id] || {item:{}};
// set data[id].item to whatever you want here
},
get: function(id) {
data[id] = data[id] || {item:{}};
return data[id];
}
};
})
I would opt for a different approach altogether without directives. Directives are ideal for DOM manipulation. But in this case I would stick to using just the template and a controller that manages all the data and get rid of the directives. Use ng-repeat to repeat the items
Check out this fiddle for an example of this: http://jsfiddle.net/wbrand/2xrne4k3
template:
<div ng-controller="ItemController as ic">
Masterlist:
<ul><li ng-repeat="item in ic.items" ng-click="ic.selected($index)">{{item.prop1}}</li></ul>
Detaillist:
<ul><li ng-repeat="item in ic.items" >
{{item.prop1}}
<span ng-if="item.selected">SELECTED!</span>
</li></ul>
</div>
controller:
angular.module('app',[]).controller('ItemController',function(){
this.items = [{prop1:'some value'},{prop1:'some other value'}]
this.selectedItemIndex;
this.selected = function(index){
this.items.forEach(function(item){
item.selected = false;
})
this.items[index].selected = true
}
})

How to get angular directive to execute each time a view is accessed without refreshing the page

I have created a custom directive that wraps a jquery function which transforms html into a a place where users can enter tags (similar to SO tagging functionality).
Here's my directive code:
App.directive('ngTagItWrapper', function ($resource, rootUrl) {
$("#myTags").tagit({
allowSpaces: true,
minLength: 2,
tagSource: function (search, showChoices) {
$.ajax({
url: rootUrl + "/api/Tag?searchTerm=" + search.term,
data: search,
success: function (choices) {
choices = $.map(choices, function (val, i) {
return val.Name;
})
showChoices(choices);
}
});
}
});
return {}
});
When I first navigate to the view containing the directive, the directive fires which transforms the ul html element into what I need. However, if I click onto another part of the site and then click back to the part of the site that contains the tag entry screen, the directive never fires and the html is not transformed into a nice place where I can enter tags.
Here's the view code:
<p>Hint: <input ng-model="knowledgeBit.Hint" /></p>
<p>Content: <input ng-model="knowledgeBit.Content"/></p>
<ul id="myTags" ng-tag-it-wrapper>
</ul>
<button ng-click="saveKnowledgeBit()">Save</button>
If I refresh the page, the directive fires and I get the tag entry area. So basically angular doesn't know to fire the directive again unless I completely reload the site. Any ideas?
A directive constructor only run once and then $compile caches the returned definition object.
What you need is to put your code inside a linking function:
App.directive('ngTagItWrapper', function ($resource, rootUrl) {
return {
link: function(scope, elm){
elm.tagit({
allowSpaces: true,
minLength: 2,
tagSource: function (search, showChoices) {
$.ajax({
url: rootUrl + "/api/Tag?searchTerm=" + search.term,
data: search,
success: function (choices) {
choices = $.map(choices, function (val, i) {
return val.Name;
})
showChoices(choices);
}
});
}
});
}
}
});

Resources