How to get the current value of BehaviorSubject once? - javascript

I'm trying to get the current value of a BehaviorSubject without subscribing to it because I need to make some changes afterwards, without the changes reflecting real-time because of a specific requirement. I'm using getValue().
Sample BehaviorSubject value:
{
ID_123: {
logs: [
{
id: 1,
record_id: 'ID_123',
data: { complete: false }
action: 'Modified',
description: 'Modified filename',
}
]
}
}
Service class:
private logs$ = new BehaviorSubject<any>(null);
private logsDataStore = {};
logsData = this.logs$.asObservable();
...
getData(id) {
if (this.logsDataStore[id] !== undefined) {
return this.logs$.getValue();
}
}
I call getData() from inside a component when a button is clicked to render the logs entries.
id = 'ID_123';
onClick() {
this.logsData = Object.assign({}, this.service.getData([this.id])[this.id]);
}
Rendering each logs entry:
<div *ngFor="let log of logsData.logs" class="log">
<a *ngIf="!log.data.complete" (click)="markComplete(log.record_id, log.id, log.data)">
Mark Complete
</a>
</div>
markComplete(recordId, id, data) {
let dataClone = Object.assign({}, data);
dataClone.complete = true;
this.service.updateLog(recordId, id, dataClone);
}
Back to the Service class:
updateLog(recordId: string, id: string, newValues: object) {
const selectedRecord = this.logsDataStore[recordId];
if (selectedRecord !== undefined) {
if (selectedRecord.logs && selectedRecord.logs[id]) {
let selectedLogEntry = selectedRecord.logs[id];
Object.keys(newValues).forEach(
(logKey) => {
selectedLogEntry[logKey] = newValues[logKey];
}
);
}
}
// this.logs$.next(Object.assign({}, this.logsDataStore));
}
The problem:
Somehow, the Mark Complete link disappears right away whenever you click on it, even if I didn't subscribe to the service and cloned the results from getData() before using them? As though the reference to the object from the BehaviorSubject was retained?
What am I missing?

I discovered something new.
When I used:
JSON.parse(JSON.stringify(this.service.getData([this.id])[this.id]));
It worked.
Previously, I assumed Object.assign() and JSON.parse(JSON.stringify()) worked the same way, but it turns out, JSON.parse(JSON.stringify()) is the one that creates a deep copy and does not give you a reference to the original object.

Related

Filter array of a computed property using a method in Vue.js

I hope this is not a stupid question. I have a computed property that lists ALL courses. When the user clicks a button calling a method called courseFilters() I would like to filter the computed property to only show Courses that are not archived.
Here is my computed property:
filterCourses() {
const getUser = this.$store.getters['UserData/getUser']
return this.courses.filter((item) => {
if(this.checkAuthorization(['leader'])) {
return item.createdBy === getUser.uid
} else {
return item
}
})
}
Here is my Method:
courseFilters(which) {
if(which == 'hide-archived') {
this.filterCourses.filter((item) => {
if(!item.archive) {
return item
}
})
}
if(which == 'clear') {
this.getCourses(this.$store.getters['AppData/cid'])
}
}
Currently when I click the button nothing changes to the computed property.
I don't think I fully understand the details of your problem, but here's a sketch for a solution that may inspire you:
export default {
data() {
return { areArchivedCoursesVisible: false };
},
computed: {
authorizedCourses() {
const getUser = this.$store.getters['UserData/getUser'];
// The callback in a filter should return true or false.
// I'm not sure if this is exactly what you want.
// If your original code works, then skip this.
return this.courses.filter(
(c) => this.checkAuthorization(['leader']) && c.createdBy === getUser.uid
);
},
visibleCourses() {
// This is probably what you display in your template.
// It's either all of the authorized courses, or the authorized
// courses which are not archived.
return this.areArchivedCoursesVisible
? this.authorizedCourses
: this.this.authorizedCourses.filter((c) => !c.archive);
},
},
methods: {
toggleVisible() {
// Toggle between showing and not showing the archived courses
this.areArchivedCoursesVisible = !this.areArchivedCoursesVisible;
},
},
};
This just holds some state indicating if the archived courses should be shown (toggled via a method). Then you can combine your computed properties to get the correct answer based on the state. In this example, visibleCourses uses the output of the computed property authorizedCourses + the current state.
Also note that I named the computed properties as nouns and not verbs, which I find makes the code much easier to understand.

VueJS - parent object is affected by changes in deep copy of this object in child

Lets start with explaining the structure. I have the page dedicated to a specific company and a component Classification.vue on this page which displays categories of labels and labels itself which are assigned to the current company. First of all I get all possible categories with axios get request, then I get all labels, which are assigned to the current company, and after all I map labels to respective categories. Here is the Classification.vue:
import DoughnutChart from "#comp/Charts/DoughnutChart";
import ModalDialog from '#comp/ModalDialog/ModalDialog';
const EditForm = () => import('./EditForm');
export default {
components: {
DoughnutChart, ModalDialog, EditForm
},
props: ['companyData'],
async created() {
const companyLabels = await this.$axios.get('/companies/' + this.companyData.id + '/labels');
const allLabelsCategories = await this.$axios.get('/labels/categories');
allLabelsCategories.data.map(cat => {
this.$set(this.labelsCategories, cat.labelCategoryId, {...cat});
this.$set(this.labelsCategories[cat.labelCategoryId], 'chosenLabels', []);
});
companyLabels.data.map(l => {
this.labelsCategories[l.label.labelCategory.labelCategoryId].chosenLabels.push({...l.label, percentage: l.percentage})
});
},
computed: {
portfolioChartData() {
let portfolios = [];
// 35 id stands for 'Portfolio' labels category
if (this.labelsCategories[35] !== undefined && this.labelsCategories[35].chosenLabels !== undefined) {
this.labelsCategories[35].chosenLabels.map(label => {
portfolios.push({label: label.name, value: label.percentage});
});
}
return portfolios;
},
portfolioLabels() {
let portfolios = [];
// 35 id stands for Portfolio labels category
if (this.labelsCategories[35] !== undefined && this.labelsCategories[35].chosenLabels !== undefined) {
return this.labelsCategories[35].chosenLabels;
}
return portfolios;
}
},
data() {
return {
labelsCategories: {}
}
}
}
So far so good, I get the object labelsCategories where keys are ids of categories and values are categories objects which now also have chosenLabels key, which we set up in created(). And as you can see I use computed properties, they are necessary for a chart of 'Portfolio' category. And I used $set method in created() exactly for the purpose of triggering reactivity of labelsCategories object so computed properties can respectively react to this.
Now I have a new component inside Classification.vue - EditForm.vue, which is dynamically imported. In this component I do pretty much the same thing, but now I need to get every possible label for every category, not just assigned. So I pass there prop like this:
<modal-dialog :is-visible="isFormActive" #hideModal="isFormActive = false">
<EditForm v-if="isFormActive" ref="editForm" :labels-categories-prop="{...labelsCategories}" />
</modal-dialog>
And EditForm component looks like this:
export default {
name: "EditForm",
props: {
labelsCategoriesProp: {
type: Object,
required: true,
default: () => ({})
}
},
created() {
this.labelsCategories = Object.assign({}, this.labelsCategoriesProp);
},
async mounted() {
let labels = await this.$axios.get('/labels/list');
labels.data.map(label => {
if (this.labelsCategories[label.labelCategoryId].labels === undefined) {
this.$set(this.labelsCategories[label.labelCategoryId], 'labels', []);
}
this.labelsCategories[label.labelCategoryId].labels.push({...label});
});
},
data() {
return {
labelsCategories: {}
}
}
}
And now the problem. Whenever I open modal window with the EditFrom component my computed properties from Calssification.vue are triggered and chart is animating and changing the data. Why? Quite a good question, after digging a bit I noticed, that in EditForm component I also use $set, and if I will add with $set some dummy value, for example:
this.$set(this.labelsCategories[label.labelCategoryId], 'chosenLabels', ['dummy']);
it will overwrite the labelsCategories value in the parent component (Classification.vue)
How is it even possible? As you can see I tried to pass prop as {...labelsCategories} and even did this.labelsCategorie = Object.assign({}, this.labelsCategoriesProp); but my parent object is still affected by changes in child. I compared prop and labelsCategories objects in the EditForm component by === and by 'Object.is()' and they are not the same, so I am completely confused. Any help is highly appreciated.
Btw, I can solve this issue by passing prop as :labels-categories-prop="JSON.parse(JSON.stringify(labelsCategories))" but it seems like a hack to me.
Okay, I was digging deeper in this issue and learned, that neither {...labelsCategories} nor Object.assign({}, this.labelsCategoriesProp) don't create a deep copy of an object only the shallow one. So, I suppose that was the cause of the problem. In this article I learned about shallow and deep copies of objects: https://medium.com/javascript-in-plain-english/how-to-deep-copy-objects-and-arrays-in-javascript-7c911359b089
So, I can leave my hacky way using JSON.parse(JSON.stringify(labelsCategories)) or I can use a library such as lodash:
_.cloneDeep(labelsCategories)
But according to the article I also can create a custom method. And this one option is quite suitable for me. I already had a vue mixin for processing objects, so I just added deepCopy() function there:
deepCopy(obj) {
let outObject, value, key;
if (typeof obj !== "object" || obj === null) {
return obj; // Return the value if obj is not an object
}
// Create an array or object to hold the values
outObject = Array.isArray(obj) ? [] : {};
for (key in obj) {
value = obj[key];
// Recursively (deep) copy for nested objects, including arrays
outObject[key] = this.deepCopy(value);
}
return outObject;
},

Subject emits previous value

I use Subject in my app in order to make CRUD operations. I subscribe to it in a couple of components, but it doesn't seem to change my values in real time. I have to resort to reloading the page to get the most recent values. This is probably due to my lack of understanding of how everything should be organised, but I haven't found any simmilar issues here. Could you please help me fix what I do wrong?
(using BehaviourSubject yields the same results).
Here is the code for my service:
#Injectable({ providedIn: 'root' })
export class AppBookshelfService {
filesAndFolders: ItemFile[] = [];
filteredArrOfFolders = [];
filesSubj = new Subject();
curentParent = 0;
getFiles() {
this.http.get<any>('http://localhost:3000/api/items').subscribe(
(data: any) => {
this.filesAndFolders = data.items;
this.filesAndFolders = data.items.filter(item => item.isDeleted === 0);
this.filesSubj.next(data.items.filter(item => item.isDeleted === 0));
data.items.forEach(item => {
if(item.parentId !== 0 && item.isFolder === 1) {
this.filteredArrOfFolders.push(item);
}
}); }
)
}
getFile(id: number): Observable<ItemFile> {
return this.http.get<ItemFile>("http://localhost:3000/api/items/" + id);
}
emitIdForFile(parentId: number) {
this.curentParent = parentId;
}
getCurentParent() {
return this.curentParent;
}
postFile(item: ItemFile) {
return this.http.post<ItemFile>("http://localhost:3000/api/items/", item).subscribe(result => {})
}
Here is the code of one of my key components, that does the subscribing:
ngOnInit(): void {
this.bookshelfService.getFiles();
this.sub = this.bookshelfService.filesSubj.subscribe(data => {
this.files = data;
console.log(this.files)
let groupOfCheckboxes = {};
this.files.forEach(element => {
groupOfCheckboxes[element.id.toString()] = new FormControl("");
});
this.checkboxForm = new FormGroup(groupOfCheckboxes);
});
// this.getFolders();
}
ngOnDestroy(): void {
this.sub.unsubscribe();
}
startCreatingFolder() {
this.isFolderBeingCreated = !this.isFolderBeingCreated;
}
createFile(id: number) {
this.bookshelfService.emitIdForFile(id);
this.router.navigate(['create']);
}
createFolder(event, id) {
const folder = {
name: this.folderNameInput.nativeElement.value,
description: "",
imageLink: "",
isDeleted: 0,
parentId: id,
isFolder: 1
}
this.bookshelfService.filesAndFolders.push(folder);
this.bookshelfService.getFiles();
this.bookshelfService.postFile(folder);
this.isFolderBeingCreated = false;
}
Here is a link to the full repository:
https://github.com/Not-a-whale/BookshelfApp
Here is the app working on Heroku with the said flaw:
https://bookshelf-app-nikita.herokuapp.com/
The subject is Hot(Multicast), You Should subscribe to it before emit occurs:
ngOnInit(): void {
this.sub = this.bookshelfService.filesSubj.subscribe(data => {
this.files = data;
console.log(this.files)
let groupOfCheckboxes = {};
this.files.forEach(element => {
groupOfCheckboxes[element.id.toString()] = new FormControl("");
});
this.checkboxForm = new FormGroup(groupOfCheckboxes);
});
this.bookshelfService.getFiles();
// this line must be after subscribe
// this.getFolders();
}
An Observable is cold when data is produced inside the Observable and the Observable is hot when the data is produced outside the Observable. As we just saw the hot Observable is able to share data between multiple subscribers. We call this behaviour “multicasting”.
Generating a random number is not a good real life usecase. A good usecase would be DOM events. Let’s say we’re tracking clicking behaviour and have multiple subscribers do something with the coordinates:
The data is produced outside of the Observable itself. Which makes it hot, because the data is being created regardless of if there is a subscriber or not. If there is no subscriber when the data is being produced, the data is simply lost.
More Info
If you want to save data_list in memory and then use it multiple times use a behaviorsubject to hold it, subject has no memory to hold the events.
behaviorsubject needs a default value so you must initialize it with an empty list.
When you call service just next the behaviorsubject with new data, then subscribe to it before service call to receive the changes events
// RxJS v6+
import { BehaviorSubject } from 'rxjs';
​
const subject = new BehaviorSubject(123);
​
// two new subscribers will get initial value => output: 123, 123
subject.subscribe(console.log);
subject.subscribe(console.log);
​
// two subscribers will get new value => output: 456, 456
subject.next(456);
​
// new subscriber will get latest value (456) => output: 456
subject.subscribe(console.log);
​
// all three subscribers will get new value => output: 789, 789, 789
subject.next(789);
​
// output: 123, 123, 456, 456, 456, 789, 789, 789

Updating value inside nested object of object in React Native

I have state set as follow
const [stories, setStories] = useState([]);
I fetch Data from API in array, and i map the array and set the using setStories as:
setStories(prevState => prevState.concat({user: {name: 'XYZ', profile: 'ABC', stories: [{id: 1, image: 'testing'}];
The above codes are working fine, but i am stuck, when i have to concat the latest story if the id did not matched with fetched data. I have tried below solution but it didnot help:
stories.map(story => {
if(story && story.hasOwnProperty(key)){
//where above key is the user key fetched from the another API, i.e., user key
story?.[key].stories.map(storedStory =>
id(storedStory.id !== fetchedStory.id){
story?.[key].stories.concat({story})}
but the above code did not work, as it only mutate the state and is avoiding re-rendering.
Looking for a clean and efficient method to overcome this. THanks
It's hard to tell what you're trying to accomplish without seeing a full example. But I think your main problem is that you're not using the returned value from map, and from the naming it looks like you're appending the wrong element.
It will help to simplify first.
const newState = stories.map(story => {
if (story?.hasOwnProperty(key)) {
const found = story[key].stories.find(s => s.id === fetchedStory.id);
if (found) {
return story;
} else {
// Let's make a new object with the fetchedStory
// appended into THIS user's stories
return {
...story,
[key]: {
...story[key],
stories: [
...story[key].stories,
// This is supposed to be fetchedStory
// not `story` right??
fetchedStory,
]
}
}
}
} else {
return story;
}
});
setStory(newState);
Edit: You're having a hard time expressing your business logic, and the complexity of the data structure is not helping. So keep simplifying, encapsulate the complex syntax into functions then express your business logic plainly. Ie,
const appendStory = (originalObject, userId, storyToAppend) => {
return {
...originalObject,
[userId]: {
...originalObject[userId],
stories: [
...originalObject[userId].stories,
storyToAppend,
]
}
}
};
const userExistsInList = (users, user) => {
return users?.hasOwnProperty(user);
}
const newStoryAlreadyInStories = (stories, newStory) => {
return stories.find(s => s.id === newStory.id);
}
const newState = stories.map(story => {
if (userExistsInList(story, key)) {
const found = newStoryAlreadyInStories(story[key].stories, fetchedStory);
if (found) {
// User is already in state and the new story is already in the list
// Add business logic here
} else {
// User is already in state and the new story
// is not in their list
// Add business logic here
}
} else {
// User is not in the list yet
// Add business logic here
}
});

[] vs [{...}] in browser tools, while both having same objects

If you look at the picture both arrays consist of same kind of object. first I create it with empty data as placeholder, but second one I create it with data coming from server.
writeValue(v: any) {
console.log('aaa');
console.log(v);
console.log('aaa');
this.form = new FormArray([]);
for (const value of v) {
console.log('bbb');
console.log(value);
console.log('bbb');
this.form.push(new FormControl(value));
}
this.form.valueChanges.subscribe(res => {
if (this.onChange) {
this.onChange(this.form.value);
}
});
}
for first case it goes through all of the writeValue code, for second one it doesn't go through the for(const values of v) code. why is this happening? when I print them out they seem to be the same other than one difference [{...}] vs [] in browser tools.
If you want to see how I create them. the first one is routes and the second one is routeslocal. I put them in angular formcontrol, and thats how it gets to writeValue via controlvalueaccessor. If you want to know how it works you could check my previous question here. there is more code, but it doesn't include the service.
ngOnInit() {
const routes: any[] = [];
routes.push({ ...dataI });
this.requestForm = this.fb.group({
statusId: null,
requestVehicles: this.fb.array([
this.fb.group({
garageId: 0,
routes: new FormControl(routes),
endDateTime: 0,
})
])
});
if (this.data.isEdit) {
this.Title = 'Edit';
this.data.fService.getRequest(this.data.requestId).subscribe(thisRequest => {
this.requestForm = this.fb.group({
statusId: thisRequest.status,
requestVehicles: this.fb.array([
])
});
thisRequest.requestVehicles.forEach((element, index) => {
const routeslocal: any[] = [];
element.routes.forEach((elementt, indexx) => {
this.data.fService.getAddressPoint(elementt).subscribe(sbed => {
const newRoute = {
addressPointId: sbed.addressPointId,
municipalityId: sbed.municipalityId,
regionId: sbed.regionId,
rvId: element.rvId,
sequenceNumber: indexx,
settlementId: sbed.settlementId,
regionName: sbed.regionName,
municipalityName: sbed.municipalityName,
settlementName: sbed.settlementName,
description: sbed.description,
};
routeslocal.push({...newRoute});
});
});
this.requestVehicles.push(this.fb.group({
endDateTime: new Date(element.endDateTime),
garageId: element.garageId,
routes: new FormControl(routeslocal),
}));
});
});
});
});
}
}
The opening line, [] or [{}], is immediately drawn in the console.
In the case of [], there was nothing in the array at logging time, so the browser draw it as an empty array. But the data was present when you looked at it and clicked on the small triangle, later.
You can reproduce this behavior with this code in your console:
;(function(){ let arr=[]; setTimeout(()=>{ arr[0] = {b:3}; }); return arr;})()
So the difference you saw is related to the (a)synchronicity of array filling.
Vato, you has two functions in your service:getRequest(requestId) and getAddressPoint(requestVehicles). The idea is return a whole object. You can create the function in the own service or in the component. I'd like in the service, and that return an objservable. You must use forkJoin and swithMap So . It's for me impossible check if work
**Update, see the stackblitz
getFullRequest(id): Observable<any> {
return this.getRequest(id).pipe(
switchMap((request: any) => {
//here you has the request. We create an array of observables
return forkJoin(
request.requestVehicles.map(
(r: any) => this.getAddressPoint(r))).pipe(map((res: any[]) => {
res.forEach((x: any, index: number) => {
x.sequenceNumber = index
})
return {
statusId: request.statusID,
routes: res
}
})
)
}))
}
then, in your component
if (this.data.isEdit) {
this.Title = 'Edit';
this.data.fService.getFullRequest(this.data.requestId).subscribe(thisRequest => {
this.requestForm = this.fb.group({
statusId: thisRequest.status,
requestVehicles: thisRequest.routes
});
Update 2 briefly explain about switchMap and forkJoin.
When we make this.getRequest(id) we received in request an object. In this object we has in requestVehicles an array (can be an array of objects or an array of numbers -or strings-). With each element of this array we can make a call, But instead of make the calls one to one, we want to make all these together. For this we use forkJoin. forkJoin received an array of observables and, in subscribe received the response in an array
//if we has an observable like:
getValue(id:number):Observable<any>{
return of({one:id})
}
//and an array like
myArray=[1,2]
//and an array of response whe we can store the responses
response:any[]
//we can do
for (let id of myArray)
{
this.getValue(id).susbcribe(res=>{
this.response.push(res)
})
}
//or
observables:any[]
for (let id of myArray)
{
this.observables.push(this.getValue(id))
}
forkJoin(this.observables).subscribe((res;any[])=>{
//in res[0] we have the result of this.getValue(1)
//in res[1] we have the result of this.getValue(2)
//so, simply
this.response=res
})
//or in a compact way
//with each element of the array
observables=myArray.map(x=>this.getValues(x))
forkJoin(this.observables).subscribe((res;any[])=>{
this.response=res
})
Well, there are two problems more. We want add a new propertie "sequenceNumber" to all the response. So we use res.forEach(...) to add the property. And we want return an object with somes properties of our original request (statusID) and in "routes" the array with the response. So we use map to transform the response. In our simple example above
//not return simple {one:1}
//return {id:1,one:1}
getResponse(2).pipe.map(res=>{
return {
id:1,
one:res.one
}
}

Categories