prevent part of array saving to localStorage in angular - javascript

I am trying to prevent resultComment being saved into localStorage, how can I achieve that? Currently when I delete localStorage data from browser and calculate result, result gets pushed to my resultHistory array and only the result and temperatureUnit gets saved to localStorage which I want, but on the second calculation now also the resultComment gets saved. How can I prevent this or what can be done differently? Here is StackBlitz: https://stackblitz.com/edit/angular-ivy-mdhnnt?file=src%2Fapp%2Fapp.component.html,src%2Fapp%2Fapp.module.ts,src%2Fapp%2Fapp.component.ts,src%2Fapp%2Fapp.component.css
Steps to recreate problem:
Clear localStorage data and refresh page,
type number in both inputs and click on button -> result and unit gets saved.
Now type different numbers in inputs and click on button -> and now the resultComment gets saved as well). Ignore weird calculation results. Its missing parts which I didn't add to StackBlitz.
component ts
class HistoryResult {
constructor(
public result: number,
public temperatureUnit: string,
public resultComment?: string
) {}
}
resultHistory: HistoryResult[] = [];
//in ngOnInit iam initializing resultHistory so it stays on page when i refresh and adding comment to each result
ngOnInit() {
...
..code..
...
this.resultHistory = JSON.parse(localStorage.getItem('result')) || [];
this.resultHistory.map((item) => {
item.resultComment = this.heatIndexService.determineComment(
item.result,
item.temperatureUnit
);
});
}
onCalculateButtonClick(): void {
this.result = null;
this.resultTemperatureUnit = this.temperatureUnit.value;
if (this.heatIndexForm.invalid) {
this.heatIndexForm.controls['temperature'].markAsDirty();
this.heatIndexForm.controls['humidity'].markAsDirty();
return;
}
this.result = this.heatIndexService.calculateHeatIndex(
this.temperatureValue,
this.humidityValue,
this.resultTemperatureUnit.code
);
this.heatIndexService.saveResultInLocalStorage(
this.result,
this.temperatureUnit.value.code,
this.resultHistory
);
this.resultHistory.map((item) => {
item.resultComment = this.heatIndexService.determineComment(
item.result,
item.temperatureUnit
);
});
}
service functions
saveResultInLocalStorage(
result: number,
unit: string,
historyResults: HistoryResult[]
): void {
// Check if result is the same as last one
if (
historyResults.length === 0 ||
historyResults.slice(-1)[0].result !== result
) {
historyResults.push(new HistoryResult(result, unit));
// Remove oldest result if more than 3 results
if (historyResults.length > 3) {
historyResults.shift();
}
localStorage.setItem('result', JSON.stringify(historyResults));
}
}
determineComment(temperature: number, units: string): string {
if (units === 'C') {
temperature = (temperature * 9) / 5 + 32;
}
if (temperature >= 75 && temperature <= 90) {
return 'Caution: fatigue is possible with prolonged exposure and activity. Continuing activity could result in heat cramps.';
} else if (temperature > 90 && temperature <= 105) {
return 'Extreme caution: heat cramps and heat exhaustion are possible. Continuing activity could result in heat stroke.';
} else if (temperature > 105 && temperature <= 130) {
return 'Danger: heat cramps and heat exhaustion are likely; heat stroke is probable with continued activity.';
} else {
return 'Extreme danger: heat stroke is imminent.';
}
}
and HTML where the previous calculations are shown
<p-table [value]="resultHistory" [tableStyle]="{'min-width': '50rem'}">
<ng-template pTemplate="header">
<tr>
<th>Heat Index</th>
<th class="heat-index-effect">Effect on the body</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-history>
<tr>
<td>{{history.result | number : '1.2-2'}}°{{history.temperatureUnit}}</td>
<td>{{history.resultComment}}</td>
</tr>
</ng-template>
</p-table>

Opening question response
You are mutating the array when calling this.historyResults.map(...) and then saving that value in saveResultInLocalStorage when calling localStorage.setItem(historyResults).
Change your value to be saved as a new array with only the properties you want then call localStorage.setItem(newValue).
To pick out only the properties you want and save them you could do the following:
const resultsToSave: Pick<HistoryResult, 'result' | 'temperatureUnit'>[] = historyResults.map(h => ({ result: h.result, temperatureUnit: h.temperatureUnit }));
localStorage.setItem('result', JSON.stringify(resultsToSave));
An alternative to using Pick above to get the reduced type could be to use Omit which may be shorter depending on how many fields you specify, e.g.:
const resultsToSave: Omit<HistoryResult, 'resultComment'>[] = historyResults.map(h => ({ result: h.result, temperatureUnit: h.temperatureUnit }));
First comment response
When you .map() over the history results it looks like you aren't reassigning the value correctly. Try returning a new object when iterating over the array and saving it back to itself.
From this:
this.resultHistory.map((item) => {
item.resultComment = this.determineComment(
item.result,
item.temperatureUnit
);
});
To this:
this.resultHistory = this.resultHistory.map((item) => ({
...item,
resultComment: this.determineComment(
item.result,
item.temperatureUnit
),
}));

Related

Getting a null in my list component variable. Cannot read properties of null (reading 'date')

I have a list card component which displays devices in a list with their name, brand, image, bookedUntil(for how long is it booked if it's booked). All of this information is coming from a Json file. I have two questions:
Is there a better way of doing the if(!bookedUntil) { ... } part ? Because I tried doing it with ternary operators inside the return( ) but I couldn't get it to display both of the and the text if it's booked or not. So I resorted to displaying it with different variables.
I keep getting an error that the date is null and I can't display it even in the console. Tried a different if statement but I still kept getting the same error. The main thing I tried to accomplish is that the cycle would get information about the item is it not booked (checks if bookedUntil is null, if it's null, then displays AvailableItemIcon with text "Available".) and if it's booked (else if bookedUntil is not null there are dates with how long each device is booked for. So if there are X dates and X amount of devices it would display BookedItemIcon text "booked until" and one of the earliest dates out of X dates, because all of the devices are boooked).
ListCard component:
const ReservationsListCard = ({
device: { id, name, brand, bookedUntil, quantity, image },
liked = false,
}) => {
let stateIcon;
let stateText;
var min = null;
let dateArray = [];
if (!bookedUntil) {
stateIcon = <AvailableItemIcon />;
stateText = "Available";
} else {
stateIcon = <BookedItemIcon />;
stateText = "Booked until";
dateArray = bookedUntil;
for (var i = 0; i < dateArray.length; i++) {
var current = dateArray[i];
if (min != null || current.date < min.date) {
min = current;
}
}
}
return (
<a href={"/device/" + id} className="reservations-list-card">
<img className="reservations-list-card__image" alt="device" src={image} />
<div className="reservations-list-card__brand ">{brand}</div>
<div className="reservations-list-card__name">{name}</div>
<div className="reservations-list-card__availability">
{stateIcon}
{stateText}
{console.log(min.date)}
</div>
<div className="reservations-list-card__quantity">
QUANTITY: {quantity}
</div>
</a>
);
};
ReservationsListCard.propTypes = {
device: PropTypes.object,
};
export default ReservationsListCard;
The condition if (!bookedUntil) will never run and will always go to the else block. Empty arrays are also "true".
When the bookedUntil array is empty, and you assign its first value to current you get undefined. Later when checking current.date, you basically check undefined.date which throws an error.
You need to check the array's length:
if (!bookedUntil.length) {
// ... rest of the code
}

Custom function in function chain - To get Length of Array

I am trying to extract the length of an array while mapping it.
Here is what happens:
First I have an array of objects. Each object has a key of posts where I store the posts for that object. My code takes all the posts from all the objects and maps them to a new array so that I can show all the posts from all the objects to the user on the front end.
I'd like to show only 10 posts at a time. So I put a .slice(0, page * 10) - the variable page is controlled by a button at the bottom of the page. If the user hits the button, the page then increases the number of posts on the screen.
This all works great. BUT - I'd like to be able to count the total number of posts and only show the button when there are more posts available. Is there a way to extract the number of posts while still allowing it to map the results from this function below?
{
bandTypes === "all"
? allBands
.filter(band => {
if (showType !== 'Show Type') {
return band.showTypes.includes(showType)
} else {
return band
}
})
.reduce(
(allPosts, band) =>
allPosts.concat(
(band.youtube.length > 0 &&
band.bandBio !== "n/a" &&
band.bandGenre !== "n/a")
? band.posts.map((post) => ({ post, band }))
: []
),
[]
)
.sort((a, b) => new Date(b.post.date) - new Date(a.post.date))
.slice(0, page * 10)
.map(({ post, band }) => <div key={uuidv4()}>{convertPost(post, band)}</div>)
: null
}
It would be great if I could just put an anonymous function in there somewhere that sets the state to the length of the array.
I think that trying to accomplish this without any temporary variables is not going to be very efficient and probably would be quite ugly.
I think you should first create the array of all posts and then simply use it's length inside the return of your component.
Here's an example of how I would do it:
const MyComponent = () => {
let allPosts = []
if (bandTypes === "all") {
allPosts = allBands
.filter((band) => {
if (showType !== "Show Type") {
return band.showTypes.includes(showType)
} else {
return band
}
})
.reduce(
(allPosts, band) =>
allPosts.concat(
band.youtube.length > 0 &&
band.bandBio !== "n/a" &&
band.bandGenre !== "n/a"
? band.posts.map((post) => ({
post,
band,
}))
: []
),
[]
)
.sort((a, b) => new Date(b.post.date) - new Date(a.post.date))
}
return (
<div>
{allPosts.slice(0, page * 10).map(({ post, band }) => (
<div key={uuidv4()}>{convertPost(post, band)}</div>
))}
{allPosts.length < page * 10 && <button>Show more posts</button>}
</div>
)
}
(BTW using something like uuidv4 for the key isn't ideal, because React is less efficient with rendering then. It's better base the key on something that is unique to each post and doesn't change for each render, for example an id from the database or something of this sort)

Angular6 pipe filter run slow on large array

I have this filter that return a subset of items from a big array (around 500 items in this array)
import { Injectable, Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'searchFilter'
})
#Injectable()
export class FilterArrayPipe implements PipeTransform {
transform(value: any, ...args): any {
// tslint:disable-next-line:no-unused-expression
console.log(value, args);
if ( typeof args[0] === 'undefined') {
return value;
} else if (value) {
return value.filter(item => {
// tslint:disable-next-line:prefer-const
for (let key in item) {
if ((typeof item[key] === 'string' || item[key] instanceof String) &&
(item[key].toUpperCase().indexOf(args[0].toUpperCase()) !== -1)) {
return true;
}
}
});
}
}
}
Whenever i type in the text box, it takes a few sec for the character actually show up in the box, and another few sec to return update the list to return the search items.
I tried to add in the slice:0:10 to limit the number of item on screen, but it has the same issue, so i am guessing the slowness is due to the big array and not the screen rendering.
I have talked to the back end developer, and since this array is used by other people, he won't be modifying it. What can i do to improve the performance?
edit: include html code:
<form *ngFor='let subarray of array | searchFilter: filterText | slice:0:20 ; let i = index;' #form="ngForm" (ngSubmit)="Save(form)">
<fieldset>
<input type="text" name="country" [ngModel]="subarray.country ">
<input type="number" name="number" [ngModel]="subarray.number ">
............about 24 input fileds in total ..............
</fieldset>
</form>
Some performance improvements you should try:
Use trackBy-function for ngForOf
Use provide a trackBy function for the ngForOf directive, for example track all DOM elements by their index:
// template:
<div *ngFor="let subarray of array; trackBy:trackByIndex">
...
</div>
// component:
public trackByIndex(index: number, value: any)
{
return index;
}
Don't use a pipe to filter the data (for impure pipes)
Don't use a pipe to filter your large array, because it'll be invoked everytime the changedetection runs for the component.
You could for example filter the large array if the search input changes:
// component:
public originalData: any[] = [...];
public filteredData: any[] = this.originalData;
// ...
public filterData(searchString: string)
{
this.filteredData = this.originalData.filter(item => {
// your filter logic
})
}
Enrich your every item by a searchable string
Enrich every item inside the array by a searchable string. If you want to search all values of an item just map the array once and append a new key to the item like:
this.originalData = this.dataService.getData().map(item => {
return {
...item,
searchableString: Object.values(item).join(':'),
}
})
This prevents you from running through every item while filtering. You will just search inside that property for the input string.
item.searchableString.indexOf(input) > -1
Other techniques
Debounce user search inputs
Filter the data inside a service using a BehaviorSubject together with ChangeDetectionStrategy.OnPush and making the data an input of your list component
Sidenote
Keep in mind that the performance in development mode of angular is far worse than in production mode. In development mode every changedetection is run twice. Also AOT-mode will bring a lot of performance improvements.
You need to remember the previous state of the filter in order to gain performance.
Let's assume you have a long list of 500 strings that looks something like this.
const originalValues = ['a', 'and', 'goo', 'apple', 'antelope', ...];
When the user types a you filter the above values like this.
const newValues = values.filter((value) => value.indexOf(input) !== -1);
console.log(newValues);
The above will print something like this.
['a', 'and', 'apple', 'antelope']
The next letter the user types updates the search input and you can filter newValues a second time. Each time the user adds a letter to their input term you can filter the previous results again. As long as the user is making the input string longer.
This can not be done via a pipe, because pipes don't remember the previous state. You'll have to do something special in the component to handle this.
You also need to abort when you hit the result limit. This way you are not iterating the entire collection when you've found a limit of 20.
const newValues = [];
for(let value of values) {
if(newValues.length === 20) {
break;
}
if(value.indexOf(input) !== -1) {
newValues.push(value);
}
}
Another possibility would be a memoization pattern.
If, e.g., that is your pipe
// excerpt
transform(value: any, ...args: any[]): any {
return this.myOwnTransformingMethod(args, value);
}
you can use memo-decorator for this method, like
#memo()
private myOwnTransformingMethod(args: any[], value: any) {
// do some filtering or transformations and
return value;
}
You don't have to implement the pattern on your own. There is one you can use (MIT license):
npm i memo-decorator -S
and import it in your whatever.pipe.ts
import memo from 'memo-decorator';

Inserting an item in array every X places and "attempted to assign to readonly property"

I have an array of objects coming from the server. I use this to display cards on the screen. I want to insert an "advertisements card" every X cards. I thought I'd done this on the web with the following code, but it seems to not be working with my react-native app.
class DeckArea extends React.Component {
constructor(props){
super(props);
}
render(){
let { data, likeJob } = this.props;
// place an object into the array of jobs data
data.jobs.forEach((item, i) => {
let remainder = i%4;
if (remainder === 0) {
console.log(remainder === 0)
data.jobs.splice(i, 0,{ isAd: true, _id: uuidV4() })
}
});
return (
<View style={styles.container}>
<Swipe
data={data.jobs}
renderCard={(job) => {
//check if isAd === true,
// if so, show the AdCard not the deck card
if (job.isAd) {
return <AdCard adsManager={adsManager} />
}
return <DeckCard job={job} isLikedCard={false} />
}}
/>
</View>
);
}
}
I've done some quick testing and the problem is definitely happening here:
data.jobs.forEach((item, i) => {
let remainder = i%4;
if (remainder === 0) {
console.log(remainder === 0)
data.jobs.splice(i, 0,{ isAd: true, _id: uuidV4()})
}
});
resulting in "attempted to assign to readonly property"
Additional resources:
Attempted to assign to readonly property ECMAScript React Native
Inserting items in an array every nth place in octave / matlab
First of all, by inserting in the original jobs array with data.jobs.splice(), you are modifying an array passed via props, which is considered to be bad practice.
Instead, you should create a new array, which would include job adds. Example code:
// Will show an add after each 4 jobs
const jobsWithAds = data.jobs.reduce((accumulator, job) => {
const add = { isAd: true, _id: uuidV4() }
const remainder = accumulator.length % 5; // 5 is needed to include job ad
// Additional check is required to skip first add
if (remainder === 0 && accumulator.length > 0) {
accumulator.concat(add);
}
// Add an ad after first 4 jobs, which have no ad
if (accumulator.length === 4) {
accumulator.concat(add);
}
accumulator.concat(job);
}, [])

mongo/mongoid MapReduce on batch inserted documents

Im creating my batch and inserting it to collection using command i specified below
batch = []
time = 1.day.ago
(1..2000).each{ |i| a = {:name => 'invbatch2k'+i.to_s, :user_id => BSON::ObjectId.from_string('533956cd4d616323cf000000'), :out_id => 'out', :created_at => time, :updated_at => time, :random => '0.5' }; batch.push a; }
Invitation.collection.insert batch
As stated above, every single invitation record has user_id fields value set to '533956cd4d616323cf000000'
after inserting my batch with created_at: 1.day.ago i get:
2.1.1 :102 > Invitation.lte(created_at: 1.week.ago).count
=> 48
2.1.1 :103 > Invitation.lte(created_at: Date.today).count
=> 2048
also:
2.1.1 :104 > Invitation.lte(created_at: 1.week.ago).where(user_id: '533956cd4d616323cf000000').count
=> 14
2.1.1 :105 > Invitation.where(user_id: '533956cd4d616323cf000000').count
=> 2014
Also, I've got a map reduce which counts invitations sent by each unique User (both total and sent to unique out_id)
class Invitation
[...]
def self.get_user_invites_count
map = %q{
function() {
var user_id = this.user_id;
emit(user_id, {user_id : this.user_id, out_id: this.out_id, count: 1, countUnique: 1})
}
}
reduce = %q{
function(key, values) {
var result = {
user_id: key,
count: 0,
countUnique : 0
};
var values_arr = [];
values.forEach(function(value) {
values_arr.push(value.out_id);
result.count += 1
});
var unique = values_arr.filter(function(item, i, ar){ return ar.indexOf(item) === i; });
result.countUnique = unique.length;
return result;
}
}
map_reduce(map,reduce).out(inline: true).to_a.map{|d| d['value']} rescue []
end
end
The issue is:
Invitation.lte(created_at: Date.today.end_of_day).get_user_invites_count
returns
[{"user_id"=>BSON::ObjectId('533956cd4d616323cf000000'), "count"=>49.0, "countUnique"=>2.0} ...]
instead of "count" => 2014, "countUnique" => 6.0 while:
Invitation.lte(created_at: 1.week.ago).get_user_invites_count returns:
[{"user_id"=>BSON::ObjectId('533956cd4d616323cf000000'), "count"=>14.0, "countUnique"=>6.0} ...]
Data provided by query, is accurate before inserting the batch.
I cant wrap my head around whats going on here. Am i missing something?
The part that you seemed to have missed in the documentation seem to be the problem here:
MongoDB can invoke the reduce function more than once for the same key. In this case, the previous output from the reduce function for that key will become one of the input values to the next reduce function invocation for that key.
And also later:
the type of the return object must be identical to the type of the value emitted by the map function to ensure that the following operations is true:
So what you see is your reduce function is returning a signature different to the input it receives from the mapper. This is important since the reducer may not get all of the values for a given key in a single pass. Instead it gets some of them, "reduces" the result and that reduced output may be combined with other values for the key ( possibly also reduced ) in a further pass through the reduce function.
As a result of your fields not matching, subsequent reduce passes do not see those values and do not count towards your totals. So you need to align the signatures of the values:
def self.get_user_invites_count
map = %q{
function() {
var user_id = this.user_id;
emit(user_id, {out_id: this.out_id, count: 1, countUnique: 0})
}
}
reduce = %q{
function(key, values) {
var result = {
out_id: null,
count: 0,
countUnique : 0
};
var values_arr = [];
values.forEach(function(value) {
if (value.out_id != null)
values_arr.push(value.out_id);
result.count += value.count;
result.countUnique += value.countUnique;
});
var unique = values_arr.filter(function(item, i, ar){ return ar.indexOf(item) === i; });
result.countUnique += unique.length;
return result;
}
}
map_reduce(map,reduce).out(inline: true).to_a.map{|d| d['value']} rescue []
end
You also do not need user_id in the values emitted or kept as it is already the "key" value for the mapReduce. The remaining alterations consider that both "count" and "countUnique" can contain an exiting value that needs to be considered, where you were simply resetting the value to 0 on each pass.
Then of course if the "input" has already been through a "reduce" pass, then you do not need the "out_id" values to be filtered for "uniqueness" as you already have the count and that is now included. So any null values are not added to the array of things to count, which is also "added" to the total rather than replacing it.
So the reducer does get called several times. For 20 key values the input will likely not be split, which is why your sample with less input works. For pretty much anything more than that, then the "groups" of the same key values will be split up, which is how mapReduce optimizes for large data processing. As the "reduced" output will be sent back to the reducer again, you need to be mindful that you are considering the values you already sent to output in the previous pass.

Categories

Resources