Autocomplete - select option programmatically - angular-material2

I am having an issue when trying to programmatically select an option in an autocomplete.
MatAutocomplete has no select method, so I tried using the select method on the MatOption. This does not appear to do anything.
matAutocomplete.options.find(opt => opt.id === 1).select();
Using the Autocompelete _emitSelectEvent(option) method results in firing the optionSelected method but does not update the UI or actually set the option to be selected.
matAutocomplete._emitSelectEvent(option);
Is there a way to programmatically select an option so that it updates the UI and calls the optionSelected event emitter?
<input [matAutocomplete]="autocomplete1" />
<mat-autocomplete #autocomplete1="matAutocomplete" [displayWith]="display1()" (optionSelected)="selected1($event.option.value)">
<mat-option *ngFor=let opt of filteredOptions | async" [value]="opt">
{{ opt.name }}
</mat-option>
</mat-autocomplete>
<input [matAutocomplete]="autocomplete2" />
<mat-autocomplete #autocomplete2="matAutocomplete" [displayWith]="display2()" (optionSelected)="selected2($event.option.value)">
<mat-option *ngFor="let opt of filteredOptions2 | async" [value]=opt>
{{ opt.name }}
</mat-option>
</mat-autocomplete>
export class obj {
public id: number;
public name: string;
}
#ViewChild("autocomplete2") autocomplete2: MatAutocomplete;
selected1(value: obj): void {
const opt = this.autocomplete2.options.find(opt => (opt.value as obj).id === 1);
// Does nothing
opt.select();
// Fires optionSelected on autocomplete2, does not update the UI
opt._selectViaInteraction();
// Fires optionSelected on autocomplete2,
// does not set the option to be selected or update the UI
this.autocomplete2._emitSelectEvent(opt);
}
I am using Angular & Material version 5.2.4

I was able to solve this issue by setting the value of the input2 with the full object that I wanted to select from the second autocomplete.
This would then be displayed in the input using the displayWith function set in the mat-autocomplete.
<input [matAutocomplete]="autocomplete1" [formControl]="input1" />
<mat-autocomplete #autocomplete1="matAutocomplete" (optionSelected)="selected1($event.option.value) [displayWith]="display1()">
<mat-option *ngFor="let opt of options1" [value]="opt">
{{ opt.name }}
</mat-option>
</mat-autocomplete>
<input [matAutocomplete]="autocomplete2" [formControl]="input2" />
<mat-autocomplete #autocomplete2="matAutocomplete" (optionSelected)="selected2($event.option.value) [displayWith]="display2()">
<mat-option *ngFor="let opt of options2" [value]="opt">
{{ opt.name }}
</mat-option>
</mat-autocomplete>
export class Obj {
public id: number;
public name: string;
}
let randomObj: Obj = new Obj();
input2: FormControl = new FormControl();
select1(value: Obj): void {
this.input2.setValue(this.randomObj);
}
You do not need to go into the autocomplete and get the specific option that you want, you can just set anything to the value that can then be displayed with the displayWith function.
For example, if you displayWith function is as follows:
displayWith(value: Obj): string {
return value.name;
}
if you set the value of the input with any object that has a name property then it will display as the text in the input.
input.setValue({ name: "HelloWorld" });
Even though the object passed into the setValue method is not of the type Obj it will still work with the displayWith function. If you set the value to an object that cannot be displayed using the displayWith function then all that happens is that the text in the input will be blank (whilst the value of the input is still set to the object set).
so input.setValue({ foo: "bar" }) will display nothing in the textbox but input.value will be { foo: "bar" }

Related

Use methods and computed properties in child component

In my List component I have a method which count the length of the array within certain categories.
methods: {
getLengthofaCategory(cat) {
const LowerCaseSearch = this.search.toLowerCase();
let categoryCount = this.products.filter(
product =>
(product.name.toLowerCase().includes(LowerCaseSearch) ||
product.category.toLowerCase().includes(LowerCaseSearch)) &&
(!this.checked.length || this.checked.includes(product.category)) &&
product.category === cat
);
return categoryCount.length;
}
}
See here my setup in this sandbox.
But I want the values next to the checkboxes (which are coming from my CheckBox component).
How do I get the logic from the method getLengthofaCategory into my CheckBox component?
So I am able to use {{ getLengthofaCategory('tennis') }} in the v-for loop, inside the CheckBox component. And then maybe I can also use category.value instead of hardcoding e.g 'tennis' as the paramater?
In your list.vue, you can use the already created computed function filteredData instead of doing the filter again. This saves some performance because in Vue, computed properties are "cached" once run.
So you can create a new computed function that creates an object with keys per category and value can either be just the amount or an array of products in this category.
I would then pass this computed value to the CheckBox component via a prop, then inside the CheckBox component, you can display the .length or value regarding how many items each category has:
List.vue:
computed: {
//...
amountPerCategory() {
return this.filteredData.reduce((categories, product) => {
if (!(product.category in categories)) {
categories[product.category] = [];
}
categories[product.category].push(product);
return categories;
}, {});
}
}
CheckBox.vue:
<span class="ml-2 text-gray-700 capitalize">{{ category.value }}</span> -
<span
v-if="count[category.value]"
class="ml-2 text-gray-700 capitalize"
>{{ count[category.value].length }}</span>
count: {
type: Object,
default: () => ({})
}
https://codesandbox.io/s/admiring-ellis-4hojl?file=/src/components/CheckBox.vue

Output user first name

I want to get the name of the user to put it on an h1.
What dies this line stand for?
#select="option => selected = option">
I'm using Buefy for the vue components.
<template>
<section>
<div class="field">
<b-switch v-model="keepFirst">
Keep-first <small>(will always have first option pre-selected)</small>
</b-switch>
</div>
<p class="content"><b>Selected:</b> {{ selected }}</p>
<b-field label="Find a name">
<b-autocomplete
v-model="name"
placeholder="e.g. Anne"
:keep-first="keepFirst"
:data="filteredDataObj"
field="user.first_name"
#select="option => selected = option">
</b-autocomplete>
</b-field>
</section>
</template>
<script>
import data from '#/assets/data_test.json'
// Data example
// [{"id":1,"user":{"first_name":"Jesse","last_name":"Simmons"},"date":"2016-10-15 13:43:27","gender":"Male"},
// {"id":2,"user":{"first_name":"John","last_name":"Jacobs"},"date":"2016-12-15 06:00:53","gender":"Male"},
// {"id":3,"user":{"first_name":"Tina","last_name":"Gilbert"},"date":"2016-04-26 06:26:28","gender":"Female"},
// {"id":4,"user":{"first_name":"Clarence","last_name":"Flores"},"date":"2016-04-10 10:28:46","gender":"Male"},
// {"id":5,"user":{"first_name":"Anne","last_name":"Lee"},"date":"2016-12-06 14:38:38","gender":"Female"}]
export default {
data() {
return {
data,
keepFirst: false,
name: '',
selected: null
}
},
computed: {
filteredDataObj() {
return this.data.filter((option) => {
return option.user.first_name
.toString()
.toLowerCase()
.indexOf(this.name.toLowerCase()) >= 0
})
}
}
}
</script>
# is shorthand for v-on:, so it's handling a select event with a function that receives option as a parameter and assigns it to selected.
Since v-model is bound to name, you should be able to do <h1>{{name}}</h1> to have the same value show up in an H1.
The data section has the main variables for your object. name is there. There is also a computed (named filteredDataObj) that should return an array (length of zero or one) with the matching test data. If you want other fields (like id) you would need to look there. Something like
{{filteredDataObj.length ? filteredDataObj.id : ''}}
would give the id if name matched anything in the data set.

Angular2 - Custom validator using old data

I'm having an issue with a custom validator that I've written. The validator is used to validate all the data in a component.
Here's the setup:
The component displays a table with X rows of data. Some of the rows are display-only mode and others will be in edit mode (certain cells in the table will have inputs, selects, etc). Backing the table is an array of data objects (tableData).
If a row is in edit mode, each input is two-way bound to the appropriate element in the tableData array.
My custom validator is applied to the form tag and takes tableData as input. Everything mostly works. The validation data looks at each row in the table and does everything I need it to do.
The issue is that the data based to my custom validator is old data. So if a row is in edit mode, when I change a value in a select, tableData is updated, but the version of it that's passed to the validator is before the update. So I'm always validating an old version of tableData.
I'm not sure how to get the validation to use the up to date version of tableData. I think the issue may be related to the fact that the select binding changes a value of an object in the tableData array, but the tableData array itself doesn't actually change.
I tried adding callback to the (change) event on the select in the row being edited. The method called on (change) manually triggers change detection using a ChangeDetectorRef, but that didn't work.
I don't want to spam everyone with all the entire files, so I've tried to just add the important snippets.
Here's the template:
<form #f="ngForm" novalidate custom-validator="{{tableData | json}}">
<p-dataTable [value]="tableData">
...
<p-column [header]="'Program Name'">
<template let-row="rowData" let-idx="rowIndex" pTemplate type="body">
<span *ngIf="!row['edit']">
{{row['data'].programName}}
</span>
<div *ngIf="row['edit']">
<select #progName="ngModel" [(ngModel)]="row['data'].programCode"
title="Select Program" required (change)="onProgramChange($event, idx)"
name="programSelect-{{idx}}">
<option [value]=""></option>
<option *ngFor="let prog of programList" [value]="prog.code">
{{prog.name}}
</option>
</select>
</div>
</template>
</p-column>
...
</p-dataTable>
</form>
Here's the backing component:
//imports...
...
private tableData: PersonAssignmentRowData[] = [];
private programList: Program[] = [];
...
onProgramChange(event: any, index: number) {
for(let prog of this.programList) {
if(prog.code == event.target.value) {
this.tableData[index].data.programAcronym = prog.acronym;
this.tableData[index].data.programLocation = prog.location;
this.tableData[index].data.programName = prog.name;
break;
}
}
}
...
Here's the validator:
#Directive({
selector: '[custom-validator]',
providers:[{provide: NG_VALIDATORS, useExisting: CustomValidator, multi: true}]
})
export class CustomValidator implements Validator{
#Input('custom-validator') tableDataString: string;
validate(control: AbstractControl) {
if(this.tableDataString == null || this.tableDataString.length == 0) {
return null;
}
let tableData: PersonAssignmentRowData[] = [];
tableData = JSON.parse(this.tableDataString);
let message: string = '';
//logic that tests the validity of the data and sets any error messages in the message variable
if(message.length > 0) {
return {'validationMessage': message};
} else {
return null;
}
}
}
Now it's clear. Of course it will not work. The only data source that the validator should check is a control that's passed to validate() method. No #Inputs() or anything of the kind. The only difference is that control.value below will contain all values of all controls in the form, not just your table, so you should pick the correct nested value where your table is.
#Directive({
selector: '[custom-validator]',
providers:[{provide: NG_VALIDATORS, useExisting: forwardRef(() => CustomValidator), multi: true}]
})
export class CustomValidator implements Validator {
validate(control: AbstractControl) {
tableData = control.table.value; // need to figure out exact path based on your form structure
let message: string = '';
//logic that tests the validity of the data and sets any error messages in the message variable
if(message.length > 0) {
return {'validationMessage': message};
}
return null;
}
}

Polymer: multiple filters in template dom-repeat

I have a simple dom repeat like this:
<template is="dom-repeat" items="{{projects}}" as="project" filter="{{computeFilter(searchString)}}">
[[project.name]] - [[project.number]]
</template>
<paper-input name="filter-name" label="Filter by project name" value="{{searchString}}"></paper-input>
And there is a function that filters the projects by name:
computeFilter: function(keyword) {
if (!keyword) {
return null;
} else {
keyword = keyword.toLowerCase();
return function(project) {
var name = project.name.toLowerCase();
return (name.indexOf(keyword) != -1);
};
}
}
All good. Now, how would I go about adding another filter, if for example I'd also like ot filter by project number?
I would have another paper button binding to {{searchString2}}, but then how would I link this to a filter - in other words, how can I set up multiple filters on a dom-repeat?
There's a way to filter a dom-repeat using multiple filters.
First, here's the template:
<paper-input name="filter-name" label="Filter by project name" value="{{filterText::input}}"></paper-input>
<paper-input name="filter-name" label="Filter by project type" value="{{filterText2::input}}"></paper-input>
<template id="resultList" is="dom-repeat" items="{{ projects }}" filter="filterProject" as="project">
<div>
[[project.name]] - [[project.number]]
</div>
</template>
You must define for each filter an observer to refresh the filter:
this.$.resultList.render();
Then, you must use a filter function with your filters:
filterProject: function(item) {
return (this.filterText && item.name.match(new RegExp(this.filterText, 'i'))) ||
(this.filterText2 && item.name.match(new RegExp(this.filterText2, 'i')));
},
Here you can see an example of this method.

AngularJS Form Validation inside an ng-repeat

So I am trying to validate the input of one item inside of an ng-repeat. For examples sake lets say that I have 5 items (1,2,3,4,5) and I only want to validate the form if the 4th item is selected.
I have used ng-pattern before to validate forms, but not one that had a dropdown menu to select item.name
I have included the regex I would like the 4th item to be validated with inside the ng-pattern.
<div>
<select name="name" ng-model="item.name" ng-options="item for item in items" required></select>
</div>
<div>
<input name="results" type="text" ng-model="item.results" ng-pattern="/^\d\d\d\/\d\d\d/" required>
</div>
Any suggestions as to the correct way to validate this situation would be greatly appreciated. I have thought about creating a directive to validate this, but that feels like is an overly complicated solution to this since I would not use the directive more than once in this app.
//////////////////////////////////////////////////
It wouldn't let me answer my own question so here is the answer I figured out.
What I ended up having to do was use ng-pattern and pass it a function.
<input name="results" type="text" ng-model="vital.results" ng-pattern="vitalRegEx()" required>
Here is the controller code
$scope.item4RegEx = /^\d{2,3}\/\d{2,3}$/;
$scope.itemRegEx = function() {
if($scope.item && $scope.item.name === "fourth item")
return $scope.item4RegEx;
else return (/^$/);
};
or else...
add ng-change directive on the select dropdown which calls a Controller method and that controller method sets a flag whether to validate form or not.
eg.
<select ng-change="checkIfFormShouldbeValidated()" ng-model="item.name"></select>
// Inside controller
$scope.checkIfFromShouldBeValidated = function(){
if( $scope.item.name == 4th Item ) $scope.shouldValidate = true;
else $scope.shouldValidate = false;
};
$scope.formSubmit = function(){
if(($scope.shouldValidate && form.$valid) || (!$scope.shouldValidate)){
// Submit Form
}
};
See if it helps.
I wrote this recursive function inside my controller to check the validity of all child scopes.
function allValid(scope) {
var valid = true;
if (scope.$$childHead) {
valid = valid && allValid(scope.$$childHead);
}
if (scope.$$nextSibling) {
valid = valid && allValid(scope.$$nextSibling);
}
if (scope.scorePlannerForm) {
valid = valid && scope.myForm.$valid;
}
return valid;
}
Then in my controller I check this with the controller scope.
function formSubmit() {
if (allValid($scope)) {
// perform save
}
}

Resources