AngularJS form and null/empty values - javascript

I am working with a somewhat dynamic AngularJS form. In other words, I am able to add rows of input fields, etc. So my approach was to start with a $scope.formData empty object, to encapsulate all the properties that are bound to both static and dynamic HTML form elements.
The AngularJS code is as follows:
(function() {
var formApp = angular.module("formApp", []);
formApp.controller("FormCtrl", function ($scope, $timeout) {
$scope.formData = {};
$scope.formData.subscribers = [
{ name: null, email: null }
];
$scope.addSubscriber = function() {
$scope.formData.subscribers.push({ name: null, email: null });
};
});
})();
The HTML for the AngularJS form:
<body data-ng-app="formApp">
<div data-ng-controller="FormCtrl">
<p>
Name of Topic: <input type="text" data-ng-model="formData.title" placeholder="enter a title" />
</p>
Subscribers:
<button data-ng-click="addSubscriber()">Add subscriber</button>
<table>
<tr>
<th>Name</th>
<th>Email</th>
</tr>
<tr data-ng-repeat="subscriber in formData.subscribers">
<td><input type="text" data-ng-model="subscriber.name" placeholder="enter name" /></td>
<td><input type="text" data-ng-model="subscriber.email" placeholder="enter email" /></td>
</tr>
</table>
<hr style="margin:1em 0;" />
<p>
<em>Debug info</em>: {{ formData }}
</p>
</div>
</body>
Note the Debug info section at the end, which displays the $scope.formData object and its contents. I have a couple of issues with the way I have implemented this form.
When the page first loads, there is no formData.title property in the $scope, but since it is bound to the Title input text field, when I start typing a value, the title property gets added to the $scope. However when I delete the value in the input text field, the formData.title property still exists in the $scope as an empty string. I suppose this is okay, but I really want to clean up empty or null values when submitting the form. I would like to do it on the client side if it is easy to do so, so the server side code does not have to clean up anything.
With the dynamic Subscribers section, I can keep adding as many rows as I want, but ultimately, I would like to filter out all empty subscriber objects on the client side.
Does AngularJS have any options for detecting and cleaning null/empty values in the $scope before further processing, such as a $http POST?
Note I have set up a jsFiddle for this example.

Simply use ngModelController $parsers and overwrite the default HTML input element.
With this implementation you can have control over the model value all the time.
So in your case you can set the model to null whenever the view value is empty String.
var inputDefinition = function () {
return {
restrict: 'E',
require: '?ngModel',
link: function (scope, element, attr, ngModel) {
if (ngModel) {
var convertToModel = function (value) {
return value === '' ? null : value;
};
ngModel.$parsers.push(convertToModel);
}
}
};
/**
* Overwrite default input element.
*/
formApp.directive('input', inputDefinition);
Here is the updated JSFiddle: https://jsfiddle.net/9sjra07q/

I try and avoid using watchers for performance reasons. With that being said, this really isn't as much of an Angular question as it is a JavaScript question. Since you have total control over when the data is passed off to the service I would simply clean it up first. This is fairly simple since your data structure is so shallow.
https://jsfiddle.net/1ua6oj5e/9/
(function() {
var formApp = angular.module("formApp", []);
formApp.controller("FormCtrl", function ($scope, $timeout) {
// Remove junkiness
var _remove = function remove(item) {
if ($scope.formData.title === undefined || $scope.formData.title === '') {
delete $scope.formData.title;
}
};
$scope.formData = {};
$scope.formData.subscribers = [
{ name: null, email: null }
];
$scope.addSubscriber = function() {
$scope.formData.subscribers.push({ name: null, email: null });
};
// Submit to test functionality
$scope.submit = function() {
// Remove title if empty
_remove($scope.formData.title);
/* Remove name and email if empty.
* If your list of fields starts to get large you could always
* just nest another iterator to clean things up.
*/
angular.forEach($scope.formData.subscribers, function(sub) {
_remove(sub.name);
_remove(sub.email);
});
};
});
})();

function replacer(key, value) {
if (value == "" || value == null) {
return undefined;
}
return value;
}
var foo = {foundation: "", model: {year: 2015, price:null}, week: 45, transport: "car", month: 7};
foo = JSON.stringify(foo, replacer);
foo =JSON.parse(foo);
console.log(foo);

Added Watcher on formData,
$scope.$watch('formData',function(n,o){
if(typeof $scope.formData.title !== 'undefined' && $scope.formData.title === "" ){
delete $scope.formData.title;
}
},true);
Updated fiddle: https://jsfiddle.net/1ua6oj5e/6/
For all the dynamic fields you should use angular form validation, you should see this: https://docs.angularjs.org/guide/forms

Related

Number input box in Knockout JS

I'm trying to create a number input box which will accept numbers only.
My initial value approach was to replace value and set it again to itself.
Subscribe approach
function vm(){
var self = this;
self.num = ko.observable();
self.num.subscribe(function(newValue){
var numReg = /^[0-9]$/;
var nonNumChar = /[^0-9]/g;
if(!numReg.test(newValue)){
self.num(newValue.toString().replace(nonNumChar, ''));
}
})
}
ko.applyBindings(new vm())
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input type="text" data-bind="textInput: num" />
Now this approach works but will add another cycle of subscribe event, so I tried to use a custom binding so that I can return updated value only. New to it, I tried something but not sure how to do it. Following is my attempt but its not working. Its not even updating the observable.
Custom Binding attempt
ko.bindingHandlers.numeric_value = {
update: function(element, valueAccessor, allBindingsAccessor) {
console.log(element, valueAccessor, allBindingsAccessor())
ko.bindingHandlers.value.update(element, function() {
var value = ko.utils.unwrapObservable(valueAccessor());
return value.replace(/[^0-9]/g, '')
});
},
};
function vm() {
this.num = ko.observable(0);
this.num.subscribe(function(n) {
console.log(n);
})
}
ko.applyBindings(new vm())
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div>
<input type="text" data-bind="number_value: num, valueUpdate:'keyup'">
<span data-bind="text: num"></span>
</div>
So my question is, Can we do this using custom bindings and is it better approach than subscribe one?
Edit 1:
As per #user3297291's answer, ko.extenders looks more like a generic way for my subscribe approach. I'm looking for an approach (if possible in Knockout), which would clean value before it is set to observable.
I have taken reference from following articles:
How to update/filter the underlying observable value using a custom binding?
How can i update a observable in custom bindings?
Note: In the first example, they are using jQuery to set the value. I would like to avoid it and do it using knockout only
I´m on favor of use extender as user3297291's aswer.
Extenders are a flexible way to format or validate observables, and more reusable.
Here is my implementation for numeric extender
//Extender
ko.extenders.numeric = function(target, options) {
//create a writable computed observable to intercept writes to our observable
var result = ko.pureComputed({
read: target, //always return the original observables value
write: function(newValue) {
var newValueAsNum = options.decimals ? parseFloat(newValue) : parseInt(newValue);
var valueToWrite = isNaN(newValueAsNum) ? options.defaultValue : newValueAsNum;
target(valueToWrite);
}
}).extend({
notify: 'always'
});
//initialize with current value to make sure it is rounded appropriately
result(target());
//return the new computed observable
return result;
};
//View Model
var vm = {
Product: ko.observable(),
Price: ko.observable().extend({
numeric: {
decimals: 2,
defaultValue: undefined
}
}),
Quantity: ko.observable().extend({
numeric: {
decimals: 0,
defaultValue: 0
}
})
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Edit
I get your point, what about and regular expression custom binding to make it more reusable?
Something like this.
function regExReplace(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var observable = valueAccessor();
var textToReplace = allBindingsAccessor().textToReplace || '';
var pattern = allBindingsAccessor().pattern || '';
var flags = allBindingsAccessor().flags;
var text = ko.utils.unwrapObservable(valueAccessor());
if (!text) return;
var textReplaced = text.replace(new RegExp(pattern, flags), textToReplace);
observable(textReplaced);
}
ko.bindingHandlers.regExReplace = {
init: regExReplace,
update: regExReplace
}
ko.applyBindings({
name: ko.observable(),
num: ko.observable()
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input type="text" data-bind="textInput : name, regExReplace:name, pattern:'(^[^a-zA-Z]*)|(\\W)',flags:'g'" placeholder="Enter a valid name" />
<span data-bind="text : name"></span>
<br/>
<input class=" form-control " type="text " data-bind="textInput : num, regExReplace:num, pattern: '[^0-9]',flags: 'g' " placeholder="Enter a number " />
<span data-bind="text : num"></span>
I think you can divide the problem in to two parts:
Making sure the user can only input numbers, or
Making sure your viewmodel value is a number rather than a string.
If you only need part 1, I'd advice you to use default HTML(5) features:
<input type="number" step="1" />
<input type="text" pattern="\d*" />
If you want to make sure the user cannot enter any other than a number, and want to use the value in your viewmodel as well, I'd use an extender. By extending the observable, you can change its value before any subscriptions are fired.
The knockout docs provide an excelent example on their documentation page:
Note that when you use the extender, you don't need to worry about the pattern or type attribute anymore; knockout modifies the value instantly as soon as it's set.
ko.extenders.numeric = function(target, precision) {
//create a writable computed observable to intercept writes to our observable
var result = ko.pureComputed({
read: target, //always return the original observables value
write: function(newValue) {
var current = target(),
roundingMultiplier = Math.pow(10, precision),
newValueAsNum = isNaN(newValue) ? 0 : +newValue,
valueToWrite = Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier;
//only write if it changed
if (valueToWrite !== current) {
target(valueToWrite);
} else {
//if the rounded value is the same, but a different value was written, force a notification for the current field
if (newValue !== current) {
target.notifySubscribers(valueToWrite);
}
}
}
}).extend({
notify: 'always'
});
//initialize with current value to make sure it is rounded appropriately
result(target());
//return the new computed observable
return result;
};
var vm = {
changes: ko.observable(0),
myNumber: ko.observable(0).extend({
numeric: 1
})
};
vm.myNumber.subscribe(function() {
vm.changes(vm.changes() + 1);
});
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input type="text" data-bind="value: myNumber">
<div>2 times <span data-bind="text: myNumber"></span> is <span data-bind="text: myNumber() * 2"></span>.</div>
<div> Changes: <span data-bind="text: changes"></span></div>
Using Knockout Validation | LIVE PEN:
<input data-bind="value: Number">
ko.validation.init();
function VM() {
var self = this;
self.Number = ko.observable().extend({
required: true,
pattern: {
message: 'Invalid number.',
params: /\d$/
}
});
}
ko.applyBindings(window.v=new VM());
Following is a mimic of knockout's textInput binding, but with custom parsing. Note, I know, this has added few extra lines of duplicate code but I guess its worth.
I thought of creating my custom code, but reinventing the wheel will have lots of issues, hence appreciating Knockout teams effort and copying it.
Numeric Only - JSFiddle.
Characters Only - JSFiddle
I have updated code in following
updateModel: To fetch only parsed value from element. This will prevent updating incorrect value.
updateView: To check if user have entered incorrect value. If yes, replace previous value, else update previous value as current value and proceed.
Usability
I have tried to increase scope of this binding beyond this question. I have added a special data attributes (data-pattern and data-flag) to create regex will parse accordingly.

How to show different value of input element with ng-model?

In the controller if have a variable that tracks the index (starting at 0) of the page for a pagination table:
var page {
pageNumber: 0;
}
Question: how can I show this pageNumber variable in the html, but always incremented by +1? (as the index=0 page is obviously the 1st page and should thus be shown as Page 1)
<input type="text" ng-model="page.pageNumber">
Also, when the model gets updated, the value in the input should automatically change (again: also incremented by +1).
I think this is a use-case for $formatters and $parsers. They operate on the model's property and there is no need to create a dummy property on the model. Documentation here. Please correct me if this is not the use case for $formatters and $parsers.
Please see below.
HTML markup
<body ng-app="app" ng-controller="mainCtrl">
{{page}}
<input paginated-index type="text" ng-model="page">
</body>
js
var app = angular.module('app', []);
app.controller('mainCtrl', function($scope) {
$scope.page = 0;
});
app.directive('paginatedIndex', function()
{
return{
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModelController)
{
ngModelController.$formatters.push(function(value)
{
return value+1;
})
ngModelController.$parsers.push(function(value)
{
return value-1;
})
}
}
});
In your controller, change your page object to this:
$scope.page = {
displayedPage: function(num) {
if(arguments.length) {
$scope.page.pageNumber = num - 1;
return num;
} else {
return $scope.page.pageNumber + 1;
}
},
pageNumber: 0
}
And then yourelement to this:
<input type="text" ng-model="page.displayedPage" ng-model-options="{ getterSetter: true}" />
This will display the page number plus 1, but leave the actual page.pageNumber variable the way it should be.
The getterSetter: true options I've added in will bind the model to a getter/setter function, which allows you to pass in the argument - in this case, your entered page number - and return from that function. You can read more information on this in the documentation for ngModel
you can try using something like this.
$scope.data=$scope.page.pageNumber+1;
$scope.fuc=function(){
$scope.page.pageNumber=$scope.data-1;
};
and your Html will be like
<input type="text" ng-model="data" ng-change="fuc()" >
check this plunk Plunker

AngularJS currency filter on input field

I have the following input field
<input type="text" class="form-control pull-right" ng-model="ceremony.CeremonyFee | number:2">
it is showing up correctly but has been disabled. The error I am receiving is "[ngModel:nonassign] Expression 'ceremony.CeremonyFee | number:2' is non-assignable". I understand why it is in error, but do not know how to get this to work on an input field. Thanks.
input with ng-model is for inputting data, number filter is for displaying data. As filter values are not bindable, they are not compatible, as you can see. You have to decide what you want to do with that input.
Do you want it to be an input? User can input his own number and you only needs to validate? Use i.e. pattern attribute:
<input type="text" ng-model="ceremony.CeremonyFee" pattern="[0-9]+(.[0-9]{,2})?">
Do you want it to be an output? User does not need to input his own value? Do not use ng-model, use value instead:
<input type="text" value="{{ceremony.CeremonyFee | number:2}}" readonly>
UPDATE:
really I don't understand what you need, but, if you want just that users can insert only two digits you should use a simple html attributes, have a look on min, max, step...
Follows a pure js solution, but I don't suggest something like that!
angular.module('test', []).controller('TestCtrl', function($scope) {
var vm = $scope;
var testValue = 0;
Object.defineProperty(vm, 'testValue', {
get: function() { return testValue; },
set: function(val) {
val = Number(val);
if(angular.isNumber(val) && (val < 100 && val > 0)) {
console.log(val);
testValue = val;
}
}
});
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<section ng-app="test">
<div ng-controller="TestCtrl">
<input style="display:block; width: 100%; padding: 1em .5em;" type="number" ng-model="testValue" />
</div>
</section>
the ng-model directive requires a viewmodel assignable (or bindable) property, so, you cannot add a pipe...
angular.module('test', [])
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="test" ng-init="testValue = 0">
<label ng-bind="testValue | currency"></label>
<input style="display:block;" ng-model="testValue" type="number"/>
</div>
As an error states you have got an 'non-assignable' expression in your ng-model attribute.
You should use only ceremony.CeremonyFee.
| is used on ng-repeat to indicate what expression should be used as filter.
If you want to have that <input> populated with initial data in your controller/link you should give it an initial value ex.
$scope.ceremony = {
CeremonyFee: 'My first ceremony'
}
And every time your <input> element data will be changed CeremonyFee will be updated as well.
I found and used the solution found on this page.
http://jsfiddle.net/k7Lq0rns/1/
'use strict';
angular.module('induction').$inject = ['$scope'];
angular.module('induction').directive('format',['$filter', function ($filter) {
  return {
require: '?ngModel',
link: function (scope, elem, attrs, ctrl) {
if (!ctrl) return;
ctrl.$formatters.unshift(function (a) {
return $filter(attrs.format)(ctrl.$modelValue)
});
elem.bind('blur', function(event) {
var plainNumber = elem.val().replace(/[^\d|\-+|\.+]/g, '');
elem.val($filter(attrs.format)(plainNumber));
});
}
  };
}]);
relatively easy to apply it.

Does AngularJS store a value in the $error.maxlength object?

I've got a UI page setup through Angular, and I'm trying to take advantage of the built in ng-maxlength validator on an input element. Long story short, I know about $scope.form.$error and how that object has a maxlength property in the case that the validation fails. But I want to display an error message specific to the character length that was violated, and I don't see anywhere that the length that I specified was stored on this object. Does anyone know if it's possible to access this, so I don't have to write out a separate error message for each input that has the max length violated?
EDIT: To answer your question, yes angular does store a boolean value in the $error object that is accessible to your via the key(s) that are set in the object. In the case of the code I provided below and in th jsFiddle, we are setting the key for angular, and the value of either true or false.
Be mindful when setting the value as it is reversed. ex. $setValidity( true ), flips the $error to false.
Ok, here is what I think you were looking for...
In Angularjs v1.2.13 you will not have access to ng-message or the $validator pipeline,
which is why are are using $formatters and $parsers.
In this case, I am using named inputs, but perhaps in your case you need dynamic input names?
Plus, if you are using inputs but no form, then getting the error message to display would have to be done with a separate custom directive.
If so, then please look here for dynamically named input fields for some help.
dynamic input name in Angularjs link
Let me know if this works; I'll make changes as needed to HOOK YOU UP!
In case you don't know, you can write over Angular's maxlength for each individual input.
If you changed 'maxlength' in the updateValidity() function in the directive below, to something like 'butter', then $scope.form.inputname.$error would be something like
$scope.formname.inputname.$error { butter: true }
if you also used ng-maxlength="true", then it would be
$scope.formname.inputname.$error { butter: true, maxlength: true }
Another example if you used ng-maxlength, and capitalized the 'maxlength' in the directive to 'Maxlength'
Then you would get
$scope.formname.inputname.$error { maxlength: true(angular maxlength), Maxlength: true(your maxlength)
And of course if you name it the same, then yours writes over angulars
$scope.formname.inputname.$error { maxlength: true };
The point is YOU can add your own names to the angular $error object; you can write over Angular's; and you can just use what Angular gives you when you use Angular's directives: like ng-required="true", or ng-maxlength="true"
Link to YOUR angularjs version on jsFiddle
jsFiddle LInk
<div ng-app="myApp">
<form name="myForm">
<div ng-controller="MyCtrl">
<br>
<label>Input #1</label>
<br>
<input ng-model="field.myName" name='myName' my-custom-length="8" />
<span ng-show="myForm.myName.$error.maxlength">
Max length exceeded by {{ myForm.myName.maxlength }}
</span>
<br>
<br>
<label>Input #2</label>
<br>
<input ng-model="field.myEmail" name='myEmail' my-custom-length="3" />
<span ng-show="myForm.myEmail.$error.maxlength">
Max length exceeded by {{ myForm.myEmail.maxlength }}
</span>
</div>
</form>
</div>
var app = angular.module('myApp', []);
app.controller('MyCtrl', function ($scope) {
$scope.field = {};
});
app.directive("myCustomLength", function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
if (!ctrl) { return } // ignore if no ngModel controller
ctrl.$formatters.push(validateInput);
ctrl.$parsers.unshift(validateInput);
function validateInput(value) {
if (!value) {
updateValidity(false);
return;
}
inputLength(value);
var state = value.length > attrs.myCustomLength;
updateValidity(state);
}
function inputLength(value) {
ctrl.maxlength = null;
var length = value.length > attrs.myCustomLength;
if (length) {
ctrl.maxlength = (value.length - attrs.myCustomLength).toString();
}
}
function updateValidity(state) {
ctrl.$setValidity('maxlength', !state);
}
} // end link
} // end return
});
CSS Here if you need it.
input.ng-invalid {
border: 3px solid red !important;
}

AngularJS ng-model in template passed to directive controller

I've got a directive with a controller, that builds a form for posting comments to an API via CommentsService
My directive looks a bit lik this:
app.directive('appComments', function( CommentService ) {
return {
restrict: 'E',
scope: {
event: '='
},
controller: function( $rootScope, $scope, $element ) {
$scope.comments = [];
$scope.comment_text = '';
// load comments if event ID has changed
$scope.$watch( 'event', function() {
if( typeof $scope.event != 'undefined' ) {
CommentService.get( $scope.event ).then(
function( comments ) {
$scope.comments = comments;
}
);
}
});
// post comment to service
$scope.postComment = function() {
if( $scope.comment_text != '' ) {
CommentService.post(
$scope.event,
$scope.comment_text,
function() {
// code to reload comments
}
);
}
};
},
templateUrl: '/partials/comments.html'
};
});
This is my comments.html for the directive
<div class="event-comments">
<p ng-if="!comments.length">
<span>This event has no comments.</span>
</p>
<div
class="event-comment"
ng-repeat="comment in comments"
>
<div class="comment-text">{{comment.text}}</div>
</div>
</div>
<div class="insert-comment-container" ng-if="!loading">
<form ng-submit="postComment()">
<textarea
ng-model="comment_text"
></textarea>
<div
ng-tap="postComment()"
>Post</div>
</div>
</div>
This is how I'm placing it in my main view:
<app-comments event="event.id"></app-comments>
My comments are loading, and the event id is getting passed, but when I try and post a comment the comment_text is blank.
I think I'm getting my scopes mixed up or something, I'm still not completely clear on directives
** update **
I've just realised if I set
$scope.comment_text = 'Initial text'
in the directive, it appears when the template renders inside the textarea, and the if statement in the postComments() function fires. But if I change the text in the textarea, and tap the post button, the contents of $scope.comment_text is still "Initial text", it seems to be a one way binding.
Since you are using form i believe it creates a new scope scope. As per documentation
If the name attribute is specified, the form controller is published
onto the current scope under this name.
Try to give your form a name. Or else try to pass the property as object property like
<textarea
ng-model="comment.comment_text">
</textarea>
Ok so by changing comment_text to comment.text it solved the problem, as recommended by this SO answer:
angular-bootstrap (tabs): data binding works only one-way
Using an object instead of a primitive just uses the same reference to the object property, instead of copying the primitive into the new scope.

Categories

Resources