Rendering all form values after service call using AngularJS - javascript

I have an Angular article form that I'm populating using a service call. The problem is that, in order to get validation and data binding to work, I have to use $setViewValue and $render on each form input. What I'd like to do is simply set the data model for the form and then render the entire form somehow.
Here is a sample of what I have that works:
var _promise = articleService.getArticle($scope.params.articleId);
_promise.then(
function(data) {
$scope.articleForm.title.$setViewValue(data.item.title);
$scope.articleForm.title.$render();
$scope.articleForm.bodytext.$setViewValue(data.item.body);
$scope.articleForm.bodytext.$render();
$scope.articleForm.keywords.$setViewValue(data.item.keywords);
$scope.articleForm.keywords.$render();
},
function() {
$scope.setMessage('There was a network error. Try again later.', 'error');
}
);
The code below accomplishes the same result visually (just doesn't render bindings, like updating fields to dirty in validation):
var _promise = articleService.getArticle($scope.params.articleId);
_promise.then(
function(data) {
// $scope.article breakdown: article.title, article.body, article.keywords
$scope.article = angular.copy(data.item);
// some sort of complete form render???
},
function() {
$scope.setMessage('There was a network error. Try again later.', 'error');
}
);
HTML:
<form name="articleForm" novalidate role="form">
<!-- TITLE -->
<div class="form-group" ng-class="{ 'has-error' : (articleForm.title.$invalid && !articleForm.title.$pristine) || (submitted && articleForm.title.$pristine) }">
<label>Title</label>
<input type="text" name="title" ng-model="article.title" ng-minlength="3" required>
<p ng-show="(articleForm.title.$error.required && !articleForm.title.$pristine) || (submitted && articleForm.title.$pristine)" class="help-block">A title is required.</p>
<p ng-show="articleForm.title.$error.minlength" class="help-block">Title is too short.</p>
</div>
<!-- BODY -->
<div class="form-group" ng-class="{ 'has-error' : articleForm.bodytext.$invalid && !articleForm.bodytext.$pristine }">
<label>Article Body</label>
<div text-angular ng-model="article.body" ng-change="updateBody()" id="bodytext" name="bodytext"></div>
</div>
<!-- KEYWORDS -->
<div class="form-group" ng-class="{ 'has-error' : (articleForm.keywords.$invalid && !articleForm.keywords.$pristine) || (submitted && articleForm.keywords.$pristine) }">
<label>Keywords</label>
<input type="text" name="keywords" ng-model="article.keywords" ng-minlength="3" ng-maxlength="150" required>
<p ng-show="(articleForm.keywords.$error.required && !articleForm.keywords.$pristine) || (submitted && articleForm.keywords.$pristine)" class="help-block">At least one keyword is required.</p>
<p ng-show="articleForm.keywords.$error.minlength" class="help-block">Keywords is too short.</p>
<p ng-show="articleForm.keywords.$error.maxlength" class="help-block">Keywords is too long.</p>
</div>
</form>
I feel like there should a simple solution to this, since it's a common scenario, but I've searched high and low without a clear answer. Maybe I'm just approaching this the wrong way?

Well, I found a solution that works, but I don't think it's a better solution than just setting all the form fields individually. I'll post it here though, in case it helps anyone else in some way.
// set all the values via the angular model
$scope.article = angular.copy(data.item);
// without a timeout all the fields will still be unset (being asynchronous)
$timeout(function() {
// loop through all the form values
angular.forEach($scope.articleForm, function(value, key){
if(typeof value === 'object' && key !== '$error'){
// reset the view value and render, to process the updates on the form
$scope.articleForm[key].$setViewValue($scope.articleForm[key].$viewValue);
$scope.articleForm[key].$render();
}
});
});

Related

Getting user input text with $watch in AngularJS not working on ng-if

I have a cross platform app built using AngularJS, Monaca and OnsenUI.
I have a login view that checks if the user has logged in before by querying a SQLite database. Based on whether there is data in the database, I display a welcome message to the user OR I display the login text field.
I have a method that queries the SQLite database and this is working as intended. When a entry is found on the database I set a boolean value to display the welcome message - else the boolean displays the login text field.
On my view I do the following;
<!-- User not in DB -->
<div ng-if="showLoginUI">
<div class="input">
<input type="password" placeholder="User ID" ng-model="userID"/>
</div>
</div>
I watch for changes in the text field to save the user input, but no actions are registered in the example as above. This is my method to register user action on the text field.
$scope.$watch("userID", function (newVal, oldVal)
{
if (newVal !== oldVal) {
$scope.newUserID = newVal; // Not getting here
}
});
However, when I remove the ng-if from the example above - the user events are registered. How do I keep my ng-if while still registering events on the text-filed?
I tried adding a $timeout to my $watch function but this did not help either.
It happens because ngIf directive creates a child $scope.. the problem is that you're using ng-model without the Dot Rule or controller-as-syntax.
The whole problem was already explained by Pankaj Parkar in this question.
So, to make it work, you have to create a new object, ex:
$scope.model = {};
Then, build your ng-model like this:
ng-model="model.userID"
Take a look on this simple working demo:
angular.module('app', [])
.controller('mainCtrl', function($scope) {
$scope.model = {};
$scope.$watch("model.userID", function(newVal, oldVal) {
if (newVal !== oldVal) {
$scope.model.newUserID = newVal;
}
});
});
<html ng-app="app">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular.min.js"></script>
</head>
<body ng-controller="mainCtrl">
<button type="button" ng-click="showLoginUI = !showLoginUI">Hide/Show</button>
<div ng-if="showLoginUI">
<div class="input">
<input type="password" placeholder="User ID" ng-model="model.userID" />
</div>
</div>
<div ng-if="model.newUserID">
<hr>
<span ng-bind="'Working: ' + model.newUserID"></span>
</div>
</body>
</html>

When to use a directive, when a service and when a controller in angularjs?

I am a bit confused on when to use what in angularjs. I know the basic concept of controller, service/factory and directive but I'm not sure what to use in my case.
Scenario: A form that allows a user to post a link. The form itself requests some information about the link from an external service and presents it immediately to the user. Posting is possible via the API of a NodeJS app (not that that matters). The form should be reusable so I want the code to be DRY. I don't like the use of ng-include since directives seem to be the way to go.
So far I have a factory to deal with requesting information (linkservice) and a factory to deal with creating posts (posts). I then use a directive with it's own controller to display the form and handle user actions. But I'm not sure if I should move the content of the directives' controller into a normal controller or even a service, since directives shouldn't deal with requesting data (as I understand). Or maybe this is already the right way.
The Directive
// The form to publish a new post
myModule.directive('postForm', [
'linkservice',
'posts',
'$state',
function(linkservice, posts, $state){
return {
templateUrl : '/js/app/views/partials/post-form.html',
controller: function ($scope) {
$scope.analyzeURL = function() {
$scope.filtered_url = $scope.link.url.match(/(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,#?^=%&:/~+#-]*[\w#?^=%&/~+#-])?/gmi);
if($scope.filtered_url !== null) {
linkservice.extractURL($scope.filtered_url).then(function(res) {
var website_info = res.data;
$scope.link = {
title: website_info.title,
description: website_info.description,
medium: website_info.provider_name,
medium_thumbnail_url: website_info.favicon_url,
url: $scope.filtered_url[0]
}
// Image
if(website_info.images.length > 0 && website_info.images[0].width >= 500) {
$scope.link.thumbnail_url = website_info.images[0].url;
} else { $scope.link.thumbnail_url = null; }
// Keywords
$scope.link.keywords = [];
if(website_info.keywords.length >= 2) {
$scope.link.keywords[0] = website_info.keywords[0].name;
$scope.link.keywords[1] = website_info.keywords[1].name;
}
$scope.show_preview = true;
});
}
},
// addPost
$scope.addPost = function(){
if(!$scope.post || $scope.post.text === '' || !$scope.link || $scope.link.url === '') { return; }
posts.create({
post: $scope.post,
link: $scope.link
}).success(function() {
delete $scope.post;
delete $scope.link;
});
}
}
}
}]);
The template
<form ng-submit="addPost()" style="margin-top:30px;">
<h3>Add a new Post</h3>
<div class="form-group">
<input type="text"
class="form-control"
placeholder="URL"
ng-model="link.url" ng-change="analyzeURL()"></input>
</div>
<div class="form-group">
<textarea type="text"
class="form-control"
placeholder="Description / TLDR"
ng-model="post.text" ></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Post</button>
</div>
<div class="form-group">
<input type="hidden" ng-model="link.title"></input>
<input type="hidden" ng-model="link.description"></input>
<input type="hidden" ng-model="link.thumbnail_url"></input>
<input type="hidden" ng-model="link.medium"></input>
<input type="hidden" ng-model="link.medium_thumbnail_url"></input>
<input type="hidden" ng-model="link.keywords"></input>
</div>
<div class="lp-container" ng-show="show_preview">
<span class="lp-provider"><img src="{{link.medium_thumbnail_url}}" class="lp-favicon"> {{link.medium}}</span>
<h2 class="lp-title">{{link.title}}</h2>
<div class="lp-description">{{link.description}}</div>
<img class="lp-thumbnail" ng-show="link.thumbnail_url" src="{{link.thumbnail_url}}">
<div class="lp-keywords">
<span ng-repeat="kw in link.keywords" class="lp-keyword">{{kw}}</span>
</div>
</div>
</form>
The best way to do it, is to keep in mind that Angular is a MVVM-like framework.
Your directives define the view, how to print data, events, etc.
Your services are singletons so they are the best place to store data and to put all data management (web services requests, etc). As they will be instanciated only once, your data won't be duplicated.
Your controllers are instanciated each time you link them to a directive (ng-controller etc.). So you should avoid to store data here. Controllers should be used as link between services and directives. They could contains low-level data check etc, then call the services.
In your example you can simplify you code by moving your controller in another place to avoid to mix it all. Ex: Here your directive directly depends to linkservice when it's only the controller which needs it.

Angular ng-options behaves odd

I have this typahead with angular strap:
<div class="form-group" ng-class="newGroupForm.placeReference.$invalid ? 'has-error' : ''">
<label for="placeReference" class="col-lg-2 control-label">Group Location</label>
<div class="col-lg-10">
<input type="text" name="placeReference"
ng-model="newGroup.reference"
ng-options="place.reference as place.name
for place in getPlaces($viewValue)"
bs-typeahead min-length="0" required >
</div>
</div>
getPlaces returns array of objects which looks like this:
{
reference: "ccj32213SIJD",
name: "some name",
}
When I am typing I am getting correct results, but when I select the wonted option the value that I see in my input is the reference (instead of the name).
Can any one point out my mistake?
Here is the controller code:
$scope.getPlaces = function(viewValue) {
var input = viewValue || $scope.currentPlace;
return googleService.placesAutocomplete(input).then(
function(places) {
return places;
}
);
};
If ng-options here behaves exactly like <select ng-options> (sorry, I'm not familiar with bs-typeahead directive), then you should change it to:
<input ...
ng-model="selectedPlace"
ng-options="place as place.name for place in getPlaces($viewValue)">
Then you can use get the name elsewhere:
<div>{{selectedPlace.name}}</div>
It's probably best to get the actual place object {name: "..", reference: "..."}, but if you just need the name, then you could do this:
<input ...
ng-model="selectedPlaceName"
ng-options="place.name as place.name for place in getPlaces($viewValue)">

live search with angularjs

In my angularjs app http://1ffa3ba638.url-de-test.ws/zombieReport/popup.html#/lord i try to make a live instant search : i want nothing show at starting and search start when two letters minimum is writed. And after the searching is again performed for 3 or more letters, new query for each new letter.
/* INIT .myForm */
$scope.myForm_lord = {};
$scope.posts = {};
/* AJAX POST QUERY : calling lord watching */
$scope.$watch('myFormZR_lord.$valid', function() {
// ng-show things
$scope.successLordZR = true;
// RETRIEVE DATAS
var dataName = $scope.myForm_lord.search;
// CONSOLE LOG CONTROL
console.log(defineCLC + "LORD search requested by name : " + dataName);
// $resource
var Post = $resource(
urlLordRest,
{name: dataName},
{'query': {method: 'GET', isArray: true, cache: false, headers: {'Accept': 'application/json'}}}
);
$scope.posts = Post.query();
console.log($scope.posts);
});
html:
<form name="myFormZR_lord" id="myFormZR_lord" class="form-horizontal" role="form" novalidate="">
<div class="form-inline form-inline-sm">
<!-- form search -->
<div class="input-group">
<input class="form-control" name="search" type="text" ng-required="true" minlength="2" ng-minlength="2" ng-model="myForm_lord.search" placeholder="{{ 'TRS_CTRL3_FORM1' | translate }}" autocomplete="off" />
</div>
</div>
<span class="myForm_error" ng-show="myFormZR_lord.$invalid">{{ 'TRS_CTRL3_FORM2' | translate }}</span>
</form>
<div ng-show="successLordZR">
<p>{{ 'TRS_CTRL3_TEXT1' | translate }} :</p>
<ul>
<li ng-repeat="post in posts">
<p>{{post.prefixe}} {{post.name}}</p>
</li>
</ul>
</div>
Problem is actually results are showing at starting, and there is only a query for two letters, not if we put 3 or more. And the query is executed two times (see console log), what's wrong ? i use $watch, it's not good ?
You are watching for the change in the validity of the form. You put in minlength as 2 so as soon as you type in two characters the form becomes valid and thus fires your request. As soon as you backspace from 2 to 1 characters there is a change of validity again and thus firing another request. Instead of watching for the form validity, watch for changes in the form.

AngularJS form Validation not working with directives

i'm trying to do basic validation of the form fields in angular. It works properly when i inline the input field(s) with the form, however when I'm trying to use directives for inserting the input form field, the validation does not work as expected.
here is the JSbin showing the problem. I would appreciate some help thanks!
http://jsbin.com/sufotecenovi/2/edit
How Angular Validation Works
Angular uses the 'name' attribute to create the $scope variables used for validation.
For example, if you have an input field with a 'required' attribute:
<form name="myform">
<input name="firstname" ng-model="firstname" type="text" required/>
</form>
Then to access the validation properties on $scope, you would do:
var validationFailed = $scope.myform.firstname.$error.required;
Where $error is an object that has 'required' as a Boolean property.
In the 'required' directive, you would see something like this:
if(attrs.value == ''){
ngModel.$setValidity('required', true); // failed validation
} else {
ngModel.$setValidity('required', false); // passed validation
}
You can pass any string to $setValidity, and it will set the $error property for you. For example, if you did:
$setValidity('test', true)
Then there would be an $error property named 'test' and it would be set to true. You can then access the property like this:
$scope.myform.firstname.$error.test
Other validation properties that are available are:
$scope.myform.firstname.$valid
$scope.myform.firstname.$invalid
$scope.myform.firstname.$pristine
$scope.myform.$valid
$scope.myform.$invalid
$scope.myform.$pristine
Hope this helps to answer your question.
Click Here
you can use this code.
function MyCtrl($scope) {
$scope.formFields = [
{
name: 'firstName',
type: 'text'
},
{
name: 'email',
type: 'email'
}
];
}
myApp.directive("dynamicName",function($compile){
return {
restrict:"A",
terminal:true,
priority:1000,
link:function(scope,element,attrs){
element.attr('name', scope.$eval(attrs.dynamicName));
element.removeAttr("dynamic-name");
$compile(element)(scope);
}
};
});
<div ng-controller="MyCtrl">
<form name="myForm">
<p ng-repeat="field in formFields">
<input
dynamic-name="field.name"
type="{{ field.type }}"
placeholder="{{ field.name }}"
ng-model="field.value"
required
>
</p>
<code class="ie">
myForm.firstName.$valid = {{ myForm.firstName.$valid }}
</code>
<code class="ie">
myForm.email.$valid = {{ myForm.email.$valid }}
</code>
<code class="ie">
myForm.$valid = {{ myForm.$valid }}
</code>
<hr>
</form>
</div>

Categories

Resources