I am trying to make an Angular 1.5 directive work within an ng-repeat for a table. A few things I need this to do (and please, if I am going at this the completely wrong way, let me know):
1) Update the html whenever new results are provided. Most importantly, the link function gets called once, with an empty result, and never called again. So, my table is blank. Always.
2) Inside the 'template' of the directive, I reference the controller's name and a method which is type coupling. Is the 'right' way to do this?
Here's my code snippets...
module-a.view.html:
<table>
<tr ng-repeat="(ii,result) in moduleACtrl.results">
<td>This is a normal TD</td>
<td-result result="{{result}}" ctrlName="moduleACtrl"></td-result>
</tr>
</table>
UPDATED
common.td-result.directive.js
(function(){
'use strict';
/* global angular */// ESLINT
angular.module('common').directive('tdResult',Directive);
Directive.$inject = ['$compile'];
function Directive($compile){
return {
restrict : 'E',
scope : {
result : '#',
ctrlName : '#'
},
template: '<td>{{result.prop1}}</td>\
<td>{{result.prop2}}</td>\
<td>{{result.prop3}}</td>\
<td><div ng-click="ctrlName.doSomething()">Something Goes Here {{ctrlName}}</div></td>'
};
}
})();
After reading a number of related questions on StackOverflow, and re-reviewing Angular's Directives documentation, I cannot figure out how to do what I want.
The overall context that got me here is that I have an OpenLayers 3 map, displaying result data from a query. I have the result data on the map interactively displaying result data when you click on it, like this: http://openlayers.org/en/latest/examples/getfeatureinfo-image.html
The difference though, is that I am using Angular 1.5. I have a ModuleA with a controller and view that house the map. I have another ModuleCommon that houses a service that handles creation of the map object and hands it to ModuleA.controller (or any controller who wants the map service). So, now, I have to create an interface for the map service that can generically handle displaying data to any client who wants to render said data.
I decided this was all a bad idea and that I need to step way back. First, the map object should not be responsible for displaying the data. The map is responsible for handling the 'singleclick' event and getting me a feature.
So, I decided that the controller, which instantiates the map object via the map service, will simply be responsible for creating and sending a callback to the map object so the map object knows what it's client wants on a singleclick. The getFeature is the map object's responsibility, and what to do with the feature's id is the client's (i.e. controller's) responsibility.
Whew!
Map code:
map.on('singleclick',function(event){
map.forEachFeatureAtPixel(event.pixel,function(feature){
callback(feature.getId());
});
});
Controller Code:
...
function mapCallback(someId){
var elem = angular.element('#info');
if ( someId === undefined ) {
elem.slideUp(); // Hide element
} else {
for (var data, ii = 0, len = vm.dataList.length; ii < len; ii++) {
data = vm.dataList[ii];
if ( someId === data.property ) {
vm.currentData = data;
$scope.$apply();
elem.slideDown(); // Show element
break;
} else {
elem.slideUp(); // Hide element
}
}
}
}
...
1) The Map is de-coupled from the data pertaining to the feature.
2) The Controller controls what happens when the map selects its data.
3) The HTML stays in the Controller's associated Component's templateUrl where it belongs!
Related
I have 4 controllers in my Angular 1.x app:
houseController
apartmentController
addressController
contactInfoController
With addressController & contactInfoController I update the address and contact info of a house or an apartment.
I work in the following way:
<div ng-controller="houseController" ng-init="getHouseInformation()>
{{house.contact_info.email}}
{{house.contact_info.mobile_phone_number}}
<a ng-controller="contactInfoController" ng-click="showContactInfoEdit(house.contact_info.id)">Edit</a>
</div>
When the update of the contact information is successfull within the contactInfoController, I want to update the house information getHouseInformation() in the houseController (read: make a new call to the API to get the updated information).
Since I have more than one (and in the future even more) house/apartment/... controllers, I need to have a solid way on how to 'refresh' the scope of these apartments, houses, ... on the fly.
What would be the best solution in my case?
Edit:
My scopes look like this:
// in houseController
$scope.house = {
id : 1,
title : "House title",
contact_info : {
email: '',
mobile_phone_number : ''
}
}
// in apartmentController
$scope.apartment = {
id : 1,
title : "Apartment title",
contact_info : {
email: '',
mobile_phone_number : ''
}
}
// in contactInfoController
$scope.contact_info : {
email: '',
mobile_phone_number : ''
}
So when updating the contact information scope, I'm not directly changing the house scope... Hope this helps.
I disagree with the other advice you got here - directives help you intelligently manipulate the DOM with your data but not necessarily share it between scopes, which is what you're asking about. As I wrote to you on Slack, what you seem to be looking for is a Service which will contain all your data (either declared in it or linked to an external file or API), and then injected into every controller that needs access to that data. That's one of the main uses for services!
For posterity, here's what I wrote to you on Slack:
"...You’re currently creating unrelated objects that don’t communicate - why should they? They’re in different scopes. You’re actually not getting any use out of Angular and could use vanilla Javascript for that:)
The idea is to use persistent data that is shared across your web app. $scope.anything will never communicate outside its scope unless you bind it to something outside the scope, like a service. Whether the service draws data from an external API or really just an object model that lies outside of Angular, (on your server/file structure, for example), or even defined within the service itself doesn’t matter (although the latter is far from a best practice) - but you need something persistent outside of the local scopes that's shared between your controllers/views.
Then there are a few ways to connect the dots. A very common (and probably the best) design pattern is to create a service (NOT directive! I don’t know why they gave you that advice) that encapsulates that data. So, for example,
myApp.service(‘dataModel', function(){
$scope.houses = {}; //an object containing ALL the houses, each one containing ALL the apartments, each apt.containing ALL the contact info, etc. etc.
});
Then in your controllers you pass the dataModel service, and then declare and link the local scope 'reference' of the same object to it, for example:
myApp.controller(‘buildingsView’, [dataModel, function(dataModel){
//and link to that service’s object locally:
$scope.houses = dataModel.houses;
}]);
Then, once you affect that model in one view/controller, the data in the other views/controllers will magically change too!
And that is the angular way:)
Hope this makes sense!
(To clarify, there are two issues here: getting your data INTO angular and then SHARING that same object by injecting the same service to your various controllers. That’s sort of the exact idea of services - they’re singletons, which means only one copy of them exists at any time and if they’re referred to more than once, it’ll always be to the same object, which is why manipulating it in one place will change it in another.)"
Put your data within a $scope variable, and make your controllers watch this varaible from scope. When the event is triggered, you can then do what you want.
$scope.houses = [];
$scope.$watch('houses', function(newValue, oldValue) {
// This is triggered when $scope.houses changes directly
// Add true as param to this function if you want to watch properties nested within the houses object
});
If houses is within a controller, use the following:
(in controller)
var self = this;
self.houses = [];
// This tells angular to watch the controller property houses instead of a scope variable
$scope.$watch(function(){return self.houses}, function(newValue, oldValue) {
// This is triggered when $scope.houses changes directly
// Add true as param to this function if you want to watch properties nested within the houses object
});
I suggest to use directive, then it's easier to exchange data. And that is the reason why directive exists. I try to explain how to build your use case with directives. Assume that you have one controller (houseController) and for every sub requirements you make a directive. One for the contact, one for the appartment and one for the address. Then you define your whole object inside houseController. You pass all necessary data as a parameter to the directive and you can access them from the houseController or from inside the directive, because of the two way data binding. In this way you don't have to exchange something or to call update functions. You adjuste just the object.
your html
<div ng-controller="houseController">
<contact data="house.contact"></contact>
<apartment data="house.apartment"></apartment>
...
</div>
house controller
$scope.house = {
apartment:{
floor: 1,
number:34,
...
},
contact:{
id:2,
email:"peter#test.com",
mobile_phone_number:"123456789",
...
},
...
}
contact directive
.directive('contact', function() {
return {
restrict: 'E',
scope: {
data: '='
},
link: function(scope, element, attrs) {
scope.showContactInfoEdit = function(id){
// do your stuff here
scope.data.name = "Peter";
}
},
templateUrl: 'contact.html'
};
});
contact.html
{{data.email}}
{{data.mobile_phone_number}}
<a ng-click="showContactInfoEdit(data.id)">Edit</a>
I made a plunkr: http://plnkr.co/edit/8tRMtgztaXRC3EKyQhcH?p=preview
I am working on an application were index page and inside that I am showing multiple views, In index nav bar I have to show notification count and User name which comes from 2 different controllers,
I am able to display the notification count and User name successfully, but the issue is the values are not changing dynamically.
We need to refresh the page for the new values.
What can I do in this situation can any one please guide me.
I'm guessing you're watching the value directly and not by some object wrapper. In this case javascript isn't actually updating the variable, but assigning a complete new one. Anything out of the function scope that updates the variable will never receive the new value.
The solution is simple; wrap the value in an object and share/inject that object.
angular.module('myApp')
.value('myPageState', {
notificationCount: 0
});
angular.module('myApp')
.controller('myController', function($scope, myPageState) {
$scope.myPageState = myPageState;
});
<div class="my-notification-thingy"> {{ myPageState.notificationCount }} </div>
You can achieve it by:
Maintain those values in rootScope so that you
will have the two way binding.
Making use of emit to notify the parent controller about value changes. This will work only if those two controllers are present in child elements.
In child controllers fire event on value update:
$scope.$emit('valueChanged', {value: val});
In parent controller receive event value:
$scope.$on('valueChanged', function(event, args) {
console.log(args.value);
});
Trying to learn angular I got the following situation.
My application returns product objects from a complex and slow database using ASP.NET Web Api. Because of the slow speed i wanna page the objects returned from the server. I created a viewmodel containing an array of products, the current page and the last page of the products.
For displaying a paging widget I wanna fill my $scope object with an array of pagenumbers for use with the ng-repeat directive.
I tried filling an array using a function:
(in pseudo)
$scope.pages = function(){
var pageNumberArray = [];
pagecounter = 1;
while (pagecounter <= maxPage){
pageNumberArray.push(pageCounter);
pageCounter++;
}
};
with in my view an ng-repeat using this function:
<ul>
<li ng-repeat="pageNr in pages()">{{pageNr}}</li>
</ul>
It seems to work for displaying but my developer tools console shows errors which seem legit: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
How can I initialize the $scope array (field) without calling a $scope function from the view.
p.s. the array should reinitialize when an ajax request returns a new page (not all page numbers are shown (5 beneath, 5 above the current page).
I think the two-way binding is disturbed by using a function for your ng-repeat.
I would say $scope.pages should be a variable in your scope. If you later update this variable, your view will be automatically updated by the grace of two-way binding.
So something like this:
in controller:
$scope.pages = []
for (var i; i < maxPage; i += 1) {
$scope.pages.push(i);
}
$http.get(...).success(data){
// update $scope.pages
}
In view:
<div ng-repeat="page in pages">{{page}}</div>
I have a basic application in AngularJS. The model contains a number of items and associated tags of those items. What I'm trying to achieve is the ability to filter the items displayed so that only those with one or more active tags are displayed, however I'm not having a lot of luck with figuring out how to manipulate the model from the view.
The JS is available at http://jsfiddle.net/Qxbka/2 . This contains the state I have managed to reach so far, but I have two problems. First off, the directive attempts to call a method toggleTag() in the controller:
template: "<button class='btn' ng-repeat='datum in data' ng-click='toggleTag(datum.id)'>{{datum.name}}</button>"
but the method is not called. Second, I'm not sure how to alter the output section's ng-repeat so that it only shows items with one or more active tags.
Any pointers on what I'm doing wrong and how to get this working would be much appreciated.
Update
I updated the method in the directive to pass the data items directly, i.e.
template: "<button class='btn' ng-repeat='datum in data' ng-click='toggle(data, datum.id)'>{{datum.name}}</button>"
and also created a toggle() method in the directive. By doing this I can manipulate data and it is reflected in the state HTML, however I would appreciate any feedback as to if this is the correct way to do this (it doesn't feel quite right to me).
Still stuck on how to re-evaluate the output when a tag's value is updated.
You can use a filter (docs) on the ng-repeat:
<li ng-repeat="item in items | filter:tagfilter">...</li>
The argument to the filter expression can be many things, including a function on the scope that will get called once for each element in the array. If it returns true, the element will show up, if it returns false, it won't.
One way you could do this is to set up a selectedTags array on your scope, which you populate by watching the tags array:
$scope.$watch('tags', function() {
$scope.selectedTags = $scope.tags.reduce(function(selected, tag) {
if (tag._active) selected.push(tag.name);
return selected;
}, []);
}, true);
The extra true in there at the end makes angular compare the elements by equality vs reference (which we want, because we need it to watch the _active attribute on each tag.
Next you can set up a filter function:
$scope.tagfilter = function(item) {
// If no tags are selected, show all the items.
if ($scope.selectedTags.length === 0) return true;
return intersects($scope.selectedTags, item.tags);
}
With a quick and dirty helper function intersects that returns the intersection of two arrays:
function intersects(a, b) {
var i = 0, len = a.length, inboth = [];
for (i; i < len; i++) {
if (b.indexOf(a[i]) !== -1) inboth.push(a[i]);
}
return inboth.length > 0;
}
I forked your fiddle here to show this in action.
One small issue with the way you've gone about this is items have an array of tag "names" and not ids. So this example just works with arrays of tag names (I had to edit some of the initial data to make it consistent).
My case is that I want to use the createSearchChoice feature of the Select2 widget. So I found out I need to use an html input element instead of a select element and so I cannot use ng-repeat to populate the select2 widget. I find out there is a 'data' option and have been able to populate the select2 widget with static data, but not when I've tried to fill it dynamically.
What works:
html:
<input class='select2' ui-select2='sel2props' type='hidden'>
in the controller:
$scope.sel2props = {
createSearchChoice: ...
data: [
{ id: 0, text: 'yabba' }
etc
]
};
But if I try to set data to a variable which I can then set to whatever the database feeds me the widget isn't populated.
data: $scope.data;
function to retrieve data {
$scope.data = retrieved data;
}
the retrieved data is exactly in the way specified.
If i set up a button to append the data key it will work:
$scope.appenddata = function () {
$scope.data.push({id:1, text: 'anot'});
};
So I'm thinking it's a timing issue and I try $digest and $apply but they don't work in controllers. I tried to set up a directive and actually can do simple widgets, but not select2, so I was hoping not to go down that path, well that is to say I went down that path and drowned. If anyone could help out that would be great.
The solution is straight forward. Just push the elements onto the select2 data array rather than referencing another array.
function (result) {
$scope.lookupOptions.data.length = 0; // remove old items
angular.extend($scope.lookupOptions.data, result.data); // add new items
}
A trick I've recently made use of is to use Select2's query option to pass in your latest data on demand.
I've put together an example, wrapped in a custom directive. See this Plunk.