Background:
I am currently working on an application with tabs; and I'd like to list the fields / sections that fail validation, to direct the user to look for errors in the right tab.
So I tried to leverage form.$error to do so; yet I don't fully get it working.
If validation errors occur inside a ng-repeat, e.g.:
<div ng-repeat="url in urls" ng-form="form">
<input name="inumber" required ng-model="url" />
<br />
</div>
Empty values result in form.$error containing the following:
{ "required": [
{
"inumber": {}
},
{
"inumber": {}
}
] }
On the other hand, if validation errors occur outside this ng-repeat:
<input ng-model="name" name="iname" required="true" />
The form.$error object contains the following:
{ "required": [ {} ] }
yet, I'd expect the following:
{ "required": [ {'iname': {} } ] }
Any ideas on why the name of the element is missing?
A running plunkr can be found here:
http://plnkr.co/x6wQMp
As #c0bra pointed out in the comments the form.$error object is populated, it just doesn't like being dumped out as JSON.
Looping through form.$errors and it's nested objects will get the desired result however.
<ul>
<li ng-repeat="(key, errors) in form.$error track by $index"> <strong>{{ key }}</strong> errors
<ul>
<li ng-repeat="e in errors">{{ e.$name }} has an error: <strong>{{ key }}</strong>.</li>
</ul>
</li>
</ul>
All the credit goes to c0bra on this.
Another option is to use one of the solutions from this question to assign unique names to the dynamically created inputs.
I made a function that you pass the form to. If there are form errors it will display them in the console. It shows the objects so you can take a look. I put this in my save function.
function formErrors(form){
var errors = [];
for(var key in form.$error){
errors.push(key + "=" + form.$error);
}
if(errors.length > 0){
console.log("Form Has Errors");
console.log(form.$error);
}
};
Brett DeWoody's answer is correct. I wanted to do the logic in my controller though. So I wrote the below, which is based off of the answer user5045936 gave. This may also help some of you who want to go the controller route. By the way Im using the toaster directive to show my users validation messages.
if (!vm.myForm.$valid) {
var errors = [];
for (var key in vm.myForm.$error) {
for (var index = 0; index < vm.myForm.$error[key].length; index++) {
errors.push(vm.myForm.$error[key][index].$name + ' is required.');
}
}
toaster.pop('warning', 'Information Missing', 'The ' + errors[0]);
return;
}
If you have nested forms then you will find this helpful:
function touchErrorFields(form) {
angular.forEach(form.$error, function (field) {
angular.forEach(field, function(errorField) {
if (!errorField.hasOwnProperty('$setTouched')) {
touchErrorFields(errorField);
} else {
errorField.$setTouched();
}
})
});
}
Related
I'm working on a vuejs/(vuex) for state management/firebase project of posts.
So I have a firestore collection of posts (array of objects that have name id owner content and timestamp for creation ...)
I'm retrieving that data by using the onSnapshot methode and it's stored on blogPosts variable... and we show theme all, when a user want to visit a single post it will redirect him to the route of the single post (..../view-post/postid) and i filter that array using the id of the post to have an array of one element (which is the post he visited)
when the filter complete i got all the data and i fill theme on the template like these
<template>
<NavBarView />
<section class="preview" v-if="currentBlog">
<h2>{{ currentBlog[0].blogTitle }}</h2>
<p>Posted on: <span>{{ dateFormat() }}</span> <span v-if="currentBlog[0].editTime">(Last edit:
{{ editFormat() }})</span></p>
<img class="blogCover" :src="currentBlog[0].blogCoverFileURL" :alt="currentBlog[0].blogCoverPhoto">
<div class="html-blog" v-html="currentBlog[0].blogHTML"></div>
<hr />
</section>
</template>
<script>
export default {
data() {
return {
currentBlog: null
}
},
mounted: {
this.currentBlog = this.blogPosts.filter((post) => {
return post.blogID == this.$route.params.id
})
},
computed: {
...mapState(['blogPosts'])
}
//note that i have exported all the requirements such as mapState and firebase functions ... didn't write theme here
}
</script>
now the problem is that the filter is occurring before the data are fetched from firebase and i can't handle that so it's always returning can't read property of null (currentBlog[0])
i found a solution which is sending a getDoc request to firebase but it's a bad idea, (why i send request and i have all the posts here so i can filter it directly and get the specific post) which didn't work!!!!!
any solution??
Looking at your template, I'm under the impression currentBlog should not be an array, as you don't ever have more than 1 element in that array (you're filtering by blogID, which I'm guessing is a unique identifier). You need to .find() the blog entry, not .filter() it:
computed: {
blogID() {
return this.$route.params.id;
},
currentBlog() {
return this.$store.state.blogPosts.find((b) => b.blogID === this.blogID);
}
}
Note: if state.blogPosts does not start as an empty array (as it should), you might want to use:
currentBlog() {
return (this.$store.state.blogPosts || []).find(
(b) => b.blogID === this.blogID
);
}
And now replace all currentBlog[0]s with currentBlog:
<template>
<NavBarView />
<section class="preview" v-if="currentBlog">
<h2>{{ currentBlog.blogTitle }}</h2>
<p>
Posted on: <span>{{ dateFormat() }}</span>
<span v-if="currentBlog.editTime">(Last edit: {{ editFormat() }})</span>
</p>
<img
class="blogCover"
:src="currentBlog.blogCoverFileURL"
:alt="currentBlog.blogCoverPhoto"
/>
<div class="html-blog" v-html="currentBlog.blogHTML"></div>
<hr />
</section>
</template>
Important: make sure in every single method/computed/watch in the script part of the component, if using currentBlog, you're first checking if it's not falsy. Generic example:
computed: {
blogHTML() {
return this.currentBlog?.blogHTML
/* shorthand for:
* return this.currentBlog && currentBlog.blogHTML
*/
}
}
I would suggest that rather than trying to do this operation once on mounted that you instead re-run the filter every time that state.blogPosts is updated. You can easily do this through a computed property. See below:
export default {
data() {
return {
}
},
computed: {
...mapState(['blogPosts']),
currentBlog(){
const posts = this.$store.state.blogPosts;
if(Array.isArray(posts)) {
return posts.filter((post) => {
return post.blogID == this.$route.params.id
});
} else {
return null
}
}
}
}
Hey I am following another guide and really struggling to get it working for me. Somewhat new to Angular so I am sure this is a simple issue. Can anyone help me?
The front end shows all the JSON objects at the page load but when I type anything they all disappear.
_ninjaFilter:string
get ninjaFilter():string{
return this._ninjaFilter;
}
set ninjaFilter(value:string){
this._ninjaFilter = value
console.log(this.filteredNinjas, this.ninjaFilter)
this.filteredNinjas = this.ninjaFilter ? this.performFilter(this.ninjaFilter) : this.ninjas
}
performFilter(filterBy: string): any{
filterBy = filterBy.toLocaleLowerCase();
console.log(filterBy)
return this.ninjas.filter(ninja=>{
ninja.name.toLocaleLowerCase().includes(filterBy)
//tried a if statement here to console log match and it does log out match
//also have tried .indexOf(filterby) !== -1
})
}
filteredNinjas: any
ninjas=[{
'name':'yoshi',
'belt':'red'
},
{
'name':'luigi',
'belt':'black'
},
{
'name':'Ryu',
'belt':'green'
}]
constructor(private route: ActivatedRoute) {
this.filteredNinjas = this.ninjas //create new array to filter on
this.ninjaFilter='' //set initial filter string to null
}
and the view:
<h2>Ninja Listing</h2>
<input type='text' id="filter"
[(ngModel)]='ninjaFilter' />
<ul id="ninja-listing">
<li *ngFor="let ninja of filteredNinjas">
<div class='single-ninja'>
<span [ngStyle]="{background: ninja.belt}">{{ninja.belt}} belt</span>
<h3>{{ninja.name}}</h3>
</div>
</li>
</ul>
Here is console log (first page load and then me typing)
(3) [{…}, {…}, {…}] "r"
directory.component.ts:23 r
directory.component.ts:17 [] "ry"
directory.component.ts:23 ry
directory.component.ts:17 [] "ryu"
directory.component.ts:23 ryu
You don't return anything inside your filter function. You should return a condition there:
return this.ninjas.filter(ninja => {
return ninja.name.toLocaleLowerCase().includes(filterBy);
});
I am having below data as input json,
"values":[
{"_attributes":{"name":"data.domain"},"_text":"${url}"},
{"_attributes":{"name":"data.port"}},
{"_attributes":{"name":"data.comments"},"_text":"Defaults Comments"},
{"_attributes":{"name":"data.concurrent"},"_text":4}]
]
I am showing comments in my html page using this directive,
<b style="padding-top:5px;"> Comments:</b>
<span class="input">
<input class="inputtxt" type="text" ng-repeat="x in ValueArr" ng-if="x._attributes.name == 'data.comments'" ng-model="x._text">
</span>
which is working fine and showing as expected. But the problem is that sometimes, the comments node
{"_attributes":{"name":"data.comments"},"_text":"Defaults Comments"},
may not be present in array. like this,
"values":[
{"_attributes":{"name":"data.domain"},"_text":"${url}"},
{"_attributes":{"name":"data.port"}},
{"_attributes":{"name":"data.concurrent"},"_text":4}]
]
In that case it shows only "Comments:" and blank screen after that. It does not show input box. In this case, I want to show empty input box. I tried ng-default and and ng-init but that didn't work. How to display input box for absence of matching condition ?
jsfiddle
Not getting any idea, how to achieve that. Please suggest something.
Thanks
The solution works with ng-init.
Just put it on the input like this:
<span class="input" ng-init="initValues()">
<input class="inputtxt" type="text" ng-repeat="x in values" ng-show="x._attributes.name == 'data.comments'" ng-model="x._text" ng-init="initInput(x)">
</span>
And add the function in the Controller:
$scope.initInputComments = function(x) {
if (!x._text || x._text == "") {
x._text = "Defaults Comments"
}
}
Note that you need to test that x._text exists/is defined.
$scope.initValues = function() {
var containsComment = false;
for (var i = 0; i < $scope.values.length; ++i) {
if ($scope.values[i]._attributes.name == 'data.comments') {
containsComment = true;
}
}
if (!containsComment) {
$scope.values.push({
"_attributes": {
"name": "data.comments"
}
});
}
}
Check the working fiddle
So this works with static data, but when I push data with a $http this autocomplete does not work. The data pushes to the empty array of airport_list but something is happening when I try to use airport_list in for the autocomplete. Not sure what is is. I can only find answers which pertain to static data.
This is updated per everyones help.
Here is the controller
app.controller('selectCtrl', function($scope, $http) {
$scope.airport_list = null;
$http({
url: 'someUrl.com',
method: 'GET'
})
.then((response) => {
angular.forEach(response.data.airports, function(value, key) {
$scope.airport_list = response.data.airports;
})
$scope.airports = $scope.airport_list;
});
$scope.selectAirport = function(string) {
$scope.airport = string;
$scope.hidelist = true;
};
})
Here is the template
<div class="control">
<div>
<input
type="text"
name="airport"
id="airport"
ng-model="airport"
ng-change="searchFor(airport)"
placeholder="From..."
/>
<div class="airport-container-dropdown" ng-hide="hidelist">
<div
class="airport-list"
ng-repeat="airport in airports"
ng-click="selectAirport(airport)"
>
{{ airport.name }}
</div>
</div>
</div>
</div>
I really would like to do this without using bootstrap typeahead.
Thank you for looking at this.
I have made changes as recommended by below answers and the $http request is feeding into the autocomplete as a whole list but searching by name does not work and clicking on name sets [object, object]
this would be the code which is specific to that functionality.
$scope.searchFor = function(string) {
$scope.hidelist = false;
const output = [];
angular.forEach($scope.airport_list, function(airport) {
if (airport[0].toLowerCase().indexOf(string.toLowerCase(airport)) >=
0) {
output.push(airport);
}
});
$scope.airports = output;
};
$scope.selectAirport = function(string) {
$scope.airport = string;
$scope.hidelist = true;
};
Try this:
$scope.airport_list = response.data.airports;
What I am seeing is that you have an array: $scope.airport_list = [];
When you make your http request, you push what I would understand to be an array of airports into that array. So you end up with your airport array from the backend at the first position of $scope.airport_list, vs. $scope.airport_list being the actual list.
For your search method, you should change the following:
In your HTML:
ng-change="searchFor(airport.name)"
In your JS:
I've renamed your function and changed the input variable to be more clear. You were passing in a full airport, but treating it as a string. You need to compare your provided airport name to that of the airports in the array. So you iterate over the array, and compare each element's name property to what you pass in.
$scope.searchFor = function(airportName) {
$scope.hidelist = false;
const output = [];
angular.forEach($scope.airport_list, function(airport) {
if (airport.name.toLowerCase() === airportName) {
output.push(airport);
}
});
$scope.airports = output;
console.log($scope.airports);
};
I have provided minimal changes to your code to implement this, however I suggest you look at this SO post to filter drop down data more appropriately.
Angularjs Filter data with dropdown
If you want to simply filter out what is displayed in the UI, you can try this in your HTML template. It will provide a text field where you supply a partial of the airport name. If at least one character is entered in that box, the list will display on the page, with the appropriate filtering applied. This will avoid having to call functions on change, having a separate array, etc.
<input type="text" name="airport" id="airport" ng-model="airportSearch.name" placeholder="From..." />
<div class="airport-container-dropdown" ng-hide="!airportSearch.name">
<div class="airport-list"
ng-repeat="airport in airport_list | filter:airportSearch"
ng-click="selectAirport(airport)">
{{ airport.name }}
</div>
</div>
In an Index-gsp, I want to be able to select an arbitrary number of lines and then by clicking a link send all those lines to a controller for processing e.g. creating new objects of a different kind.
I've no idea how selection can be done or how to collect these selected lines in a GSP. Maybe I should use a checkbox on each line if that's possible?
It's a list of products which is displayed using a modified index.gsp.
Each product-line has a checkbox in front.
What I want is to make a list of the products that are checked an then transmit this list to a controller.
a part of this index.gsp:
<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
<li><g:link class="create" action="createOffer"><g:message code="default.new.label" args="[entityName]" params="toOffer" /></g:link></li>
</ul>
</div>
<div id="list-prodBuffer" class="content scaffold-list" role="main">
<h1><g:message code="default.list.label" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>
<table>
<thead>
<tr>
<td> Välj</td>
<td> ID</td>
</tr>
</thead>
<tbody>
<g:each in="${prodBufferList}" status="i" var="prodBuffer">
<tr class="${ (i % 2) == 0 ? 'even': 'odd'}">
<td><g:checkBox name="toOffer" value="${prodBuffer.id}" checked="false" /></td>
<td>${prodBuffer.id}</td>
So this not an ordinary form, just a list where I want to use a link to transmit it to the controller.
I'm a beginner and have no idea how to do it.
You can collect all necessary data from page using javascript, and then send all data to your controller for processing.
There are a lot of ways to do it.
For example send via JQuery:
<script>
//some code
var items = [1,2,3];
//some code
$('#add-location').click(function () {
$.ajax({
type: "POST",
url: "${g.createLink(controller:'myController', action: 'myControllerMethod')}",
data: {items: items},
success: function (data) {
console.log(data)
}
});
});
</script>
I will answer this but have to slow down since it feels like i am beginning to write your project:
In gsp you will need to have a hidden field followed by a check box amongst data you are trying to capture, checkbox should contain all the data elements required to build your output.
<g:hiddenField name="userSelection" value=""/>
<g:checkBox name="myCheckBox" id='myCheckBox' value="${instance.id}"
data-field1="${instance.field1}" data-field1="${instance.field1}"
checked="${instance.userSelected?.contains(instance.id)?true:false}" />
In the java script segment of the page you will need to add the following
This will then auto select selection and add to javascript array
// Customized collection of elements used by both selection and search form
$.fn.serializeObject = function() {
if ($("[name='myCheckBox']:checked").size()>0) {
var data=[]
$("[name='myCheckBox']:checked").each(function() {
var field1=$(this).data('field1');
var field2=$(this).data('field2');
data.push({id: this.value, field1:field1, field2:field2 });
});
return data
}
};
Most importantly will your data sit across many different gsp listing pages if so you will need to hack pagination:
//Modify pagination now to capture
$(".pagination a").click(function() {
var currentUrl=$(this).attr('href');
var parsedUrl=$(this).attr('href', currentUrl.replace(/\&userSelection=.*&/, '&').replace(/\&userSelection=\&/, '&'));
var newUrl=parsedUrl.attr('href') + '&userSelection=' + encodeURIComponent($('#userSelection').val());
window.location.href=newUrl
return false;
});
Then in the controller parse the JSON form field and make it into what you want when posted
def u=[]
def m=[:]
if (params.userSelection) {
def item=JSON.parse(params.userSelection)
item?.each {JSONObject i->
// When field1 is null in JSON set it as null properly
if (JSONObject.NULL.equals(i.field1)) {
i.field1=null
}
if (resultsGroup) {
if (!resultsGroup.contains(i.id as Long)) {
u << i
}
} else {
u << i
}
}
m.userSelected=item?.collect{it.id as Long}
m.results=u
}
return m