AngularJS + Rails $scope model is updated but not firing a re-render - javascript

Not sure if I'm using Angular correctly. I basically want to make a simple Link/Story sharing site.
var Story = $resource("api/stories/:id", {}, {
query: { method: "GET", isArray: false }
});
Story.query().$promise.then(function (data) {
$scope.stories = data.stories;
$scope.test = 1;
});
$scope.submitForm = function(formData) {
Story.save({}, formData, function(data) {
$scope.$apply(function() {
$scope.stories = data.stories;
$scope.test = 2;
});
});
}
Changes to $scope.test get reflected, but not stories.
Edit: Uhhh so apparently if I remove "ng-controller"
<ul ng-controller="storyController">
<li ng-repeat="story in stories">{{ story.title }}</li>
<p>{{ test }}</p>
</ul>
Everything works now..... I'm not even sure why....

In the query() method, you are setting $scope.stories to an Array however, it appears you are then attempting to set $scope.stories to, what I assuming, is a single story object. What exactly are you expecting to see when $scope.stories changes? A new story in the list of stories? If so, you need to do the following:
$scope.stories.push(data.stories);
You are not replacing the array, you are adding a new index to it.

Okay so apparently the answer was that I had already defined ng-view + a route that assigned a storyController to that view already. I was assigning it twice essentially by adding an ng-controller directive.

Related

angularjs ng-click not working on dynamic html elements

For some reason when using this function('testclickfn') as ng-click on dynamic elements, it doesn't invoke the function. Here is the angularjs file:
app.controller('testctrl',function($scope){
testfn($scope);
$scope.showelements = function(){
displayTestRows();
}
});
function testfn($scope){
$scope.testclickfn = function(){
alert('testing click fn');
};
}
function displayTestRows(){
for(var i=0; i < 5; i++){
$("#testdiv").append('<p ng-click="testclickfn()">click me</p><br>');
}
}
HTML page that calls angularjs controller 'testctrl':
<div id="testdiv" ng-controller="testctrl">
<button ng-click="showelements()">Show dynamic elements</button><br>
</div>
I'm assuming since the 'click me' tags are being generated after angular has loaded the page, it doesn't know of anything after page is generated so ng-click="testclickfn()" doesn't get registered with angularjs.
How do I get around this situation?
You're creating elements in a way angular has no idea about (pretty bad practice), but not to worry, you can let angular know!
Change the controller signature to
controller('testctrl', function($scope, $compile) {
Then run compile the new elements manually to get the ng-click directive activated
$scope.showelements = function(){
displayTestRows();
$compile($("#testdiv").contents())($scope);
}
If you cant tell, having to use jquery selectors inside your controller is bad, you should be using a directive and the link function to attach the element to the scope (ie, what if you have multiple testctrl elements?), but this'll get you running
As promised
The general rules are that no JS should be outside the angular functions, and that DOM manipulation, where appropriate should be handled by angular also.
Example 1: powerful
Have a look
<div ng-controller="ctrl">
<button ng-click="show('#here')">
create
</button>
<div id="here">
I'll create the clickables here.
</div>
</div>
use controllers for things that share stuff between a lot of different things
.controller('ctrl', ['$scope', '$compile', function($scope, $compile) {
$scope.sharedVariable = 'I am #';
$scope.show = function(where) {
where = $(where).html('');
//lets create a new directive, and even pass it a parameter!
for (var index = 0; index < 5; ++index)
$('<div>', {'test':index}).appendTo(where);
$compile(where.contents())($scope);
};
}])
use directives for non-unique elements that each have their own states
.directive('test', function() {
return {
//these too have their own controllers in case there are things they need to share with different things -inside them-
controller : ['$scope', function($scope) {
$scope.test = function() {
//see, no selectors, the scope already knows the element!
$scope.element.text(
//remember that parent controller? Just because we're in another one doesnt mean we lost the first!
$scope.$parent.sharedVariable +
$scope.index
);
}
}],
//no need to do things by hand, specify what each of these look like
template : '<p>click me</p>',
//the whole "angular way" thing. Basically no code should be outside angular functions.
//"how do I reference anything in the DOM, then?"; that's what the `link` is for: give the controller access using `scope`!
link : function(scope, element, attributes) {
//you can assign "ng-click" here, instead of putting it in the template
//not everything in angular has to be HTML
scope.element = $(element).click(scope.test);
//did you know you can accept parameters?
scope.index = Number.parseInt(attributes.test) + 1;
},
//just some set up, I'll let you look them up
replace : true,
restrict : 'A',
scope : {}
};
})
Example 2: Simple
But that is just a very generic and powerful way of doing things. It all depends on what you need to do. If this very simple example was indeed all you needed to do you can make a very simple, almost-all-html version:
<div ng-controller="ctrl">
<button ng-click="items = [1, 2, 3, 4, 5]">
create
</button>
<p ng-repeat="item in items" ng-click="test($event)">
<span>click me</span>
<span style="display:none">I am #{{item}}</span>
</p>
</div>
.controller('ctrl', ['$scope', function($scope) {
$scope.test = function($event) {
$($event.currentTarget).children().toggle();
};
}])
That's it, works the same almost

How can I watch a filtered collection in Angular.JS?

I'm trying to make an event fire whenever a filtered collection is changed. The filtered list is attached to the non-filtered list in ng-repeat.
<tr ng-repeat="item in $scope.filtered = (vm.list | filter:vm.searchText) | limitTo:vm.limit:vm.begin">
And here's my event I want to fire:
$scope.$watchCollection('filtered', function () {
alert($scope.filtered.length);
}, true);
It fires once when the page first loads, before my ajax call populates vm.list, so the alert says 0, but then it should fire again after vm.list gets populated, and every time a change to vm.searchText causes a change to $scope.filtered, but it's not.
I also tried making the $watchCollection method like this:
$scope.$watchCollection('filtered', function (newList, oldList) {
alert(newList.length);
});
But that had the same result.
I also tried doing as is suggested here, and it ended up like this:
<tr ng-repeat="item in catchData((vm.list | filter:vm.searchText)) | limitTo:vm.limit:vm.begin">
$scope.catchData = function (filteredData) {
alert(filteredData.length);
return filteredData;
}
That seemed like it fixed it at first. It now fired when the API call populated the list, and fired again whenever the searchText caused the filtered list to change. Unfortunately it made it so changing the begin option on the limitTo filter no longer worked. Changing the limit option still worked, but not the begin. Changing the begin does still work with the $watchCollection method.
Does anyone have any ideas?
When you create some variables in view, it added as property to current scope. So, in your case you create $scope.filtered, and this added to current scope.
To get it in watch, you just need use same declaration
$scope.$watchCollection('$scope.filtered', function () {
console.log($scope.$scope.filtered.length)
}
But better not use variable name like $scope, so as not to confuse them with angular variables.
so, you can change it ro simple: filtered
angular.module('app', [])
.controller('ctrl', function($scope) {
$scope.$watchCollection('$scope.filtered', function(nval) {
if(!nval) return; //nval - new value for watched variable
console.log('as $scope.filtered in view', $scope.$scope.filtered.length);
}, true);
$scope.$watchCollection('filtered', function(nval) {
if(!nval) return; //nval - new value for watched variable
console.log('as filtered in view', $scope.filtered.length);
}, true);
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<input type="text" data-ng-model="search" />
<h3>as $scope.filtered</h3>
<div ng-repeat="item in $scope.filtered = ([11,12,23]| filter:search)">item_{{item}} from {{$scope.filtered}}</div>
<h3>as filtered</h3>
<div ng-repeat="item in filtered = ([11,12,23]| filter:search)">item_{{item}} from {{filtered}}</div>
</div>
you will want to use a function to return the filtered list and set object equality to true.
$scope.$watch(function () {
return $scope.filtered;
}, function (newList) {
alert(newList.length);
}, true);

retrieve image url from object values - angular js

Very noob here to Angular JS.
I'm working on a project and I have a solution to this, but I was wondering if theres a better approach
thats a bit more "angular-esque" if you will.
Essentially for each videoid, it performs an ajax request locating that videoid's image
and places that into the src attribute.
In pure javascript this would be no problem, and I've figured out one solution but it sort of deviates away from the angular approach.
I was wondering if it's possible to do something sort of what I have here. (obviously this doesnt work yet)
Markup
<ul class="storysContainer">
<li video-on-click ng-repeat="video in videos" id="video{{$index}}" >
<img ng-src="{{getVideoThumb()}}">
</li>
</ul>
JS
$scope.videos = [
{
'videoid': '1122345'
},
{
'videoid': '1134567'
},
{
'videoid': '2234456'
}
];
$scope.getVideoThumb = function() {
$.get("http://blahblah.com/id=" + url,function(data){
var urlMain = data.channel.item["media-group"]["media-thumbnail"]["#attributes"].url;
array.push(urlMain);
});
return array;
}
Thanks
edit:
This is the solution that i came up with..not sure if it's necessarily the best angular-esque appraoch, but it works.
angular.forEach($scope.videos, function(data) {
var dataid = "http://blablah.com?id=" + data.videoid;
console.log(dataid);
$.get(dataid, function(img){
var urlMain = img.channel.item["media-group"]["media-thumbnail"]["#attributes"].url;
$('#thumb' + data.videoid).attr('src', urlMain);
//array.push(urlMain);
});
});
I would declare the URL for each object within the videos object array. That way you can just bind the value in your presentation layer.
So maybe something like
$scope.videos = [
{
'videoid': '1122345'
},
{
'videoid': '1134567'
},
{
'videoid': '2234456'
}
];
//modified this a little. I take it this call accepts the videoID as a parameter
//and returns the url for a single video?
//I wasn't sure what the 'array' variable you were using was.
//I am also using angular $http service to make this ajax call
$scope.getVideoThumb = function(videoID) {
$http.get("http://blahblah.com/id=" + videoID).success(function(data){
var urlMain = data.channel.item["media-group"]["media-thumbnail"]["#attributes"].url;
return urlMain;
});
}
//iterate through each object and assign it a 'URL' property to the result of 'getVideoThumb(videoID)'
for(var i = 0; i < $scope.videos.length; i++){
$scope.videos[i].URL = $scope.getVideoThumb($scope.videos[i].videoid);
}
and now in our presentation layer we can just bind the value of URL to the ng-src
<li video-on-click ng-repeat="video in videos" id="video{{$index}}" >
<img ng-src="{{video.URL}}">
</li>

Data from directive not displaying within ng-repeat

I have broken this problem down into it's simplest form. Basically I have a directive that, for the demo, doesn't yet really do anything. I have a div with the directive as an attribute. The values within the div, which come from an object array, are not displayed. If I remove the directive from the div, they are displayed OK. I am clearly missing something really obvious here as I have done this before without any problems.
Here's the Plunk: http://plnkr.co/edit/ZUXD4qW5hXvB7y9RG6sB?p=preview
Script:
app.controller('MainCtrl', function($scope) {
$scope.tooltips = [{"id":1,"warn":true},{"id":2,"warn":false},{"id":3,"warn":true},{"id":4,"warn":true}];
});
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
}
};
});
HTML
<div ng-repeat="tip in tooltips" class="titlecell" cm-tooltip="true">
A div element: {{ tip.id }}
</div>
<br><br>
Just to prove it works without the directive:
<div ng-repeat="tip in tooltips" class="titlecell">
A div element: {{ tip.id }}
</div>
There is a hack to make it working in earlier versions of angular by making use of transclusion, like that:
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
},
transclude: true,
template : '<div ng-transclude></div>'
};
});
PLNKR
As by Beyers' comment above and below, the behaviour the question is about no longer exists in at least 1.2.5
To be clearer; this has nothing to do with ng-repeat, you can remove it and there still will be no tip ( or tooltips ).
See this question on what the = and other configs mean and what it is doing for you.
Basically for your situation when you use = the scope of the directive will be used in the underlying elements, you no longer have your controller's scope. What this means for you is that there is no {{ tip.id }} or not even tip. Because the directive doesn't supply one.
Here's a plunker that demonstrates what you can do with it.
Basically all i did was
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
},
link: function($scope){ // <<
$scope.tip = { id: 1 }; // <<
} // <<
};
});
This creates the tip object on the scope so it has an id.
For your situation you would probably just not use = and look at this question for your other options depending on what you want.
In my opinion this isn't the way to go.
I would use Objects.
JS code:
function tooltip(id,warn){
this.id = id;
this.warn = warn;
}
tooltip.prototype.toString = function toolToString(){
return "I'm a tooltip, my id = "+this.id+" and my warn value = "+this.warn;
}
$scope.tooltips = [new tooltip(1,true),new tooltip(2,false),new tooltip(3,true),new tooltip(4,true)];
HTML:
<div ng-repeat="tip in tooltips" class="titlecell">
A div element: {{ tip.toString() }}
</div>

Deleting entry with Restangular

I am using Restangular in my AngularJS app. I have a table with a delete link for each item. I would like to delete the item and have the row automatically removed. But as things are it only deletes from DB. How can I refactor things so that it the DOM is updated automatically?
// The controller
angular.module('myApp').controller('ManageCtrl', function($scope, Restangular) {
$scope.delete = function(e) {
Restangular.one('product', e).remove();
};
Restangular.all('products').getList({}).then(function(data) {
$scope.products = data.products;
$scope.noOfPages = data.pages;
});
});
// The view
<li ng-repeat="product in products">
</li>
I would also love to find an example of this - even with Angular resource. All the admin/data table demos seem to work from static data.
According to Restangular https://github.com/mgonto/restangular#restangular-methods they mention that you should use the original item and run an action with it, so in your html code you should:
<li ng-repeat="product in products">
</li>
Then in your controller:
$scope.delete = function( product) {
product.remove().then(function() {
// edited: a better solution, suggested by Restangular themselves
// since previously _.without() could leave you with an empty non-restangular array
// see https://github.com/mgonto/restangular#removing-an-element-from-a-collection-keeping-the-collection-restangularized
var index = $scope.products.indexOf(product);
if (index > -1) $scope.products.splice(index, 1);
});
};
Notice they use the underscore.js without which will remove the element from the array. I guess that if they post that example in their readme page that means the .remove() function doesn't remove the original item from the collection. This makes sense, since not every item you remove you want removed from the collection itself.
Also, what happens if the DELETE $HTTP request fails? You don't want to remove the item then, and you have to make sure to handle that problem in your code.
In my case the above didn't quite work. I had to do the following:
$scope.changes = Restangular.all('changes').getList().$object;
$scope.destroy = function(change) {
Restangular.one("changes", change._id).remove().then(function() {
var index = $scope.changes.indexOf(change);
if (index > -1) $scope.changes.splice(index, 1);
});
};

Categories

Resources