check store for object before calling api - ngxs

You know how they say you don't need state management until you know you need it. Well turns out my project needs it. So I need some help wit best practice as I am adding ngxs to an existing angular project.
I have an action called getServiceDetail and my statemodel has a list of objects called DriverListsStopInfoViewModel. each of these objects have a unique ID. The html template of the consuming component uses a selector for the property currentStopDetail, which is a state property that gets set in my action.
GOAL:
in my action I want to check the list of objects in my store to see if an object with the same id exists and return that object, and if it does not exist, call and api to get it.
EXAMPLE:
The following code works, but I would like to hear if this is the right way to do it. do I even need to return the object from the action function if its found, or can I just use patch state to assign it to the currentStopDetail
export interface SignServiceStateModel {
searchResults: ServiceSearchModel[];
driverStopsDetails: DriverListsStopInfoViewModel[];
driverStopsList: DriverListsStopsViewModel[];
driverStopsMarkers: DriverStopsMarkerViewModel[];
currentStopDetail: DriverListsStopInfoViewModel;
}
const SIGNSERVICE_STATE_TOKEN = new StateToken<SignServiceStateModel>(
'signservice'
);
#State<SignServiceStateModel>({
name: SIGNSERVICE_STATE_TOKEN,
defaults: {
searchResults: [],
driverStopsDetails: [],
driverStopsList: [],
driverStopsMarkers: [],
currentStopDetail: null
},
})
#Injectable()
export class SignServiceState {
constructor(private driverListsService: DriverListsService) {}
#Action(DriverList.GetServiceDetail)
getServiceDetail(
ctx: StateContext<SignServiceStateModel>,
action: DriverList.GetServiceDetail
) {
if (action.serviceId === undefined || action.serviceId <= 0) {
return;
}
// check if record already in list and return
const currentState = ctx.getState();
const existingStopDetail = currentState.driverStopsDetails.find(s => s.SignServiceId === action.serviceId);
if (existingStopDetail !== undefined) {
const currentStopDetail = existingStopDetail;
ctx.patchState({ currentStopDetail });
return currentStopDetail;
}
// else get new record, add it to list and return
return this.driverListsService.getDriverListsInfo(action.serviceId).pipe(
tap((currentStopDetail) => {
ctx.patchState({ currentStopDetail });
ctx.setState(
patch({
driverStopsDetails: append([currentStopDetail])
})
);
})
);
}
#Selector()
static currentStopDetail(state: SignServiceStateModel) {
return state.currentStopDetail;
}
}
I only included the relevant code from my state class
QUESTION:
is this the best way to check the store for an item and call api if it does not exist?
Thanks in advance

Short answer is yes, what you have done here is a typical way of handling this scenario (in my experience). There's a couple of improvements you could make:
do I even need to return the object from the action function if its found, or can I just use patch state to assign it to the currentStopDetail
No, you don't return anything from these action handlers, other than possibly an Observable that NGXS will handle (so in your case if there is no matching item found, you return the Observable that fetchs it from the API and patches the state).
Also when you do make the API call, you should only need a single update to the state:
return this.driverListsService.getDriverListsInfo(action.serviceId).pipe(
tap((result) => {
ctx.setState(
patch({
currentStopDetails: result
driverStopsDetails: append([result]),
})
);
})
);

Related

Angular 14 Behaviorsubject is returning the previous value in ngOnInit() when component is initialized

I have a component with following ngOnInit and save method. I have a behaviorsubject which i am using tonotify when save action is performed. Default value is null. but after i save and reopen the component, it is coming up with previous value and again making service call which it shouldn't. What am i doing wrong here?
component:
ngOnInit(): void {
this.templateSaveService.templateSaveSubject
.pipe(
takeUntil(this.destroy$),
filter((auditTemplate) => auditTemplate !== null),
switchMap((auditTemplate) => {
return this.auditTemplateSaveService
.createTemplate(auditTemplate) //it is getting called for first time as well.
.pipe(
map((response) => ({
...auditTemplate,
id: response.body.id
})),
catchError((error) => this.handleError(error))
);
})
)
.subscribe((template) => {
this.isSavingData = false;
this.toasterService.success('Template has been saved successfully');
});
}
save(): void {
this.templateSaveService.saveAuditTemplate(this.auditTemplate);
}
service:
templateSaveSubject = new BehaviorSubject<AuditTemplate>(null);
saveAuditTemplate(auditTemplate: AuditTemplate): void {
this.templateSaveSubject.next(updatedTemplate);
clearSaveSubject(): void {
this.templateSaveSubject .next(null);
}
I can fix by set the subject to null in component ngDestroy()
ngOnDestroy(): void {
this.auditTemplateSaveService.clearSaveSubject();
this.destroy$.next(true);
this.destroy$.complete();
}
But would like to know why the behaviour subject value not cleared initially and is there any other better solution for this?
I don't see your whole code but I guess that your service is provided in the root, which means that it won't get destroyed once your component gets destroyed.
if my assumptions are correct you have 2 options -
u can provide the service at the component level instead of the app root.
you can maintain the behavior subject in your component.

How do I create a generic reducer plugin, that fires on all forms?

Currently my code works, but only for the forms specified specifically in the combine reducer function. But, I would like to have my code work generally for all forms loaded in my single page app.
Here is the relevant code:
import { reducer as formReducer } from 'redux-form';
export default combineReducers({
someReducer,
anotherReducer,
form: formReducer.plugin({
specificFormId: (state, action) => { // <-- I don't want this only for specificFormId, I want this to happen for all my forms,
// or at least have a dynamic way of adding more forms
const {type, payload} = action;
switch(type) {
case 'RESET_LINK_TYPE_FIELDS': {
return {
...state,
registeredFields: {
...state.registeredFields,
// Do some custom restting here based on payload
}
};
}
default:
return state;
}
}
})
});
So, anytime my <Field ..of a certain type/> fires off this the RESET_LINK_TYPE_FIELDS action, I want the correct form to respond to it.
In the action payload, I can specifically the form identifier or anything else I would need to make this work.
In fact, if the .plugin let me do my own form state slicing, I could easily do this, but because it forces me to pass an object, with a hardcoded form identifier it doesn't work.
Is there a way to have the plugin give me the WHOLE form state, and then I will slice as needed, and return state as needed based on payload?
There is currently no way to do this with the existing API.
You could jury rig a solution by wrapping the redux-form reducer in your own thing.
export default combineReducers({
someReducer,
anotherReducer,
form: resetHack(formReducer)
})
function resetHack(formReducer) {
return (state, action) => {
if(action.RESET_LINK_TYPE_FIELDS) {
// manipulate slice somehow
} else {
return formReducer(state, action)
}
}
}

Why session.getSaveBatch() is undefined when child record was added - Ext 5.1.1

Well the title says it all, details following.
I have two related models, User & Role.
User has roles defined as:
Ext.define('App.model.security.User', {
extend: 'App.model.Base',
entityName: 'User',
fields: [
{ name: 'id' },
{ name: 'email'},
{ name: 'name'},
{ name: 'enabled', type: 'bool'}
],
manyToMany: 'Role'
});
Then I have a grid of users and a form to edit user's data including his roles.
The thing is, when I try to add or delete a role from the user a later call to session.getSaveBatch() returns undefined and then I cannot start the batch to send the modifications to the server.
How can I solve this?
Well after reading a lot I found that Ext won't save the changed relationships between two models at least on 5.1.1.
I've had to workaround this by placing an aditional field on the left model (I named it isDirty) with a default value of false and set it true to force the session to send the update to the server with getSaveBatch.
Later I'll dig into the code to write an override to BatchVisitor or a custom BatchVisitor class that allow to save just associations automatically.
Note that this only occurs when you want to save just the association between the two models and if you also modify one of the involved entities then the association will be sent on the save batch.
Well this was interesting, I've learned a lot about Ext by solving this simple problem.
The solution I came across is to override the BatchVisitor class to make use of an event handler for the event onCleanRecord raised from the private method visitData of the Session class.
So for each record I look for left side entities in the matrix and if there is a change then I call the handler for onDirtyRecord which is defined on the BatchVisitor original class.
The code:
Ext.define('Ext.overrides.data.session.BatchVisitor', {
override: 'Ext.data.session.BatchVisitor',
onCleanRecord: function (record) {
var matrices = record.session.matrices
bucket = null,
ops = [],
recordId = record.id,
className = record.$className;
// Before anything I check that the record does not exists in the bucket
// If it exists then any change on matrices will be considered (so leave)
try {
bucket = this.map[record.$className];
ops.concat(bucket.create || [], bucket.destroy || [], bucket.update || []);
var found = ops.findIndex(function (element, index, array) {
if (element.id === recordId) {
return true;
}
});
if (found != -1) {
return;
}
}
catch (e) {
// Do nothing
}
// Now I look for changes on matrices
for (name in matrices) {
matrix = matrices[name].left;
if (className === matrix.role.cls.$className) {
slices = matrix.slices;
for (id in slices) {
slice = slices[id];
members = slice.members;
for (id2 in members) {
id1 = members[id2][0]; // This is left side id, right side is index 1
state = members[id2][2];
if (id1 !== recordId) { // Not left side => leave
break;
}
if (state) { // Association changed
this.onDirtyRecord(record);
// Same case as above now it exists in the bucket (so leave)
return;
}
}
}
}
}
}
});
It works very well for my needs, probably it wont be the best solution for others but can be a starting point anyways.
Finally, if it's not clear yet, what this does is give the method getSaveBatch the ability to detect changes on relationships.

How to properly update nested data in redux

This is my current object which i need to update:
[
{ id: q1,
answers:[
{ id: a1,
answered: false
},
...
]
},
...
]
I can't figure out how to update this object and set for example answered = true.
Is there any better way saving this kind of object? I tried to use the update addon from React but can't get it to work properly.
You can update the answers list this way, in your reducer:
function update(state, action) {
// assuming you are passing an id of the item to be updated via action.itemId
let obj = state.whatever_list.filter(item => item.id === action.itemId)[0]
//assuming you are passing an id of the answer to be updated via action.answerId
//also, assuming action.payload contains {answered: true}
let answers = obj.answers.map(answer => answer.id === action.answerId ?
Object.assign({}, answer, action.payload) : answer)
obj = Object.assign({}, obj, {answers: answers})
return {
whatever_list: state.whatever_list.map(item => item.id == action.itemId? Object.assign({}, item, obj) : item)
}
}
Here is what your action might look like:
function updateAnswer(itemId, answerId, payload) {
return {
type: UPDATE_ANSWER,
itemId: itemId,
answerId: answerId,
payload: payload
}
}
In your react component class, assuming there is an event handler for monitoring whether if a question is answered:
export default class Whatever extends React.Component {
...
// assuming your props contains itemId and answerId
handleAnswered = (e) => {
this.props.dispatch(updateAnswer(this.props.itemId, this.props.answerId, {answered: true}))
}
...
}
So basically what happens is this:
Your event handler calls the action and pass the updated data to it
When your action is called, it returns the updated data along with a type parameter
When your reducer sees the type parameter, the corresponding handler will be triggered (the first piece of the code above)
The reducer will pull out the existing data from the list, replace the old data with the new one, and then return a list containing the new data
You can create a sub-reducer for the answers key. Look at this example:
https://github.com/rackt/redux/blob/master/examples/async/reducers/index.js
You could use dot-prop-immutable and an update would be as simple as:
return dotProp.set(state, 'quiz.0.answers.0.answered', true);

Angular Meteor objects not acting as expected

I am working with Angular Meteor and am having an issue with my objects/arrays. I have this code:
angular.module("learn").controller("CurriculumDetailController", ['$scope', '$stateParams', '$meteor',
function($scope, $stateParams, $meteor){
$scope.curriculum = $meteor.object(CurriculumList, $stateParams.curriculumId);
$scope.resources = _.map($scope.curriculum.resources, function(obj) {
return ResourceList.findOne({_id:obj._id})
});
console.log($scope.resources)
}]);
I am attempting to iterate over 'resources', which is a nested array in the curriculum object, look up each value in the 'ResourceList' collection, and return the new array in the scope.
Problem is, sometimes it works, sometimes it doesnt. When I load up the page and access it through a UI-router link. I get the array as expected. But if the page is refreshed, $scope.resources is an empty array.
My thought is there is something going on with asynchronous calls but have not been able for find a solution. I still have the autopublish package installed. Any help would be appreciated.
What you're going to do is return a cursor containing all the information you want, then you can work with $meteor.object on the client side if you like. Normally, publishComposite would look something like this: (I don't know what your curriculum.resources looks like)
Use this method if the curriculum.resources has only ONE id:
// this takes the place of the publish method
Meteor.publishComposite('curriculum', function(id) {
return {
find: function() {
// Here you are getting the CurriculumList based on the id, or whatever you want
return CurriculumList.find({_id: id});
},
children: [
{
find: function(curr) {
// (curr) will be each of the CurriculumList's found from the parent query
// Normally you would do something like this:
return ResourceList.find(_id: curr.resources[0]._id);
}
}
]
}
})
This method if you have multiple resources:
However, since it looks like your curriculum is going to have a resources list with one or many objects with id's then we need to build the query before returning anything. Try something like:
// well use a function so we can send in an _id
Meteor.publishComposite('curriculum', function(id){
// we'll build our query before returning it.
var query = {
find: function() {
return CurriculumList.find({_id: id});
}
};
// now we'll fetch the curriculum so we can access the resources list
var curr = CurriculumList.find({_id: id}).fetch();
// this will pluck the ids from the resources and place them into an array
var rList = _.pluck(curr.resources, '_id');
// here we'll iterate over the resource ids and place a "find" object into the query.children array.
query.children = [];
_.each(rList, function(id) {
var childObj = {
find: function() {
return ResourceList.find({_id: id});
}
};
query.children.push(childObj)
})
return query;
});
So what should happen here (I didn't test) is with one publish function you will be getting the Curriculum you want, plus all of it's resourceslist children.
Now you will have access to these on the client side.
$scope.curriculum = $meteor.object(CurriculumList, $stateParams.curriculumId);
// collection if more than one, object if only one.
$scope.resources = $meteor.collection(ResoursesList, false);
This was thrown together somewhat quickly so I apologize if it doesn't work straight off, any trouble I'll help you fix.

Resources