Reactive form: how to blur and submit in one click - javascript

I have a Reactive Form (#myform) with a button "foo", to which is attached (click) = myform.submit() so that the form will only submit when that button is clicked, and not on Enter events. The component listens for Enter events, and dispatches blur in that case, and the FormControls all have {updateOn: 'blur'} so blur events trigger updates (i.e., valueChanges, etc).
My problem is this: If I key data into an Input (or select an item in a Select) without hitting Enter or Tabbing out of the element and then click on the "foo", i.e. "Submit" button it seems that clicking the "foo" button only causes a blur event without triggering submission of the form (through myform.submit). I.e., the submit gets "swallowed" by the blur event.
How can I set this up so that if I don't Enter or Tab out of an element, and immediately click "foo", the element will be updated through the blur event AND THEN the form will ALSO be submitted?
I've tried things such as: 1. first processing the blur and then trying to programatically call myform.submit() in the component (by accessing the Form through #ViewChild), and 2. adding a (blur)= (submitNow && setTimeout(()=>myform.submit(),50) listener in the template view. At this point, I haven't even got this setTimeout call to compile, and none of the approaches I've tried seem to work so far.
The original code (simplified) is like this:
template details.component.html
<form #myform [formGroup]="form" method="POST" action="http://localhost:3000/users/{{userID}}/details">
<ng-container *ngFor="let sec of datArr">
<ng-container *ngFor="let row of sec; let i = index">
<input type="text"
[appTag]="row[datColEnum.tabCol]"
(keydown.enter)="onEnter($event)"
tabindex = "{{row[datColEnum.tabCol]}}"
name="{{row[datColEnum.controlNameCol]}}" value="{{row[datColEnum.valueCol]}}"
formControlName="{{row[datColEnum.controlNameCol]}}"/>
</ng-container>
</ng-container>
<button name="foo" type="button" (click)="myform.submit()">Submit</button>
</form>
component details.component.ts
export class DetailsComponent implements OnInit, AfterViewInit {
#ViewChildren(TagDirective) ipt!: QueryList<ElementRef>;
#ViewChild('myform', {static: true}) myform: any;
data: Object;
meta: Object;
datArr: any[] = [];
formCtls: any = {};
form: FormGroup = new FormGroup({});
tabIdx: number = 0;
focusItm: number = 0;
submitNow: boolean = false;
constructor(private member: MemberService, private route: ActivatedRoute, private el: ElementRef) { }
ngAfterViewInit() {
this.ipt.changes.subscribe(list=>{
setTimeout(()=>
list.filter(itm=>+itm.id===this.focusItm%this.tabIdx).forEach(itm=>{
if (!this.first_pass) this.navigateTo(itm.id);
}
),100)
})
}
doSubmit() {
this.submitNow = true;
}
navigateTo(ti: number) {
console.log(`NAVIGATING TO ITEM WITH TAB INDEX: ${ti}`)
this.ipt["_results"][ti-1].el.nativeElement.focus();
this.ipt["_results"][ti-1].el.nativeElement.style.borderWidth = "3px";
}
onEnter(e: Event) {
let cname = (<HTMLInputElement | HTMLSelectElement>e.target).name;
let idx = 0;
for (let itm of this.ipt["_results"]) {
if (itm.el.nativeElement.name === cname) {
this.focusItm = (itm.id+1)%this.ipt["_results"].length;
itm.el.nativeElement.style.borderWidth = "1px";
break;
}
}
e.target.dispatchEvent(new Event('blur'));
}
ngOnInit() {
. . .
. . .
//SET EVERYTHING UP
this.renderDataArray();
. . .
. . .
})
};
// SET UP ALL THE FormControls, etc. AND THE DATA MODEL datArr USED TO RENDER THE TEMPLATE
renderDataArray() {
this.datArr = [];
this.tabIdx = 0;
Object.keys(this.meta).forEach((sec,idx) => {
let rowCnt = this.getActiveArrayLen(sec);
for (let i = 0; i < rowCnt; i++) {
Object.keys(this.data[sec]).
.map((fld,idx)=>{
let itm = this.data[sec][fld];
this.tabIdx += 1;
let val = getVal();
let ctlName = sec+this.FLD_SPLIT_CHAR+fld;
this.removeControl(ctlName);
this.formCtls[ctlName] = new FormControl(val, {updateOn: 'blur'});
this.form.addControl(ctlName, this.formCtls[ctlName]);
of(this.tabIdx).
pipe(switchMap(ti=>this.formCtls[ctlName].valueChanges.
pipe(map(newVal => [newVal,ti]))
)
).subscribe(([newVal,ti])=>{
if (Array.isArray(itm["value"])) {
itm["value"][+(itm["value"].length || 1)-1]=newVal || '';
} else {
itm["value"]=newVal || '';
}
this.focusItm = (ti+1)%this.ipt["_results"].length;
this.renderDataArray();
});
. . .
. . .

Related

onchange event being triggered when form submits

I would like the user to be able to record an action that they have carried out. I grouped the actions by category and then using two select menus and JS the user is only showed the actions from the category that they have selected. There is also a quantity input that is generated depending on which action is selected.
My issue is that when I submit the form, I get the error:
Uncaught TypeError: Cannot set property 'onchange' of null
The select box and the functionality implemented by the JS work until the form is submitted.
index.js
document.addEventListener("DOMContentLoaded", () => {
// First select
let cat_select = document.getElementById("id_post_cat");
cat_select.onchange = () => handleCatChange(cat_select.value);
// Second select
let desc_select = document.getElementById("id_post_action");
desc_select.onchange = () => handleDescChange(desc_select.value);
});
const handleCatChange = (cat) => {
let quantity_div = document.getElementById("quantity_div");
quantity_div.innerHTML = "";
// Fetching the actions in the category selected and populating second select
fetch(`/action/${cat}`)
.then((response) => response.json())
.then((data) => {
let desc_select = document.getElementById("id_post_action");
let optionHTML = "<option>---------</option>";
data.actions.forEach((action) => {
optionHTML +=
'<option value"' + action.id + '">' + action.desc + "</option>";
});
desc_select.innerHTML = optionHTML;
})
.catch((err) => console.log(err));
};
const handleDescChange = (desc) => {
let quantity_div = document.getElementById("quantity_div");
quantity_div.innerHTML = "";
let time_actions = [
"Public transport instead of driving",
"Walk/cycle instead of drive",
];
let quant_actions = [
"Unplug unused electrical items",
"Repurpose a waste object",
"Use a reusable bag",
"Buy an unpackaged item",
"Buy a locally produced item",
"Buy a second hand item",
"Buy an object in bulk",
"Use a refillable bottle/to-go mug",
"Drink a tap beer instead of bottled beer",
];
if (time_actions.includes(desc)) {
formAdder("Distance* (km)");
} else if (quant_actions.includes(desc)) {
formAdder("Quantity*");
}
};
const formAdder = (label_content) => {
let quantity_div = document.getElementById("quantity_div");
// Label
let label = document.createElement("label");
label.innerHTML = `${label_content}`;
label.setAttribute("for", "id_post_quantity");
label.classList += "requiredField";
// Input
let input = document.createElement("input");
input.setAttribute("id", "id_post_quantity");
input.setAttribute("name", "post_quantity");
input.setAttribute("required", "");
input.classList += "form-control";
quantity_div.append(label, input);
};
views.py
#login_required
def record(request):
if request.method == 'POST':
form = NewPostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.poster = request.user
if request.POST.get('id_post_quantity'):
post.post_quantity = request.POST.get('id_post_quantity')
post.save()
return HttpResponseRedirect(reverse('index'))
return render(request, 'my_app/record.html', {
'form': NewPostForm
})
record.html
<form action="{% url 'record' %}" method="post">
{% csrf_token %}
{{form.post_cat|as_crispy_field}}
{{form.post_action|as_crispy_field}}
<div class="form-group" id="quantity_div">
</div>
<button class="btn btn-success" type="submit">Post</button>
</form>
A pointer in the right direction would be greatly appreciated thank you.
It seems the onchange was being called on every page that was loaded. That meant that, when redirecting to the index page (where the select boxes don't exist), the getElementById was returning a null value etc.
I changed this by adding conditionals to check if the elements are present on the page:
document.addEventListener("DOMContentLoaded", () => {
// First select
let cat_select = document.getElementById("id_post_cat");
if (!!cat_select) {
cat_select.onchange = () => handleCatChange(cat_select.value);
}
// Second select
let desc_select = document.getElementById("id_post_action");
if (!!desc_select) {
desc_select.onchange = () => handleDescChange(desc_select.value);
}
});
Is there a more elegant/correct way of solving the same problem?
for me it got fixed when i changed on submit event to btn click event.

Required field in form doesn´t change to red after erasing input content

Basic form validation
In this question, you’re going to make sure a text box isn’t empty. Complete the following steps:
Create a text input box.
Write a function that turns the text box’s border red if the box is empty.
(It’s empty if the value equals "").
The border should go back to normal if the value is not empty.
When the user releases a key (onkeyup), run the function you just created.
please correct my code where I'm coding wrong?
let form = document.getElementById("form_Text").value;
document.getElementById("form_Text").onfocus = function() {
if (form == "") {
document.getElementById("form_Text").style.backgroundColor = "red";
document.getElementById("showText").innerHTML = "Form is empty";
} else {}
document.getElementById("form_Text").onkeyup = function() {
document.getElementById("form_Text").style.backgroundColor = "white";
document.getElementById("showText").innerHTML =
"Form is not Empty, No red Background";
};
};
Fill Your Form:
<input id="form_Text" type="text" />
<div id="showText"></div>
You are trying to get the input value with let form = document.getElementById("form_Text").value; right after the js is loaded. Therefore it will always be empty. You need to call it inside the event listener.
document.getElementById("form_Text").onfocus = function() {
let form = document.getElementById("form_Text").value;
...
}
But instead of writing two separate event listeners, you can use input event instead of focus and keyup
const formText = document.getElementById("form_Text");
const showText = document.getElementById("showText");
formText.addEventListener('input', function(evt) {
const inputValue = evt.target.value;
if (inputValue == '') {
formText.style.backgroundColor = "red";
showText.innerHTML = "Form is empty";
} else {
formText.style.backgroundColor = "white";
showText.innerHTML = "Form is not Empty, No red Background";
}
})
Fill Your Form:
<input id="form_Text" type="text" />
<div id="showText"></div>
UPDATE
You can find the other way of binding below. Instead of using two separate events (keyup and focus), you can use oninput event listener.
Here's a SO thread comparing keyup and input events: https://stackoverflow.com/a/38502715/1331040
const formText = document.getElementById("form_Text");
const showText = document.getElementById("showText");
formText.oninput = function(evt) {
const inputValue = evt.target.value;
if (inputValue == '') {
formText.style.backgroundColor = "red";
showText.innerHTML = "Form is empty";
} else {
formText.style.backgroundColor = "white";
showText.innerHTML = "Form is not Empty, No red Background";
}
}
Fill Your Form:
<input id="form_Text" type="text" />
<div id="showText"></div>

How to add multiple dropdown values and normal text into a texbox

I have a email subject textbox where I can input combination of dynamic dropdown value and other text entered by user, here I can enter only one dynamic value at start, cant add another dynamic value after that please help.
its an email subject for example there are 3 dynamic value here ["creditcard","accountno","Amount"].
sample output - "Hi jack your creditcard with accoutno has due Amount" here creditcard accoutno and Amount are dynamic value from dropdown and rest are normal text
HTML
<div class="textbox">
<label class="textbox__label" [for]="id">{{label}}</label>
<input class="textbox__input" [type]="type" [id]="id" [placeholder]="placeholder" [value]="value" [name]="name"
autocomplete="off" (input)="onChange($event)" (keyup.Space)="doSomething()" [(ngModel)]="model" />
<ul class="textbox__dropdown" *ngIf="show">
<ng-container *ngFor="let list of listData ; let i = index">
<li (click)="handleSetValue(list)">
<span [style.background-color]="colors[i % colors.length]">{{list.value.charAt(0)}}</span>
{{list.value}}
</li>
</ng-container>
</ul>
</div>
TS
import { Component, VERSION,Output, EventEmitter } from '#angular/core';
import { isEmpty } from 'lodash';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
name = 'Angular ' + VERSION.major;
public dynamicList = ["creditcard","accountno","Amount"]
public value;
public listData: any;
public show: boolean = true;
public model: any;
#Output() selectedValue = new EventEmitter();
onChange(event: any) {
if (event.target.value !== '' && this.dynamicList) {
console.log(event.target.value);
this.show = true;
const item = this.dynamicList.filter((items) => items.toLowerCase().includes(event.target.value));
if (!isEmpty(item)) {
this.listData = item;
console.log(item)
} else {
this.show = false;
this.selectedValue.emit(event.target.value)
}
} else {
this.listData = this.dynamicList;
this.show = false
}
}
Stackblitz-link
You can split the event.target.value that you are making in onChange() method like,
const getLastSearch = event.target.value.split(' ');
Then in the filter method you can use the last recent search into includes to check and display the latest search result like,
const item = this.dynamicList.filter((items) => items.toLowerCase().includes(getLastSearch[getLastSearch.length - 1]));
In app.component.html , for the input box you could use space bar event and call a method to show the dropdown items like,
<input class="textbox__input" type="type" id="id" name="name"
autocomplete="off" (input)="onChange($event)" (keyup.Space)= "spaceEvent($event)" [(ngModel)]="model" />
And the spaceEvent() as follows,
spaceEvent(event: any){
this.listData = this.dynamicList;
this.show = true;
}
Then finally you could split the strings available in the text box and then you can join it by removing the last one and append the clicked list item to as the last item like,
handleSetValue(list) {
let splittedSearch = this.model.split(' '); // Split each string with space
splittedSearch[splittedSearch.length - 1] = ''; // Make the last string empty
splittedSearch = splittedSearch.join(' ') // Join all the splitted string with space
splittedSearch += list; // Concat the splitted search with the selected list item
this.model = splittedSearch; // Assign the splittedsearch to model
this.show = false;
}
Forked Stackblitz here...
Please make the changes as per this
. In TS file
public model: any = "";
onChange(event: any) {
var value = event.target.value;
var inputarray = value.split(" ");
var lastinput = inputarray[inputarray.length - 1]
console.log(lastinput)
if ((value !== '' || inputarray.length> 1) && this.dynamicList) {
console.log(value);
this.show = true;
const item = this.dynamicList.filter((items) => items.toLowerCase().includes(lastinput));
if (!isEmpty(item)) {
this.listData = item;
console.log(item)
} else {
this.show = false;
this.selectedValue.emit(value)
}
} else {
this.listData = this.dynamicList;
this.show = false
}
}
handleSetValue(list) {
debugger
if(this.dynamicList.find(s=>s == list)){
this.model += " "+list;
}
this.show = false;
// this.selectedValue.emit(list.key)
}
in HTML
<div class="textbox">
<input class="textbox__input" type="type" id="id" name="name"
[ngModel]="model" autocomplete="off" (input)="onChange($event)"/>
<ul class="textbox__dropdown" *ngIf="show">
<ng-container *ngFor="let list of listData">
<li (click)="handleSetValue(list)">
{{list}}
</li>
</ng-container>
</ul>
</div>

Create a Dynamic Checkbox Validation in Angular With Data from API

So I have a function that displays a list from an API:
displayEventTicketDetails() {
this.Service
.getEventTicketDetails().subscribe((data: any) => {
this.eventTicketDetails = data.map(ticket => ticket.ticket_name);
console.log(this.eventTicketDetails);
});
}
This is the result from the function above:
["Regular", "VIP", "Table", "Testing", "Cabana"]
Here is how the form array is declared:
ngOnInit() {
this.composeMessage = this.fb.group({
ticket: new FormArray([])
});
Then I use this function below to track if the check boxes are checked
onChange(event: any, isChecked: boolean){
const control = <FormArray>this.composeMessage.controls.ticket;
if(isChecked){
control.push(new FormControl(event))
} else{
const index = control.controls.findIndex(x => x.value === event);
control.removeAt(index)
}
}
Then finally in my ts file, here is my onsubmit function that submits the data on the form:
submitComposeMessage() {
this.submitted = true;
if (this.composeMessage.invalid) {
return;
}
const ticket = this.f.ticket.value;
this.Service
.createMessage(
ticket)
.subscribe(
(res: any) => {
if (res.err) {
this.toaster.errorToastr(res.message, null, { toastTimeout: 5000 });
return false;
}
this.toaster.successToastr(res.message, null, { toastTimeout: 5000 });
console.log("Message successfully created");
},
err => {
console.log(err);
}
);
}
So in my Html file here is my input field:
<ng-container *ngFor="let event of eventTicketDetails; let i = index" >
<label class="w-checkbox checkbox-field" >
<input
type="checkbox"
id="{{i}}"
name="checkbox-9"
class="w-checkbox-input checkbox"
(change)="onChange(event, $event.target.checked)"
[checked]="composeMessage.controls.ticket.value.indexOf(event)>=0">
<span class="no-margin w-form-label">{{event}}</span>
</label>
</ng-container>
With that loop, I'm able to get this result
So, I need help with two things:
1). I want all the checkbox to be checked by default when the page loads at first instance.
2). I want to validate the checkbox to ensure at least one checkbox is checked on submission.
I'll appreciate any help I can get.
If you want to only show validation message after submit, I would suggest the following, where we instead iterate the formarray in template, initially set all checked (as that is what you wish). We would listen to valueChanges of the formarray, but filter out as long as form is not submitted. We would introduce a new variable, for example isEmpty, which based on we would show/hide validation message. So all in all....
TS:
isEmpty = false;
submitted = false;
constructor(private fb: FormBuilder) {
const ctrls = this.eventTicketDetails.map(control => this.fb.control(true));
this.composeMessage = this.fb.group({
ticket: this.fb.array(ctrls)
});
this.tickets.valueChanges.pipe(
filter(() => !!this.submitted)
).subscribe((value) => {
value.some(x => x === true) ? this.isEmpty = false : this.isEmpty = true;
})
}
get tickets() {
return this.composeMessage.get("ticket") as FormArray;
}
onSubmit() {
this.submitted = true;
const selectedTickets = this.tickets.value
.map((checked, i) => (checked ? this.eventTicketDetails[i] : null))
.filter(value => !!value);
selectedTickets.length ? this.isEmpty = false : this.isEmpty = true
}
HTML:
<label formArrayName="ticket" *ngFor="let t of tickets.controls; index as i">
<input type="checkbox" [formControlName]="i">
{{eventTicketDetails[i]}}
</label>
<small *ngIf="isEmpty">Choose at least one checkbox</small>
STACKBLITZ
change Id to something like this
id="ticket{{i}}"
In this method write like this and call displayEventTicketDetails on ngOnit. This will check all the values:
displayEventTicketDetails() {
this.Service
.getEventTicketDetails().subscribe((data: any) => {
this.eventTicketDetails = data.map(ticket =>ticket.ticket_name);
setTimeout(() => {
for(var i= 0;i < this.evenTicketDetails.length ; i++){
var id = "ticket" + i;
(<HTMLInputElement>document.getElementById(id)).checked = true;
console.log(this.eventTicketDetails);
}, 500);
});
}
2. In submit method write something like this
submitComposeMessage() {
for(var i= 0;i < this.evenTicketDetails.length ; i++){
var id = "ticket" + i;
var resval = (<HTMLInputElement>document.getElementById(id)).value;
if(resval){
// this will check atleast one value is checked and if it true we are coming
out of the loop and performing other operations..
i = this.evenTicketDetails.length;
}
else{
// show error message or block from going forward..
}
});
}
This will solve your issues.

How to handle "Go"/"Enter" keyboard button Ionic2 <ion-input>

What is the event to handle "enter" or "go" keyboard key on an input?
The input is not used within a form. So clicking on it will not "submit". I just need the event.
(Running android + Ionic 2 on Beta 11)
I did like this:
<ion-input type="text" [(ngModel)]="username" (keyup.enter)="handleLogin()"></ion-input>
And:
handleLogin() {
// Do your stuff here
}
For my case, I'm not getting next button within a form for both Android and IOS. I'm only getting done. so I handled done as a next by using following directive.
import { Directive, HostListener, Output, EventEmitter, ElementRef, Input } from '#angular/core';
import { Keyboard } from '#ionic-native/keyboard';
#Directive({
selector: '[br-data-dependency]' // Attribute selector
})
export class BrDataDependency {
#Output() input: EventEmitter<string> = new EventEmitter<string>();
#Input('br-data-dependency') nextIonInputId: any = null;
constructor(public Keyboard: Keyboard,
public elementRef: ElementRef) {
}
#HostListener('keydown', ['$event'])
keyEvent(event) {
if (event.srcElement.tagName !== "INPUT") {
return;
}
var code = event.keyCode || event.which;
if (code === TAB_KEY_CODE) {
event.preventDefault();
this.onNext();
let previousIonElementValue = this.elementRef.nativeElement.children[0].value;
this.input.emit(previousIonElementValue)
} else if (code === ENTER_KEY_CODE) {
event.preventDefault();
this.onEnter();
let previousIonElementValue = this.elementRef.nativeElement.children[0].value;
this.input.emit(previousIonElementValue)
}
}
onEnter() {
console.log("onEnter()");
if (!this.nextIonInputId) {
return;
}
let nextInputElement = document.getElementById(this.nextIonInputId);
// On enter, go to next input field
if (nextInputElement && nextInputElement.children[0]) {
let element: any = nextInputElement.children[0];
if (element.tagName === "INPUT") {
element.focus();
}
}
}
onNext() {
console.log("onNext()");
if (!this.nextIonInputId) {
return;
}
let nextInputElement = document.getElementById(this.nextIonInputId);
// On enter, go to next input field
if (nextInputElement && nextInputElement.children[0]) {
let element: any = nextInputElement.children[0];
if (element.tagName === "INPUT") {
element.focus();
}
}
}
}
const TAB_KEY_CODE = 9;
const ENTER_KEY_CODE = 13;
How to use?
<form [formGroup]="loginForm" (ngSubmit)="login(loginForm.value)">
<ion-input br-data-dependency="password" type="text" formControlName="username" placeholder="USERNAME" (input)="userNameChanged($event)"></ion-input>
<ion-input id="password" password type="password" formControlName="password" placeholder="PASSWORD"></ion-input>
<button submit-button ion-button type="submit" block>Submit</button>
</form>
Hope this help someone!!
Edit: Let me know if you are abled to show next button for the first input box?
The right way to do that might be to use Ionic2 forms. I'v found this: https://blog.khophi.co/ionic-2-forms-formbuilder-and-validation/
Otherwise - If you "just want the "Enter" event handler" this is quite complex (!) and not out of the box as you might be thinking:
HTML:
<ion-input id="myInput" #myInput type="submit" [(model)]="textValue" (input)="setText( $event.target.value )" placeholder="Send Message ..." autocorrect="off"></ion-input>
TS:
...
declare let DeviceUtil: any;
...
export class Component_OR_PAGE
{
public textValue: string;
#ViewChild( 'myInput') inputElm : ElementRef;
#HostListener( 'keydown', ['$event'] )
keyEvent( e )
{
var code = e.keyCode || e.which;
log.d( "HostListener.keyEvent() - code=" + code );
if( code === 13 )
{
log.d( "e.srcElement.tagName=" + e.srcElement.tagName );
if( e.srcElement.tagName === "INPUT" )
{
log.d( "HostListener.keyEvent() - here" );
e.preventDefault();
this.onEnter();
DeviceUtil.closeKeyboard();
}
}
};
...
setText( text )
{
log.d( "setText() - text=" + text );
this.textValue = text;
}
onEnter()
{
console.log( "onEnter()" );
this.inputText.emit( this.textValue );
this.textValue = "";
// ionic2 beta11 has issue with data binding
let myInput = document.getElementById( 'myInput' );
let innerInput: HTMLInputElement = <HTMLInputElement>myInput.children[0];
innerInput.value = "";
}
}
JS:
DeviceUtil =
{
closeKeyboard: function()
{
cordova.plugins.Keyboard.close();
}
}

Categories