I have a select field like this:
<div *ngFor="let filter of filters; let idx = index">
<select [id]="'name' + idx" [(ngModel)]="filter.name" (change)="changeFilter(idx, $event)">
<option val="a">A</option>
<option val="b">B</option>
</select>
</div>
My change() function on the component doesn't detect the change instantly. Simplified:
#Component()
export class Filters {
public filters = [{name: "a"}, {name: "b"}, {name: "a"}];
public change(idx: number, $event: Event) {
console.log(this.filters[idx].name === $event.target.name); // false here
setTimeout(() => {
console.log(this.filters[idx].name === $event.target.name); // Now it's true
}, 10);
}
}
Now, if I change between the options, the change() function needs some time - usually less then 3 milliseconds on that setTimeout, but sometimes more.
Now, I am sure this is not the best way to detect the change, and I'll find out how to do it properly, but I'm curious as to how to determine when is the change reflected on my model?
ngModel doesn't support binding to variables created by ngFor.
Use instead
[(ngModel)]="filters[idx].name"
You could also try
(ngModelChange)="changeFilter(idx, $event)"
ngModelChange is probably emitted after the value was changed while for (change) it depends on the browser what event and event handler is processed first (AFAIR ngModel uses input)
Related
Let's say I have the following array in an angular controller:
somelist = [
{ name: 'John', dirty: false },
{ name: 'Max', dirty: false },
{ name: 'Betty', dirty: false }
];
I want to ng-repeat through it in my view, and generate editable fields for each record:
<div ng-repeat="i in somelist">
<input type="text" ng-model="i.name"/>
</div>
How would I go about efficiently marking the field as dirty if someone edits the textbox(model)?
I realize that I could use ng-change on the text field, however, that fires every time a user makes a single change(enters a key) on the textbox, making loads of calls unnecessarily.. Is there a more efficient way of doing this which I am missing?
With JavaScript...
*Edited: if that textareas don't have any other 'change' event to run, you can try inline onchange event, and replace it's value after have run once. Just making onchange="once(this)" become into this — onchange="" *In background. The code will still stay in your HTML. Demo:
(also exist input and keyup events... in Angular as well)
function once(e){
e.style.color="red";
e.onchange = "";
//just demo... remove this.
const d = document.getElementById('demo');
d.innerText = Number(d.innerText) + 1;
}
<textarea class="moo" onchange="once(this)">Change me!</textarea>
<textarea class="moo" onchange="once(this)">Me too!</textarea>
<textarea class="moo" onchange="once(this)">And me!</textarea>
<br><br>
Triggered times: <span id="demo">0</span>
Running eventListener function once (not really):
let once = [];//creating empty array
const moo = document.getElementsByClassName('moo');//getting all textareas
for(let i = 0; i < moo.length; i++ ){//looping, to add 'change' event to each element
once.push(1);//adding '1' to array 'i' times. Here it will look like [1,1,1];
moo[i].addEventListener('change', function(){
if(once[i]==0){return}//if array element equals 0 = return and don't run the function
this.style.color = "red";
once[i] = 0;//after triggered = making array element = 0;
//just demo... remove this.
const d = document.getElementById('demo');
d.innerText = Number(d.innerText) + 1;
});
}
<textarea class="moo">Change me!</textarea>
<textarea class="moo">Me too!</textarea>
<textarea class="moo">And me!</textarea>
<br><br>
Triggered times: <span id="demo">0</span>
*function is still working each time... but returning immediately, which is better, than "full" run.
To improve efficiency, reduce the number of watchers by using :: and eliminating two-way binds, e.g.ng-model:
<div ng-repeat="i in ::somelist">
<input type="text" value="{{i.name}}"
ng-blur="$emit('nameChanged', i)"/>
</div>
Then, in your controller:
$scope.$on('nameChanged', (event, i) => updateName(i));
Then a quick, simple function that updates the name with the corresponding ID using i.id and i.name, assuming you have:
$scope.someList = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Max' },
{ id: 3, name: 'Betty }'
Explanation
If people aren't going to be added/removed from the list, then you can use :: on someList, also known as a one-time binding, to improve efficiency. This circumvents setting up a watcher.
Also, by setting value={{i.name}}, you effectively set up a one-way bind from the controller to the DOM, rather than two-way, meaning that the value of the input isn't being checked every loop of the $digest cycle, but any changes to the model will update the DOM.
Just an idea, feel free to play with variations, such as dropping blur and using a single button that would update all changed fields at once.
You won't get much more efficient than that, unless you also remove the watcher from value="{{i.name}}" like so value="{{::i.name}}", and then you manually update the DOM when the event is received.
I'm writing a function to update a custom checkbox when clicked (and I don't want to use native checkbox for some reasons).
The code for checkbox is
<div class="tick-box" :class="{ tick: isTicked }" #click="() => isTicked = !isTicked"></div>
which works find.
However, there are so many checkboxes, so I use object to keep track for each item. It looks like this
<!-- (inside v-for) -->
<div class="tick-box" :class="{ tick: isTicked['lyr'+layer.lyr_id] }" #click="() => {
isTicked['lyr'+layer.lyr_id] = !isTicked['lyr'+layer.lyr_id]
}"></div>
Now nothing happens, no error at all.
When I want to see isTicked value with {{ isTicked }}, it's just shows {}.
This is what I define in the <script></script> part.
export default {
data() {
return {
isTicked: {},
...
};
},
...
}
Could you help me where I get it wrong?
Thanks!
Edit:
I know that declaring as isTicked: {}, the first few clicks won't do anything because its proerty is undefined. However, it should be defined by the first/second click not something like this.
Objects does not reflect the changes when updated like this.
You should use $set to set object properties in order to make them reactive.
Try as below
<div class="tick-box" :class="{ tick: isTicked['lyr'+layer.lyr_id] }" #click="onChecked"></div>
Add below method:
onChecked() {
this.$set(this.isTicked,'lyr'+this.layer.lyr_id, !this.isTicked['lyr'+this.layer.lyr_id])
}
VueJS watches data by reference so to update object in state you need create new one.
onChecked(lyr_id) {
const key = 'lyr'+lyr_id;
this.isTicked = {...this.isTicked, [key]: !this.isTicked[key]};
}
I know for a fact there is something pretty obvious here that I am completely missing, so your help is greatly appreciated.
I have a feature that provides two dropdowns. They contain the same data (the feature allows a trade between two people, the people is the data), but I want them each to get their own copy of said data.
Another part of this feature is that by picking Person A in the first dropdown, I want to disable Person A in the second dropdown, and vice versa, so I have the ng-options tag paying attention to a disabled property on the object.
The issue I have is that even with using a method such as Lodash's clone to properly create a "new" array upon first time assignment, every time I access Person A in ONE array (and specifically do NOT access the other array) invariably I am seeing that when I touch Person A, that object is updated in BOTH arrays, which has me flustered.
This feels like a down-to-the-metal, barebones Javascript issue (standard PEBCAK, I feel like I'm clearly misunderstanding or straight up missing something fundamental), maybe with a bit of AngularJS rendering-related fun-ness involved, but... What gives?
angular.module('myApp', [])
.controller('weirdDataController', function($scope) {
$scope.$watch('manager1_id', () => {
if (angular.isDefined($scope.manager1_id) && parseInt($scope.manager1_id, 10) > 0) {
$scope._disableManagerInOtherDropdown(false, $scope.manager1_id);
}
});
$scope.$watch('manager2_id', () => {
if (angular.isDefined($scope.manager2_id) && parseInt($scope.manager2_id, 10) > 0) {
$scope._disableManagerInOtherDropdown(true, $scope.manager2_id);
}
});
$scope._gimmeFakeData = () => {
return [{
manager_id: 1,
manager_name: 'Bill',
disabled: false
},
{
manager_id: 2,
manager_name: 'Bob',
disabled: false
},
{
manager_id: 3,
manager_name: 'Beano',
disabled: false
},
{
manager_id: 4,
manager_name: 'Barf',
disabled: false
},
{
manager_id: 5,
manager_name: 'Biff',
disabled: false
},
];
};
const data = $scope._gimmeFakeData();
$scope.firstManagers = _.clone(data);
$scope.secondManagers = _.clone(data);
$scope._disableManagerInOtherDropdown = (otherIsFirstArray, managerId) => {
const disableManagers = manager => {
manager.disabled = manager.manager_id === managerId;
};
if (otherIsFirstArray) {
$scope.firstManagers.forEach(disableManagers);
} else {
$scope.secondManagers.forEach(disableManagers);
}
console.log('Is the first item the same??', $scope.firstManagers[0].disabled === $scope.secondManagers[0].disabled);
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<div ng-app="myApp" ng-controller="weirdDataController">
<div class="col-xs-12 col-sm-6">
<select class="form-control" ng-model="manager1_id" ng-options="manager.manager_id as manager.manager_name disable when manager.disabled for manager in firstManagers track by manager.manager_id">
<option value="" disabled="disabled">Choose one manager</option>
</select>
<select class="form-control" ng-model="manager2_id" ng-options="manager.manager_id as manager.manager_name disable when manager.disabled for manager in secondManagers track by manager.manager_id">
<option value="" disabled="disabled">Choose another manager</option>
</select>
</div>
</div>
<br /><br />
I threw everything relevant on $scope just for the sake of getting it working and illustrating the issue, but here's how it goes:
On init, I grab the array, then clone a copy for each dropdown
When each dropdown changes the model property (the object ID), I have a scope listener then call a method to handle disabling the selected object/person in the OPPOSITE list
Within this method, I determine which of the two lists/arrays to iterate through and mark the disabled object
At the end of this method, I do a simple console.log call to check the value of a given object. For quick-and-dirty simplicity, I just grab item at index 0 .
What I expected: one object have a disabled value of true, and the opposite object to have false. What I see: they both have true (assuming you select the first "real" item in the dropdown)
What's the deal? How big of an idiot am I being?
The answer to my question was: clone() does not perform a "deep" clone by default, therefore I was dealing with the same array despite making the flawed attempt that I was not. Using Lodash's cloneDeep() method solved my issue, but as Patrick suggested I reevaluated how I wrote the method in question and refactored it, which I removed the need to use any cloning at all.
I have multiple <mat-button-toggle> elements generated in my app and I want always only one selected. The problem that I now have is, how to get the component reference to the last selected toggle-button when another toggle button is clicked.
I really searched quite a while but couldn't understand how to do it.
component.html
<mat-button-toggle (click)="onKeywordSelect($event)" *ngFor="let keyword of keywords" [id]="keyword.id" [attr.id]="keyword.id" [value]="keyword.id" class="keyword">
<div class="text">{{ keyword.name }}</div>
</mat-button-toggle>
component.ts
// imports and #Component
export class NavbarComponent implements OnInit {
keywords = [new Keyword('name1'), new Keyword('name2')]; // sample data
$selectedKeyword: $ | any; // I've imported JQuery
onKeywordSelect(event: any) {
// This element depends on where you mouse was positioned when clicking
// Most often not the <mat-button-toggle> but some child element
const target = event.target;
// To get to the <mat-button-toggle> component that was clicked
const matButton = $(target).closest('mat-button-toggle');
if (this.$selectedKeyword !== undefined) {
// At the start there is no selected keyword
// TODO: Set the 'checked' property of the cur selected keyword to false
}
this.$selectedKeyword = $matButton;
}
}
I tried it with #ViewChild() but because the id of the selected keyword changes when the user selects one I don't know how to keep track of the selected component reference.
Edit
Forgot to mention: Yes I'm aware of mat-button-toggle-group but I don't want to use it because of some styling. Is there no other way to solve this?
Edit: Updated my ans as your requirement is not to use mat-button-toggle-group:
You can use checked property and set current and last selected value on change event like this:
component.html:
<mat-button-toggle
*ngFor="let keyword of keywords"
value="{{keyword.id}}"
[id]="keyword.id"
[attr.id]="keyword.id"
(change)="this.onChange(keyword.id)"
[checked]="this.currValue === keyword.id">
{{keyword.name}}
</mat-button-toggle>
<div class="example-selected-value">
Last Selected value: {{this.lastValue}}
</div>
<div class="example-selected-value">
Current Selected value: {{this.currValue}}
</div>
component.ts:
keywords: any = [
{id: 1, name: "name1"},
{id: 2, name: "name2"},
{id: 3, name: "name3"},
]
lastValue: string = "";
currValue: string = "";
onChange = (value) => {
this.lastValue = this.currValue;
this.currValue = value;
}
Check Demo Here.
Add mat-button-toggle-group to select only one button and get value of group to get last selected button
see: https://stackblitz.com/angular/yrqebgjbxao?file=app%2Fbutton-toggle-exclusive-example.ts
Trying to build a react component where I need to control checked status of checboxes and select options when change event occurs. But I don't know how it is possible to get value of the checked checkbox(es) and set the state.
We're using custom data-binding. On page load, we're assigning selected value of the select, with jQuery.
Programmatically changing value of the select must update matching check-boxes.
When user checks/unchecks a checkbox, corresponding value must be toggled on the select.
With jQuery I would loop trough check-boxes and build array with checked values then assign this value to the select on checkbox change. And when select change event is triggered, I would uncheck all check-boxes and check the ones matching selected items.
This is my simplified code.
state = {
items: [
{Key: 1, Value: "A"},
{Key: 29, Value: "Z"}
],
selected: [1, 29]
}
function onSelectChange(){
// Update checked checkboxes
}
function onCheckboxChange(){
// Update selected options
}
<div>
<select multiple onChange={onSelectChange} className="hidden">
{this.state.items.map((item, i) =>
<option value={item.Key}>{item.Value}</option>
)}
</select>
<div className="checkboxes">
{this.state.items.map((item, i) =>
<input
type="checkbox"
key={i}
checked={this.state.selected.indexOf(item.Key) >= 0}
onChange={onCheckboxChange} />
)}
</div>
</div>
You would use this.setState({}) inside the event handler to update the state of a component in React. This triggers a rerender in React which allows you to query the the updated state (this.state.selected).
Be advised that this.setState() expects an immutable object, so you should never change the previous, but always set a new state object!
Answer to comment:
For selectItem:
onSelectChange = event => this.setState({selected:event.target.value})
and for checkboxes (note the prevState):
onCheckboxChange = item => event => this.setState(({selected,...prevState})=> ({
...prevState,
selected: event.target.checked? selected.concat(item): selected.filter(it=> it!== item)
}))
and usage:
{this.state.items.map((item, i) =>
<input
type="checkbox"
key={i}
checked={this.state.selected.indexOf(item.Key) >= 0}
onChange={onCheckboxChange(item)} />
)}
This has the downside that it will create a new function on each rerender, so it's better to create a custom CheckboxItem and pass the item to it and use a handleClick.
onChange function give event where you could check whether the select box is being checked or not using this you can update your state accordingly.
function onCheckboxChange(e){
console.log("checked", e.target.checked);
// Then, on the basis of boolean you can update your state
}