Not mandatory option selection in Autocomplete Angular Material [duplicate] - html

I'm trying to implement the autocomplete component from Angular Material:
https://material.angular.io/components/autocomplete/overview
It works well for letting the user select a particular item from the suggested list but I also want to allow the user to add items not in the list.
So lets say the suggested list has the following items:
Cats
Birds
Dogs
And the user starts typing "Do" and the autocomplete shows "Dogs" as the suggested option (because I'm also filtering the list based on what they type). But then the user continues typing "Dolls" and now nothing is displayed in the autocomplete suggestions. Then the user hits enter and it gets added to the list.
Current behavior is that if what the user typed doesn't exist in the list then they are unable to add the item.

If you add an enter key listener to the input field, you can process the entered value and add it to the options if it doesn't exist. You can also dynamically add whatever the user enters to the list of filtered options as an "add new item" option, or add an "add" icon to the field (e.g. as a matSuffix). Or you can do all three:
Stackblitz
HTML
<form class="example-form">
<mat-form-field class="example-full-width">
<input matInput placeholder="Item" aria-label="Item" [matAutocomplete]="auto" [formControl]="itemCtrl" (keyup.enter)="addOption()">
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="optionSelected($event.option)">
<mat-option *ngFor="let item of filteredItems | async" [value]="item">
<span>{{ item }}</span>
</mat-option>
</mat-autocomplete>
<button *ngIf="showAddButton && itemCtrl.value" matSuffix mat-button mat-icon-button (click)="addOption()"><mat-icon matTooltip='Add "{{itemCtrl.value}}"'>add</mat-icon></button>
</mat-form-field>
</form>
TS
import { Component } from '#angular/core';
import { FormControl } from '#angular/forms';
import { Observable } from 'rxjs/Observable';
import { startWith } from 'rxjs/operators/startWith';
import { map } from 'rxjs/operators/map';
/**
* #title Autocomplete with add new item option
*/
#Component({
selector: 'autocomplete-overview-example',
templateUrl: 'autocomplete-overview-example.html',
styleUrls: ['autocomplete-overview-example.css']
})
export class AutocompleteOverviewExample {
itemCtrl: FormControl;
filteredItems: Observable<any[]>;
showAddButton: boolean = false;
prompt = 'Press <enter> to add "';
items: string[] = [
'Cats',
'Birds',
'Dogs'
];
constructor() {
this.itemCtrl = new FormControl();
this.filteredItems = this.itemCtrl.valueChanges
.pipe(
startWith(''),
map(item => item ? this.filterItems(item) : this.items.slice())
);
}
filterItems(name: string) {
let results = this.items.filter(item =>
item.toLowerCase().indexOf(name.toLowerCase()) === 0);
this.showAddButton = results.length === 0;
if (this.showAddButton) {
results = [this.prompt + name + '"'];
}
return results;
}
optionSelected(option) {
if (option.value.indexOf(this.prompt) === 0) {
this.addOption();
}
}
addOption() {
let option = this.removePromptFromOption(this.itemCtrl.value);
if (!this.items.some(entry => entry === option)) {
const index = this.items.push(option) - 1;
this.itemCtrl.setValue(this.items[index]);
}
}
removePromptFromOption(option) {
if (option.startsWith(this.prompt)) {
option = option.substring(this.prompt.length, option.length -1);
}
return option;
}
}

It's weird that the user can add an item in the suggested list. The list is suggested to the user by someone who knows what to suggest. But anyway...
The user can type anything in the field and ignore the suggestions. By ignoring the suggested Dogs and typing Dolls, user can press an "Add" button which will add whatever is typed in (Dolls) to the options array.
For example, you can do it by listening to the submit event on the form:
(ngSubmit)="options.push(myControl.value); myControl.reset()"
Here's the complete demo as well.

Related

Push the checked/selected item of checkbox at the top of the list in Angular9 and above

I am trying to get the selected checkbox on the top the list Say If I
selected fourth checkbox among five checkboxes(12345) the result
should be (41235) please help for your reference I have added work
done till now and add link also.
TS file
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
options = ['1', '2', '3', '4', '5', '6'];
selected = [];
messages = [];
// check if the item are selected
checked(item) {
if (this.selected.indexOf(item) != -1) {
return true;
}
}
// when checkbox change, add/remove the item from the array
onChange(checked, item) {
if (checked) {
this.selected.push(item);
} else {
this.selected.splice(this.selected.indexOf(item), -1);
}
console.log(this.selected);
}
save() {
this.messages.push(this.selected.sort());
}
}
*** HTML File ***
<h1>Angular Checkbox List</h1>
<h3>Options</h3>
{{options | json}}
<h3>Selected</h3>
{{selected | json}}
<br>
<h3>List</h3>
<div *ngFor="let item of options">
<input type="checkbox"
(change)="onChange($event.target.checked, item)"
[checked]="checked(item)"
>
{{item}}
</div>
<br>
{{selected.length}} items selected <br>
<button (click)="save()">Save</button>
<h3 *ngIf="messages.length != 0">Log</h3>
<div *ngFor="let item of messages">
save: {{item}}
</div>
list given
[] one (say I selected first this checkbox)
[] two
[] three (second this checkbox)
[] four
[] five (next this checkbox)
excepted resulted
[]five
[]three
[]one
[]two
[]four
working here stackblitz
Add the index to the *ngFor in your HTML file.
<div *ngFor="let i = index; let item of options">
<input type="checkbox"
(change)="onChange($event.target.checked, item, i)"
[checked]="checked(item)">
{{item}}
</div>
So you can use it as parameter in your onChange function. That way you can remove from that index and push it to the top as this:
// when checkbox change, add/remove the item from the array
onChange(checked, item, index) {
if (checked) {
// Removes it from the index
this.options.splice(index, 1);
// Push it in the first position
this.options.unshift(item);
this.selected.push(item);
} else {
this.selected.splice(this.selected.indexOf(item), 1);
}
console.log(this.selected);
}
Here I modified your Stackbliz code.
Snapshot of the result
If you want the item to return to its original index when it is deselected:
// when checkbox change, add/remove the item from the array
onChange(checked, item, index) {
if (checked) {
// Removes it from the index
this.options.splice(index, 1);
// Push it in the first position
this.options.unshift(item);
this.selected.push(item);
} else {
// Removes it from the index
this.options.splice(index, 1);
// Place it in the original index
this.options.splice(this.optionsCopy.indexOf(item), 0, item);
this.selected.splice(this.selected.indexOf(item), 1);
}
console.log(this.selected);
}
Here the working Stackblitz.
Update:
Add these two lines in onChange method
this.options.splice(this.options.indexOf(item), 1);
this.options.unshift(item);
Note: Stackblitz link is updated.
Old:
Just remove .sort() method from the line this.messages.push(this.selected.sort());. It will give the exactly selected items in the order you selected them.
I have modified it in your Stackblitz link- https://stackblitz.com/edit/angular-wmimex

Change the value of checkbox to string

I already saw many questions but I still can't change the value of a checkbox to a string. I need to change the value according to true - false -> 'A' - 'B'. I am using reactive forms in angular.
I got this:
...
..*ngFor="let item of myForm.get('people')['controls'];
...
<mat-checkbox formControlName="check"
[checked]="item.get('check').value === 'A' ? true : false"
(change)="item.get('check').setValue($event.checked ? 'A' : 'B')">
</mat-checkbox>
It has to be in check true if the value that comes is 'A' and in false if it is 'B', I really don't see whydon't set that value on change. I need to send as a string the value of the checkbox
<form [formGroup]="form" (ngSubmit)="submit()">
{{form.value | json}} // just for to see the output
<ng-container formArrayName="people" *ngFor="let item of people.controls; let i = index">
<div [formGroupName]="i">
<mat-checkbox formControlName="check" [checked]="item.get('check').value === 'A' ? true : false"
(change)="setCheck($event.checked, i)">
</mat-checkbox>
</div>
</ng-container>
<button mat-button>submit</button>
</form>
import { FormArray } from '#angular/forms';
import { Component, OnInit } from "#angular/core";
import { FormBuilder, FormGroup } from '#angular/forms';
#Component({
selector: "app-test",
templateUrl: "./test.component.html",
styleUrls: ["./test.component.scss"],
})
export class TestComponent implements OnInit {
form: FormGroup
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.createForm();
}
createForm(){
this.form = this.formBuilder.group({
people: this.formBuilder.array([
this.createItem()
])
})
}
createItem(){
return this.formBuilder.group({
check: ''
})
}
addItem(){
this.people.push(this.createItem());
}
get people(){
return this.form.get("people") as FormArray;
}
setCheck(checked, index){
this.people.controls[index].get("check").setValue(checked ? 'A' : 'B');
}
submit(){
console.log(this.form.value);
}
}
and one more thing, try to use presentation logic in the component class.
style guide: Style 05-17.
link: https://angular.io/guide/styleguide#put-presentation-logic-in-the-component-class
First, if you create many many checkboxes inside a loop, and you set formControlName to a specific one, every checkbox will be bind to 1 control. (You will check and uncheck all of them with the same click.)
On second, you should use formControlName, in formGroup:
<div [formGroup]="myForm">
...
..*ngFor="let item of myForm.get('people')['controls'];"
...
<mat-checkbox formControlName="check"
[checked]="item.get('check').value === 'A' ? true : false"
(change)="item.get('check').setValue($event.checked ? 'A' : 'B')">
</mat-checkbox>
</div>
On Third, I suggest to use brackets in ngFor and '' to look for control name as string.
[formControlName]="'check'"
And finally, I suggest to use mat-checkbox's (change) event, and call a custom function, and do whatever you want on ts side:
<mat-checkbox [formControlName]="'check'" (change)="someFunction(item, $event)"></mat-checkbox>
And in ts:
someFunction(item, event){
...//Do your black magic here
}
By the way: If you use reactive form, and bind a controls to a checkbox, it's value always will be boolean, cuz it's logic... You should decide to map the value later to 'A' or 'B', but you shouldn't use the same time.
And backwards: The checkbox will always work with true or false, width FormControl.
Think about it: You want a Control to control true or false value, but you force 'A' or 'B' in it... It's illogic.
Please re-think your solution.

Unwanted component method execution during a mouse click event

I'm currently working on a component that displays a list of items using material grid list and material cards, where an item will be displayed only if it is exists in a given datasource. So far I am getting the result I need, but upon further inspection, I tried to log the method that I am calling to check if the item exists into the console and that's where I discovered that anytime I click on the page during testing/debugging, the method gets executed. I am just worried if this will somehow affect the performance of the app.
I haven't specifically tried anything yet as I am still unaware how this is happening (I am a beginner to angular, please bear with me)
HTML
<mat-grid-list cols="4" rowHeight=".85:1">
<div *ngFor="let item of items">
<mat-grid-tile *ngIf="item.isActive">
<mat-card class="mat-elevation-z10 item-card">
<mat-card-header>
<mat-card-title>{{item.title}}</mat-card-title>
<mat-card-subtitle>{{item.subtitle}}</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="{{item.icon}}" alt="{{item.name}}">
<mat-card-content>{{item.description}}</mat-card-content>
<mat-divider [inset]="true"></mat-divider>
<mat-card-actions>
<button mat-button
[disabled]="!isAccessible(item.name)">Action1</button>
</mat-card-actions>
</mat-card>
</mat-grid-tile>
</div>
</mat-grid-list>
COMPONENT
export class ItemComponent implements OnInit {
items: any;
dataSource: ItemDataSource; //items from the back end server
constructor(private store: Store<AppState>) { }
ngOnInit() {
this.items = fromConfig.ITEMS;
this.dataSource = new ItemDataSource(this.store);
this.dataSource.load();
}
isAccessible(itemName: string) {
return this.dataSource.isAccessible(itemName);
}
}
DATASOURCE
export class ItemDataSource implements DataSource<Item> {
itemSubject = new BehaviorSubject<Item[]>([]);
constructor(private store: Store<AppState>) { }
isAccessible(itemName: string): boolean {
let exists = false;
for (const itemSubject of this.itemSubject.value) {
console.log('Parameter Item Name: ' + itemName + '; Subject Item Name: ' + itemSubject.name);
if (itemSubject.name === itemName ) {
exists = true;
break;
}
}
return exists;
}
connect(collectionViewer: CollectionViewer): Observable<Item[]> {
return this.itemSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.itemSubject.complete();
}
}
Expected result would be that the method will be executed only once during initialization or after refresh.
You are using square brackets bind the disable property of the button. This binds the function with that button state. So, the function is called every time the page is being rendered. To use the function only once (as you intended), remove the braces.
<button mat-button disabled="!isAccessible(item.name)">Action1</button>
This will call the function only once when the page is rendered intially.

Change behaviour of enter key in a phone - Angular 5

I am working with inputs but I am not really sure about how is the configuration of the navigation done (I guess that it are predefined behaviours).
I am not in the last input the enter key goes to the next one. This one is working as I want.
Nevertheless, when I am on the last input, when I press enter, it automatically clicks on the next button.
This is what I am trying to avoid. Is there any way to change this behaviour? Just to close the keyboard or to click on another button?
I have tried with keyup.enter and it pseudo works. It calls to the method but also clicks on the next button
HTML
<input
type="text"
class="form-control"
id="validationCustomSurname"
placeholder="e.g. Lopez"
required
(keyup.enter)="onNavigate(1, 'forward')"
[(ngModel)]="values.store.surname"
name="surname"
/>
This method should work on a phone, so I guess that keydown is not an option since $event.code does not give me any code in the phone.
Some time ago I make a directive see stackblitz that you apply in a div (or in a form) in the way
<form [formGroup]="myForm" (submit)="submit(myForm)" enter-tab>
Each input or button add a reference variable #nextTab like
<input name="input1" formControlName="input1" #nextTab/>
<button type="button" #nextTab/>
</form>
The directive use ContentChildren to add a keydown.enter to all the components that have #nextTab to focus to the next control
export class EnterTabDirective {
#ContentChildren("nextTab") controls: QueryList<any>
nextTab
constructor(private renderer: Renderer2, private el: ElementRef) {
}
ngAfterViewInit(): void {
this.controls.changes.subscribe(controls => {
this.createKeydownEnter(controls);
})
if (this.controls.length) {
this.createKeydownEnter(this.controls);
}
}
private createKeydownEnter(querycontrols) {
querycontrols.forEach(c => {
this.renderer.listen(c.nativeElement, 'keydown.enter', (event) => {
if (this.controls.last != c) {
let controls = querycontrols.toArray();
let index = controls.findIndex(d => d == c);
if (index >= 0) {
let nextControl = controls.find((n, i) => n && !n.nativeElement.attributes.disabled && i > index)
if (nextControl) {
nextControl.nativeElement.focus();
event.preventDefault();
}
}
}
})
})
}
Here's a very simple approach, with just a few lines of code:
First, in your Template when you dynamically create your Input elements: 1. populate the tabIndex attribute with a unique number, 2. populate a super-simple custom "Tag" Directive with the same unique number as the tabIndex, and 3. set up a Keydown "Enter" event listener:
Template:
<ng-container *ngFor="let row in data">
<input tabindex ="{{row[tabCol]}}" [appTag]="{{row[tabCol]}}" (keydown.enter)="onEnter($event)" . . . />
</ng-container>
In your component, your super-simple event-listener onEnter():
#ViewChildren(TagDirective) ipt!: QueryList<ElementRef>;
onEnter(e: Event) {
this.ipt["_results"][(<HTMLInputElement>e.target).tabIndex%(+this.ipt["_results"].length-1)+1].el.nativeElement.focus();
}
Note: The modulus (%) operation is just to make sure that if you're at the last Input, you'll get cycled back to the first input.
Super-simple, bare-minimum "Tag" Directive
import { Directive, ElementRef, Input } from '#angular/core';
#Directive({
selector: '[appTag]'
})
export class TagDirective {
#Input('appTag') id: number;
constructor(public el: ElementRef) { }
}
There's probably even a way to get rid of the "Tag" `Directive altogether and make it even more simple, but I haven't had time to figure out how to do that yet . . .

Value not showing for input field using one way binding angular2

Objective: Get a collection of values based on the dropdown selection and place them in hidden input fields to be included in my model;
The relative html:
<select class="selectFoo" (change)="onSelect($event.target.value)" name="FooName" ngModel>
<option selected="selected">--Select--</option>
<option *ngFor="let foo of foos" [value]="foo.ID">{{foo.Name}}
</option>
</select>
<input type="hidden" [value]="fooAddress" name="FooAddress" ngModel/>
In the code above I called a function named OnSelect to get the data about the selected foo. The foos are populated using a webservice call. Here is the snippet from my ts file.
import { Component, OnInit } from '#angular/core';
import { Foo } from './model';
import { DataService } from './data.service';
#Component({
moduleId: module.id,
selector: 'add-on',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
foos : Foo[];
selectedFoo: Foo;
fooAddress: string;
onSelect(fooID){
this.selectedFoo = null;
for(var i = 0; i < this.foos.length; i++)
{
console.log(this.foos[i].ID);
if(this.foos[i].ID == fooID){
this.selectedFoo = this.foos[i];
this.fooAddress = this.selectedFoo.Address.toString();
}
}
}
}
I originally tried one way binding my value to the selectedFoo but I was getting an error indicating my Address value wasn't defined. I noticed I could set the value equal to selectedFoo and it didn't error. So i created a new variable that was set to the fooAddress based on the selected foo. I get no value even though while stepping through the code I see it has a value.
How can I get my value to populate so I can use it in my model? Let me know if I need to provide anything else.
Thanks!
If I am correctly understanding what you are after then something like this would work:
<select name="FooName" [(ngModel)]="selectedFoo">
<option>--Select--</option>
<option *ngFor="let foo of foos" [ngValue]="foo" >{{foo.Name}}</option>
</select>
<input type="hidden" [value]="selectedFoo?.Address" name="FooAddress" />
//Assuming your 'foo' items are e.g. { ID: 1, Name: 'Hello', Address: '123 Hello St'}
Here you can bind the Address property of the selectedFoo directly to your hidden input field, rather than needing to handle the (change) event.