So I have a FormArray containing FormGroups with 3 controls.
Visually it looks like a table with 3 inputs on each row. Here is how it looks:
I want when the user presses tab or enter at the last input in the row - a new empty row to be added after it. So I added (keydown)="addRow($event.keyCode, i)" to the last input and created the function:
public addRow(keyCode, index)
{
if (keyCode !== 9 && keyCode !== 13) {
return;
}
let formItems = this.form.get('items') as FormArray;
formItems.insert(
index + 1,
this.formBuilder.group({
'start': ['', Validators.required],
'title': ['', Validators.required],
'category': [''],
})
);
}
Afther the FormGroup is pushed, I can see the controls correctly in the form array, however the view is updated strangely. After for example I press tab on the last input in the first row I get this result:
Last row is removed and I get two empty rows in the after the first. I couldn't find out why. Here is the FormArray after the push, the items are OK there:
Here is the view code:
<div formArrayName="items">
<div *ngFor="let item of form.controls.items.controls; let i=index" [formGroupName]="i" class="row item-index-{{ i }}">
<div class="form-group">
<div class="col-sm-2">
<input type="text" class="form-control" placeholder="Start" autocomplete="off" formControlName="start">
</div>
<div class="col-sm-4">
<input type="text" class="form-control" placeholder="Title" autocomplete="off" formControlName="title">
</div>
<div class="col-sm-4">
<input type="text" class="form-control" placeholder="Category" autocomplete="off" formControlName="category" (keydown)="addRow($event.keyCode, i)">
</div>
<div class="col-sm-2">
<a class="btn btn-icon-only default">
<i class="fa fa-arrow-up"></i>
</a>
<a class="btn btn-icon-only default">
<i class="fa fa-arrow-down"></i>
</a>
<a class="btn btn-icon-only red">
<i class="fa fa-times"></i>
</a>
</div>
</div>
</div>
</div>
Any idea why this happens?
In case someone has this problem but the cause is different, check if you're adding your controls correctly:
let arr = new FormArray([]);
arr.push(....); // Make sure you do this
arr.controls.push(....); // And not this!
I've encountered this issue once earlier when not inserting formgroup at the end of formarray. Seems Angular is having trouble tracking the index in template for some reason, and by using trackBy solved it. So try:
<div *ngFor="let item of form.get('items').controls; let i=index; trackBy:trackByFn"
[formGroupName]="i" class="row item-index-{{ i }}">
and in TS:
trackByFn(index: any, item: any) {
return index;
}
Related
I created the following HTML form inside a jqxWindow widget for a Laravel project:
<div id="provinceWindow">
<div id="provinceWindowHeader"></div>
<div id="provinceWindowContent">
<form id="provinceForm" method="POST" action="{{route('province.store')}}">
{{ csrf_field() }}
<input type="hidden" name="provinceId" id="provinceId" value=""/>
<div class="form-group row">
<div class="col-6"><label>English province name</label></div>
<div class="col-6"><input type="text" name="provinceNameEn" id="provinceNameEn" maxlength="20"/></div>
</div>
<div class="form-group row">
<div class="col-6"><label>Spanish province name</label></div>
<div class="col-6"><input type="text" name="provinceNameSp" id="provinceNameSp" maxlength="20"/></div>
</div>
<br/>
<div class="form-group row justify-content-center">
<input type="button" value="Submit" id="submitBtn" class="btn btn-sm col-3" />
<span class="spacer"></span>
<input type="button" value="Cancel" id="cancelBtn" class="btn btn-sm col-3" />
</div>
</form>
</div>
</div>
This is the javascript file:
$(document).ready(function () {
var datarow = null;
$.ajaxSetup({
headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')}
});
//-----------------------------
// Window settings
//-----------------------------
$('#provinceWindow').jqxWindow({
autoOpen: false,
isModal: true,
width: 400,
height: 160,
resizable: false,
title: 'Province name',
cancelButton: $('#cancelBtn'),
initContent: function () {
$('#submitBtn').jqxButton();
$('#submitBtn').on('click', function () {
$('#provinceForm').submit();
});
}
}).css('top', '35%');
The file routes\web.php has only one resourse route defined for this page:
// Routes for province maintenance
Route::resource('province', 'provinceController');
Checking the available routes with php artisan route:list command, I get these:
Method URI Name Action
GET|HEAD / APP\Http\Controllers\homeController#index
GET|HEAD province province.index APP\Http\Controllers\provinceController#index
POST province province.store APP\Http\Controllers\provinceController#store
GET|HEAD province/create province.create APP\Http\Controllers\provinceController#create
GET|HEAD province/{province} province.show APP\Http\Controllers\provinceController#show
PUT|PATCH province/{province} province.update APP\Http\Controllers\provinceController#update
DELETE province/{province} province.destroy APP\Http\Controllers\provinceController#destroy
GET|HEAD province/{province}/edit province.edit APP\Http\Controllers\provinceController#edit
My controller action:
public function store(Request $request)
{
$fields = $request->all();
if ($request->provinceId == '') {
$province = new province($fields);
$validator = Validator::make($fields, $province->rules());
if ($validator->fails()) {
return redirect('')->withErrors($validator)->withInput();
}
else {
$province->save();
}
return view('province/index');
}
}
The form is shown on top of a jqxGrid widget, as a modal window, in order to capture the required information and perform the CRUD operations for the corresponding DB table.
The problem is, when I click the "Submit" button the window is closed and nothing else happens. The form is not posted to the indicated action and the data entered get lost.
It does not matter if I initialize the submitBtn inside the initContent or outside of it. The form is never posted.
I also tried the Close event of the jqxWindow to no avail.
If I take a look to the generated HTML it looks like this:
<form id="provinceForm" method="POST" action="http://mis:8080/province">
<input type="hidden" name="_token" value="Y9dF5PS7nUwFxHug8Ag6PHgcfR4xgxdC43KCGm07">
<input type="hidden" name="provinceId" id="provinceId" value="">
<div class="form-group row">
<div class="col-6">
<label>English province name</label>
</div>
<div class="col-6">
<input type="text" name="provinceNameEn" id="provinceNameEn" maxlength="20">
</div>
</div>
<div class="form-group row">
<div class="col-6">
<label>Spanish province name</label>
</div>
<div class="col-6">
<input type="text" name="provinceNameSp" id="provinceNameSp" maxlength="20">
</div>
</div>
<br>
<div class="form-group row justify-content-center">
<input type="button" value="Submit" id="submitBtn" class="btn btn-sm col-3 jqx-rc-all jqx-button jqx-widget jqx-fill-state-normal" role="button" aria-disabled="false">
<span class="spacer"></span>
<input type="button" value="Cancel" id="cancelBtn" class="btn btn-sm col-3">
</div>
</form>
Pressing the submit button takes me to the home page and nothing gets added to the DB table.
I guess the issue has something to do with Laravel routes because I get no errors.
What is missing or what is wrong with this approach?
Any light is appreciated.
Alright, because I erroneously added in my Model a validation, that was not needed, for a column (there was already a constraint defined at the DB table level), it was not possible to fulfill the validator when saving a new record. I commented that line in my Model and, that was it!
protected $rules = array(
'provinceNameEn' => 'required|alpha|max:20',
'provinceNameSp' => 'required|alpha|max:20'
//'deleted' => 'required|in:M,F' // This is already validated in a DB constraint.
);
I noticed the control was returned to my home page but without any message or error, but if you pay attention to my controller it was precisely the behavior programmed. However, there is no implementation to display the error messages, so I added dd($validator); before that return statement. There I read the message and finally found the solution.
This is my html
<input type="text" class="form-control pl-0" placeholder="Search" aria-label="Username" aria-describedby="basic-addon1">
<li>
</i> UI Elements <i class="fal fa-angle-right float-right menu-arrow"></i>
<ul class="sub-menu">
<li>Forms</li>
<li>Charts</li>
<li>Buttons</li>
<li>Tabs</li>
</ul>
</li>
In the above html i have list of elements, when user enter data on input field more than 2 characters then filter list data using angular6
You need to bind the li tag to an array and then filter it:
data = ['Forms', 'Charts', 'Buttons', 'Tabs'];
filteredData: any[];
ngOnInit(){
this.filteredData = this.data;
}
In your html view:
<ul class="sub-menu" *ngFor="let item of filteredData">
<li>{{item}}</li>
</ul>
Now fire event each time user type inside the input:
<input type="text" class="form-control pl-0" placeholder="Search" aria-label="Username" aria-describedby="basic-addon1" (change)="filter($event)">
Inside your component:
filter($event){
let reg = $event.target.value;
this.filteredData = this.data.filter(x => return x.toLowerCase().indexOf(reg.toLowerCase()) != -1);
}
And the list will automatically change with the new filtered data.
I am looping through an array of key values to create a list of checkboxes each with a sibling disabled input. On check of each checkbox, the sibling input text field becomes enabled and is required. In this view there is a 'previous' and 'next' button and the 'next' button should be disabled if a user selects a checkbox and then does not enter anything in it's required sibling input. I almost have this working, however the 'next' button should become disabled as soon as a user checks the box as this would mean they have not entered anything in the required text input. Right now the 'next' button only becomes disabled if a user checks the checkbox, focuses on the sibling input and then leaves without entering.
My HTML...
<div *ngFor="let promotion of promotionOptions; let i = index">
<div class="col-md-6 input-container radio-label">
<mat-checkbox [checked]="!promotion.key" (change)="promotion.key = !promotion.key">
{{ promotion.name }}
</mat-checkbox>
</div>
<mat-input-container>
<input matInput [disabled]="promotion.key" placeholder="Cost" name="promotionCost{{i}}" #promotionCost="ngModel" [ngModel]="" (keyup)="promotionCostInput($event.target.value)"
[required]="!promotion.key" type="number">
<div *ngIf="promotionCost.errors && (promotionCost.dirty || promotionCost.touched)" class="alert alert-danger cost-alert">
<div [hidden]="!promotionCost.errors.required">Please enter the checked promotion's cost</div>
</div>
</mat-input-container>
</div>
<div class="clearfix"></div>
<div class="button-container">
<button class="main-btn dark icon-left" (click)="updateStep(1)"><i class="fa fa-angle-left"></i>Previous</button>
<button class="main-btn icon-right" (click)="updateStep(3)" [disabled]="!promotionCostValid">Next<i class="fa fa-angle-right"></i></button>
</div>
And the method I'm using in my .ts file for disabling the 'next' button:
promotionCostInput(value) {
if (!value) {
this.promotionCostValid = false;
} else {
this.promotionCostValid = true;
}
}
How can I validate the sibling input when a user checks the checkbox?
Your problem is that the state of your next button is only updated when the keyup event is fired on any of your inputs. Besides, it is updated with only the value of one input but according to what you say, youwant to check that every inputs of your ngFor is filled.
I suggest you to store the value of your inputs in your model and to check that promotion cost is valid for all promotions any time the input change or a checkbox is checked.
<div *ngFor="let promotion of promotionOptions; let i = index">
<div class="col-md-6 input-container radio-label">
<mat-checkbox [checked]="!promotion.key" (change)="promotion.key = !promotion.key; checkPromotionCost();">
{{ promotion.name }}
</mat-checkbox>
</div>
<mat-input-container>
<input
matInput
[disabled]="promotion.key"
placeholder="Cost"
name="promotionCost{{i}}"
(keyup)="promotion.cost = $event.target.value; checkPromotionCost();"
[required]="!promotion.key" type="number"
>
<div *ngIf="promotionCost.errors && (promotionCost.dirty || promotionCost.touched)" class="alert alert-danger cost-alert">
<div [hidden]="!promotionCost.errors.required">
Please enter the checked promotion's cost</div>
</div>
</mat-input-container>
</div>
<div class="clearfix"></div>
<div class="button-container">
<button class="main-btn dark icon-left" (click)="updateStep(1)"><i class="fa fa-angle-left"></i>Previous</button>
<button class="main-btn icon-right" (click)="updateStep(3)" [disabled]="!promotionCostValid">Next<i class="fa fa-angle-right"></i></button>
</div>
And in the controller:
checkPromotionCost() {
this.promotionCostValid = true;
this.promotionOptions.forEach(promotion => {
if (promotion.key && promotion.cost === '') {
this.promotionCostValid = false;
}
});
}
on my angularjs application i show a modal window to edit an event and add/remove event dates(i use bootstrap datepicker and timepicker).
the event has some fixed dates, i dont have problem with that because i have them created first and i use them on ng-model (of the datepicker & timepicker).
the problem is when the user press the add button to add new event dates dynamically, i dont have date variable to assign to the ng-model of the datepicker .
what i do to achieve that:
inside .controller('ModalEditEventP4ctrl',.. i manipulate the modal window(edit event).here i have a an empty object that i use it to add new eventdates into the directive addNewDate.
$scope.datesObj = {}
the add new eventdate button is a directive, where i pass a dates array object from the controller. Inside the directive i create new dates objects and push them on the array, so as to assign it into the html template:
.directive('addNewDate', function($compile){
return {
restrict: 'AE',
scope: {
onClick: '&',
dyndatesObj: '='
},
link: function (scope, element, attrs) {
element.bind('click', function () {
/*1. here i create new date object and push it on the array */
scope.dyndatesObj.push({dynDateStart:new Date(),dynDateEnd:new Date(),dtStatus:'1'});
/*2. get the last item */
var items = $(".row.basicDates").length-1;
/*3. compile another directive 'newDateBlock'*/
/* and pass it into the DOM*/
/* the directive it is compiled but the datepickers are empty*/
$('.row.basicDates:eq('+items+')').append($compile("<new-date-block />")(scope));
scope.$apply();
});
}
}
})
the directive newDateBlock that has the DOM elements which i compile it through the ebove directive is this:
.directive('newDateBlock', function(){
return {
restrict: 'AE',
scope: {
onClick: '&',
myDate:'='
},
templateUrl: 'assets/modules/part4/templates/addNewDate.tpl.html',
link: function (scope, element, attrs) {
element.bind('click', function () {
console.log('inside directive');
});
}
}
});
the template file addNewDate.tpl.html(i dont show it all). everything works ok , except from the datepickers that ,although i assign them ng-model= datesObj[datesObj.length-1][dynDateStart], they are empty.
<div class="row" >
<div class="col-md-6">
<div class="row">
<div class="col-md-4" style="padding-left: 0;">
<label>Start Date</label>
<label>Start Time</label>
</div>
<div class="col-md-8" style="padding-left: 0;">
<p class="input-group">
<input type="text" class="form-control" style="width:100px"
datepicker-popup="{{format}}"
/*ng-model seems not to work*/
ng-model="datesObj[datesObj.length-1][dynDateStart]"
is-open=""
datepicker-options="dateOptions"
ng-required="true"
close-text="Close"/>
<span class="input-group-btn" style="float:left">
<button type="button" class="btn btn-default" ng-click="">
<i class="glyphicon glyphicon-calendar"></i>
</button>
</span>
</p>
<timepicker ng-model="" ng-change="changed()" hour-step="1" minute-step="10" show-meridian="false"></timepicker>
</div>
</div>
it seems that the value inside the ng-model is not compiled , i dont know exactly what is the problem. any help would be appreciated.
to be more specific, i get the error (on the browser) that says:
TypeError: Cannot read property 'initDate' of undefined
at link (http://.../bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js:8:23435)
at http://.../bower_components/angular/angular.min.js:70:141
at $ (http://.../bower_components/angular/angular.min.js:70:197)
at B (http://..../bower_components/angular/angular.min.js:59:255)
at g (http://..../bower_components/angular/angular.min.js:51:335)
at g (http://..../bower_components/angular/angular.min.js:51:352)
at g (http:/..../bower_components/angular/angular.min.js:51:352)
at g (http://.../bower_components/angular/angular.min.js:51:352)
at g (http://..../bower_components/angular/angular.min.js:51:352)
at g (http://.../bower_components/angular/angular.min.js:51:352) <input type="text" class="form-control ng-pristine ng-untouched ng-valid ng-isolate-scope" style="width:100px" datepicker-popup="{{format}}" ng-model="datesObj[0].dynDateStart" is-open="" datepicker-options="dateOptions" ng-required="true" close-text="Close">
After many hours and too many tests i quited the directives option and i made it work with functions inside the controller where i $compile the whole html template each time i add new date ,with ng-repeat , like refresh the whole DOM, i just keep the html template because it works perfect.
what i did:
1. inside my controller i created the addNewDate() function:
$scope.addNewDate = function(){
//i push the new item inside my events.eventDates object and not in another array object as previously
$scope.eventmodal.eventDates.push({eventStartDate:new Date(),eventEndDate:new Date(),dateStatus:'1'});
$templateRequest('/assets/modules/part4/templates/addNewDate.tpl.html').then(function(html){
// Convert the html to an actual DOM node
var template = angular.element(html);
// clear completely(not append) the previous block of dates and add the new ones
$('.basicDatesBlock').html(template);
// finally $compile the template and show it.
$compile(template)($scope);
});
};
2. my addNewDate.tpl.html i use the ng-repeat to show all the eventDates (the old + the new ones) like this:
<div class="row basicDates" ng-repeat="eventdate in eventmodal.eventDates track by $index" ng-show="eventdate.dateStatus == 1">
<!-- start date -->
<div class="col-md-6">
<div class="row">
<div class="col-md-4" style="padding-left: 0;">
<label>Start Date*</label>
<label>Start Time</label>
</div>
<div class="col-md-8" style="padding-left: 0;">
<p class="input-group">
<input type="text" class="form-control" style="width:100px"
datepicker-popup="{{format}}"
ng-model="eventdate.eventStartDate"
is-open="openIndex[$index]"
datepicker-options="dateOptions"
ng-required="true"
close-text="Close"/>
<span class="input-group-btn" style="float:left">
<button type="button" class="btn btn-default" ng-click="openme($index)">
<i class="glyphicon glyphicon-calendar"></i>
</button>
</span>
</p>
<timepicker ng-model="eventdate.eventStartDate" ng-change="changed()" hour-step="1" minute-step="10" show-meridian="false"></timepicker>
</div>
</div>
</div>
<!-- end date -->
<div class="col-md-6">
<div class="row">
<div class="col-md-5" style="width:31.6%;padding-right:0;">
<label>End Date*</label>
<label>End Time</label>
</div>
<div class="col-md-7">
<p class="input-group">
<input type="text" class="form-control" style="width:100px"
datepicker-popup="{{format}}"
ng-model="eventdate.eventEndDate"
is-open="openIndex[$index]"
datepicker-options="dateOptions"
ng-required="true"
close-text="Close"/>
<span class="input-group-btn" style="float:left">
<button type="button" class="btn btn-default" ng-click="openme($index)">
<i class="glyphicon glyphicon-calendar"></i>
</button>
</span>
</p>
<!-- i show the '+' button only for the first row-->
<div style="float:right" ng-show="$index==0">
<div class="plusCircle" style="margin-top:15px">
<!--<div class="plusSymbol" add-new-date dyndates-obj = datesObj>+</div>-->
<div class="plusSymbol" ng-click="addNewBasicDate()">+</div>
</div>
</div>
<!-- i show the 'X' button only for 2nd row and greater-->
<div style="float:right" ng-show="$index>0">
<div class="plusCircle" style="margin-top:15px">
<div class="plusSymbol">x</div>
</div>
</div>
<timepicker ng-model="eventdate.eventEndDate" ng-change="changed()" hour-step="1" minute-step="10" show-meridian="false"></timepicker>
</div>
</div>
</div>
And that's it. i tried to make myself clear , i hope it helps other guys with similar problems.
I have this working piece of code that is repeated multiple times, hence would be great for a ng-repeat loop.
For example, two instances of my code are the following.
<div>
<input type="text" ng-model="searchParamaters.userName" placeholder="User Name"/>
<i class="fa fa-times" ng-click="resetSearchField(filterParamDisplay[0].param)" ng-show="showParam(filterParamDisplay[0].param)"></i>
</div>
<div>
<input type="text" ng-model="searchParamaters.userEmail" placeholder="User Email"/>
<i class="fa fa-times" ng-click="resetSearchField(filterParamDisplay[1].param)" ng-show="showParam(filterParamDisplay[1].param)"></i>
</div>
This is the filterParamDisplay array in Javascript:
$scope.filterParamDisplay = [
{param: 'userName', displayName: 'User Name'},
{param: 'userEmail', displayName: 'User Email'}
];
I have been trying to do that into a ng-repeat loop, but without success so far.
This is what I have coded atm.
<div ng-repeat="param in filterParamDisplay">
<input type="text" ng-model="searchParams[{{param}}]" placeholder="{{param.displayName}}"/>
<i class="fa fa-times" ng-click="resetSearchField(filterParamDisplay[$index].param)" ng-show="showParam(filterParamDisplay[$index].param)"></i>
</div>
The problems are into the ng-model variable above, and into the $index in the ng-click and ng-show.
Not sure if this can be done at all, any help is much appreciated, thanks!
UPDATE:
Thanks for all the answers, using
<div ng-repeat="p in filterParamDisplay">
...
ng-model="searchParams[p]"
Works great!
Still struggling on the showParam and resetSearchField functions which do not work properly yet using $index. Here is my code.
$scope.searchParams = $state.current.data.defaultSearchParams;
$scope.resetSearchField = function (searchParam) {
$scope.searchParams[searchParam] = '';
};
$scope.showParam = function (param) {
return angular.isDefined($scope.searchParams[param]);
};
As you bind your ng-models to searchParameters.userName and searchParameters.userMail at first example, you must use searchParameters[param.param] for ng-model in ng-repeat. Also like others said, you don't need to use $index, you got your object as param in ng-repeat scope.
<div ng-repeat="param in filterParamDisplay">
<input type="text" ng-model="searchParameters[param.param]" placeholder="{{param.displayName}}"/>
<i class="fa fa-times" ng-click="resetSearchField(param.param)" ng-show="showParam(param.param)"></i>
</div>
Here is working FIDDLE
<div ng-app="dynamicAPP">
<div ng-controller="dynamicController">
<div ng-repeat="param in filterParamDisplay">
<input type="text" ng-model="searchParams[param.param]" placeholder="{{param.displayName}}" /> <i class="fa fa-times" ng-click="resetSearchField(filterParamDisplay[$index].param)" ng-show="showParam(filterParamDisplay[$index].param)"></i>
</div>{{searchParams}}</div>
</div>
Jsfiddler link this one for get a single object like 'username' or 'email'
you want single value in ng-show and ng-click use above one. or other wise use belowed one.
<div ng-app="dynamicAPP">
<div ng-controller="dynamicController">
<div ng-repeat="param in filterParamDisplay">
<input type="text" ng-model="searchParams[param.param]" placeholder="{{param.displayName}}" /> <i class="fa fa-times" ng-click="resetSearchField(.param)" ng-show="showParam(param)"></i>
</div>{{searchParams}}</div>
</div>
Jsfiddler link this one is get whole object based on the control
this will passes the whole set of object list.
You don't need to interpolate angular variables inside ng-* directives.
Try:
HTML:
<div ng-repeat="p in filterParamDisplay">
<input type="text" ng-model="searchParams[p]" placeholder="{{p.displayName}}"/>
<i ng-click="printme(p.param)">click</i>
</div>
Controller:
$scope.filterParamDisplay = [
{param: 'userName', displayName: 'User Name'},
{param: 'userEmail', displayName: 'User Email'}
];
$scope.printme = function(v) {
console.log(v);
};
jsfiddle
As #aarosil said you do not need to use $index.
I wrote a small jsfiddle, I don't know your logic behind showParam so I mocked it.
View :
<div ng-controller="Ctrl">
<div ng-repeat="param in filterParamDisplay">
<input type="text" ng-model="searchParams[param.param]" placeholder="{{param.displayName}}"/>
<i class="fa fa-times" ng-click="resetSearchField(param)" ng-show="showParam(param)"></i>
</div>
</div>
and controller :
$scope.searchParams = {};
$scope.filterParamDisplay = [
{param: 'userName', displayName: 'User Name'},
{param: 'userEmail', displayName: 'User Email'}
];
$scope.resetSearchField = function(param){
$scope.searchParams[param.param] = "";
};
$scope.showParam = function(param){ ... }
http://jsfiddle.net/29bh7dxe/1/