Losing Controller Scope in ng-repeat - javascript

Well . . . a cursory look at my code should save me from having to explain that I'm brand new to Angular, I'm sure.
I'm building an app that allows search text from a user, queries a database when the value of the text input changes, then produces a list of matches. The back end is simple and is working. On the front, I've got the search field and the results container:
<div id="search" ng-controller="FormController">
<input type="text" class="form-control input-lg" placeholder="Start typing . . ." ng-keypress="search()" ng-model="searchField" id="search-field">
<div id="results" class="alert alert-success">
<a href="#" ng-href="#" ng-repeat="item in items" ng-click="add(item)">
<p class="lead text-left">
<span>{{item.DisplayName}} -</span>
<span> {{item.Description}} -</span>
<span> {{item.Count}} ct. -</span>
<span> ${{item.Price}}</span>
<span class="glyphicon glyphicon-check pull-right"></span>
</p>
</a>
<h4>{{noResults}}</h4>
</div>
</div>
The two methods being called in my controller:
$scope.search = function()
{
$scope.$watch('searchField', function (newValue)
{
if (newValue)
{
$http.post('/search',
{
val: newValue
})
.success(function (response)
{
if (response.length > 0)
{
$scope.items = response;
$scope.noResults = '';
}
else
{
$scope.noResults = 'No Matches Found';
$scope.items = '';
}
})
.error(function (response)
{
console.log('Oooops, error: ' + response);
});
}
});
};
$scope.add = function(item)
{
console.log('added');
};
$scope.search(), while probably a little messy, is working. But the add() method is not called on click. I'm guessing I'm simply not in the scope of the controller at that point, but after a LOT of searching around, I turn to you, stumped. I'm at the "banging-your-head-against-the-keyboard-and-hoping-for-it-to-magically-work" stage.
Is this an inheritance issue?
** Update **
Here is the entire controller (with the $watch removed as suggested in the comments):
var app = angular.module('AppModule', ['toastr']);
app.controller('FormController', ['$scope', '$http', 'toastr', function($scope, $http, toastr)
{
$scope.search = function()
{
var searchText = $scope.searchField;
$http.post('/search',
{
val: $scope.searchField
})
.success(function (response)
{
if (response.length > 0)
{
$scope.items = response;
$scope.noResults = '';
}
else
{
$scope.noResults = 'No Matches Found';
$scope.items = '';
}
})
.error(function (response)
{
console.log('Oooops, error: ' + response);
});
};
$scope.add = function(item)
{
console.log('added');
};
}]);
Update 2
Here is a plunker showing that everything is working up til the add() method (I may have renamed that method in this version). Of course, in place of my $http post, I've hard-coded a fake of the response that comes back from the server.

CSS issue. Comment out line, ugh, 8382 of your CSS (setting #results display to none). It'll work then. How you eventually resolve this in your CSS is a different issue.

Went through your plunker and i must say that the issue found is kinda silly.
At first, i put the code for removing the class has-value from resultsContainer in a timeout. That made the call for addItem working.
setTimeout(function() {
resultsContainer.removeClass('has-value');
}, 1000);
This may act as a solution, but setTimeout ?? Not happening.
Digging a little deep revealed that you are using display:none for #results. So i removed the display css and used opacity instead.
#results {
position: absolute;
/*display: none; */
opacity : 0;
top: 100%;
left: 0;
width: 100%;
}
#results.has-value {
display: block;
opacity : 1;
}
This got it working without the timeout. **Now i have myself faced issues where display:none screws the functionalities. So you either tweak your css or use a timeout instead. **
Also, consider moving that code in a directive.
Updated plunker here

Related

Why can`t I access scope via controller?

I am working on passion project. And I cant access scope to pass data to back-end frame work.
Here is my index file
<div id="main-menu" ng-controller="appCtrl">
//some other code
<div id="includedDocumentsFilter" style="float:right; display:none; padding-right: 10px;">
<my-documents validate-options="validateDialogOptions()" call-dialog="showDialog()"> </my-documents>
</div>
//some other code
</div>
My custom directive
'use strict';
dbApp
.directive('myDocuments', [
function () {
var documentTemplate =
' <div class="caption-row">' +
'<kendo-button style="width:62px" ng-click="changeDocument(true)"> Ok </kendo-button>'+
'<kendo-button style="width:62px" ng-click="changeDocument(false)" > Revert changes </kendo-button>'+
'</div>'
}
return {
scope: true,
template: documentTemplate
}
}]
)
My controller
$scope.changeDocument = function (applyFilter) {
if (applyFilter === true) {
//Here is where I cant access $scope
}
}
Firstly, I see a extra closing curly braces in your directive. Secondly in your html code there is display:none in div with id "includedDocumentsFilter". Just wondering if you are hiding the div, how will you be able to see the template defined in your directive. I have added a working jsfiddle link below using your above mentioned code
dbApp.directive('myDocuments', [
function () {
var documentTemplate =
' <div class="caption-row">' +
'<kendo-button style="width:62px" ng-click="changeDocument(true)"> Ok </kendo-button>'+
'<kendo-button style="width:62px" ng-click="changeDocument(false)" > Revert changes </kendo-button>'+
'</div>'
return {
scope: true,
template: documentTemplate
}
}]
)
JsFiddle link: https://jsfiddle.net/anilsarkar/gk2dfh1p/21/
Note: I have replaced kendo-button with span in jsfiddle

Why is angular $scope not removing old value?

I have the following controller
angular.module('publicApp')
.controller('URLSummaryCtrl', function ($scope, $location, Article, $rootScope, $timeout) {
$scope._url = "";
$scope._title = "";
$scope._article = "";
$scope._authors = "";
$scope._highlights = [];
$scope._docType = "";
$scope.summarizeURL = function(){
Article.getArticleInfo($scope.url, "").then(
function(data){
$scope._url = data.url;
$scope._title = data.title;
$scope._authors = data.authors.join(', ');
$scope._highlights = data.highlights;
$scope._docType = data.documentType;
if($scope._docType == 'html'){
$scope._article = data.article[0].article;
}
else{
$scope._article = data.article;
}
var _highlights = [];
$scope._highlights.forEach(function (obj) {
_highlights.push(obj.sentence);
});
// wait for article text to render, then highlight
$timeout(function () {
$('#article').highlight(_highlights, { element: 'em', className: 'highlighted' });
}, 200);
}
);
}
and the following view
<form role="form" ng-submit="summarizeURL()">
<div class="form-group">
<input id="url" ng-model="url" class="form-control" placeholder="Enter URL" required>
</div>
<button class="btn btn-success" type="submit">Summarize</button>
</form>
<div class="col-lg-8">
<h2>{{ _title }}</h2>
<p> <b>Source: </b> {{_url}}</p>
<p> <b>Author: </b> {{_authors}} </p>
<p> <b>Article: </b><p id="article">{{_article}}</p></p>
</div>
When I give a url in the text field initially and click Summarize it works as expected. But when I change the value in the text field and click the button again every thing is updated properly, with the new values, but the $scope._article gets the new value and doesn't remove the old value. It displays both the new and the old value that was there before.
Why is this happening?
EDIT #1: I added more code that I had. I found that when I remove the $timeout(function(){...}) part it works as expected. So now the question is, why is $scope._article keeping the old value and pre-pending the new value?
EDIT #2: I found that $timeout(...) is not the problem. If I change
$timeout(function () {
$('#article').highlight(_highlights, { element: 'em', className: 'highlighted' });
}, 200);
to
$('#article').highlight(_highlights, { element: 'em', className: 'highlighted' });
it still behaves the same way. So now I'm assuming it's because I'm changing the $scope._article to be something else? What's happening is that I'm displaying the $scope._article value and then modifying what's displayed to contain highlights <em class='highlighed'> ... </em> on what ever I want to highlight.
EDIT #3: I tried to remove the added html before making the request to get new data but that doesn't work either. Here's the code I tried.
angular.module('publicApp')
.controller('URLSummaryCtrl', function ($scope, $location, Article, $rootScope, $timeout) {
$scope._url = "";
$scope._title = "";
$scope._article = "";
$scope._authors = "";
$scope._highlights = [];
$scope._docType = "";
$scope.summarizeURL = function(){
//Remove added html before making call to get new data
$('.highlighted').contents().unwrap();
Article.getArticleInfo($scope.url, "").then(
function(data){ ... }
);
Jquery in angular controllers = headache.
The problem is probably here for you
$timeout(function () {
$('#article').highlight(_highlights, { element: 'em', className: }, 200);
#article.html() here, is going to give weird output, because angular has it's own sync system and the jquery library you're using has it's own way of working with the DOM. Throw in the fact that asynchronous javascript is already a pain if you're working with multiple things.
What you want instead is to set the html to the angular scope variable before you work with it in jquery so you know what the jquery is working with, i.e.:
$timeout(function () {
$('#article').html($scope._article);
$('#article').highlight(_highlights, { element: 'em', className: }, 200);

AngularJS directive - ng-class in ng- repeat should it be a $watcher to toggle style?

I am currently implementing a spike to further my understanding on angular directives etc.
The premise is to create a FX watch list on a number of currency pairs.
My data feed is set up for my price updates via socket.io.
The stumbling block that i have is being able to change the css dependent on price change ie up arrow for up, down arrow for down.
I feel a watcher function is what i need but struggled on where to start so was looking for some sort of expression in ng-class to do the job ... but the method not only started to look like a $watcher it was also flawed as saving the previous price to scope on my directive meant there was only ever one old value not one for each price.
There for my question is : Is the solution with ng-class or in setting up a $watcher function ?
Heres my code ...
HTML template
<div ng-repeat="rate in rates" ng-click="symbolSelected(rate)">
<div class="col-1-4">
{{rate.symbol}}
</div>
<div class="col-1-4">
<span ng-class='bullBear(rate.bidPoint)' ></span> {{rate.bidBig}}<span class="point">{{rate.bidPoint}}</span>
</div>
<div class="col-1-4">
<span ng-class='bullBear(rate.offerPoint)' ></span> {{rate.offerBig}}<span class="point">{{rate.offerPoint}}</span>
</div>
<div class="col-1-4">
{{rate.timeStamp | date : 'hh:mm:ss'}}
</div>
</div>
My directive currently looks like this ... as noted this will not work and the bullBear method was starting to look like a $watcher function.
.directive('fxmarketWatch', function(fxMarketWatchPriceService){
return {
restrict:'E',
replace:'true',
scope: { },
templateUrl:'common/directives/fxMarketWatch/marketwatch.tpl.html',
controller : function($scope, SYMBOL_SELECTED_EVT,fxMarketWatchPriceService){
$scope.symbolSelected = function(currency){
$scope.$emit(SYMBOL_SELECTED_EVT,currency);
}
$scope.bullBear = function(newPrice){
if ($scope.oldPrice> newPrice ){
return ['glyphicon glyphicon-arrow-down','priceDown'];
}
else if ($scope.oldPrice > newPrice ){
return ['glyphicon glyphicon-arrow-up','priceUp'];
}
}
$scope.$on('socket:fxPriceUpdate', function(event, data) {
$scope.rates = data.payload;
});
}
}
})
You could modify the ng-class and move the logic into the view, because styling and placing classes shouldn't be done in code.
<div class="col-1-4">
<span class="glyphicon" ng-class="{'glyphicon-arrow-up priceUp': oldPrice > rate.bidPoint, 'glyphicon-arrow-down priceDown':oldPrice > rate.bidPoint}"></span> {{rate.bidBig}}<span class="point">{{rate.bidPoint}}</span>
</div>
Or like this:
<span class="glyphicon {{oldPrice > rate.bidPoint ? 'glyphicon-arrow-down priceDown':'glyphicon-arrow-up priceUp'}}></span> {{rate.bidBig}}<span class="point">{{rate.bidPoint}}</span>
I will recommend you to use both ng-class and $watcher. The two can actually compliment each other:
UPDATE: To make the code works with ng-repeat, we need to migrate all of CSS classes logic to another controller:
app.controller('PriceController', function($scope) {
// we first start off as neither up or down
$scope.cssBid = 'glyphicon';
$scope.cssOffer = 'glyphicon';
var cssSetter = function(newVal, oldVal, varName) {
if (angular.isDefined(oldVal) && angular.isDefined(newVal)) {
if (oldVal > newVal) {
$scope[varName] = 'glyphicon glyphicon-arrow-down priceDown';
} else if (newVal > oldVal) {
$scope[varName] = 'glyphicon glyphicon-arrow-up priceUp';
} else {
$scope[varName] = 'glyphicon';
}
}
};
// watch for change in 'rate.bidPoint'
$scope.$watch('rate.bidPoint', function(newVal, oldVal) {
cssSetter(newVal, oldVal, 'cssBid');
});
// watch for change in 'rate.offerPoint'
$scope.$watch('rate.offerPoint', function(newVal, oldVal) {
cssSetter(newVal, oldVal, 'cssOffer');
});
});
Next, we bind this PriceController onto ng-repeat div. By doing so, Angular will create one controller instance for each rate in rates. So this time rate.bidPoint and rate.offerPoint should be available for $watch-ing:
<div ng-repeat="rate in rates" ng-click="symbolSelected(rate)" ng-controller="PriceController">
<div class="col-1-4">
<span ng-class='cssBid'></span> {{rate.bidBig}}<span class="point">{{rate.bidPoint}}</span>
</div>
<div class="col-1-4">
<span ng-class='cssOffer'></span> {{rate.offerBig}}<span class="point">{{rate.offerPoint}}</span>
</div>
</div>
Now, directive's controller will be much shorter than before:
controller: function($scope, SYMBOL_SELECTED_EVT, fxMarketWatchPriceService){
$scope.symbolSelected = function(currency) {
$scope.$emit(SYMBOL_SELECTED_EVT, currency);
}
$scope.$on('socket:fxPriceUpdate', function(event, data) {
$scope.rates = data.payload;
});
}

Rendering unicode-utf-8 turkish characters with angular-js

Characters from a binded JSON text, Turkish the letters are shown with wrong encoding for eg. Özlem Güzelharcan which shall look like "özlem güzelharcan". I added <meta characters="utf-8"> in the head still no solution and there was no problem with laravel blade views.
If necessary this is how I get and use data:
view:
<div class="comment" ng-hide="loading" ng-repeat="comment in comments">
Comment #{{ comment.id }} </h3> <p>{{comment.title}}</p>
{{comment.author_id}} / {{comment.author.name}}
Services:
// public/js/services/commentService.js
angular.module('commentService', [])
.factory('Comment', function($http) {
var data = {
// get all the comments
get : function() {
return $http.get('/api/comments/');
}
}
console.log(data);
return data;
});
//controller (shortly)
.controller('mainController', function($scope, $http, Comment) {
// object to hold all the data for the new comment form
$scope.commentData = {};
// loading variable to show the spinning loading icon
$scope.loading = true;
// get all the comments first and bind it to the $scope.comments object
// use the function we created in our service
// GET ALL COMMENTS ====================================================
Comment.get()
.success(function(data) {
$scope.comments = data;
$scope.loading = false;
});
});
Which method is used to clean characters with AngularJS?
Thanks
Eventually, after trying many things, I discovered that you have to use ng-bind-html or ng-bind-html-unsafe (with ngSanitize) to get the correct encoding. Here is how it works in my view:
Comment #<span ng-bind-template="{{comment.id}}"></span> </h3>
<span ng-bind-html="comment.title "></span>
<p><div ng-bind-html="comment.content | truncate:25"></div></p>

Populate jQuery UI accordion after AngularJS service call

I'm currently trying to build an AngularJS app where I'm using a jQuery UI accordion control.
The problem is, that the jQuery UI accordion is initiated before my AngularJS service is done loading data from the server. In other words: the accordion doesn't have any data when it's initiated and thus does not show when the data from AngularJS is populated.
The view looks like this:
<!-- Pretty standard accordion markup omitted -->
$("#b2b-line-accordion").togglepanels();
My AngularJS controller looks like this:
app.controller('orderController', function ($scope, orderService, userService) {
// Constructor for this controller
init();
function init() {
$scope.selected = {};
$scope.totalSum = 0.00;
$scope.shippingDate = "";
$scope.selectedShippingAddress = "";
$scope.orderComment = "";
$scope.agreements = false;
$scope.passwordResetSuccess = false;
$scope.passwordResetError = true;
userService.getCurrentUser(2).then(function (response) {
$scope.user = response.data;
orderService.getProductCategoriesWithProducts($scope.user).then(function (d) {
$scope.categories = d.data;
});
});
}
// Other methods omitted
});
And my AngularJS services looks like this:
app.service('orderService', function ($http) {
this.getProductCategoriesWithProducts = function (user) {
return $http.post('url to my service', user);
};
});
app.service('userService', function ($http) {
this.getCurrentUser = function(companyId) {
return $http.get('url to my service' + companyId + '.aspx');
};
this.resetPassword = function() {
return true;
};
});
Is there any way to tell the accordion to "wait" to initialise until the data is returned from the service? :-)
Thanks in advance!
Update
I tried chaining the methods and added some logging and it seems that the accordion is in fact initiated after the JSON is returned from the service.
userService.getCurrentUser(2).then(function(response) {
$scope.user = response.data;
}).then(function() {
orderService.getProductCategoriesWithProducts($scope.user).then(function(d) {
$scope.categories = d.data;
console.log("categories loaded");
}).then(function () {
$("#b2b-line-accordion").accordion();
console.log("accordion loaded");
});
});
However, it doesn't display the accordion :-( The first accordion div looks fine in the generated DOM:
<div id="b2b-line-accordion" class="ui-accordion ui-widget ui-helper-reset" role="tablist">
...
</div>
But the rest of the markup (which is databound with angular) itsn't initiated.
Complete markup:
<div id="b2b-line-accordion">
<div ng-repeat="productCategory in categories">
<h3>{{ productCategory.CategoryName }}</h3>
<div class="b2b-line-wrapper">
<table>
<tr>
<th>Betegnelse</th>
<th>Str.</th>
<th>Enhed</th>
<th>HF varenr.</th>
<th>Antal</th>
<th>Bemærkninger</th>
<th>Beløb</th>
</tr>
<tr ng-repeat="product in productCategory.Products">
<td>{{ product.ItemGroupName }}</td>
<td>{{ product.ItemAttribute }}</td>
<td>
<select ng-model="product.SelectedVariant"
ng-options="variant as variant.VariantUnit for variant in product.Variants"
ng-init="product.SelectedVariant = product.Variants[0]"
ng-change="calculateLinePrice(product); calculateTotalPrice();">
</select>
</td>
<td>{{ product.ItemNumber }}</td>
<td class="line-amount">
<span class="ensure-number-label" ng-show="product.IsNumOfSelectedItemsValid">Indtast venligst et tal</span>
<input type="number" class="line-amount" name="amount" min="0" ng-change="ensureNumber(product); calculateLinePrice(product); calculateTotalPrice();" ng-model="product.NumOfSelectedItems" value="{{ product.NumOfSelectedItems }}" />
<td>
<input type="text" name="line-comments" ng-model="product.UserComment" value="{{ product.UserComment }}" /></td>
<td><span class="line-sum">{{ product.LinePrice | currency:"" }}</span></td>
</tr>
</table>
</div>
</div>
</div>
SOLUTION
Finally I found a way around this! I'm not entirely sure if it's that pretty and if it's the Angular-way of doing stuff (I guess it isn't)
Made a directive with the following code:
app.directive('accordion', function () {
return {
restrict: 'A',
link: function ($scope, $element, attrs) {
$(document).ready(function () {
$scope.$watch('categories', function () {
if ($scope.categories != null) {
$element.accordion();
}
});
});
}
};
});
So basically when the DOM is ready and when the categories array changes (which it does when the data has been loaded), I'm initiating the jQuery UI accordion.
Thanks a lot t #Sgoldy for pointing me in the right direction here!
Yes you need a directive and you can handle this more angular way !
In HTML define the directive
<div ui-accordion="accordionData" ></div>
Return promise from your service and pass the promise to the directive.
In controller
$scope.accordionData = myService.getAccordionData();
The ui-accordion directive looks like
.directive('uiAccordion', function($timeout) {
return {
scope:{
myAccordionData: '=uiAccordion'
},
template: '<div ng-repeat="item in myData"><h3 ng-bind="item.title"></h3><div><p ng-bind="item.data"></p></div></div>',
link: function(scope, element) {
scope.myAccordionData.then(function(data) {
scope.myData = data;
generateAccordion();
});
var generateAccordion = function() {
$timeout(function() { //<--- used $timeout to make sure ng-repeat is REALLY finished
$(element).accordion({
header: "> div > h3"
});
});
}
}
}
})
When your service call succeed then you create your accordion. Here you can define your own accordion-template like
<div ng-repeat="item in myData">
<h3 ng-bind="item.title"></h3>
<div>
<p ng-bind="item.data"></p>
</div>
</div>
Template binds with your model data myData. I use ng-repeat inside the template to create accordion-header and accordion-body HTML.
In the generateAccordion method i use $timeout to make sure the ng-repeat is really finished rendering because $timeout will execute at the end of the current digest cycle.
Check the Demo
My best practice is to resolve your asynchronous services before controller is initiated.
As you can see in the document, http://docs.angularjs.org/api/ngRoute.$routeProvider
resolve - {Object.=} - An optional map of
dependencies which should be injected into the controller. If any of
these dependencies are promises, the router will wait for them all to
be resolved or one to be rejected before the controller is
instantiated. If all the promises are resolved successfully, the
values of the resolved promises are injected and $routeChangeSuccess
event is fired. If any of the promises are rejected the
$routeChangeError event is fired.
Your controller and view won't be even started before your service is resolved or rejected.
There is a good video tutorial about this, https://egghead.io/lessons/angularjs-resolve
In your case, you can config routes like the following
var myApp = angular.module('myApp', ['ngRoute']);
myApp.config(function($routeProvider) {
$routeProvider.when('/', {
templateUrl: 'main.html',
controller: orderController,
resolve: {
categories: function(orderService) {
return orderService.getProductCategoriesWithProducts();
},
user: function(userService) {
return userService.getCurrentUser();
}
}
});
Then, with your controller
app.controller('orderController', function($scope, categories, user) {
//categories and user is always here, so use it.
});
I have also found a similar question and answer here

Categories

Resources