I have a page with a couple of controllers and ng-includes.
Before coming to actual problem i should give a little detail about my AngularJS app
It is on top of node and express
It is communicating with passport running on express for authentication
Now i wanted to have the same page for authenticated as well as unauthenticated users with some differences, lets say the first change was the profile option in nav bar -
ul.nav.navbar-nav.navbar-right(ng-show="identity.isAuthenticated()")
this unordered list has the options to be shown to authenticated users which works fine as soon as i login it is shown which means that the variables identity and the method is isAuthenticate are working fine
But the problem is with the profile form which is a bootstrap modal box -
div.modal.fade(id="profileModal")
div.modal-dialog
div.modal-content
div.modal-header
button.close(data-dismiss="modal") ×
h4 Update Profile
div.modal-body
form.form-horizontal(name="profileForm")
fieldset
.form-group
label.col-md-2.control-label(for="email") Email
.col-md-10
input.form-control(name="email", type="email", placeholder="Email", ng-model="email", required)
.form-group
label.col-md-2.control-label(for="fname") First Name
.col-md-10
input.form-control(name="fname", type="text", placeholder="First Name", ng-model="fname", required)
.form-group
label.col-md-2.control-label(for="lname") Last Name
.col-md-10
input.form-control(name="lname", type="text", placeholder="Last Name", ng-model="lname", required)
.form-group
label.col-md-2.control-label(for="password") Password
.col-md-10
input.form-control(name="password", type="password", placeholder="Password", ng-model="password")
.form-group
div.modal-footer
button.btn.btn-primary(ng-click="update()", ng-disabled="profileForm.$invalid") Submit
|
a.btn.btn-default(data-dismiss="modal") Cancel
Now here i am using a different controller mvProfileCtrl which is as follows -
angular.module('app').controller('mvProfileCtrl', function($scope, mvAuth, mvIdentity, mvNotifier) {
$scope.email = mvIdentity.currentUser.username;
$scope.fname = mvIdentity.currentUser.firstName;
$scope.lname = mvIdentity.currentUser.lastName;
$scope.update = function() {
var newUserData = {
username: $scope.email,
firstName: $scope.fname,
lastName: $scope.lname
}
if($scope.password && $scope.password.length > 0) {
newUserData.password = $scope.password;
}
mvAuth.updateCurrentUser(newUserData).then(function() {
mvNotifier.notify('Your user account has been updated');
}, function(reason) {
mvNotifier.error(reason);
})
}
})
The problem here is as soon as i open the page and the user is not logged in the currentUser is empty object so it doesn't have anything inside it.
So i get this error in console -
TypeError: Cannot read property 'username' of undefined
at new <anonymous> (http://localhost:5000/app/account/mvProfileCtrl.js:2:42)
at invoke (http://localhost:5000/vendor/angular/angular.js:3899:17)
at Object.instantiate (http://localhost:5000/vendor/angular/angular.js:3910:23)
at http://localhost:5000/vendor/angular/angular.js:7164:28
at http://localhost:5000/vendor/angular/angular.js:6566:34
at forEach (http://localhost:5000/vendor/angular/angular.js:327:20)
at nodeLinkFn (http://localhost:5000/vendor/angular/angular.js:6553:11)
at compositeLinkFn (http://localhost:5000/vendor/angular/angular.js:6007:15)
at compositeLinkFn (http://localhost:5000/vendor/angular/angular.js:6014:13)
at publicLinkFn (http://localhost:5000/vendor/angular/angular.js:5916:30)
But as soon as i refresh the page the currentUser has all the required data and thats why there is no error. So, how do i make sure that when the user is authenticated then only bind the $scope variables to this controller or after that only start the controller.
The project source is a bit big thats why i did not include complete detail here, so if i missed out on any detail please suggest
Edit:: another possible bug after removing the mvIdentity and the currentUser part for debugging when i tried this -
angular.module('app').controller('mvProfileCtrl', function($scope, mvAuth, mvNotifier) {
$scope.update = function() {
console.log($scope.email);
var newUserData = {
username: $scope.email,
firstName: $scope.fname,
lastName: $scope.lname
}
if($scope.password && $scope.password.length > 0) {
newUserData.password = $scope.password;
}
mvAuth.updateCurrentUser(newUserData).then(function() {
$('#profileModal').modal('toggle');
mvNotifier.notify('Your user account has been updated');
}, function(reason) {
mvNotifier.error(reason);
})
}
});
It gave me undefined , which suggests that my $scope is not bound to the profileForm object but if i use the watcher and then use the previous code suggested by then i find that the input elements indeed have the currentUsers firstname and lastname which tells that the $scope is attached there. I am not understanding what is going here, please can anyone explain a bit
i am applying the controller like this -
div(ng-include="'/partials/account/authNavBar'",ng-controller="mvProfileCtrl")
where authNavBar has this bootstrap modal code and the profileForm
Use the scope and watchers to your advantage. If you set mvIdentity on your scope with a reference to the service, you can add watchers and only update those properties when currentUser exists.
angular.module('app').controller('mvProfileCtrl', function($scope, mvAuth, mvIdentity, mvNotifier) {
$scope.mvIdentity = mvIdentity;
$scope.$watch('mvIdentity.currentUser', function(currentUser) {
if (!currentUser) return;
$scope.email = currentUser.username;
$scope.fname = currentUser.firstName;
$scope.lname = currentUser.lastName;
});
$scope.update = function() {
var newUserData = {
username: $scope.email,
firstName: $scope.fname,
lastName: $scope.lname
}
if($scope.password && $scope.password.length > 0) {
newUserData.password = $scope.password;
}
mvAuth.updateCurrentUser(newUserData).then(function() {
mvNotifier.notify('Your user account has been updated');
}, function(reason) {
mvNotifier.error(reason);
})
}
});
Related
Alright, so I'm having a bit of a problem. I have an app that displays some facts via a search input. If that fact isn't existing, I want to display an error.
I'm producing that error via Node on the backend, and via EJS sends the error message to the HTML (.ejs) and javascript files.
Long story short, the error message displays correctly, but the error popup also displays when refreshing the page, even though there isn't any errors to display.
Error.js
var clientError = "<%=clientError%>"; //<--- (1) see comment below
$("#error").hide();
if(clientError !== "") { //<--- (2) see comment below
$("#error").fadeIn().show();
setTimeout(function(){
$("#error").fadeOut().hide();
}, 4000);
}
(1) This is being interpreted as the word "clientError" and characters "<%=%>" of "<%=clientError%>", and NOT the value of the .ejs query, for example, "An error occurred". This leads to problem no. 2, see below.
(2) Because "<%=clientError%>" isn't being read as an empty string, even if there aren't any errormessages, it runs the code either way and displays the error-popup. So when I refresh the website I get the popup, because the string isn't empty (even though it doesn't display any message, because there aren't any errors).
I have also tried some other variants of the error.js code, for example:
if(clientError.length >= 17) ...executes error popup // no luck with this either, see comment 1 and 2 above.
//I have also tried not defining clientError in App.js:
var clientError;
//And then inside error.js:
if(clientError !== undefined) ...executes error popup //no luck with this, since error.js reads it as a defined string.
App.js
var clientError = ""; //<-- (3)
...
...
app.get("/:option/:input", function(req, res) {
var opt = req.params.option;
var inp = req.params.input;
Article.find({
option: lodash.capitalize(opt),
input: lodash.capitalize(inp)
}, function(err, foundArticle) {
if (err) {
clientError = "Internal Server Error. Contact Administrator.";
setTimeout(function(){
clientError = "";
},4000);
console.log(err);
}
else if ((!foundArticle) || (foundArticle.length <= 0)) {
const notFound = new Notfound({
option: searchOptions,
input: searchInput
});
clientError = "Article not found. Try again."
setTimeout(function(){
clientError = "";
},4000);
res.redirect("/");
} else {
Article.findById(someIdOrSomething, function(err, someArticle){
res.render("page", {
...
clientError: clientError,
});
});
}
});
})
(3) An empty string. So the string in error.js should be read as an empty string, shouldn't it?
At last, we have the error.EJS
error.ejs
<div id="error" class="error-popup">
<h4>An error occurred.</h4>
<p id="errormessage"><%=clientError%></p>
</div>
One idea might be to have an input instead of the paragraph element above that's disabled as such...
<input id="errormessage" disabled type="text" value="<%=clientError%>">
... and then use Jquery to get the value of the input.
EDIT:
The idea above worked! Here is the new code:
error.js
$("#error").addClass("error-popup");
if($("#errormessage").val() !== "") {
$("#error").fadeIn().addClass("show-error-popup");
setTimeout(function(){
$("#error").fadeOut().removeClass("show-error-popup");
}, 4000);
}
error.ejs
<div id="error" class="error-popup">
<h4>An error occurred</h4>
<input id="errormessage" disabled type="text" value="<%=clientError%>">
</div>
Next step is just to style the input so it doesn't look like a regular input.
I have single view displaying investments + two others which are modals to register new investment which show up when user clicks 'add' (two modals because of two steps of registration). I created factory which is used in step1 and then in step2 in order to keep information regarding investment being registered - it works when you switch between step1 and step2, back and forth.
The problem is that within first view displaying investments I have icon "edit" and within its handler (edit method) I assign selected investment to factory but no change is reflected in step1 view, alas.
View displaying investments:
var module = angular.module("application", []);
module.controller("investmentsController", function ($scope, investmentsFactory, newInvestmentFactory) {
$scope.edit = function (id) {
for (var i = 0; i < $scope.Investments.length; i++) {
if ($scope.Investments[i].Id == id) {
newInvestmentFactory.update($scope.Investments[i]);
}
}
$("#addInvestmentStep1Modal").modal("show");
};
});
View step1 of registration
var module = angular.module("application");
module.factory("newInvestmentFactory", function () {
return {
investment: {
Name: "",
Description: "",
Category: "",
InterestRate: "",
Duration: "",
AmountRange: "",
Url: "",
Subscription: 0,
PaymentType: "",
Icon: ""
},
update: function (investment) {
this.investment = investment;
}
};
});
module.controller("newInvestmentStep1Controller", function ($scope, newInvestmentFactory) {
$scope.Investment = newInvestmentFactory.investment;
});
View step2 of registration
var module = angular.module("application");
module.controller("newInvestmentStep2Controller", function ($scope, newInvestmentFactory) {
$scope.Investment = newInvestmentFactory.investment;
});
The step1 view displaying registration is following
<form id="newInvestmentStep1Form" class="form-horizontal">
<div class="input-group">
<span class="input-group-addon input-group-addon-register">Name</span>
<input id="Name" name="Name" type="text" class="form-control" ng-model="Investment.Name" required title="Pole wymagane" />
</div>
Assignining new object to factory's object (newInvestmentFactory.investment) does not seem to be working but when I assign brand new value to some property of factory like
newInvestmentFactory.investment.Name = "name"
then it displays value correctly.
I can only suspect newInvestmentFactory's update method code. It is reassigning investment object to new investment object like this.investment = investment. By that line new investment object gets created, and old investment loose the reference. To keep the investment object to not create a new variable in update method, you could use angular.extend/angular.merge method. This method will not create a new reference of an object, but it ensures that all object property got updated.
update: function (investment) {
angular.extend(this.investment, investment);
}
In your step controllers
$scope.Investment = newInvestmentFactory.investment;
is just one time assignment to $scope variable, this is not two way binding, so even if value of newInvestmentFactory.investment changes scope won't be updated. What you can do is to watch the factory variable newInvestmentFactory.investment and on change update the scope manually.
Hope this helps
[EDIT] i'm using Meteor
Hi everyone,
i've searched and tried many things but i can't do what i want.
I'm doing a tutorial to do a ToDo list, and when u check a task, i want to put the name of the user who checked the task.
<template name="task">
<li class="{{#if checked}}checked{{/if}}">{{#if checked}}<!-- the name of the user who checked it -->{{/if}}
<!-- the rest isn't useful for my question -->
I've tried with {{currentUser.username}} but when i log with someone else the name change...
There is the JS for the event handler
'click .toggle-checked'() {
// Set the checked property to the opposite of its current value
Meteor.call('tasks.setChecked', this._id, !this.checked);
}
And the JS for the method call
'tasks.setChecked'(taskId, setChecked) {
check(taskId, String);
check(setChecked, Boolean);
Tasks.update(taskId, { $set: { checked: setChecked } });
}
Thank you for the help
If you use {{currentUser.username}} you will always have the data of the logged in user.
To get what you want you need to register the _id of the user who checked the task in your method:
'tasks.setChecked'(taskId, setChecked) {
check(taskId, String);
check(setChecked, Boolean);
// Check that 'setChecked' is true and that the user is logged in,
// Otherwise just update the status
if (setChecked && this.userId) {
Tasks.update(taskId, {
$set: {
checked: setChecked,
userId: this.userId,
}
});
} else {
Tasks.update(taskId, {
$set: {
checked: setChecked,
}
});
}
}
Make sure you update your schema accordingly if you are using one.
Then in your template, retrieve the user data and display it:
// file task.js
import './task.html';
Template.task.helpers({
checkerUser() {
// The template data are those of the current task, check if userId is defined and task is checked
const { userId, checked } = Template.currentData();
/* Or you can do
* const userId = Template.currentData().userId;
* checked = Template.currentData().checked;
*/
if (userId && checked) {
return Meteor.users.findOne({ _id: userId }).profile.username;
}
return null;
}
});
Template.task.events({
'click .toggle-checked'() {
// Set the checked property to the opposite of its current value
Meteor.call('tasks.setChecked', this._id, !this.checked);
}
})
And finally in HTML:
// file task.html
<template name="task">
<li class="{{#if checked}}checked{{/if}}">
{{#if checkerUser}}Completed by {{checkerUser}}{{/if}}
</li>
</template>
Technically, in your method you should also check more in-dept the different situations. For instance, when you uncheck a task, it should remove the userId from the record so that if a non logged-in user checks it again, the name won't be the one of the first user (or you could $unset userId if user is not logged in when setting checked = true)
I started this journey trying to get some settings to persist with localStorage, has some problems and posted about it here (without a solution): Why won't this data bind? An odd case in Angularjs
I've abandoned that method as I learnt about ngStorage. In theory ngStorage lets you 2-way bind into and out of Angular models. It's a great, great theory.
I'm having problems with it though. It half works.
The ideas is this:
Test for permission selection (true or false).
If no selection (first time use) pop-up a choice.
Store the choice.
On restart use the stored choice to set the permission true or false.
Allow user to change the permission from within the app.
It works up to number 4.
Testing shows that although on first use I can set $storage.analytics to true or false subsequent changes are not being stored and retrieved from local storage.
Here is the code:
permissionCallback = function(permission){
if(permission===1){
console.log("analytics allowed");
analytics.startTrackerWithId('UA-45544004-1');
$scope.$storage.analytics=true;
navigator.notification.alert('You can turn analytics off in the Data Tracking section at any time.', null, 'Analytics On', 'OK');
}else{
console.log("analytics denied");
$scope.$storage.analytics=false;
navigator.notification.alert('You can turn analytics on in the Data Tracking section at any time.',null , 'Analytics Off', 'OK');
}
}
if(typeof $scope.$storage.analytics === 'undefined'){
navigator.notification.confirm('This app would like your permission to collect data on how you use the app. No personal or user identifiable data will be collected.', permissionCallback, 'Attention', ['Allow','Deny']);
}
else{
console.log('start analytics are', $scope.$storage.analytics);
if(typeof analytics !== 'undefined'){
console.log("analytics functioning");
analytics.startTrackerWithId('UA-45544004-1');
$scope.trackClick = function(category, action){
analytics.trackEvent(category, action);
console.log('Tracking category: ' + category + ', Section: ' + action + '.');
}
}
}
$scope.counter = 0;
$scope.change = function(){
$scope.counter++;
console.log('analytics are ' + $scope.$storage.analytics);
}
And here is the html.
<li class="item item-toggle">
<i class="icon ion-cloud"></i> Data Tracking is {{$storage.analytics}} {{counter}}
<label class="toggle toggle-balanced">
<input type="checkbox" ng-model="$storage.analytics" ng-change="change()">
<div class="track">
<div class="handle"></div>
</div>
</label>
</li>
It's either a fault with my logic or, and I think this more likely, a misunderstanding about the scope of the data.
The odd thing is the console log in the change() function (which is purely for tracking these things) is always correct. So using $storage.analytics in the html is the correct way to do it (using $scope.storage.analytics causes all sorts of errors) and it is indeed binding from the html into $scope.storage.analytics.
So why isn't it saving it to local storage when using the toggle?
I ran into a similar problem with ng-storage. When the page was loaded/reloaded anything bound to a value in $sessionStorage was updated correctly. However any changes to $sessionStorage afterwards were not reflected in my view. What I ended up doing was creating a service for storing changes and using $sessionStorage as a temporary data store.
app.controller('TestController', funciton($scope, $sessionStorage, Service) {
// if we have session data set our service
if($sessionStorage.data) {
Service.data = $sessionStorage.data;
} else {
$sessionStorage.data = {};
}
// now bind scope to service
scope.data = Service.data;
// on update we set both Service and $sessionStorage
// scope.data will be automatically updated
scope.update = function(val) {
Service.data.value = val;
$sessionStorage.data.value = val;
}
});
app.service('TestService', function() {
var service = {
data: {
value: 'Hello World'
}
};
return service;
});
<div ng-controller="TestController">{{data.value}}</div>
<button ng-click-"update('Hello Universe')">Update</button>
This is a very rudimentary example of how my solution works but hopefully it gets anyone else stuck in the same situation on the right track.
So I have the following form:
template(name='editUser')
.row
.col-md-4.col-md-offset-4
.page-header
h1 Edit user
form#edit-user-form
.form-group
label(for='name') Name
input#user-name.form-control(type='text' placeholder='Name' value='{{user.name}}')
.form-group
label(for='email') E-Mail
input#user-email.form-control(type='text' placeholder='E-Mail' value='{{getEmail user}}')
button.btn.btn-primary(type='submit') Update
the following handlebars.js-Helper:
Handlebars.registerHelper('getEmail', function (user) {
if (user.emails && user.emails[0] && user.emails[0].address)
return user.emails[0].address;
return '';
});
and the following iron-Router code:
EditUserController = RouteController.extend({
template: 'editUser',
waitOn: function () {
return Meteor.subscribe('user', this.params._id);
},
data: function () {
return {
user: Meteor.users.findOne( { _id: this.params._id } )
};
}
});
If I run my application and click on the link to the edit-User-Form I can see the E-Mail Address. But if I change my code and Meteor automatically refreshes the page, the E-Mail-Field is empty and the console says, that it can't fetch the value of undefined.
If I use the same form, but with a with-Helper, the E-Mail is displayed even if Meteor automatically refreshes the page:
template(name='editUser')
.row
.col-md-4.col-md-offset-4
.page-header
h1 Edit user
form#edit-user-form
with user
.form-group
label(for='name') Name
input#user-name.form-control(type='text' placeholder='Name' value='{{name}}')
.form-group
label(for='email') E-Mail
input#user-email.form-control(type='text' placeholder='E-Mail' value='{{getEmail this}}')
button.btn.btn-primary(type='submit') Update
Why is this so? And should I always use the with-Helper if I get single Results (only one Result to display)?
Thanks in advance!
Replace Meteor.users.findOne with Meteor.users.find.
When findOne doesn’t find anything, it returns undefined which causes your error; when find doesn’t find anything, it returns an empty cursor which Meteor knows what to do with. Essentially all you were doing by adding with was to cause Meteor to check if the value was undefined, but that check isn’t necessary for a cursor, empty or otherwise.