I'm an experienced C# and MVC programmer, but just starting out with AngularJS. I've been using ngTable (trying to anyway) with limited success, but one issue I cannot resolve - no matter what I do, I cannot get a particular table to refresh when the data has changed.
I'm getting data from a Web API via a factory - it's a list of suppliers associated with a particular Product. The first time the screen is loaded, the data is brought back and the table displays fine - any subsequent call, the data is returned, but the table is not updating. Other areas of the page are updating as expected.
I've done some console logging, and can see that the data is coming back. I've tried the tableParams.reload() function, and setting the tableParams.total() but nothing seems to trigger the table to refresh.
This is my function in the Controller:
function getStockSupplier() {
console.log("getStockSupplier()");
$scope.stockSupplierTableParams = {};
stockSupplier.getAll({ url: "localhost:52457", sku: $scope.model.sku })
.$promise.then(function (response) {
$scope.stockSupplier = response;
$scope.stockSupplierTableParams = new NgTableParams({
}, {
dataset: response
});
console.log("Got result");
console.log("Length: " + $scope.stockSupplierTableParams.settings().dataset.length);
$scope.stockSupplierTableParams.reload();
}, function (response) {
alert('no stock item');
$scope.stockSupplier = null;
});
}
And this is my HTML code:
<div ng-controller="stockController">
<div>
<table ng-table="stockSupplierTableParams" class="table table-condensed table-bordered table-striped">
<tr ng-repeat="issue in $data">
<td data-title="'Supplier1'">
{{issue.SupplierName}}
</td>
<td data-title="'On Order'">
{{issue.OnOrder}}
</td>
</tr>
</table>
</div>
</div>
I'm at a complete loss - it may be something fundamental I'm missing, but it's driving me crazy, so any help great appreciated.
EDIT: Here's the code for the Web API Service call, in case that has any relevance. Also, I should point out that the HTML above is used in a Directive, if that makes any difference.
var myService = angular.module('myService', ['ngResource']);
myService.factory('stockSupplier', [
'$resource',
function ($resource) {
return $resource('http://:url/api/StockInfo?Sku=:sku&StockSupplier=true',
{
url: '#url',
sku: '#sku'
},
{
getAll: {
method: 'GET',
isArray: true
},
});
}
]);
I have a simple solution, you can re-render table when data loaded:
HTML
<div ng-controller="stockController">
<div data-ng-if="tableIsReady">
<table ng-table="stockSupplierTableParams" class="table table-condensed table-bordered table-striped">
<tr ng-repeat="issue in $data">
<td data-title="'Supplier1'">
{{issue.SupplierName}}
</td>
<td data-title="'On Order'">
{{issue.OnOrder}}
</td>
</tr>
</table>
</div>
<div data-ng-if="!tableIsReady">
// you can put loader here
</div>
</div>
JS
function getStockSupplier() {
console.log("getStockSupplier()");
$scope.stockSupplierTableParams = {};
$scope.tableIsReady = false; // here we hide table, and show loader
stockSupplier.getAll({ url: "localhost:52457", sku: $scope.model.sku })
.$promise.then(function (response) {
$scope.stockSupplier = response;
$scope.stockSupplierTableParams = new NgTableParams({
}, {
dataset: response
});
console.log("Got result");
console.log("Length: " + $scope.stockSupplierTableParams.settings().dataset.length);
$scope.stockSupplierTableParams.reload();
$scope.tableIsReady = true; // here we show table again
$scope.$apply() // start digest
}, function (response) {
alert('no stock item');
$scope.stockSupplier = null;
});
}
Otherwise, you code is fine, you just may be forget put $scope.$apply() after $scope.stockSupplierTableParams.reload();
I got it working! Though I'm not 100% sure how, so if any experienced Angular developers could explain it, I would be grateful. There are two parts to it.
Basically - instead of setting the dataset to the response object of the API call, I set it to the variable $scope.stockSuppler. Then, I explicitly empty this variable before the update - code below:
function getStockSupplier() {
console.log("getStockSupplier()");
$scope.tableIsReady = false;
$scope.stockSupplierTableParams = {};
$scope.stockSupplier = [];
stockSupplier.getAll({ url: "localhost:52457", sku: $scope.model.sku })
.$promise.then(function (response) {
$scope.stockSupplier = response;
$scope.stockSupplierTableParams = new NgTableParams({
}, {
dataset: $scope.stockSupplier
});
console.log("Got result");
console.log("Length: " + $scope.stockSupplierTableParams.settings().dataset.length);
$scope.stockSupplierTableParams.reload();
$scope.tableIsReady = true;
}, function (response) {
alert('no stock item');
$scope.stockSupplier = null;
});
}
Also, I removed the ngController="stockController" from the directive HTML code - the directive is called in a div which already defined the controller. Having the controller defined twice appears to have been confusing it as well. I should emphasis that only be making both of these changes, I got to to work. One or the other only, it still doesn't work correctly.
The table is now updating as expected with the other parts on the page.
I'm not sure why using the scope variable rather than the response from the API makes a difference, but it's done the trick.
Related
I am able to get the aggreate values from server to client, but could not display it on the template. Am i missing something here.Appreciate your help.Iam a newbie in meteor.
//client side javascript
Template.DashboardCategoriesChart.helpers({
'CategoryAggregateItem':function(){
var res;
Meteor.call("getAggregateCategoriesSum",function(errors,results){
console.log("results value: "+ JSON.stringify(results))
return results ;
};
};
});
//stringfy value returned
results value: [
{"_id":"Household","totalAmount":420},
{"_id":"Insurance","totalAmount":235},
{"_id":"Family","totalAmount":1358},
{"_id":"Utilities","totalAmount":5371.5},
{"_id":"Automobile","totalAmount":500},
{"_id":"Home Office","totalAmount":290},
{"_id":"Travel","totalAmount":14},
{"_id":"Food","totalAmount":303}
]
//Template
{{#each CategoryAggregateItem}}
<tr>
<td>{{_id}}</td><td>{{totalAmount}}</td>
</tr>
{{/each}}
The following code worked, thanks a ton JeremyK for leading to right direction.
Template.DashboardCategoriesChart.helpers({
'CategoryAggregateItem':function(){
return Template.instance().CategoryAggregateItem.get(); //this.CategoryAggregateItem;
}
});
Template.DashboardCategoriesChart.onCreated(function () {
var instance = this;
this.CategoryAggregateItem = new ReactiveVar(null);
Meteor.call("getAggregateCategoriesSum",function(errors,results){
console.log("results value: "+ JSON.stringify(results))
instance.CategoryAggregateItem.set(results);
})
})
Try changing your client side javascript to this:
Template.DashboardCategoriesChart.onCreated(function () {
_this = this;
_this.CategoryAggregateItem = new ReactiveVar(null);
Meteor.call("getAggregateCategoriesSum",function(errors,results){
console.log("results value: "+ JSON.stringify(results))
_this.CategoryAggregateItem.set(results);
}
});
Template.DashboardCategoriesChart.helpers({
'CategoryAggregateItem':function(){
return Template.instance().CategoryAggregateItem.get();
});
When the Callback from Meteor.Call is triggered, it will update the ReactiveVar. This causes the template to rerender and display the retrieved data.
You may also want to provide alternative markup in your template for when the helper returns null, before the data is received by the client.
I am working on an app I had deployed, and trying to get everything up to the latest version and update the code to take advantage of the latest processes, like subscribing in the Template.onRendered, but I have seemingly broken my sortable.
My template (simplified somewhat)
<template name="formEdit">
<div id="formContainer">
{{#if Template.subscriptionsReady}}
{{#with form this}}
<table id="headerFieldsTable" class="table">
<tbody id="headerFields">
{{#each headerFields}}
<tr class="headerFieldRow">
<td>{{> headerFieldViewRow }}</td>
</tr>
{{/each}}
</tbody>
</table>
<h5>Form Fields</h5>
<table id="formFieldsTable" class="table">
<tbody id="formFields">
{{#each formFields}}
<tr class="formFieldRow">
<td>{{> formFieldViewRow }}</td>
</tr>
{{/each}}
</tbody>
</table>
{{/with}}
{{else}}
Loading...
{{/if}}
</div>
</template>
And the template's onRendered():
Template.formEdit.onRendered(function() {
var formsSubscription = this.subscribe('formById', this.data);
var headerFieldsSubscription = this.subscribe('headerFieldsForForm', this.data);
var formFieldsSubscription = this.subscribe('formFieldsForForm', this.data);
var formEditTemplate = this;
this.autorun(function() {
if (formsSubscription.ready() && headerFieldsSubscription.ready() && formFieldsSubscription.ready()) {
formEditTemplate.$(''));
formEditTemplate.$('#headerFields').sortable({
axis: "y",
stop: function(event, ui) {
var headersToSave = [];
$('#headerFieldsTable div.headerField').each(function(idx, headerFieldDiv) {
var header = Blaze.getData(headerFieldDiv);
header.sequence = idx;
headersToSave.push(header);
});
_.each(headersToSave, function(header) { header.save(); });
}
});
formEditTemplate.$('#formFields').sortable({
axis: "y",
stop: function(event, ui) {
var feildsToSave = [];
$('#formFieldsTable div.formField').each(function(idx, formFieldDiv) {
var field = Blaze.getData(formFieldDiv);
field.sequence = idx;
feildsToSave.push(field);
});
_.each(feildsToSave, function(field) { field.save(); });
}
});
}
});
});
But for both the headers and footers, the formEditTemplate.$('#headerFields') and formEditTemplate.$('#formFields') both seem to return no results. It seems like the DOM is not actually present. I thought the .ready() call on all the subscriptions would correct that, but think there is a timing issue where Blaze hasn't fixed up the DOM yet, but the subscriptions are indeed done. I say this because I put a breakpoint in Chrome right at the first line of the if, and the browser was still showing "Loading...".
I also attempted to hot-wire things by having a helper that setup the sortable placed at the end of the {{#with}} block, hoping that maybe it would be rendered last, but that didn't work either.
I found some articles on the Meteor forums that seemed to suggest adding a timer, but this seems very "hackish". Is there a new pattern for running JS that requires the DOM to be fully initialized?
Instead of the time delay hack, I recommend you use Tracker.afterFlush() to guarantee that the DOM has been created and updated. Here is a description from Meteor docs:
Schedules a function to be called during the next flush, or later in
the current flush if one is in progress, after all invalidated
computations have been rerun. The function will be run once and not on
subsequent flushes unless afterFlush is called again.
So inside of your if statement, you can wrap the code block like so
if (formsSubscription.ready() && headerFieldsSubscription.ready() && formFieldsSubscription.ready()) {
Tracker.afterFlush( function () {
//Code block to be executed after subscriptions ready AND DOM updated
});
}
Here is a reference with examples using Tracker.afterFlush.
This question will be similar to this one I asked earlier. I got it working, copied solution, but there must be something Im missing here. I start with the code:
router.js:
this.route('note',{
path: '/note/:_id',
data: function() { return Notes.findOne(this.params._id); },
});
this.route('notes', {
waitOn: function() { return Meteor.subscribe('notes')}
});
template 'notes' :
<table id="notes-table">
{{#each notes}}
<tr id="table-row">
<td id="indicator"></td>
<td id="remove-note" class="icon-close notes-table-class"></td>
<td id="notes-title" class="Nbody notes-table-class">{{this.title}}</td>
<td id="notes-body-prev" class="Nbody notes-table-class">{{this.body}}</td>
</tr>
{{/each}}
</table>
helpers.js :
Template.notes.events({
'click .Nbody': function(events,template){
console.log('displaying note : ' + this._id);
Router.go('/note/'+this._id);
}
});
Template 'note' is simple {{title}} and {{body}}
The problem is, when I click on the note table body it does take me where it should be, which is single note, but its text just flashes for a second and disappear immediately and never comes back, so I see nothing..
Question: What is the problem?
I do not get any error in the console.
The Differences between this and 'memo' solutions are:
- here im using table instead of span's
- I dropped wrapping clickable body in 's tags ( I think this might be the reason )
You have to subscribe in 'note' route to be able to retrieve it:
client:
this.route('note',{
path: '/note/:_id',
waitOn: function() { return Meteor.subscribe('note',this.params._id )}
data: function() { return Notes.findOne(this.params._id); },
});
this.route('notes', {
waitOn: function() { return Meteor.subscribe('notes')}
});
server:
Meteor.publish('note', function(noteId){
return Notes.find(this.params._id);
})
In comment you wrote that it started to work when you : moved waitOn to Router.configure. Route.configure waitOn is working for all routes and because Method.publish('notes') function probably returns Notes.find() then note starts to work correct.
I have something like this:
<template name ="products">
<br />
<h2>Products</h2>
<table>
<tr>
<td>Name</td>
<td>Price</td>
</tr>
{{#each products.items}}
<tr>
<td>{{name}}</td>
<td>{{price}}</td>
</tr>
{{/each}}
<tr>
<td>Total:</td><td>{{products.totalPrice}}</td>
</tr>
</table>
</template>
Template.products.helpers({
products: function () {
try {
var user = Session.get("user");
return JSON.parse(localStorage[user]); //this return*
} catch (e) {
}
}
});
*this returns something like this {totalPrice: 30, items:[{"productId1","name1","10"},{"productId2","name2","20"}]}
The question is: I need to have this info stored in localStorage and not in a Meteor.Collection as i dont want to go to the server until moment X (Doesn't matter really). But I cant make this thing auto update whenever I change localStorage value. Is there any way to do this?
Thanks in advance.
That's what Dependencies are for. Simplest example:
var array = [];
var arrayDep = new Deps.Dependency();
Template.name.helper = function() {
arrayDep.depend();
return array;
};
var change = function() {
// do things to Array contents
arrayDep.changed();
};
Store the info in the Session as it is reactive so your template will change every time the value in the Session changes.
You could also use the browser-store package that seems to make localstorage reactive.
I using the following function to delete a row in a table
//delete individual row
jQuery('.stdtable img.delete').click(function(){
var c = confirm('Continue delete?');
if(c) jQuery(this).parents('tr').fadeOut(function(){
jQuery(this).remove();
});
return false;
});
This code is in a separate js file and is common to all pages.
Now I would like to add an Ajax action that deletes the row from the database. But depending on which page I'm on, it must call different controller.
Example:
Product page must call delete in ProductController
ProductGroup page must call delete in ProductGroupController
How to handle this?
If you need some method of defining which controller to call, you could put a data attribute on the table. Something like this:
<table class="stdtable" data-remove-url="#Url.Action("DeleteRow", "MyController")">
<tr data-id="1">
AAA
<img class="delete" src="foo.jpg" />
</tr>
<tr data-id="2">
BBB
<img class="delete" src="foo.jpg" />
</tr>
</table>
Then in your jQuery you can get this value as the url parameter of your request, along with the id to delete.
jQuery('.stdtable img.delete').click(function(e) {
e.preventDefault();
if (confirm('Continue delete?')) {
var $el = $(this);
var $tr = $el.closest('tr');
var url = $el.closest('table').data('remove-url');
var id = $tr.data('id');
$tr.fadeOut(function() {
$el.remove();
$.post(url, { rowId = id }); // do the delete on the server
});
}
});
You could add a custom attribute to your table or row with contains the url off the controller you need to call. In your method you can read this custom attribute to get the url of your controller.Look here for the JQuery attr methode.
If I understand your question right, you want to click the image with class delete to make a call to delete that row in the database. The simplest solution would be
<img data-rowid="1" src="something.jpg"/>
jQuery('.stdtable img.delete').click(function(e){
e.preventDefault();
var c = confirm('Continue delete?');
var that = this;
if(c) {
var id = this.data("rowid");
jQuery.ajax({ url : "/resource/"+id, method : "DELETE"
statusCode: {
200: function() {
jQuery(that).parents('tr').fadeOut(function(){
jQuery(that).remove();
});
}
}});
}
return false;
});