Looks so simple, yet I don't know how to solve this efficiently.
I have two arrays activeClasses and doneClasses that each contain JavaScript Objects as their elements.
Each element should be able to be marked as "active" or "done" and should be deleted from the current, and added to the other array if its status changes after clicking "Save".
How can I achieve this without mixing up my array indices?
Behaviour is as expected except when selecting multiple elements:
https://stackblitz.com/edit/angular-etzocz?file=src%2Fapp%2Fapp.component.html
TS
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
activeChanged:Array<boolean> = [];
doneChanged:Array<boolean> = [];
toggleActive(i) {
this.activeChanged[i] = !this.activeChanged[i];
console.log('activeChanged:');
console.log(this.activeChanged);
}
toggleDone(i) {
this.doneChanged[i] = !this.doneChanged[i];
console.log('doneChanged:');
console.log(this.doneChanged);
}
save() {
var activeToBeDeleted:Array<number> = [];
var doneToBeDeleted:Array<number> = [];
//Check if active classes have changed
this.activeChanged.forEach(function (elem, index) {
//Has changed
if (elem) {
this.doneClasses.push(this.activeClasses[index]);
//Add to activeToBeDeleted
activeToBeDeleted.push(index)
}
}.bind(this))
//Check if done classes have changed
this.doneChanged.forEach(function (elem, index) {
//Has changed
if (elem) {
this.activeClasses.push(this.doneClasses[index]);
//Add to doneToBeDeleted
doneToBeDeleted.push(index)
}
}.bind(this))
console.log('before deletion')
console.log(this.activeClasses)
console.log(this.doneClasses)
//Delete array elements that were changed
activeToBeDeleted.forEach(function(elem) {
this.activeClasses.splice(elem,1)
}.bind(this))
doneToBeDeleted.forEach(function(elem) {
this.doneClasses.splice(elem,1);
}.bind(this))
console.log('after deletion')
console.log(this.activeClasses)
console.log(this.doneClasses)
//Rewrite activeChanged and doneChanged arrays again with false
this.activeChanged = new Array(this.activeClasses.length).fill(false)
this.doneChanged = new Array(this.doneClasses.length).fill(false)
}
//As from database
activeClasses:Array<Object> = [
{
name: 'test1'
},
{
name: 'test2'
}
];
doneClasses:Array<Object> = [
{
name: 'test3'
},
{
name: 'test4'
}
];
ngOnInit() {
//Fill activeChanged and doneChanged with false by default
this.activeChanged = new Array(this.activeClasses.length).fill(false)
this.doneChanged = new Array(this.doneClasses.length).fill(false)
}
}
HTML
<div *ngFor="let active_class of activeClasses; let i = index" style="background-color: blue; text-align: center; padding: 20px; color: white;">
<button *ngIf="!activeChanged[i]" (click)="toggleActive(i)">Mark as done</button>
<button *ngIf="activeChanged[i]" (click)="toggleActive(i)">Mark as active</button>
{{ active_class.name }}
</div>
<div *ngFor="let done_class of doneClasses; let i = index" style="background-color: red; text-align: center; padding: 20px; color: white;">
<button *ngIf="!doneChanged[i]" (click)="toggleDone(i)">Mark as active</button>
<button *ngIf="doneChanged[i]" (click)="toggleDone(i)">Mark as done</button>
{{ done_class.name }}
</div>
<button (click)="save()">Save</button>
It's because when you splice the items in natural sort order, the array indexes change for the items after the first one you remove.
The solution is to do call reverse() before splicing, which allows you to progress down the array without impacting indexes.
This fixes it:
//Delete array elements that were changed
activeToBeDeleted.reverse().forEach(function(elem) {
this.activeClasses.splice(elem,1)
}.bind(this))
doneToBeDeleted.reverse().forEach(function(elem) {
this.doneClasses.splice(elem,1);
}.bind(this))
Why is it working?
First, activeChanged and doneChanged are arrays storing booleans at the index of the item modified (active, or done, see toggle methods). When you first loop over these arrays in the Save method, it loops over the items in ascending order, and thus, you are storing the indexes in ascending order into the activeToBeDeleted and doneToBeDeleted arrays.
So, after that, when you loop over the activeToBeDeleted and doneToBeDeleted arrays and delete from the activeClasses or doneClasses, then while the first delete works, none of the other deletes can work, because the first delete action removed an item from the beginning of the array, and caused all following indexes to be shifted and incorrect.
The solution works because by reversing the list of indexes (going in descending order), you are deleting from the end of the arrays working towards the beginning, which naturally preserves all the indexes. I'd recommend you use pen and pencil, it's a classic pattern actually.
Related
I am new to React.
My child component (SmithchartSeriesDirective) successfully displays the data passed from the server, when the parent component (SimplePanel) is loaded for the first time. On subsequent calls the data received from server changes, it is reflected in the props, but once I bind this data to the child component, it does not reflect the updated data in the component.
I am binding the data in listResult array.
Below is Parent Component SimplePanel
export class SimplePanel extends Component<Props> {
render() {
var reactance: number[] = [];
var resistance: number[] = [];
this.props.data.series.map((anObjectMapped, index) => {
if(index==0)
{
reactance = anObjectMapped.fields[0].values.toArray();
}
else
{
resistance = anObjectMapped.fields[0].values.toArray();
}
});
var resultArr =
{
resistance:0,
reactance:0
};
let listResult =[];
for (let index = 0; index < resistance.length; index++) {
var newObj = Object.create(resultArr);
newObj.resistance = Number(resistance[index]);
newObj.reactance=reactance[index];
listResult.push(newObj);
}
return (<div className='control-pane' style={{ height:'100%', width:'100%', backgroundColor:'#161719' }} >
<div className='col-md-12 control-section' style={{ height:'100%', width:'100%' }}>
<SmithchartComponent id='smith-chart' theme="MaterialDark" legendSettings={{ visible: true, shape: 'Circle' }}>
<Inject services={[SmithchartLegend, TooltipRender]}/>
<SmithchartSeriesCollectionDirective>
<SmithchartSeriesDirective
points= {listResult}
enableAnimation={true}
tooltip={{ visible: true }}
marker={{ shape: 'Circle', visible: true, border: { width: 2 } }}
>
</SmithchartSeriesDirective>
</SmithchartSeriesCollectionDirective>
</SmithchartComponent>
</div>
</div>);
welcome to stack overflow.
First remember that arrays saved by reference in JavaScript. So if you change any array by push() or pop() methods, reference to that array doesn't change and React can't distinguish any change in your array (to re-render your component).
let a = [2];
let b = a;
b.push(4);
a == b; //a is [2] and b is [2,4] but the result is true.
You can use this approach as a solution to this problem:
let listResult = [...oldListResult, newObj]; // ES6 spread operator
Also consider for rendering array elements you need to use key prop, so React can render your components properly. more info can be found here.
I have two functions one to dequeue and one to enqueue a user-inputted item from/to an array respectively.
What I know about queues
What I know about queues is pretty limited, I know they are similar to stacks but rather than being Last-In-First-Out (LIFO) as stacks are they are First-In-First-Out (FIFO) so basically whatever elements go into the array first will be taken out of the array first.
What I am trying right now
What I am trying to do right now is with the Enqueue button I am adding items to an array while incrementing the variable count by 1 with each push of the button so as to add each new user input to the next array position. In the dequeue function, I am setting each element of the dequeue array equal to each element of the original array by incrementing the dequeueCount variable.
What is the problem
The problem here is this, when I push the dequeue button I basically need to reindex everything so that after I dequeue an element the element at index 1, now takes the position index 0 and basically I always want to dequeue the item at element 0.
queue.component.ts
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'app-queue',
templateUrl: './queue.component.html',
styleUrls: ['./queue.component.css']
})
export class QueueComponent implements OnInit {
#Input() userInput: String
array = []
arrayCount = 0
dequeueCount = 0
dequeueArray = []
dequeued = null
constructor() { }
ngOnInit() {
}
enQ() {
this.array[this.arrayCount++] = this.userInput;
}
deQ() {
deQ() {
if (this.arrayCount > 0) {
this.dequeueArray[this.dequeueCount++] = this.array[this.dequeueCount-1]
this.dequeued = this.dequeueArray[this.dequeueCount-1]
this.arrayCount--
}
else {
this.dequeued = "There is nothing else to dequeue"
}
}
}
and I am trying to show the current value of the array here
<div>
<label for="userInput">Input to Array:
<input [(ngModel)]="userInput" type="text">
</label><br>
<button (click)="enQ()">Enqueue</button>
<button (click)="deQ()">Dequeue</button>
<h3>Arrays</h3>
<p *ngFor = "let item of array"> {{ array }} </p>
<h3>Dequeued Item</h3>
<p> {{ dequeued}} </p>
</div>
when I press the enqueue function everything seems to work correctly and a value is added to the array at the correct position so no problems with the enqueue function, however with the dequeue function, I need to somehow be able to dequeue the items and also remove the first item from the array and redisplay {{ array }} with the dequeued item removed.
Try the snippet, it should solve the problem.
import { Component } from "#angular/core";
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
userInput: String;
array = [];
dequeued = [];
enQ() {
this.array.push(this.userInput);
}
deQ() {
if (this.array.length > 0) {
this.dequeued.push(this.array[this.array.length - 1]);
this.array.shift();
}
}
}
<div>
<label for="userInput">Input to Array:
<input [(ngModel)]="userInput" type="text">
</label><br>
<button (click)="enQ()">Enqueue</button>
<button (click)="deQ()">Dequeue</button>
<h3>Arrays</h3>
<p> {{ array |json}} </p>
<h3>Dequeued Item</h3>
<p> {{ dequeued|json}} </p>
</div>
In your code you missing the pipe on dequeue variable, so when it hold the array the values will not be rendered by angular
Demo
I think your approach is complicated and vague, I made a new one based on your intention.
Even if this code doesn't match your needs, at least you can start from this one.
queue.component.ts
import { Component, Input } from "#angular/core";
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
#Input() userInput;
array = [];
arrayCount = 0;
dequeuedCount = 0;
dequeued;
get queue() {
const q = [];
for (let i = this.dequeuedCount; i < this.arrayCount; i++) {
q[i - this.dequeuedCount] = this.array[i];
}
return q;
}
enQ() {
this.array[this.arrayCount++] = this.userInput;
}
deQ() {
if (this.arrayCount > this.dequeuedCount) {
this.dequeued = this.array[this.dequeuedCount++];
} else {
this.dequeued = "There is nothing else to dequeue";
}
}
}
queue.component.html
<div>
<label for="userInput">Input to Array:
<input [(ngModel)]="userInput" type="text">
</label><br>
<button (click)="enQ()">Enqueue</button>
<button (click)="deQ()">Dequeue</button>
<h3>Arrays</h3>
<p> {{ queue | json }} </p>
<h3>Dequeued Item</h3>
<p> {{ dequeued | json }} </p>
</div>
stackblitz: https://stackblitz.com/edit/angular-bt8ufd
I have a problem with deleting an item from an array. My current piece of code is removing the item. But it it always the last one not the one that I choose. I have found some clues that index needs to be checked against -1
Array
ngOnInit() {
this.todos = [
{
text: 'Pick up'
},
{
text: 'Meeting'
},
{
text: 'Dish washing'
}
];
}
Functions
addTodo(){
this.todos.push({
text: this.text
});
}
deleteTodo(todoText){
this.todos.splice(this.todos.indexOf(todoText), 1);
}
Do not pass only the todoText to your deletion method, pass the whole todo, so if your template looks like this:
<div *ngFor="let todo of todos" (click)="deleteTodo(todo)">
{{todo.text}}
</div>
and then your delete method works fine in your TS:
deleteTodo(todo){
this.todos.splice(this.todos.indexOf(todo), 1);
}
the problem in your code is that you are compare by object reference, so do this :
deleteTodo(todoText){
this.todos
.splice(this.todos.map(todo=>todo.text)
.indexOf(todoText.text), 1);
}
I'm assuming that todoText argument is an object that has text property.
if you want to delete by text:
deleteTodo(text){
this.todos
.splice(this.todos.map(todo=>todo.text)
.indexOf(text), 1);
}
I have a key value pair defined as below, which is being used for select using ng-options
$scope.BucketEnum = [
{ display: 'Error', value: 0 },
{ display: '1', value: 1 },
{ display: '2', value: 2 },
{ display: '3', value: 3 },
{ display: '4', value: 4 },
{ display: '5', value: 5 },
{ display: 'Flows', value: 125 },
{ display: 'Recovery', value: 151 }
];
I am using this key value pair to display select box in ng-options
<select ng-model="selectedBucket" ng-options="row.value as rows.display for row in BucketEnum" multiple="multiple" ></select>
now if I set ng-model i.e. $scope.selectedBucket = 10, I want to display the text Error. Is it possible to show value Error for all the values which are not there in $scope.BucketEnum array.
NOTE
I am looking at a more generic way to do this e.g a filter for doing this
SCENARIO
There is certain historical data in database, which has some garbage and some good data.
For each garbage value, i need to show the current garbage value as well as the valid values to select from, so for the end users to fix it.
Would this fit your needs ?
jsfiddle
app.filter('bootstrapValues', function(){
return function(initial, baseBucket){
var result = [];
for(var i=0; i<initial.length; i++){
var flag = false;
for(var j=1; j<baseBucket.length; j++){ //from 1 or 0.. you call
if(initial[i] === baseBucket[j].value){
flag = true;
result.push(baseBucket[j]);
break; // if there are repeated elements
}
}
if(!flag)
result.push(baseBucket[0])
}
return result;
};
});
Using it to start the selectedBucket, in your controller:
// setting initials
$scope.selectedBucket = $filter('bootstrapValues')(initialSet, $scope.bucketEnum);
Does it help?
Edit: Here is other jsfiddle with little modifications, if the value is not in the bucket it add the element to the list with Error display and as a selected value.
Using ng-options generates multiple HTML <select> elements for each item in your BucketEnum array and 'returns' the selected value in your ng-model variable: selectedBucket. I think the only way to display the options without an additional blank entry is to ensure the value of selectedBucket is a valid entry in BucketEnum.
Your question states:
if I set ng-model i.e. $scope.selectedBucket = 10, I want to display
the text Error.
I assume you want to display the value: {{BucketEnum[selectedBucket].display}}
So... starting with $scope.selectedBucket = 10, we want some generic way of implementing a select using ng-options which will reset this value to a default.
You could do this by implementing an attribute directive, allowing you to write:
<select ng-model="selectedBucket" select-default="BucketEnum"
ng-options="row.value as row.display for row in BucketEnum"
multiple="multiple">
An example of this approach is shown below. Note that this assumes the default value is zero and does not handle multiple selections (you'd have to iterate over the selections when comparing to each item in BucketEnum and decide what to do if there is a mix of valid and invalid selections).
app.directive("selectDefault",function(){
return{
restrict: 'A',
scope: false,
link:function(scope,element,attrs){
var arr= scope[attrs.selectDefault]; // array from attribute
scope.$watch(attrs.ngModel,function(){
var i, ok=false;
var sel= scope[attrs.ngModel]; // ng-model variable
for( i=0; i<arr.length; i++){ // variable in array ?
if( arr[i].value == sel ) // nasty '==' only for demo
ok= true;
}
if( ! ok )
scope[attrs.ngModel]=0; // set selectedBucket to 0
});
}
};
});
I've run up a jsfiddle of this here
The downside of this is that I've used a $watch on the ng-model which causes side-effects, i.e. any assignment of the named variable will trigger the $watch function.
If this is the sort of solution you were looking for, you could expand the directive in all sorts of ways, for example:
<select ng-model="selectResult"
select-default="99" array="BucketEnum" initial="selectedBucket"
ng-options="row.value as row.display for row in BucketEnum"
multiple="multiple">
...the idea being that the select-default directive would read the default value ("99" here), the array and an initial value then set selectResult accordingly
You would need to code for this explicitly. Scan the choices you want to set against the choices that are present. If you don't find it, select the Error value too.
Note also that you need to pass an array for selectedBucket and it needs to include the actual option objects not just the values inside them.
<div ng-app="myApp">
<div ng-controller="myController">
<p>Select something</p>
<select ng-model="selectedBucket"
ng-options="row as row.display for row in bucketEnum" multiple="multiple">
</select>
</div>
</div>
.
var app = angular.module('myApp', []);
app.controller('myController', function ($scope) {
var initialSet = [1, 5, 10];
$scope.bucketEnum = [
{ display: 'Error', value: 0 },
{ display: '1', value: 1 },
{ display: '2', value: 2 },
{ display: '3', value: 3 },
{ display: '4', value: 4 },
{ display: '5', value: 5 },
{ display: 'Flows', value: 125 },
{ display: 'Recovery', value: 151 }
];
var selected = [];
var error = $scope.bucketEnum[0];
angular.forEach(initialSet, function(item) {
var found;
angular.forEach($scope.bucketEnum, function (e) {
if (+item == +e.value) {
console.log('Found ', e);
found = item;
selected.push(e);
}
});
if (typeof found === 'undefined') {
selected.push(error);
}
$scope.selectedBucket = selected;
console.log(selected);
});
});
I Have an array of items like this which contains a list of animals and a list of fruits in a random order.
$scope.items = [{name:'mango',type:'fruit'},{name:'cat',type:'animal'},{name:'dog',type:'animal'},{name:'monkey',type:'animal'},{name:'orange',type:'fruit'},{name:'banana',type:'fruit'},...]
Then I Have a array of colors like
$scope.colorSeries = ['#3366cc', '#dc3912', '#ff9900',...];
$scope.setBGColor = function (index) {
return { background: $scope.colorSeries[index] }
}
I am using the items array to render the fruits only in a div with background color selected from the colorSeries based on the index like colorSeries[0] which will give me #3366cc
<div data-ng-repeat="item in items " ng-if="item.type =='fruit'">
<label ng-style="setBGColor($index)">{{item.name}}</label>
</div>
Things working fine if the length of the items array is less than length of colorSeries array.The problem arises if the length of colorSeries array is less than the items array.e.g if i have a color series with 3 colors then for this items array the last item i.e orange will need a color indexed as colorSeries[4] which is undefined where as I have rendered only three items. So, is it possible to get the index like 0,1,2 i.e the index of elements rendered with ng-if.
Instead of using ng-if, I would use a filter. then, the $index will be always correspond to the index in the result list after applying the filter
<div data-ng-repeat="item in items|filterFruit">
<label ng-style="setBGColor($index)">{{item.name}}</label>
</div>
angular.module('app.filters', []).filter('filterFruit', [function () {
return function (fruits) {
var i;
var tempfruits = [];
var thefruit;
if (angular.isDefined(fruits) &&
fruits.length > 0) {
for(thefruit = fruits[i=0]; i<fruits.length; thefruit=fruits[++i]) {
if(thefruit.type =='fruit')
tempfruits.push(thefruit);
}
}
return tempfruits;
};
}]);
Try this..
this.itemList = [{
name:'apple'
}, {
name: 'fruit'
}];
this.colorlist = [{ color: 'red' }, { color: 'orange' }, { color: 'blue' }];
<div data-ng-repeat="item in itemList " ng-if="item.name=='fruit'">
<label ng-style="colorlist [$index]">{{item.name}}</label>
</div>
If I were you, I would embed the colors inside the $scope.items objects, since you always use them coupled with a fruit.
Anyway, to address your specific code configuration, I would add a counter in my controller and use it to loop through the colors.
Something like this:
app.controller('myCtrl', function ($scope) {
$scope.colorSeries = ['#3366cc', '#dc3912', '#ff9900',...];
var colorCounter = $scope.colorSeries.length;
var colorIdx = -1;
$scope.setBGColor = function () {
// get back to the first color if you finished the loop
colorIdx = colorCounter? 0: colorIdx+1
return { background: $scope.colorSeries[colorIdx] }
}
})
And then in your view (note that there is no $index)
<div data-ng-repeat="item in items " ng-if="item.type =='fruit'">
<label ng-style="setBGColor()">{{item.name}}</label>
</div>