idiomatic way of having models and looped forms in angularjs - angularjs

If I'm building an index page for a blog in AngularJS with comments like this:
<li ng-repeat="post in posts">
<h2>{{post.title}}
<p>{{post.description}}</p>
<h3>Leave a comment</h3>
<form ng-submit="postComment()">
<input type="hidden" value="{{post.id}}">
Name: <input type="text"> <br>
Comment: <textarea></textarea> <br>
<input type="submit" value="Post comment">
</form>
</li>
I can't figure out what the right way is to associate a model with the form to access from the controller's postComment() function. If you use just one model for all the forms, then starting to leave a comment on one post will start to leave a comment on all of them—unless you do some weird stuff with the hidden post.id field, but that starts to feel unidiomatic.

You can pass an object into the function call.
<form ng-submit="postComment(post.id)">
As far as the other inputs, it really depends on the situation, but in this case, the input and textarea is inside an ngRepeat, which creates new scope. For that reason, assigning to a value on the scope will not affect other elements on the page.
<li ng-repeat="post in posts">
<h2>{{post.title}}
<p>{{post.description}}</p>
<h3>Leave a comment</h3>
<form ng-submit="postComment(post.id, commentName, commentText)">
Name: <input type="text" ng-model="commentName"> <br>
Comment: <textarea ng-model="commentText"></textarea> <br>
<input type="submit" value="Post comment">
</form>
</li>

is there any way of taking advantage of the two way communication there? (e.g. add an errmsg field from an AJAX call done in postComment or clear the form on submit?)
I suggest a directive with its own controller. With this approach, the comment model is encapsulated in the directive. The directive needs two pieces of information: 1) the postID 2) what function to call to save the comment. To support showing an error message returned from the server, a callback function is passed along with the comment. This allows the controller to feed the directive any error messages or any other information it might need after a save operation attempt.
app.directive('commentForm', function() {
var template = '<form name="commentForm" '
+ 'ng-submit="save({comment: comment, callbackFn: callbackFn})">'
+ '<h3>Leave a comment</h3>'
+ 'Name: <input type="text" ng-model="comment.name" name="commentName"> <br>'
+ '<span class="error" ng-show="commentForm.commentName.$error.myError">Error</span><br>'
+ 'Comment: <textarea ng-model="comment.text"></textarea> <br>'
+ '<input type="submit" value="Save comment">'
+ '</form>';
return {
restrict: 'E',
template: template,
scope: { postId: '#', save: '&' },
controller: function($scope) {
$scope.callbackFn = function(args) {
console.log('callback', args);
if(args.error.name) {
$scope.commentForm.commentName.$setValidity("myError", false);
} else {
// clear form
$scope.comment.name = '';
$scope.comment.text = '';
}
};
}
};
});
app.controller('MainCtrl', function($scope, $timeout) {
$scope.post = {id: 1};
$scope.saveComment = function(comment, callbackFn) {
console.log('saving...', comment);
// Call $http here... then call the callback.
// We'll simulate doing something with a timeout.
$timeout(function() {
if(comment.name === "name") {
callbackFn( {error: {name: 'try again'}} );
} else {
callbackFn( {error: {}} );
}
}, 1500)
}
});
Use as follows:
<comment-form post-id="{{post.id}}" save="saveComment(comment, callbackFn)"></comment-form>
Note the somewhat odd syntax related to the '&' syntax: when the directive is used in the HTML, you specify arguments for the saveComment() function. In the directive/template, an object/map is used to associate each argument name with its value. The value is a local/directive scope property.
Plnkr. In the plnkr I simulated an AJAX post using a $timeout of 1.5 seconds. If you enter name in the name textbox and click the save button, you'll get an error in 1.5 seconds. Otherwise the form clears after 1.5 seconds.
Depending on how far you want to take this... you could encapsulate your post template into a directive too:
<li ng-repeat="post in posts">
<post=post></post>
<comment-form ...></comment-form>
</li>
You could also put the comment-form inside the post directive template, simplifying the HTML even further:
<li ng-repeat="post in posts">
<post=post></post>
</li>
You could also have a post-list directive, and its template would contain the ng-repeat, reducing the HTML to a single element:
<post-list posts=posts></post-list>
I personally haven't decided how far one should go with custom directives yet. I would be very interested in any comments people have about this.

Related

Angularjs: How to get value of input without ng-model

I need to make some inputs by ng-repeat, and in my json file I have in object where is a property called name, like this:
"url":"find_company",
"values":[
{
"name":"company name",
"type":"input_search"
},{
"name":"company_phone",
"type":"input_search"
}
]
I want to make search in DB, in search you can find by any field or by two or more field. Field called the same as property of object. So by ng-keyup I need to send to my function
search(field, value)
two arguments. I want to do something like this
<div ng-repeat="value in param.values">
<input ng-if="value.type == 'input_search'"
ng-keyup="search(value.name, this.text)"
type="text">
How can a send to function text of this input without using ng-model? Where this.text is value of input.
since you are using ng-keyup, you can retrieve input value with $event.target.value.
comment: this is fit for normal event like onclick, but not fit for angular.
refer the below example.
angular.module("app", [])
.controller("myCtrl", function($scope) {
$scope.showValue = function(val) {
alert(val);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
<div ng-app="app" ng-controller="myCtrl">
<input type="test" ng-keyup="showValue($event.target.value)">
</div>
This is how you do it with ngModel:
<div ng-repeat="value in param.values">
<input ng-if="value.type == 'input_search'" ng-model="value.val" ng-keyup="search(value)" type="text">
And in your controller:
$scope.search = function( item ) {
console.log( item.val ); // Here you have the value using ngModel
console.log( item.name ); // Here you have the "name" property of the element inside the loop
}
As you can see, you CAN use ngModel and by passing the object itself to the function you can access its properties from the function in the controller.
Note that there's that this.text in the view - I don't know what it is exactly so I dropped it from the example to make things clearer, but you can use it in your code of course.
I know the question said without using ng-model. But I suspect you may want this because you want to customize when data-binding occurs. If that's the case, you can use ng-model-options with ng-change:
<input type="text" ng-model="yourModel" ng-model-options="{ updateOn: 'keyup' }" ng-change="search()" />
ng-change fires when the model has been updated, which is after keyup in this case. So the value of yourModel will be up to date when search() executes.

Directive template - use attribute for html text interpolation?

Angular 1.*
I am using a directive to make my code drier... or attempting to. But because of variances in the json data structure, I am not sure it's possible because of the interpolation text for each radio button.
<ppv-filter filter-name="Region" radio-value="choice.code"
sort="description" filter-data="regions"></ppv-filter>
<ppv-filter filter-name="Market" display-prop="description"
filter-data="markets"></ppv-filter>
<ppv-filter filter-name="Dealer" display-prop="code"
filter-data="dealers"></ppv-filter>
directive template:
<div ng-if="filterName === 'Region'">
<div ng-repeat="choice in filterData| orderBy: sort">
<input type="radio" value="{{choice.code}}" name="regionRadio">
{{choice.description}}
</div>
</div>
<div ng-if="filterName === 'Market'">
<div ng-repeat="choice in filterData| orderBy: 'code'">
<input type="radio" name="bob">
{{choice.code}}
</div>
</div>
<div ng-if="filterName === 'Dealer'">
<div ng-repeat="choice in filterData| orderBy">
<input type="radio" name="foo">
{{choice}}
</div>
</div>
angular.module('app')
.directive('ppvFilter', ['regionMarketDealerSrv',
function(regionMarketDealerSrv) {
return {
templateUrl: 'app/ppv/ppv-filter.dir.html',
restrict: 'E',
scope: {
filterName: '#',
sort: '#',
radioValue: '#',
filterData: '='
},
Is it possible to pass a attribute binding to take the place, for example, of {{choice.description}}? If not, then I am not really making my code drier by reusing a directive with so many code ng-if blocks.
I would create controller inside Your directive and inside it check properties sended to scope, in this particular example best would be switch statement. So in the switch set which param should be used in view.
( pseudo code in link or controller of directive )
switch (scope.filterName){
case "Market":
scope.field="code";
break;
//other possibilities
}
Next in view we need only one structure by using array syntax [field].
<div>
<div ng-repeat="choice in filterData| orderBy: 'code'">
<input type="radio" name="bob">
{{choice[field]}}<!-- HERE MOST IMPORTANT -->
</div>
</div>
I see that sorting also changes, so create second variable for sort type and assign it in the same switch in controller.
One more thing, directive attributes (props) assigned from parent scope can be used without any controller code, props are available in view, so it can be used in the same syntax like - {{someArr[filterName]}} where filterName was directive attribute.
Returning to Your problem - if we send by attribute name of property which should be used in view for example column:'#' and example value would be code,description then in view only {{choice[column]}} is needed.

Form validation with modals in Angular

I have a form inside a modal pop up. I am trying to run form validation on the inputs after the user attempts to submit the form. So far, I'm struggling to make things work.
In my view, I have the following (sorry if there are any syntax errors, I'm converting this from jade on the fly):
<script type="text/ng-template", id="modalVideoNew">
<div class="ngdialog-message">
<form class="form-horizontal" ng-submit="submitForm()" novalidate name="newVideoForm">
...
<div class="form-group">
<label> Title </label>
<div class="col-sm-8">
<input type="text" name="title", required='', ng-model="newVideoForm.title">
<span class="text-danger" ng-show="validateInput('newVideoForm.title', 'required')"> This field is required</span>
</div>
</div>
</div>
</script>
And then in my controller, where I'm calling the ng-dialog pop up, I have this:
$scope.newVideo = function() {
ngDialog.openConfirm({
template: 'modalVideoNew',
className: 'ngdialog-theme-default',
scope: $scope
}).then(function() {
$scope.validateInput = function(name, type) {
var input = $scope.newVideoForm[name];
return (input.$dirty || $scope.submitted) && input.$error[type];
};
var newVideo = $scope.newVideoForm;
...
Right now, I am still able to submit the form, but once I open it back up I see the 'This field is required' error message. Also, the input is pre-filled with [object, Object] instead of an empty text input box.
A way of cleaning your model would work with using a model var that belongs to your parent controller and cleaning it in the callback. Check out how the template has attached your parent controller's var FormData.
Check this out
So about your validation, what I would recommend you is to have your own controller in it, no matter how much code it will have. It helps you keeping concepts of modularization and a better control over your scopes. This way will also facilitate a lot when validating.

Angular directive with custom / conditional actions

I have questions about Angular directives. The following is my code:
main controller & the directive:
<div ng-controller='ShopsController'>
<update-createform shop="shop" action='update()'></update-createform>
</div>
directive js:
(this way the direction action will take the 'action' input argument)
angular.module('app')
.directive('updateCreateform', function(){
return {
templateUrl: '/form.html',
restrict : 'E',
scope: {
shop: '=',
action: '&'
}
}
})
form.html template:
<form name="shopForm" ng-submit='action(shopForm.$valid)' novalidate>
<input type='text' name='name' required/>
<input type='text' name='description' required/>
</form>
ShopsController has a method:
exports.update = function(isValid) {
if (isValid) { /* update the shop*/ }
}
What I am doing is I am passing the shop data I get from the server, send it into the form so I can view and/or update the shop info.
It's also that I want to create shop info using the same form. In this case I just send in shop = [] and action='create()' instead.
My controller has an update method that takes the argument isValid. I don't know how to pass the directive shopForm.$valid outside and send it to server.
Two questions:
how do I get isValid variable from the directive?
Following Ari Lerner's ng-book: He said it's possible to do the following:
http://www.scribd.com/doc/215682987/NG-Book-The-Complete-Book-on-AngularJS-2013
instead of using directive above we use
<update-createform shop="shop" on-update='update()' on-create='create()'></update-createform>
and the directive 'action' will change to 'update' when shop is not empty otherwise action equals to 'create'? I tried his code but I cannot get it to work..
Any help would be greatly appreciated!
You can add an argument to action=update(isValid). This then gets resolved on the form submit.
So your html would look like this
<div ng-controller='ShopsController as shopCtrl'>
<update-createform shop="shop" action='shopCtrl.update(isValid)'></update-createform>
</div>
And your form would look like like this
<form name="shopForm" ng-submit='action({isValid:shopForm.$valid})' novalidate>
<input type='text' name='name' required/>
<input type='text' name='description' required/>
<button type="submit">Submit</button>
</form>
and controller would be
.controller('ShopsController', function() {
var exports = this;
exports.update = function(isValid) {
console.log(isValid)
if (isValid) { /* update the shop*/ }
}
})
http://plnkr.co/edit/Qh3HzKGnOo1NTP9Pfsmh?p=preview
OR
There's another way, although personally i find the syntax a little odd. Not that the first solution feels that intuitive either.
http://plnkr.co/edit/CRN9ruRekJiozJIBTe80?p=preview
Found that one in an excellent post about directives by Dan Wahlin
http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-3-isolate-scope-and-function-parameters

Angular version of KnockoutJS' way to bind to scope for field shortcuts

In knockout.js it's possible to bind to an observable object inside the ViewModel:
function MyViewModel(data) {
var self = this;
this.user = new User(data);
}
And use it like this in the View:
<div data-bind="with: user">
// we can do this
<div data-bind="text: FirstName"></div>
// instead of this
<div data-bind="text: user.FirstName"></div>
</div>
Is there an equivalent of this in Angular?
As a Proof-Of-Concept example, I hacked together a small directive to do what you asked for.
DISCLAIMER:
It is just a Proof-Of-Concept demo and by no means is it thoroughly tested or guaranteed to work as expected in all cases. Yet, I believe it proves what you want is doable (with just a few lines of code) and even works inside forms (which is a useful and widely used feature).
So, here it is (scroll to the bottom for a live demo):
.directive('ngModelWith', function () {
return {
restrict: 'A',
scope: false,
template: function (tElem, tAttrs) {
var prefix = tAttrs.ngModelWith ? tAttrs.ngModelWith + '.' : '';
if (prefix) {
angular.forEach(tElem.children(), function (child) {
if (child.hasAttribute('ng-model')) {
child.setAttribute('ng-model',
prefix + child.getAttribute('ng-model'));
}
});
}
return tElem.html();
}
};
});
Then, you can use it like this:
<div ng-model-with="user">
<input type="text" name="first" ng-model="firstName" required />
<input type="text" name="last" ng-model="lastName" required />
</div>
$scope.user = {
firstName: '...',
lastName: '...'
};
or even put it inside a form/ngForm:
<div ng-form="form1">
<div ng-model-with="user">
...
</div>
</div>
Note:
For the sakes of simplicity and brevity, I ignore alternative forms of defining a directive, e.g. ng:model, data-ng-model, x-ng-model etc.
You need to either make sure you always use ng-model on elements inside of an ngModelWith parent or enhance the directive to look for all alternatives (which isn't that complex anyway).
See, also, this short demo.

Resources