Trying to find the best practices with AngularJS. Here's the deal:
There are two different pages with forms where each of them have their own form fields. But there is a one common functionality on both of the forms: they have an autocomplete field which the user can use to select multiple email addresses that exist in the system.
The selected email addresses are stored to the model/scope so that they can be shown on the HTML page. Here's an example:
<div ng-controller="MyCtrl">
<form ng-submit="selectCurrentEmail()" novalidate>
<input type="text"
class="form-control"
ng-model="responsiblePerson" />
<input type="submit" value="Add" />
<div ng-repeat="email in formData.selectedEmails">
<div>
{{email}} x
</div>
</div>
</form>
</div>
and the angularjs part:
var myApp = angular.module('myApp', []);
function MyCtrl($scope) {
$scope.formData = {selectedEmails: []};
$scope.selectEmail = function(email) {
if (email != null && $.inArray(email, $scope.formData.selectedEmails) == -1) {
$scope.formData.selectedEmails.push(email);
$scope.responsiblePerson = null;
}
};
$scope.removeEmail = function(email) {
var index = $.inArray(email, $scope.formData.selectedEmails);
if (index != -1) {
$scope.formData.selectedEmails.splice(index, 1);
}
};
$scope.selectCurrentEmail = function() {
$scope.selectEmail($scope.responsiblePerson);
};
}
http://jsfiddle.net/PqpYj/
(doesn't contain the autocomplete since it's not the main issue here..)
This all works fine, but I don't want to repeat the same logic in both of the controllers. What I would like to have is a service or a base controller that can take care of setting and removing the selected email addresses. And when the user is done, the scope would have just the selected email addresses.
So, do you think there's a good way to generalize the three functions in the scope? Any ideas making this better?
Because this is a UI element, I would put the logic into a Directive.
myApp.directive('mailSelector', function() {
return {
scope: {
emails: '='
},
template: '<form ng-submit="selectCurrentEmail()" novalidate>' +
'<input type="text"'+
' class="form-control"'+
' ng-model="responsiblePerson" />'+
'<input type="submit" value="Add" ng-click="selectCurrentEmail()" />'+
'<div ng-repeat="email in emails">'+
' <div>' +
' {{email}} x' +
' </div>' +
'</div>' +
'</form>',
link: function($scope, $element, $attrs) {
$scope.selectCurrentEmail = function() {
$scope.selectEmail($scope.responsiblePerson);
}
$scope.selectEmail = function(email) {
if (email != null && $.inArray(email, $scope.emails) == -1) {
$scope.emails.push(email);
$scope.responsiblePerson = null;
}
}
$scope.removeEmail = function(email) {
var index = $.inArray(email, $scope.emails);
if (index != -1) {
$scope.emails.splice(index, 1);
}
};
}
};
});
Controllers can retrieve a list of emails from Directive via emails parameter defined with the directive.
I have created a JSFiddle here.
To share previously input email addresses for auto-completion, I would modify the directive to use a Service, which holds the list of previously input email addresses.
Related
I got a requirement to bind a value to a particular model when the value in the other model contains a string starting with "https".
For example, I have two text fields both fields having different model
<input type="text" ng-model="modelText1">
<input type="text" ng-model="modelText2">
Suppose I type a value on the first text field "https", the first input model modelText1 have to bind to the second input model modelText2 and later on i have to maintain it as like two-way binding. i.e. the second field will automatically get the value dynamically when it contains "https" at starting of a string.
Try it like in this Demo fiddle.
View
<div ng-controller="MyCtrl">
<input type="text" ng-model="modelText1">
<input type="text" ng-model="modelText2">
</div>
AngularJS Application
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', function ($scope) {
$scope.modelText1 = '';
$scope.modelText2 = '';
var regEx = new RegExp(/^https/);
$scope.$watch('modelText1', function (newValue) {
if (newValue.toLowerCase().match(regEx)) {
$scope.modelText2 = newValue;
} else {
$scope.modelText2 = '';
}
});
});
An other approach is (that avoid using of $watch) is to use AngularJS ng-change like in this
example fiddle.
View
<div ng-controller="MyCtrl">
<input type="text" ng-model="modelText1" ng-change="change()">
<input type="text" ng-model="modelText2">
</div>
AngularJS Application
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', function ($scope) {
$scope.modelText1 = '';
$scope.modelText2 = '';
var regEx = new RegExp(/^https/);
$scope.change = function () {
if ($scope.modelText1.toLowerCase().match(regEx)) {
$scope.modelText2 = $scope.modelText1;
} else {
$scope.modelText2 = '';
}
};
});
You can use the ng-change directive like this:
<input type="text" ng-model="modelText1" ng-change="onChange()">
<input type="text" ng-model="modelText2">
and your controller:
$scope.onChange = function() {
if ($scope.modelText1 === 'https') {
$scope.modelText2 = $scope.modelText1;
else
$scope.modelText2 = '';
};
use ng-change to check the text is equal to 'https'
angular.module('app',[])
.controller('ctrl',function($scope){
$scope.changeItem = function(item){
$scope.modelText2 = "";
if(item.toLowerCase() === "https"){
$scope.modelText2 = item
}
}
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<input type="text" ng-model="modelText1" ng-change="changeItem(modelText1)">
<input type="text" ng-model="modelText2">
</div>
EDiTED
to make sure it does't fail under 'HTTPS' use toLoweCase function to make all lower case
HTML :
<input type="text" ng-model="modelText1" ng-change="updateModal(modelText1)">
JS :
var modelText1 = $scope.modelText1.toLowerCase();
$scope.updateModal = function(){
$scope.modelText2 = '';
if(modelText1.indexOf('https')!=-1){
$scope.modelText2 = modelText1;
}
}
you could also possibly do this as a directive if you want to have a more reusable solution over multiple views http://jsfiddle.net/j5ga8vhk/7/
It also keeps the controller more clean, i always try to use the controller only for controlling complex business logic and business data
View
<div ng-controller="MyCtrl">
<input type="text" ng-model="modelText1" >
<input type="text" ng-model="modelText2" model-listener="modelText1" model-listener-value="https" >
</div>
Angular JS
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', function ($scope) {
$scope.modelText1 = '';
$scope.modelText2 = '';
});
myApp.directive('modelListener', [function() {
return {
restrict: 'A',
controller: ['$scope', function($scope) {
}],
link: function($scope, iElement, iAttrs, ctrl) {
$scope.$watch(iAttrs.modelListener, function() {
if($scope[iAttrs.modelListener] === iAttrs.modelListenerValue ) {
$scope[iAttrs.ngModel] = $scope[iAttrs.modelListener];
} else {
$scope[iAttrs.ngModel] = "";
}
}, true);
}
};
}]);
I have this angular component where I like to add a custom input validator (see plunker).
I'm trying to access the ngModelController in the $onInit function. But it seams that the form is not populated at this time. Later on in the sendEmail() function it's no problem to access the the input model controller. How can I access the ngModelController and add a custom validator?
emailInput.js
(function(angular) {
'use strict';
function EmailInputController($log) {
var ctrl = this;
ctrl.$onInit = function() {
// ctrl.myForm.myEmailInput is not populated
//$log.debug("Email view value is: "+(ctrl.myForm.myEmailInput.$viewValue));
};
ctrl.sendEmail = function() {
$log.debug("EmailInputController.sendEmail");
$log.debug("Email view value is: " + (ctrl.myForm.myEmailInput.$viewValue));
};
}
angular.module('emailInputApp').component('emailInput', {
templateUrl: 'emailInput.html',
controller: EmailInputController,
});
})(window.angular);
emailInput.html
<form name="$ctrl.myForm">
<label>Email:</label>
<input name="myEmailInput" type="email" ng-model="$ctrl.email" required maxlength="15">
<button type="button" ng-click="$ctrl.sendEmail()">Send Email</button>
<p>Your Email addres is {{$ctrl.email}}</p>
<div ng-messages="$ctrl.myForm.myEmailInput.$error" role="alert">
<div ng-message="required">Please enter an email address.</div>
<div ng-message="email">This field must be a valid email address.</div>
<div ng-message="maxlength">This field can be at most 15 characters long.</div>
</div>
<code>
{{$ctrl.myForm.myEmailInput | json}}
</code>
</form>
http://plnkr.co/edit/YQfGAsix1DON4ff3EWxz?p=preview
You can add watcher and remove it when not needed anymore.
var removeWatch = $scope.$watch('$ctrl.myForm', function () {
$log.debug("Email view value is: " + (ctrl.myForm.myEmailInput.$modelValue));
removeWatch();
});
app.directive('validateEmail', function() {
var EMAIL_REGEXP = /^[_a-z0-9]+(\.[_a-z0-9]+)*#[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$/;
return {
require: 'ngModel',
restrict: '',
link: function(scope, elm, attrs, ctrl) {
// only apply the validator if ngModel is present and Angular has added the email validator
if (ctrl && ctrl.$validators.email) {
// this will overwrite the default Angular email validator
ctrl.$validators.email = function(modelValue) {
return ctrl.$isEmpty(modelValue) || EMAIL_REGEXP.test(modelValue);
};
}
}
};
});
And simply add
<input type='email' validate-email name='email' id='email' ng-model='email' required>
I am facing an issue with using ng-init and assign it model inside my html.
The following code works fine. The following code is for Add/Edit functionality. For example, when row is is opened in Edit mode than it persist existing value and shows it in textbox.
<div>
<div ng-if="title == 'Add Student'">
<input type="text" name="name"placeholder="Student Name" data-ng-model="registration.Student.FirstName" maxlength="50">
</div>
<div ng-if="title == 'Edit Student'">
<input type="text" name="name"placeholder="Student Name" data-ng-model="student.Student.FirstName" maxlength="50">
</div>
</div>
However, the following code which is short version of above code does not work. I mean when the row is opened in edit mode it shows text field but does not show existing value (first name) in it. Why?
<div ng-init="model = (title == 'Add Student' ? registration.Student : student.Student)">
<input type="text" name="name" placeholder="Student Name" data-ng-model="model.FirstName" maxlength="50">
</div>
Please suggest whether ng-init can't be used in this way or some issue in my code?
thanks
Controller
var currentState = $state.current.name;
if if (currentState == "Add")
{
$scope.registration = {
Student: {
FirstName: '',
}
var _init = function () {
}
$scope.title = " Add Student";
}
else
{
$scope.student= {};
$scope.student= response[0];
var _init = function () {
$scope.title = " Edit Student";
}
}
You are ng-init block is wrong currently it is returning true or false, you are messing with brackets.
Markup
<div ng-init="model = (title == 'Add Student') ? registration.Student : student.Student">
<input type="text" name="name" placeholder="Student Name" data-ng-model="model.FirstName" maxlength="50">
</div>
Update
In your current case you ng-init is getting executed while element rendered on the DOM, at that instance on time registration.Student & student.Student doesn't have any value. Evaluation of ng-init setting null object to the model student. I'd suggest you do set model value from the controller logic that would be more safer.
Code
var currentState = $state.current.name;
if (currentState == "Add")
{
$scope.registration = {
Student: {
FirstName: '',
}
var _init = function () {
}
$scope.title = " Add Student";
}
else
{
$scope.student= {};
$scope.student= response[0];
var _init = function () {
$scope.title = " Edit Student";
}
//shifted logic in controller
$scope.model = (title == 'Add Student' ? registration.Student : student.Student);
}
Markup
<div>
<input type="text" name="name" placeholder="Student Name"
data-ng-model="model.FirstName" maxlength="50"/>
</div>
Other way you could add one more flag like loadedData which will says that ajax response has been fetched & registration.Student & student.Student values are available in the scope.
Markup
<div ng-if="loadedData">
<input type="text" name="name" placeholder="Student Name" data-ng-model="model.FirstName" maxlength="50">
</div>
Code
var currentState = $state.current.name;
if (currentState == "Add")
{
$scope.registration = {
Student: {
FirstName: '',
}
var _init = function () {
}
$scope.title = " Add Student";
}
else
{
$scope.student= {};
$scope.student= response[0];
var _init = function () {
$scope.title = " Edit Student";
}
//set flag
$scope.loadedData = true;
}
I'm trying to pass localStorage to input field, but i'm not able to pass the values, i not getting where i'm going wrong, please help me with this, 1 thing more how can i call the angularjs function outside the controller. thanx for any help guys.
html
<div class="modal__content" ng-controller="joinctrl">
<form novalidate>
<input type="email" placeholder="Email" ng-model="email"><br />
<input type="password" placeholder="Password" ng-model="password"><br />
<input type="password" placeholder="Confirm Password" ng-model="cpassword"><br />
<input type="submit" value="Create" class="creat" ng-click="create()">
</form>
</div>
js
preauth();
function preauth() {
var user_login = window.localStorage.getItem('email');
var pass_login = window.localStorage.getItem('password');
if(user_login != undefined && pass_login != undefined) {
alert(user_login);
document.getElementById("email").value=user_login;
document.getElementById("password").value=pass_login;
angular.element(document.getElementById('loginctrl')).scope().Login();
}
}
var app = angular.module('contol', ['onsen']);
app.controller('loginctrl', function($scope, $http){
$scope.login=function(){
if (!$scope.email || !$scope.password){
alert("Please Fill Email & Password");
}
else{
var request=$http({
method:"post",
url:"http://www.example.com/login.php",
dataType: "json",
data:{
email:$scope.email,
password:$scope.password
},
headers:{'Content-Type':'application/x-www-form-urlencoded'}
});
request.success(function(retmsg){
if(parseInt(retmsg.status)==0)
{
alert(retmsg.txt);
}
else if (parseInt(retmsg.status)==1)
{
window.localStorage.setItem('email', $scope.email);
window.localStorage.setItem('password', $scope.password);
myNavigator.pushPage('home.html');
}
});
}
};
});
This may be useful,store it in variable and use it and only id name not #password
var x = document.getElementById("password").value = "Password";
alert ("The value was changed to: " + x);
In Angular applications, you should write ALL code inside modules. Otherwise, you don't use one of the useful feature of Angular: modularity.
For example:
app.factory("LocalStorage", function() {
var LS = {};
LS.getItem = function(key) {
return localStorage[key];
};
LS.setItem = function(key, value) {
localStorage[key] = value;
return value;
};
return LS;
});
app.controller('loginctrl', function($scope, $http, LocalStorage) {
$scope.email = LocalStorage.getItem('email');
$scope.password = LocalStorage.getItem('password');
$scope.login = function() {
// code...
// I'd rather to create another service "Api" or "Request", like this:
return Api.doAuth($scope.login, $scope.password).then(function(res) {
// For example, service `Request` write to `LocalStorage` status of current authentication.
if(res) {
myNavigator.pushPage('home.html');
}
});
};
});
You should not to call function in outside application (it's incorrect from the point of view Angular's philosophy). However, it's possible, you wrote right:
// get our scope:
var scope = angular.element(document.getElementById('loginctrl')).scope();
// and call our function in the scope:
scope.login();
But it's code you need to execute after Angular's bootstrap.
Also, you may to execute any function with Angular's services.
//find the `ng:app` element and get injector.
var injector = angular.element('body').injector();
//execute our own function with any services
injector.invoke(function($rootScope, $http, LocalStorage) {
$http.get('/foo.php').then(function(res) {
LocalStorage.setItem('answer', res);
});
$rootScope.bar = 1;
});
I am using a 'required' and 'email' validators on the 'email' input field. The validators make use of parsers and formatters and they work fine. However, I also have some validation on 'bind' event.
Here is the directive:
angular.module('simpleRepairApp')
.directive('uniqueEmail', function (Checker) {
return {
require:'ngModel',
restrict:'A',
link:function ($scope, element, attrs, model) {
var last = '';
var current = '';
element.bind('blur', function() {
current = element.val();
console.log(current, last);
if(current !== last){
Checker.email({ email:current }).then(
function(response) {
model.$setValidity('uniqueEmail', response.available);
}
);
}
last = current;
});
}
};
});
I need to check if the email already exists in the database or not after user clicks out of the field (I do not want to check upon every key press or change).
The problem I am having is, after the unique validation is performed, it shows the unique error message, but after you type to correct the email, the model value stays undefined. You have to click out of the input, then the value in the model in defined again.
Anyone can help me solve this, it is driving me nuts!:)
WORKING SOLUTION:
angular.module('simpleRepairApp')
.directive('emailUnique', function () {
return {
restrict: 'E',
require: ['form', 'ngModel'],
scope: {
'form': '=form',
'model': '=ngModel',
'labelClass': '#',
'inputClass': '#'
},
compile: function(element, attrs)
{
if (!attrs.labelClass) { attrs.labelClass = 'col-sm-4'; }
if (!attrs.inputClass) { attrs.inputClass = 'col-sm-8'; }
attrs.required = attrs.required == 'true';
},
controller: function($scope, Checker) {
$scope.checkEmail = function() {
var email = $scope.form.email.$viewValue;
var checkField = $scope.form.emailCheck;
Checker.email({ email:email }).then(
function(response) {
checkField.$setValidity('unique', response.available);
$scope.form.$setValidity('check', true);
checkField.hasVisit = true;
}
);
};
$scope.setUnchecked = function() {
console.log($scope.form);
$scope.form.emailCheck.hasVisited = false;
$scope.form.$setValidity('check', false);
$scope.form.emailCheck.$setValidity('unique', true);
};
},
template: '<div ng-class="{ \'has-error\' : !form.email.$valid || !form.emailCheck.$valid }">' +
'<label class="{{ labelClass }} control-label required" for="email" translate>E-mail</label>' +
'<div class="{{ inputClass }}">' +
'<input name="email" class="form-control" type="email" id="email" ng-model="model" ng-change="setUnchecked()" ng-blur="checkEmail()" ng-required="true" autocomplete="off">' +
'<div class="help-block" ng-show="(!form.email.$valid || !form.emailCheck.$valid) && !form.email.$pristine">' +
'<div ng-messages="form.email.$error">' +
'<div ng-message="required"><span translate>Please enter your e-mail.</span></div>' +
'<div ng-message="email"><span translate>Please enter a valid e-mail.</span></div>' +
'</div> ' +
'<div ng-messages="form.emailCheck.$error">' +
'<div ng-message="check"><span translate>E-mail will be checked upon blur.</span></div>' +
'<div ng-message="unique"><span translate>This e-mail is already in use.</span></div>' +
'</div> ' +
'</div>' +
'<input name="emailCheck" type="hidden" class="form-control" ng-model="checked">' +
'<div class="help-block" ng-messages="form.emailCheck.$error" ng-show="!form.emailCheck.$valid">' +
'</div>' +
'</div>' +
'</div>'
};
});
I think you might be complicating things a bit by making a directive, why don't you simply add a watcher to your controller?
Note: I'm not familiar if any of these methods will work for you, but I'm adding them as an illustrative purpose of not binding to the "blur" event, but rather have the events get triggered when the MODEL changes. Which is what angular is designed to do. I'm also making an assumption that your Checker is handling the promises correctly
First alternative method (using $watch)
<form name="myForm" ng-controller="ExampleController">
<input type="email" name="input" ng-model="text" required />
</form>
Then in your ng-app controller:
function isNullOrUndefined(value) {
return (value == 'undefined' || value == null);
}
//listener on the ng-model='text', waiting for the data to get dirty
$scope.$watch('text', function (newValue, oldValue) {
if (newValue != oldValue)
{
var truthyVal = !(isNullOrUndefined(newValue));
if (truthyVal) {
//angular's way of checking validity as well
//$scope.myForm.text.$valid
Checker.email({ email:current }).then(
function(response) {
model.$setValidity('textErr', response.available);
}
}
}
}, true);
Second alternative method (using ng-change):
<form name="myForm" ng-controller="ExampleController">
<input type="email" name="input" ng-model="text" ng-change="checkEmail()" required />
</form>
$scope.checkEmail = function(){
Checker.email({ email:current }).then(
function(response) {
model.$setValidity('textErr', response.available);
}
}
Because you are binding on the blur event, the validation won't work the way you want. You will have to click out of the field before that function can run. If you indeed want it to validate when they finish typing the email, then you will have to bind to some sort of key event.
My suggestion is to bind to keyup, and only do the server-side check when the email address appears to be of valid syntax.