Conflicts when working with scopes and controllers in AngularJS - javascript

I have a simple website that uses AngularJS with a NodeJS backend.
It has multiple pages, like a homepage, a login/register page, etc.
I'd like to implement a "Chat" page where you could send messages to other clients using socket.io. I already got that part working, using a local controller (by local, I mean active on a single page - the Chat page).
The problem is, I would like the chat system to be global (i.e. client can receive messages while being on the homepage, but they'll still only be displayed when going back on the Chat page).
I'm having an issue when setting the Chat controller global (active on all pages).
Here's how I'm including it:
<body ng-controller="AppCtrl"> <!-- include main controller -->
<div ng-include="'header.tpl.html'"></div>
<div ng-controller="ChatCtrl" class="page"> <!-- include global Chat controller -->
<div ng-view class="container"></div>
</div>
<div ng-include="'footer.tpl.html'"></div>
<!-- ...etc. -->
</body>
This works pretty well, but it seems like I can't access a value from my Chat page, though. Functions declared from the Chat controller can still be called, but the "$scope.message" value (which contains the message that's being typed) is always empty.
Here's my Chat controller (which is actually called TravelCtrl)
angular.module('base').controller('TravelCtrl', //['$scope', 'security',
function($rootScope, $scope, security, NgMap, $geolocation, socket){
$scope.messages = [];
// Socket listeners
// ================
socket.on('init', function (data) {
$scope.name = data.name;
$scope.users = data.users;
});
socket.on('send:message', function (message) {
$scope.messages.push(message);
});
socket.on('change:name', function (data) {
changeName(data.oldName, data.newName);
});
socket.on('user:join', function (data) {
$scope.messages.push({
user: 'Server',
text: 'User ' + data.name + ' has joined.'
});
$scope.users.push(data.name);
});
// add a message to the conversation when a user disconnects or leaves the room
socket.on('user:left', function (data) {
$scope.messages.push({
user: 'chatroom',
text: 'User ' + data.name + ' has left.'
});
var i, user;
for (i = 0; i < $scope.users.length; i++) {
user = $scope.users[i];
if (user === data.name) {
$scope.users.splice(i, 1);
break;
}
}
});
// Private helpers
// ===============
var changeName = function (oldName, newName) {
// rename user in list of users
var i;
for (i = 0; i < $scope.users.length; i++) {
if ($scope.users[i] === oldName) {
$scope.users[i] = newName;
}
}
$scope.messages.push({
user: 'Server',
text: 'User ' + oldName + ' has been authenticated as ' + newName + '.'
});
}
// Methods published to the scope
// ==============================
$scope.changeName = function () {
socket.emit('change:name', {
name: $scope.newName
}, function (result) {
if (!result) {
alert('There was an error changing your name');
} else {
changeName($scope.name, $scope.newName);
$scope.name = $scope.newName;
$scope.newName = '';
}
});
};
$scope.sendMessage = function () {
socket.emit('send:message', {
message: $scope.message
});
// add the message to our model locally
$scope.messages.push({
user: $scope.name,
text: $scope.message
});
// clear message box
$scope.message = '';
};
// ================
var init = function () {
$scope.newName = security.currentUser.username;
$scope.changeName();
}
if ($rootScope.hasLoaded() && $scope.name != security.currentUser.username) {
init();
} else {
$rootScope.$on('info-loaded', init);
}
}
//]
);
As well as the Chat page itself. The strange thing is that connected users and messages display correctly, but the controller can't seem to retrieve the typed message.
<div class='col'>
<h3>Users</h3>
<div class='overflowable'>
<p ng-repeat='user in users'>{{user}}</p>
</div>
</div>
<div class='col'>
<h3>Messages</h3>
<div class='overflowable'>
<p ng-repeat='message in messages' ng-class='{alert: message.user == "chatroom"}'>{{message.user}}: {{message.text}}</p>
</div>
</div>
<div class='clr'>
<form ng-submit='sendMessage()'>
Message: {{message}}<br/>
<input size='60', ng-model='message'/>
<input type='submit', value='Send as {{name}}'/>
</form>
</div>
When pressing the "Send" button, AngularJS successfully calls the sendMessage function, but retrieves the "message" value as an empty string, leading it to send an empty socket.io message.
I'm quite new to AngularJS, so my approach might be totally ridiculous. I'm convinced I'm missing something obvious but after re-reading the docs again, I really can't seem to find what.
Is this a proper way to organise an AngularJS app?
Thanks in advance for your help.

Having recently built a large scale Angular/Socket.IO application, I strongly suggest that you put all of your Socket implementation into a Service. This service will maintain all of your socket state, and allow you to inject it into any required controllers. This will allow you to have a main page for Chat, however still be able to display notifications, chat user information, etc in other areas of your application.

It's not about your problem, but I saw something I suspect to be wrong.
When you use another library with angularjs, you should use a bridge to it (angular-socket-io for example).
When you do an $http call with angular, it updates $scope correctly in the callback and the changes are seen in the view.
In your code:
socket.on('send:message', function (message) {
$scope.messages.push(message);
});
There is a problem: "socket" isn't a library included in angularjs, so when the callback is called, your "$scope" modification isn't correctly noticed to angularjs.
You have to do use $scope.$apply(function() { code here which modifies $scope });
Example:
socket.on('send:message', function (message) {
$scope.$apply(function() {
$scope.messages.push(message);
});
});
EDIT:
I would like the chat system to be global (i.e. client can receive messages while being on the homepage, but they'll still only be displayed when going back on the Chat page).
Either store the datas in a global variable, or use $rootScope which is the parent scope of all the $scope you use in the application.
EDIT 2:
In fact it should solve your problem ;)
Another things:
1) use $rootScope instead of $scope for global variables (or a global variable). In any $scope you will access $rootScope variables ($scope is a copy of either $rooScope or a parent $scope).
2) register socket.io only once. Currently, if you change pages, you will register new callbacks at EACH page change.

Related

ng-repeat does not update the html

I am new to Angular and need your help on an issue with the ng-repeat of my app.
Issue:
I have an html page (event.html) and in the corresponding controller of the file, I make a request to a firebase collection and update an array ($scope.events). The issue is that the data from firebase takes a few seconds to load and by the time data arrives to $scope.events, ng-repeat has already been executed and it displays an empty screen. The items are displayed correctly the moment I hit on a button in the HTML page (event.html).
Sequence of events:
I have a login page (login.html) where I enter a user name and phone number and I click on the register button. I've configured this click on the register button to go to the new state (event.html).
Here is the controller code for login.html:
$scope.register = function (user) {
$scope.user = user.name;
$scope.phonenumber = user.phonenumber;
var myuser = users.child($scope.user);
myuser.set({
phone : $scope.phonenumber,
Eventid : " ",
name : $scope.user
})
var userid = myuser.key();
console.log('id is ' +userid);
$state.go('event');
}
The controller of event.html (the state: event) has the following code:
var ref = new Firebase("https://glowing-torch-9862.firebaseio.com/Users/Anson/Eventid/");
var eventref = new Firebase("https://glowing-torch-9862.firebaseio.com/Events");
var myevent = " ";
$scope.events = [];
$scope.displayEvent = function (Eventid) {
UserData.eventDescription(Eventid)
//UserData.getDesc()
$state.go('myevents');
//console.log(Eventid);
};
function listEvent(myevents) {
$scope.events.push(myevents);
console.log("pushed to array");
console.log($scope.events);
};
function updateEvents(myevents) {
EventService.getEvent(myevents);
//console.log("success");
};
ref.once('value', function (snapshot) {
snapshot.forEach(function (childSnapshot) {
$scope.id = childSnapshot.val();
angular.forEach($scope.id, function(key) {
eventref.orderByChild("Eventid").equalTo(key).on("child_added", function(snapshot) {
myevents = snapshot.val();
console.log(myevents) // testing 26 Feb
listEvent(myevents);
updateEvents(myevents);
});
});
});
});
$scope.createEvent = function () {
$state.go('list');
}
event.html contains the following code:
<ion-view view-title="Events">
<ion-nav-buttons side="primary">
<button class="button" ng-click="createEvent()">Create Event</button>
<button class="button" ng-click="showEvent()">Show Event</button>
</ion-nav-buttons>
<ion-content class="has-header padding">
<div class="list">
<ion-item align="center" >
<button class= "button button-block button-light" ng-repeat="event in events" ng-click="displayEvent(event.Eventid)"/>
{{event.Description}}
</ion-item>
</div>
</ion-content>
</ion-view>
The button showEvent is a dummy button that I added to the HTML file to test ng-repeat. I can see in the console that the data takes about 2 secs to download from firebase and if I click on the 'Show Events' button after the data is loaded, ng-repeat works as expected. It appears to me that when ng-repeat operates on the array $scope.events, the data is not retrieved from firebase and hence its empty and therefore, it does not have any data to render to the HTML file. ng-repeat works as expected when I click the dummy button ('Show Event') because a digest cycle is triggerred on that click. My apologies for this lengthy post and would be really thankful if any of you could give me a direction to overcome this issue. I've been hunting in the internet and in stackoverflow and came across a number of blogs&threads which gives me an idea of what the issue is but I am not able to make my code work.
Once you update your events array call $scope.$apply(); or execute the code that changes the events array as a callback of the $scope.$apply function
$scope.$apply(function(){
$scope.events.push(<enter_your_new_events_name>);
})
If you are working outside of controller scope, like in services, directive, or any external JS. You will need to trigger digest cycle after change in data.
You can trigger digest cycle by
$scope.$digest(); or using $scope.$apply();
I hope it will be help you.
thanks
In your case you have to delay the binding time. Use $timeout function or ng-options with debounce property in your view.
you have to set a rough time taken to get the data from the rest API call. By using any one of the methods below will resolve your issue.
Method 1:
var myapp = angular.module("myapp", []);
myapp.controller("DIController", function($scope, $timeout){
$scope.callAtTimeout = function() {
console.log("$scope.callAtTimeout - Timeout occurred");
}
$timeout( function(){ $scope.callAtTimeout(); }, 3000);
});
Method 2:
// in your view
<input type="text" name="userName"
ng-model="user.name"
ng-model-options="{ debounce: 1000 }" />

Use http cookie value in an Angular template

I have angular working in one of my ASP.NET MVC applications. I am using two html templates with Angular Routing. One is a list of current Favorites that comes from the database and is serialized into json from my Web API and used by angular to list those items from the database.
The second html template is a form that will be used to add new favorites. When the overall page that includes my angular code loads, it has a cookie named currentSearch which is holding the value of whatever the last search parameters executed by the user.
I would like to inject this value into my angular html template (newFavoriteView.html) for the value of a hidden input named and id'd searchString.
I have tried using jQuery, but had problems, plus I would much rather do this inside of angular and somehow pass the value along to my template or do the work inside the view(template). However, I know the latter would be bad form. Below is the code I think is important for one to see in order to understand what I am doing.
Index.cshtml (My ASP.NET VIEW)
#{
ViewBag.Title = "Render Search";
ViewBag.InitModule = "renderIndex";
}
<div class="medium-12 column">
<div data-ng-view=""></div>
</div>
#section ngScripts {
<script src="~/ng-modules/render-index.js"></script>
}
Setting the cookie in the MVC Controller
private void LastSearch()
{
string lastSearch = null;
if (Request.Url != null)
{
var currentSearch = Request.Url.LocalPath + "?" +
Request.QueryString;
if (Request.Cookies["currentSearch"] != null)
{
lastSearch = Request.Cookies["currentSearch"].Value;
ViewBag.LastSearch = lastSearch;
}
if (lastSearch != currentSearch)
{
var current = new HttpCookie("currentSearch", currentSearch){
Expires = DateTime.Now.AddDays(1) };
Response.Cookies.Set(current);
var previous = new HttpCookie("lastSearch", lastSearch) {
Expires = DateTime.Now.AddDays(1) };
Response.Cookies.Set(previous);
}
}
}
render-index.js
angular
.module("renderIndex", ["ngRoute"])
.config(config)
.controller("favoritesController", favoritesController)
.controller("newFavoriteController", newFavoriteController);
function config($routeProvider) {
$routeProvider
.when("/", {
templateUrl: "/ng-templates/favoritesView.html",
controller: "favoritesController",
controllerAs: "vm"
})
.when("/newsearch", {
templateUrl: "/ng-templates/newFavoriteView.html",
controller: "newFavoriteController",
controllerAs: "vm"
})
.otherwise({ redirectTo: "/" });
};
function favoritesController($http) {
var vm = this;
vm.searches = [];
vm.isBusy = true;
$http.get("/api/favorites")
.success(function (result) {
vm.searches = result;
})
.error(function () {
alert('error/failed');
})
.then(function () {
vm.isBusy = false;
});
};
function newFavoriteController($http, $window) {
var vm = this;
vm.newFavorite = {};
vm.save = function () {
$http.post("/api/favorites", vm.newFavorite)
.success(function (result) {
var newFavorite = result.data;
//TODO: merge with existing topics
alert("Thanks for your post");
})
.error(function () {
alert("Your broken, go fix yourself!");
})
.then(function () {
$window.location = "#/";
});
};
};
favoritesView.html
<div class="container">
<h3>New Favorite</h3>
<form name="newFavoriteForm" ng-submit="vm.save()">
<fieldset>
<div class="row">
<div class="medium-12 column">
<input name="searchString" id="searchString" type="hidden"
ng-model="vm.newFavorite.searchString"/>
<label for="title">Name</label><br />
<input name="title" type="text"
ng-model="vm.newFavorite.name"/>
<label for="title">Description</label><br />
<textarea name="body" rows="5" cols="30"
ng-model="vm.newTopic.description"></textarea>
</div>
<div class="medium-12 column">
<input type="submit" class="tiny button radius" value="Save"/> |
Cancel
</div>
</div>
</fieldset>
</form>
</div>
My current attepts have been using jQuery at the end of the page after Angular has loaded and grab the cookie and stuff it in the hidden value. But I was not able to get that to work. I also thought about setting the value as a javascript variable (in my c# page) and then using that variable in angular some how. AM I going about this the right way?
Or should it be handled in the angular controller?...
I'm new to angular and the Angular Scope and a bit of ignorance are getting in the way. If any other info is needed I can make it available, thanks if you can help or guide me in the right direction.
You can do it by reading the cookie value using JavaScript, set it as a property of the $scope object and access it on the template.
//Inside your controllers
function favoritesController($http, $scope) {
//Get the cookie value using Js
var cookie = document.cookie; //the value is returned as a semi-colon separated key-value string, so split the string and get the important value
//Say the cookie string returned is 'currentSearch=AngularJS'
//Split the string and extract the cookie value
cookie = cookie.split("="); //I am assuming there's only one cookie set
//make the cookie available on $scope, can be accessed in templates now
$scope.searchString = cookie[1];
}
EXTRA NOTE
In AngularJS, the scope is the glue between your application's controllers and your view. The controller and the view share this scope object. The scope is like the model of your application. Since both the controller and the view share the same scope object, it can be used to communicate between the two. The scope can contain the data and the functions that will run in the view. Take note that every controller has its own scope. The $scope object must be injected into the controller if you want to access it.
For example:
//inject $http and $scope so you can use them in the controller
function favoritesController($http, $scope) {
Whatever is stored on the scope can be accessed on the view and the value of a scope property can also be set from the view. The scope object is important for Angular's two-way data binding.
Sorry if I'm misunderstanding or over-simplifying, but...assuming JavaScript can read this cookie-value, you could just have your controller read it and assign it to a $scope variable?
If JavaScript can't read the value, then you could have your ASP write the value to a JavaScript inline script tag. This feels yuckier though.
Update to show controller-as example.
Assuming your HTML looked something vaguely like this:
<div ng-controller="MyController as controller">
<!-- other HTML goes here -->
<input name="searchString" id="searchString" type="hidden" ng-model="controller.data.currentSearch"/>
Then your controller may look something like this:
app.controller('MyController', function ($scope, $cookies) {
$scope.data = {
currentSearch: $cookies.currentSearch
};
// Note that the model is nested in a 'data' object to ensure that
// any ngIf (or similar) directives in your HTML pass by reference
// instead of value (so 2-way binding works).
});

How to create own angular service with XHR properly?

I am very new about AngularJS things. Need to do file upload with other datas in form, I found some scripts and angular plugins but I am using my own service calls $xhr. I was able to send file but i got error, bug(not real error-bug, i just named like that) or i can not use AngularJS properly. Here it is:
.
JS
var app = angular.module('ngnNews', []);
app.factory('posts', [function () {...}]); // I reduced the codes
app.factory('$xhr', function () {
var $xhr = { reqit: function (components) { ... //My Xml HTTP Request codes here }}
return $xhr;
});
app.controller('MainCtrl', ['$http','$scope','$xhr','posts',
function ($http, $scope, $xhr, posts) {
$scope.posts = posts.posts;
$scope.files = [];
var newPost = { title: 'post one', upvotes: 20, downvotes: 5 };
$scope.posts.push(newPost);
$scope.addPost = function () {
$xhr.reqit({
form: document.getElementById('postForm'),
callbacks: {
success: function (result) {
if (result.success) {
console.log($scope.posts); //[FIRST OUT]
$scope.posts.push(result.post);
$scope.title = '';
console.log($scope.posts); //[SECOND OUT]
}
}
},
values: { upvotes: 0, downvotes: 0 },
files: $scope.files
});
...
}
}]);
.
HTML
<form action="/Home/FileUp" id="postForm" method="post" enctype="multipart/form-data">
<div class="form-group input-group">
<span class="input-group-addon">Post Title</span>
<input name="title" class="form-control" type="text" data-ng-model="title" />
</div>
<ul>
<li ng-repeat="file in files">{{file.name}}</li>
</ul>
<button class="btn btn-primary" type="button" data-ng-click="addPost()">Add New</button>
</form>
SCREEN
Sample post displayed in list
.
PROBLEMS
When I click first time Add New button everything works well until $scope.posts.push(result.post);. In console, [SECOND OUT] is here:
First object has $$hashKey but second object which sent from server(added by $scope.posts.push(result.post); function) doesn't have. I want to know why is this happening? But it's not only weird thing, when I second time click Add New button, everything completed successfully (No new logs in console, adding new post to list shown screen image above).
MAIN PROPLEM
I pushed returned value from the server but post list(in screen) is not affected when first click.
QUESTIONS
- What is happening? or
- What am I doing wrong? Thanks for any explanation.
You are doing nothing wrong with respect to $$hashkey if that is your concern. When you use ng-repeat with array of objects angular by default attaches a unique key to the items which is with the property $$hashkey. This property is then used as a key to associated DOM elements with the corresponding item in the array by identity. Moving the same object in array would move the DOM element in the same way in the DOM. You can avoid this (addition of additional property on the object by angular) by using track by with ng-repeat by providing a unique key on the object or a mere $index. So with that instead of creating a unique key and attaching it to $$haskey property angular will use the unique identifier you have provided to associate the DOM element with the respective array item.
ng-repeat="post in posts track by $index"
or (id you have a unique id for each of the object in the array, say id then)
ng-repeat="post in posts track by post.id"
And since you say you are using my xml http request code here, i am assuming it is not within the angular context so you would need to manually perform the digest cycle by using $scope.$apply() is on of those ways.
$scope.addPost = function () {
$xhr.reqit({
form: document.getElementById('postForm'),
callbacks: {
success: function (result) {
if (result.success) {
$scope.posts.push(result.post);
$scope.title = '';
$scope.$apply();//<-- here
}
}
},
But ideally you could wrap your xhr implementation with a $q and if you pass $q promise from your api, you wont need to perform a manual $scope.$apply() everywhere. Because $q promise chaining will take care of digest cycle invocation.

Stop a For Loop From Repeating in AngularJS

I'm trying to replace some html code with Javascript when a user searches for an email. I have it working correctly, but for some reason the error displays around 20+ times, so it will replace the div and say
"user does existuser does existuser does existuser does existuser does existuser does exist"
instead of just putting the error message once. Any idea how I can fix it?
$scope.checkEmail = function
findUsersMatchingEmail(emailAddress) {
ref.child('users').orderByChild('email').
equalTo($scope.emailAddress).once('value', function (snap) {
var output = '<div>',
myError = document.querySelectorAll('#d');
for (var key in arguments[0]) {
output += (snap.name() +
(snap.val() === null ? ' Does Not' : ' does') + ' exist');
}
output += '</div>';
for (var i = myError.length - 1; i >= 0; i--) {
myError[i].innerHTML = output;
}
});
};
As I wrote in the comments, never manipulate the DOM from anywhere but the link function of a directive. Explaining why is probably out of scope for this answer, and has been answered multiple times here on SO. A very good read is:
“Thinking in AngularJS” if I have a jQuery background?
and A Conceptual Introduction to AngularJS
So to make it short, here's a very simple demo of how you could handle your error messages.
Note that this is by no means the only nor the best way to do it. There's ngMessages or form validation in general which could be used as well.
(function (app) {
'use strict';
app.controller('EmailCtrl', ['$scope', function ($scope) {
$scope.errors = [];
$scope.checkEmail = function findUsersMatchingEmail(emailAddress) {
// clear previous errors
$scope.errors.length = 0;
// check email and add errors if needed
// using your service (ref)
$scope.errors.push({
message: 'email not unique ' + (Math.random()) // random is only used to show that the errors actually change
});
};
}]);
})(angular.module('app', []));
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.3/angular.min.js"></script>
<div data-ng-app="app" data-ng-controller="EmailCtrl">
<ul>
<li data-ng-repeat="error in errors">{{ error.message }}</li>
</ul>
<form data-ng-submit="checkEmail(email)">
<input data-ng-model="email" placeholder="email">
</form>
</div>
Side note: I would normally use the controller as syntax, though I did not want to introduce any more new topics.

Angularjs pass data in between services that exist on different pages

I have a simple book store example that I am working through for angularjs and I am trying to pass a book id from a home page into a service on an edit page so that the book details can be rendered. What I have happen is I can see the rest call being hit from my home' page with the correct book id being passed into the book service. However, I cannot seem to think of a way to have theBookCtrl` load that data when a different page invokes the rest service. The order I am expecting is:
1) User enters a book ID to edit
2) User presses Search button
3) book.html page is loaded
4) BookEdit service is invoked with ID from Steps 1 and 2
5) ng-model for book loads data.
Apologies in advance, there may be some errors as I was modifying this code from a different computer, so I couldn't copy/paste
code below:
home.html
<div ng-controller="HomeCtrl">
<div>
<label for="query">Book to edit</label>
<input id="query" ng-model ="editBook.query">
<button ng-click="loadBookById()">Search</button>
</div>
</div>
home.js:
var homeApp = angular.module('bookHome',['bookEdit']);
homeApp.controller('HomeCtrl',function($scope,$http,bookEditService)
{
$http.get('http://get/your/books/rest').success(function(data){
$scope.library = data;
});
$scope.editBook = {
query: '',
service:'bookEditService'
} ;
$scope.loadBookById = function()
{
$scope.$emit('loadBookById',{
query:$scope.editBook.query,
$service: $scope.editBook .service
}
$scope.$on('loadBookById', function(ev,search){
bookEditService.loadBook({
bookId: $scope.editBook.query
},
$scope.searchComplete,
$scope.errorSearching
);
});
$scope.searchComplete = function(results) {
$scope.results = results;
};
$scope.errorSearch= function(data,status,headers,config){
console.log(data);
// ...
};
}
book.html
<div ng-controller="BookCtrl" >
<div ng-model="details.title"></div>
<div ng-model="details.author"></div>
</div>
bookEdit.js
var bookEditApp = angular.module('bookEdit',[]);
bookEditApp.service('loadBook',function($http){
return{
loadBookById: function(params,success,error){
$http({
url: 'http://path/to/book/editing',
method: 'GET',
params:{bookId: params.bookId}).success(function(data,status,headers,config)
{
var results = data;
success(results || []);
}).error(function(){
error(arguments);
});
}
};
});
bookEditApp.controller('BookCtrl',function($scope){
$scope.details = {
title: "",
author: ""
};
});
An alternative that follows the order you are expecting is:
1) User enters book id and presses button
2) HomeCtrl routes to EditCtrl with the entered id as a route parameter (no need to use the book service yet):
app.controller('HomeCtrl', function ($scope, $location) {
$scope.editBook = function () {
$location.path('/edit/' + $scope.id);
};
});
3) EditCtrl is loaded, retrieves the route parameter and asks the book service for the correct book:
app.controller('EditCtrl', function EditCtrl($scope, $routeParams, bookService, $location) {
$scope.loading = true;
bookService.getBookById($routeParams.id)
.then(function (result) {
$scope.book = result;
$scope.loading = false;
});
4) When book is loaded the model ($scope.book) is populated and the html is updated
Here is a working example that hopefully will give some further guidance and ideas: http://plnkr.co/edit/fpxtAU?p=preview

Categories

Resources