Get last clicked element in multiple select Angular - javascript

Im trying to get the last clicked option in a multiple select, no matters if the option is selected or unselected.
The problem is that the select is not via template, is mounted dinamically via TS.
I've tried adding vanilla JS event listeners at the creation of the options but doesn't works. Actually i can get all the selected elements, but i lost the unselected option and i can't get exactly the new selected one.
My HTML
<tr *ngFor="let communityLine of communityLines">
<td>{{communityLine.name}}</td>
<td>{{communityLine.instrument.name}}</td>
<td>{{communityLine.param.name}}</td>
<td>{{communityLine.contextSource.name}}</td>
<td>{{communityLine.sampleType.name}}</td>
<td>{{communityLine.value}}</td>
<td>
<select multiple [id] ="communityLine.apiKey" (change)="eventGetChange(communityLine, $event)" [(ngModel)]="nodeKey">
</select>
</td>
</tr>
My TS function
private eventGetChange(commLineKey, event) {
console.log(this.nodeKey);
console.log(commLineKey);
console.log(event.target.value)
My TS method to mount the select, is a bit complicated because i need to show all nodes (stored in this.allNodes var) but select the nodes that are in other array (nodesInRelation var).
private mountSelect(nodesInRelation: Node[], lineApiKey: String): void {
let select = <HTMLSelectElement>document.getElementById(lineApiKey);
let copy = this.allNodes;
for (let node of nodesInRelation) {
copy.forEach((item, index) => {
if (item.name === node.name) copy.splice(index, 1);
});
}
for (let node of nodesInRelation) {
let newoption = new Option(node.name, node.apiKey, null, true);
select.add(newoption);
}
for (let node of copy) {
let newoption = new Option(node.name, node.apiKey, null, false);
select.add(newoption);
}
M.updateTextFields();
M.AutoInit();
}
In eventGetChange function first console.log i get all the current selected values, in the second i get the key and is okey and in the third i get only the first selected element in the box.
I just want the last clicked, selected or unselected.
Thanks you.

You seem to be ditching Angular and opting for direct DOM manipulation for no apparent reason. Angular is perfectly capable of populating options list programmatically. It could look something like
<td>
<select multiple [id] ="communityLine.apiKey" [(ngModel)]="nodeKey">
<option *ngFor="let option of optionList; let i = index"
[value]="option" (click)="eventGetChange(option)">{{ option }}
</option>
</select>
</td>
optionList: any[];
private mountSelect(nodesInRelation: Node[], lineApiKey: String): void {
// populate optionList here
}
private eventGetChange(commLineKey) {
// the clicked option is available here
}

If this.nodeKey as you say stores an array with all the current selected values you can simply do this:
this.nodeKey[this.nodeKey.length - 1]
It will give you the las value in the nodeKey array

Related

How to display only the text in datalist and not the value?

Let suppose that we have the following datalist, and a js variable var carID = '':
<input list="options" #change='${carID = e.target.value}'>
<datalist id="options">
<option value="ID_1">Ferrari</option>
<option value="ID_2">Lamborghini</option>
<option value="ID_3">Jeep</option>
</datalist>
I'd like to show ONLY the car names in my options, and NOT the option values (that are the IDs of the cars), and have the ID of the selected car (the value of the selected option) stored in the variable, not the car name.
I tried different solutions, I post 2 of them (one totally wrong and one right but not complete, I 've found this one in other stack overflow questions):
wrong: it simply doesn't work, e.target.carID is ''.
<input list="options" #change="${carID = e.target.carID}">
<datalist id="options">
<option carID="ID_1" value="Ferrari"></option>
<option carID="ID_2" value="Lamborghini"></option>
<option carID="ID_3" value="Jeep"></option>
</datalist>
Ok it's working, but what if I have 2 cars with the same name and different id? Yes, the second car is ignored and if I select the 2nd car I store the 1st car's ID.
<input id='inputID' list="options" #change='${this.getValue}'>
<datalist id="options">
<option data-value="ID_1" value="Ferrari"></option>
<option data-value="ID_2" value="Lamborghini"></option>
<option data-value="ID_3" value="Jeep"></option>
<option data-value="ID_4" value="Jeep"></option>
</datalist>
js:
getValue(){
let shownValue = this.shadowRoot.getElementById('inputID').value;
let rightValue =
this.shadowRoot.querySelector("#options[value='"+shownValue+"']").dataset.value;
carID = rightValue;
}
I cannot use JQuery. Do you have any solutions? Thanks!
Your code #change='${carID = e.target.carID}' cannot work, as the right hand side of the event handler binding is not callable. You need to wrap it inside an anonymous function, e.g. like so: #change=${(e) => { this.carID = e.target.value }}
That being said, this is what I understood you want to do:
Have a list, where the user can choose from.
In the list, only display the name of the car, not the ID.
Store the selected car's ID in carID, not the name.
I see two ways to do that.
Option 1: Use <select>
If the list of cars is fixed, I think you will be best served using a <select height="1"> element, resulting in a drop down box. Including the little event handler, it looks something like this:
<select #change=${(e) => { this.carID = e.target.value }}>
<option value="ID_1">Ferrari</option>
<option value="ID_2">Lamborghini</option>
<option value="ID_3">Jeep</option>
<option value="ID_4">Jeep</option>
</select>
This will display the text from the text content of the <option> elements, but set the value of the <select> from the <option>'s value attribute, and by the virtue of the onchange event handler will set the carID field on the element.
You can even have two cars with different IDs, but the same name. Note however, that your users would not know, if the display text is the same, which of the two "Jeep" entries to choose. So that might not be a good idea (but I don't know your full use case).
Option 2: Use <input> with <datalist>
Now, if the list of cars is not fixed, i.e. the users are allowed to enter arbitrary data and the selection list is not for limiting their choices, but to help them (prevent typos, speed-up entry) you can use an <input> with an associated <datalist>. But the popup will display both, the <option>'s value and text content (if they are both defined and different). If you insist on only showing the name of the car, not the ID, then the name has to go in the value attribute of the <option> (or the text content). While you could put the ID in the dataset, you really don't need to.
In any case you'll need to map the value string back to the ID through your own code. This will only work if "cars and names" is a one-to-one (aka bijective) mapping, so no two cars with the exact same name would be allowed. (Otherwise your code cannot know which one has been selected just by looking at the name.)
const CARS_BY_ID = {
ID_1: 'Ferrari',
ID_2: 'Lamborghini',
ID_3: 'Jeep',
}
class MyElem extends LitElement {
constructor() {
super();
this.carID = null;
}
render() {
return html`
<input list="myopts" #change=${this.carChanged}>
<datalist id="myopts">
${Object.values(CARS_BY_ID).map((name) => html`<option>${name}</option>`)}
</datalist>`;
}
carChanged(e) {
const name = e.target.value;
this.carID = null;
for (const [key, value] of Object.entries(CARS_BY_ID)) {
if (value === name) {
this.carID = key;
}
}
console.log(`this.carID = ${this.carID}`);
}
}
Note, that in this example the user can e.g. enter "Bugatti" and this.carID will be null.
Also note, that this.carID has not been registered as a lit-element property (it's not listed in static get properties), so there will be no update lifecycle triggered, and no re-rendering happens upon that change.

$set slowing down performance in Vue

Below is my code
<div v-for="namespace in chosenNamespaces" v-bind:key="namespace.id">
<!-- Select the Parameter-->
<select #change="updateParameter($event, namespace.id)" v-model="currParameterValues[namespace.id]">
<option v-for="parameter in getParametersForNamespace(namespace.id)">{{parameter}}</option>
</select>
<!-- Select Property -->
<select #change="updatePropertyType($event, namespace.id)" v-model="currPropertyValues[namespace.id]">
<option v-for="property in getPropertiesForNamespace(namespace.id)">{{property}}</option>
</select>
<!-- Select Item -->
<select v-model="currItemValues[namespace.id]">
<option v-for="item in getItemsForNamespace(namespace.id)">{{item}}</option>
</select>
</div>
methods: {
updateParameter(data, id){
....
this.$set(currParameterValues, id, data,target.value)
this.$set(currPropertyValues, id, someValue)
}
updatePropertyType(data, id){
...
this.$set(currPropertyValues, someThing, val)
}
}
So I have many div which loops over the list of chosenNamespaces array and creates a set of selects. Now In that, I want to change the value of the second select i.e Select for Property when I change the value of Select paramater for that corresponding namespace using updateParameter method. I do that by using $set to update the array currPropertyValues. But I observe whenever I change the parameter option it take a while(4-5 secs) to process since maybe Vue takes time to react to the change in property array value. If I simply remove $set updateParameter it responds immediately. How can I solve this?
Edit
Here I have replicated on fiddle, when I change a value in dropdown it takes time to reflect: fiddle
This happens because of using the v-model array indexes like object keys, which creates huge arrays. For example, doing the following creates an array with 152,395,893 items, which will be very slow to work with in a template:
const arr = [];
arr[152395893] = '...';
With an array, this number is not a key's name, but a numerical index. What you want is an object. This creates just 1 item:
const obj = {};
obj[152395893] = '...';
Change these both to objects:
currParameterValues: {},
currOperatorValues: {}
The adjusted Fiddle

Select first element in dropdown in Angular 7

I am getting content of dropdown list as a Map<number, string>. When I get the map, it is received sorted according to keys in ascending order.
While showing it in html, I am setting pipe keyvalue and provided a function to sort the items in ascending order of values.
Now, I am trying to select first element of this dropdown, but unable to do so.
I have tried jQuery method to select the first element.
I have tried to set ngModel of the select box to first value of the map, but it sets the value to the first value received in the Map which is sorted by key.
My HTML:
<select class="form-control" id="empId" [(ngModel)]="empId" [disabled]="!isEditable">
<option *ngFor="let emp of empNames | keyvalue:descOrder" [value]="emp.key">{{ emp.value }}</option>
</select>
My ts file:
this.commonService.getEmployeeList())
.subscribe((response) => {
this.empNames = response;
this.empId = this.empNames.keys().next().value;
});
data I am sending from server is:
{id:1,name: "Tim"},
{id:6,name: "Martha"},
{id:5,name: "Alex"},
{id:8,name: "Stacy"}
data I am receiving on screen is like:
Alex
Martha
Stacy
Tim
with Tim pre-selected
what I need is Alex should be pre-selected.
Then set the empId before subscribing.
this.empId = 5;
this.commonService.getEmployeeList())
.subscribe((response) => {
this.empNames = response;
});
Of course you might want another logic based on some kind of order. You can never be sure how the data are going to be received.
In this case you need to send the order from your api and filter by order.
Working Demo
<option *ngFor="let emp of empNames | keyvalue:descOrder" [value]="emp.key" [selected]="emp.id === 1">{{ emp.value }}</option>
you can use selected attribute like above
I would highly recommand you to use Angular's reactive forms! And set the select's value to the one you want, when you recieve your data. Don't use ngModel as it is deprecated and should have been removed by Angular 7 (Or will be soon). Check this
The best way to pre select an option is to use ngModel as you tried. Your list is sorted by keys so what you want is not to select the first item, yes it's the first but in other order so or you change the order in code or you search for the item you want to select and stores it to set on model.
I would suggest some changes that should improve the code and fix your problem.
<select class="form-control" id="empId" [(ngModel)]="currentEmployer" [disabled]="!isEditable">
<option *ngFor="let emp of employers$ | async" [value]="emp">{{ emp.value }}</option>
</select>
And order your list in a pipe with the function you prefer.
public currentEmployer: Employer = null;
private sortByNameAscending(e1, e2) {
return e1.name > e2.name ? 1 : 0;
}
this.employers$ = this.commonService.getEmployeeList().pipe(
switchMap(employers => {
const sortedList = employers.sort(this.sortByNameAscending);
if (sortedList.length > 0) {
this.currentEmployer = sortedList[0];
}
return sortedList;
})
);

Dropdown list - model binding by value - angular 2

I have a page which allows a user to update the colour of a car. There are two api calls, one to bring back the car json object and one to fill a drop down list of colours.
My issue is that Angular 2 appears to do model binding via reference and not value. This means that although the colour 'green' might be set on the car, the color 'green' will not be selected in the drop down list even when it matches as that object has come from a different api call.
Here the select list is bound to the 'colour' property of car.
<div>
<label>Colour</label>
<div>
<select [(ngModel)]="car.colour">
<option *ngFor="let x of colours" [ngValue]="x">{{x.name}}</option>
</select>
</div>
</div>
When I set up the model in the back-end, if I set the color of the car to have the same value object (in this case green), the drop down is not selected. However when I set it using the same instance from the list of values used to bind the list it is selected as expected.
ngOnInit(): void {
this.colours = Array<Colour>();
this.colours.push(new Colour(-1, 'Please select'));
this.colours.push(new Colour(1, 'Green'));
this.colours.push(new Colour(2, 'Pink'));
this.car = new Car();
//this.car.colour = this.colours[1]; // Works
this.car.colour = new Colour(1, 'Green'); // Fails
}
Here is a plunker showing the issue. Simply switch between these to lines to illustrate the issue.
this.car.colour = this.colours[1]; // Works
this.car.colour = new Colour(1, 'Green'); // Fails
https://plnkr.co/edit/m3xBf8Hq9MnKiaZrjAaI?p=preview
How can I get angular to compare objects by value not reference when binding in this way?
Regards
Steve
Update
I solved in in my use case by setting the models 'superPower' property to the matching item in the list used to populate the dropdown list.
setupUpdate(id: number): void {
this.pageMode = PageMode.Update;
this.submitButtonText = "Update";
this.httpService.get<Hero>(this.appSettings.ApiEndPoint + 'hero/' + this.routeParams.get('id')).subscribe(response => {
this.hero = response;
this.httpService.get<SuperPower[]>(this.appSettings.ApiEndPoint + 'superPower/').subscribe(response => {
this.superPowers = response;
this.hero.superPower = this.superPowers.filter(x => x.id == this.hero.superPower.id)[0];
});
});
}
That's as designed. Angular2 only compares the object reference, not properties of an object.
You can bind to primitive values then compairson works as you expect
<select [(ngModel)]="car.colour.name">
<option *ngFor="let x of colours" [value]="x.name">{{x.name}}</option>
</select>
assuming that Color has a property name that contains the string Green.
You can also do the compairson yourself by looking up car.colour in colours and setting car.colour to the Colour instance from colours that represents the same colour.
You can use the following
<select [(ngModel)]="car.colour.name" (ngModelChange)="someFunction($event)" >
<option *ngFor="let x of colours" [value]="x.name">{{x.name}}</option>
</select>
When selected value would be updated, you will handle this in someFunction

Ng-repeat and auto complete selection with modal window in Angular

I'm doing an application in Angular. It is a Table one row that contain 2 column. Each column contain one select. They are empty. When the user press a button, a modal window shows up and display a grid with all the items (from json) of the first select. When the user click on one rows and press "Confirm", modal window closes filling the first select. In the meanwhile, the second select fill with the subarray of selected item.
In a few words, there are 2 select: you choose the option on the first (by a modal window) and then you choose the item of its subarray in the second select.
Then, the user can add new rows, repeating the select.
I've tried two ways to do this, and they half work. In the FIRST CODE
you can see that, after clicked on modal window, the first select fill it self (even if it is not the first , I don't know why..). And it doesn't not iterate well, because when you choose a item in new line, it modify all the other choises, and I want to prevent this.
<select ng-model="selectedProduct" ng-options="a as a.nome for a in items" ng-change="selectLot(select1)">
<option value="">-- Select Product --</option>
</select>
<select ng-model="selectedLot" ng-options="a as a.value for a in selectedProduct.lots" ng-change="lotSelect(select2)">
<option value="">-- Select Lot --</option>
</select>
The SECOND CODE works better. It iterate well. It change automatically the second item's selection well. But when I press on the modal window, the first selection doesn't automatically fill with the choosen item.
Can you help me? I can't find a solution..... Thank you so much in advice!
The core of the issue is that if you want to have a form that edits elements in an array, you need to have separate models for each of the rows in the array. You can do this by making "selectedProduct" and "selectedLot" into objects that map the array index to the selected value for that row.
I made an updated plunker with a working example, but without looking at it here is a rundown of the changes you would need to make. You need to change your models so they reference something using the array index of the row, and also pass that index into functions that modify the row:
<button class="btn btn-default" ng-click="open($index)">OPEN!!</button>
<select ng-model="selectedProducts[$index]" ng-options="a as a.nome for a in items" ng-change="selectLot(select1, $index)">
<option value="">-- Select Product --</option>
</select>
<select ng-model="selectedLots[$index]" ng-options="a as a.value for a in selectedProducts[$index].lots" ng-change="lotSelect(select2, $index)">
<option value="">-- Select Lot --</option>
</select>
You also want to update the functions in your controller to work with the array indexes:
$scope.selectLot = function(data, index){
$scope.Subarray = [];
for(i=0; i<$scope.items.length; i++){
if(data == $scope.items[i].id){
$scope.Subarray[$index] = $scope.items[i].lots;
$scope.selectedProducts[$index] = $scope.items[i];
break;
}
}
console.log($scope.Subarray);
}
$scope.lotSelect = function(id, $index) {
for(i=0; i<$scope.Subarray[$index].length; i++){
if(id == $scope.Subarray[$index][i].id){
$scope.selectedLots[$index] = $scope.Subarray[$index][i];
break;
}
}
}
And the modal:
$scope.open = function ($index) {
// ...
modalInstance.result.then(function (selectedItem) {
$scope.selectedProducts[$index] = selectedItem;
}, function () {
$log.info('Finestra selezione prodotto chiusa alle ore: ' + new Date());
});
You probably shouldn't be using a SELECT if you are allowing the choice to happen in a modal popup. All you want to do is show the selected item which you can easily do in a number of different ways. Additionally in the first example prodIsChanged(), which is what sets the subarray, is never called. An easier solution may be something like this:
<div>{{mainProduct}}</div>
<select ng-options="a as a.value for a in selectedProduct"></select>
var myApp = myApp.controller('Cntrl', function ($scope,$watch) {
$scope.mainProduct = '';
$scope.selecedProduct = '';
$watch('mainProduct',function(old,new) {
$scope.selectedProduct = ??? // The mainProduct has changed so set the list of sub products as necessary
};
}

Categories