I am using Angularjs 1.5.3 I have 2 services one service calls Area names, the other calls the details for the Area.
So in my code, I call the first service to get the Area, then I set the ng-init to call the details. This works fine, however angular keeps only the first value for all the rows.
Here is the code;
<tbody data-ng-repeat="area in vm.Areas" ng-init='vm.getDetails(area)'>
<tr>
<td class="text-bold">{{area}}</td>
<td>{{vm.AreaDetails.Employees}}</td>
<td>{{vm.AreaDetails.Hours}}</td>
<td>{{vm.AreaDetails.Sales}}</td>
</tr>
</tbody>
Any ideas on fixing this?
Thanks
You should avoid using ng-init for this. It's an abuse of ng-init and decrease your performance drastically. See: ngInit. Try to get your details before you start rendering eg (pseydo):
vm.areas = vm.areas.map(function(area) {
return area.details = service.getDetails(area);
}
#TJ answer is right on the technical part however i think you have a design problem in your code.
If you want to load area and their details you should load all of them in one go.
Instead you'll load them one by one there.
So let's say you have 10 Area and you're Detail service load data from (i suppose) the server : that makes 11 requests : 1 for all area, 10 for details of each area.
So just load all the whole thing in one call to your service (and presumably the server) and perform a simple ng-repeat.
You can simply have the controller iterate over the areas and call getDetails for each of them and append the detail to the respective area when they arrive.
The bindings will be along:
<tbody data-ng-repeat="area in vm.Areas">
<tr>
<td class="text-bold">{{area}}</td>
<td>{{area.details.Employees}}</td>
<td>{{area.details.Hours}}</td>
<td>{{area.details.Sales}}</td>
</tr>
</tbody>
The bindings will be updated when the data arrive.
Or you can use a directive with isolated scope, something like the following:
angular.module('yourModule').directive('areaInfo', function() {
return {
scope: {
area: '=areaInfo'
},
require: "yourController", // conroller where getDetails is defined
templateUrl: "area-info.html",
link: function(scope, element, attrs, ctrl) {
scope.areaDetails = ctrl.getDetails(scope.area);
}
}
});
<script type="text/ng-template" id="area-info.html">
<tr>
<td class="text-bold">{{area}}</td>
<td>{{areaDetails.Employees}}</td>
<td>{{areaDetails.Hours}}</td>
<td>{{areaDetails.Sales}}</td>
</tr>
</script>
<tbody data-ng-repeat="area in vm.Areas" area-info="area"></tbody>
You can even move the getDetails method to the directive itself.
Related
I have the following directive which works fine.
angular.module('myApp').directive('jqdatatable', function () {
return {
restrict: 'A',
link: function (scope, element, attrs, ngModelCtrl) {
var options = {};
if (scope.dtOptions) {
options = scope.dtOptions;
}
console.log('applying data table');
element.DataTable(options);
}
};
});
And I use this directive like this:
HTML:
<table jqdatatable>
<thead>
<tr>
<th>Col1</th>
<th>Col2</th>
<th>Col3</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Col1</th>
<th>Col2</th>
<th>Col3</th>
</tr>
</tfoot>
</table>
And javascript from controller for example:
$scope.dtOptions = {
'processing': true,
'serverSide': true,
'pageLength': 25,
'ajax': 'read_data_tables.php'
};
But the problem comes when you have multiple data tables in one view.
You cannot set multiple times $scope.dtOptions.
Seems this approach is not quite efficient in this situation.
If someone has an idea how could this code be integrated to work with multiple data tables in one view it will be great.
Thanks.
As your directive is not working with an isolated scope it is essentially working in the controller's scope, which makes it impossible to control 2 directives on the same page (or at least, within the same controller scope).
The way I see it you have 2 main options:
You can create a second controller to wrap your second directive (then that controller will have its own scope in which you can set your dtOptions)
You can change your directive to work with an isolated scope and get parameters passed on the declaration, so your directive would look like
<table jqdatatable processing="true"
serverSide="true" pageLength="25"
ajax="read_data_tables.php">
And the 2nd directive could have another set of parameters.
The 2nd option is by far more robust and idiomatic of Angular, there is pretty good documentation in the Angular reference. But the first option will get you going in a pinch (there is actually an example of it on the same reference page, although they point out that it is not a best practice)
Given this controller:
angular.module("upload.app").controller("upload",[upload]);
function upload(){
var me = this;
me.uploadList = [{Name: "Test Upload",
Id: 1,
NewFiles: []
}];
me.selectedUpload = me.uploadList[0];
me.setSelected = function(upload) {
me.selectedUpload = upload;
}
...
me.addFilesToUpload = function(element){
me.selectedUpload.NewFiles = element.files;
}
and this html:
<div ng-controller="upload as vm">
<input id="filechooser" type="file" multiple onchange="angular.element(this).scope().vm.addFilesToUpload(this)" />
<table>
<tbody>
<tr ng-repeat="up in vm.uploadList" ng-click="vm.setSelected(up)">
<td>{{up.Name}}<br />{{up.NewFiles.length}}</td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr ng-repeat="file in vm.selectedUpload.NewFiles">
<td>{{file.name}}</td>
</tr>
</tbody>
</table>
</div>
I would expect that when the input onchange event calls addFilesToUpload() and the files are then added to the NewFiles property, that Angularjs would automatically update the view ... in this case, {{up.NewFiles.length}} value in the first table and the second table that lists the files.
However, nothing is being updated until I click on my row in the first table which, as you can see, fires the setSelected function on my controller.
How can I get Angular to refresh when the NewFiles property is changed as well?
Sorry, just fixed the fiddle -- forgot to save it originally
See this jsfiddle. Begin by clicking on the Test Upload. Now select files. Nothing happens. Click again on Test Upload and you'll see all the bindings refreshed.
Use $apply (DEMO):
$scope.$apply(function() {
$scope.selectedUpload.NewFiles = element.files;
});
This is usually done by angular but because you are using the native js event onchange you have to wrap it in an $apply callback yourself.
There is no default binding provided by angular to input type=file https://github.com/angular/angular.js/issues/1375, so you'll probably need to create your own directive or you can use angular-file-upload library.
Check out this answer from stackoverflow.
The problem with your updated fiddle is mainly this line
$scope.selectedUpload = null;
The moment you have selected the files and invoke the callback addFilesToUpload(), and assign the selected files to $scope.selectedUpload.NewFiles = element.files; then you'll definitely get an error:
Uncaught TypeError: Cannot set property 'NewFiles' of null
Simply change it back to your original code: $scope.selectedUpload = $scope.uploadList[0];
The next problem would be to update the current selected upload list, simply use $scope.$apply(), because you are using a native event onchange to update the $scope. Your callback should look this:
$scope.addFilesToUpload = function(element){
$scope.$apply(function() {
$scope.selectedUpload.NewFiles = element.files;
});
}
Check this updated fiddle.
I am trying to use AngularJS in my application and have been successful to some extent.
I am able to fetch data and display it to the user. And I have a button in ng-repeat via which I want to post DELETE request. Below is my code which does it.
<div class="navbar-collapse collapse">
<table class="table table-striped" ng-controller="FetchViewData">
<tr>
<td>Name</td>
<td>ID</td>
<td>Department</td>
<td></td>
</tr>
<tr ng-repeat="d in viewData">
<td>{{d.EmployeeName}}</td>
<td>{{d.EmployeeID}}</td>
<td>{{d.EmployeeDepartment}}</td>
<td>
<button class="trashButton" type="button"
name="view:_id1:_id2:_id14:_id24:btnDelete"
id="view:_id1:_id2:_id14:_id24:btnDelete"
ng-click="deleteRecord('{{d['#link'].href}}')">
<img src="/trashicon.gif"></button>
</td>
</tr>
</table>
</div>
This is the FetchViewData function which fetches the information and displays it to the user.
function FetchViewData($scope, $http) {
var test_link = "<MY LINK>";
$http.get(test_link).success( function(data) {
$scope.viewData = data;
});
}
The data is fetched and properly displayed.
But the code in ng-click="deleteRecord('{{d['#link'].href}}')" does not fire when delete button is clicked. In Google Chrome's developer tools I can see valid values are generated for code {{d['#link'].href}} but the code deleteRecord does not get fired. From this question I tried removing the braces and writing only d['#link'].href but it didn't work for me.
When I replace ng-click with onclick the deleteRecord function gets fired.
function deleteRecord(docURL) {
console.log(docURL);
$http.delete(docURL);
}
But then I receive the below error.
Uncaught ReferenceError: $http is not defined
deleteRecord
onclick
I am using jQuery 1.10.2 and AngularJS v1.0.8.
FetchViewData here is a controller, and in your html, where you have ng-controller="FetchViewData", you are telling it to look within that controller's scope for any angular methods and variables.
That means, if you want to call a method on click, it needs to be calling something attached to your controller's scope.
function FetchViewData($scope, $http) {
var test_link = "<MY LINK>";
$http.get(test_link).success( function(data) {
$scope.viewData = data;
});
$scope.deleteRecord = function(docURL) {
console.log(docURL);
$http.delete(docURL);
}
}
Here, the function exists on the scope, and any html that is inside your FetchViewData Controller has access to that scope, and you should be able to call your methods.
It's working when you use on-click because your function exists in the global namespace, which is where on-click is going to look. Angular is very heavily reliant on scoping to keep your namespaces clean, there's lots of info here: https://github.com/angular/angular.js/wiki/Understanding-Scopes
INSTEAD of this
ng-click="deleteRecord('{{d['#link'].href}}')"
TRY this
ng-click="deleteRecord(d['#link'].href)"
You don't need to use curly brackets ({{}}) in the ng-click
ENJOY...
function deleteRecord(docURL) {
console.log(docURL);
$http.delete(docURL);
}
It should be
$scope.deleteRecord = function (docURL) {
console.log(docURL);
$http.delete(docURL);
}
EDIT:
change something in html and controller ....
SEE WORKING DEMO
The deleteRecord method should be assigned in the current and correct scope
$scope.deleteRecord = function(){
....
Another possibility for why ng-click does not fire, is that you are apply a CSS style of pointer-events:none; to the element. I discovered that Bootstrap's form-control-feedback class applies that style. So, even though it raises the z-index by 2 so that the element is in front for clicking, it disables mouse-clicks!
So be careful how your frameworks interact.
As mentioned, the function should be created inside the scope:
$scope.deleteRecord = function (docURL) {
console.log(docURL);
$http.delete(docURL);
}
To use the function, first drop the "{{ }}" since you are using it from inside an ng-repeat. Another issue is the use of apostrophe in your code, you have two pairs one inside the other... well I am sure you get the problem with that.
Use the function like so:
ng-click="deleteRecord(d['#link'].href)"
Best of luck !
If you want to use as a submit button the set the type to 'submit' as:
<button type="submit" ...
I'm trying do a small reusable component in AngularJS using directives.
I have made good progress but I have a problem with the validations. For example the required validation not working. I think is "binding" issue.
My HTML code: also in http://jsfiddle.net/pQwht/17/
<html ng-app="myApp">
<body>
<form ng-controller="Ctrl"
id="paymentCallForm"
action="#"
name="paymentCallForm">
<table>
<tr tdfield
labelname="Primary Account Number:"
fieldname="primaryAccountNumber"
title="Primary title"
>
</tr>
</table>
My directive script:
angular.module('myApp').directive('tdfield', function() {
return {
restrict: 'A',
replace:false,
transclude: false,
scope: { labelname: '#', fieldname: '#', title: '#'},
templateUrl:'element.html'
};
});
My element.html code:
<td id="lbl_paymentReference" class="formInputLabelWrapper">{{labelname}}</td>
<td class="formInputTextWrapper">
<input id="{{fieldname}}"
name="{{fieldname}}"
title="{{title}}"
class="large empty"
required>
<span data-ng-show="paymentCallForm.{{fieldname}}.$error.required"
class="error">Error</span></td>
Well, I solved this, but for what a price. There is a number of issues and angular related among them. I may not recall all, so here is the working example https://github.com/yaroslav-ulanovych/soq16245177.
When you define scope parameter like scope: { labelname: '#', fieldname: '#', title: '#'}, (with an object as a value), that creates an isolated scope, which means not inherited from parent one's. Therefore here ng-show="paymentCallForm.{{fieldname}}.$error.required" is no access to the form. As a workaround ng-show="$parent.paymentCallForm.{{fieldname}}.$error.required", but I didn't check whether your inputs are published in the form in case of the isolated scope. Or scope: true and inject attributes into the scope manually.
compile: function() {
return {
pre: function (scope, element, attr) {
scope.fieldname = attr.fieldname;
}
}
}
Note on using prelink function, so that it's called before children are linked.
Next ng-show will actually use not interpolated expression and there is obviously no property named {{fieldname}} in the form. That is fixed in later versions of Angular, don't know when exactly, cause I'm using master.
But what is not fixed is ngModelController. It gets it's name very early so publishes itself on the form under wrong one. I had to fix that myself, good that there is only one line to do that, in file src/ng/directive/input.js.
// add
modelCtrl.$name = attr.name;
// before this
formCtrl.$addControl(modelCtrl);
I believe you need a controller attached to your view. The form object will be attached to property with the id of the form on $scope object of this controller. Once you add that, I think it will start showing up.
I'm trying to specify an entrance effect on elements being inserted using a knockoutjs foreach binding. Very simple setup:
myViewModel.myObservableArray.push({enter:function() { ... });
and in the markup:
foreach:{data:myObservableArray, afterRender:enter}
seems like it should work... right? But it doesn't find the enter function on the item. What I've found does work is:
myViewModel.enter = function(something, item) { item.enter(); };
foreach:{data:myObservableArray, afterRender:$root.enter}
adding an enter function to the root view model and binding afterRender to $root.enter. Enter is then passed the item as its second param so can in turn call the item's enter function, but it feels like a hack.
Can anyone explain what's going on here?
Thanks.
EDIT:
To clarify I've created a fiddle.
What this does is very simple, and is covered in more depth in the animated transitions example. It's running a function in the root view model for each dom element that's inserted using the foreach binding.
So the question is: what if I want item specific afterRender, afterAdd or beforeRemove functions? I could see this being useful. Especially if using the template binding to dynamically select a template (note 4). Is there a clean way of doing this? Right now I've got an enter function in the view model's root that simply calls the enter function on the item, but like I said above this feels like a hack.
Nope, this is the way it was designed.
From the Documenation:
Note 3: Using “afterRender”, “afterAdd”, and “beforeRemove”
Sometimes you might want to run custom post-processing logic on the DOM elements generated by your templates. For example, if you’re using a JavaScript widgets library such as jQuery UI, you might want to intercept your templates’ output so that you can run jQuery UI commands on it to transform some of the rendered elements into date pickers, sliders, or anything else.
Generally, the best way to perform such post-processing on DOM elements is to write a custom binding, but if you really just want to access the raw DOM elements emitted by a template, you can use afterRender.
Pass a function reference (either a function literal, or give the name of a function on your view model), and Knockout will invoke it immediately after rendering or re-rendering your template.
(Emphasis mine)
As it says, a custom binding is another way to do it, and may be better depending on what that enter() function does.
underscore debounce (_.debounce) is a great solution in such case.
template
data-bind=" template: {foreach:myObservableArray, afterRender: runWhenAllRenderDone }
debounce function will be executed if afterRender is not fired in last 100 milisecond.
var runWhenAllRenderDone = _.debounce(myFunction, 100);
function myFunction(){
//do some task but do it for once only
}
is't it awesome?
Found another workaround without timeout, this technique is based on virtual element <!-- ko if: $parent.afterRender($index()) --><!-- /ko -->
function ReservationsViewModel() {
// Editable data
this.seats = ko.observableArray([
{ name: "Steve", meal: "Standard (sandwich)", price: 343},
{ name: "Steve", meal: "Premium (lobster)", price: 10},
{ name: "Steve", meal: "Ultimate (whole zebra)", price: 290}
]);
this.afterRender = (i) => {
// checking element rendered is last
if (i === this.seats().length - 1) {
console.log('rendered');
// our after rendered logic goes here...
}
};
}
And it's template is
<tbody data-bind="foreach: seats">
<tr>
<td data-bind="text: name"></td>
<td data-bind="text: meal"></td>
<td data-bind="text: price"></td>
</tr>
<!-- ko if: $parent.afterRender($index()) --><!-- /ko -->
</tbody>
This extra logic i === this.seats().length - 1, will check last row is rendered.. then we can execute our afterRender logic inside.