Possible to genericize NGXS actions? - ngxs

I'd like to write just one action to perform the same CRUD operations on state, just on different slices of it, while preserving type safety.
For example, I'd like to use the following action to apply a set operation to any slice with a generic type T:
export class Set_Entity<T> {
static readonly type = '[Entity] Set';
constructor(public payload: <T>) {}
}
This is problematic because the type will always be the same. Is it possible to somehow decorate this class so a unique type property can be passed in whenever it is used as the #Action?
Something like:
/* action* /
class Set_Entity<T> {
constructor(public entity: string, public payload: <T>) {}
}
/* state */
#Action(Set_Entity('[Groups] Set Group'/* <-- Changes the `type` property */))
set_group(
context: StateContext<Model>,
action: SetEntity<{entity: string, payload: Group}>,
) {
const entity = action.entity;
const data = action.payload;
context.patchState({ [entity]: data });
}
/* facade or something */
this.store.dispatch([
new Set_Entity<GroupEntityType>(
'user', // <-- the state slice
aRecord,
),
]);
Even this solution leaves more to be desired. Generic Actions still must be written for each state slice, for each CRUD operation. It would be nice to be able to use the same generic action for each CRUD op on each state slice.
I managed to do it beautifully with NGRX via typescript-fsa and typescript-fsa-reducers. Only needed one single generic action plus one single generic reducer for the entire state, all typesafe.
The action looked like this:
function generic_set_action<T>(sliceName: string): ActionCreator<T> {
const creator = actionCreatorFactory(sliceName);
const action = creator<T>('set')
return action; // Produces type of `sliceName/set`
}
// Create the action
generic_set_action<User>('sliceName')(payload)
The reducer:
export function create_generic_reducer<T>(sliceName: string) {
const action_set = generic_set_action<T>(sliceName);
return reducerWithInitialState({} as T)
.case(action_set, (state, data) => (data))
.build();
}
And finally when creating the reducers:
export const Reducers: ActionReducerMap<State> = {
coolSlice: create_generic_reducer<MySliceModel>('coolSlice'),
// repeat for each slice..
};
It would be great to be able to reproduce this with NGXS.

Related

How to retrieve last value passed to next() of an Rxjs Observable outside the observer

I have a class with more than 30 observable attributes. Each time my server receives a payload containing these 30 attributes I call the next() method for all the corresponding attributes of the instance, so far so good.
The problem is that, sometimes, I have to check for an attribute's value, outside the scope of the observer that subscribed to that observable attribute.
What comes to mind is that I have to have duplicate attributes for everything, one is the observable and the other one is a stateful attribute to save the arriving values for later consumption.
Is there some way to avoid this with a method like: Observable.getCurrentValue()?
As requested, some example code
class Example {
public subjects = {
a1: new Subject<any>(),
a2: new Subject<any>(),
a3: new Subject<any>(),
a4: new Subject<any>(),
a5: new Subject<any>()
}
public treatPayload(data: any) {
for (const prop in data) {
if (data.hasOwnProperty(prop) && prop in this.subjects){
Reflect.get(this.subjects, prop).next(data[prop])
}
}
}
public test() {
const a1_observable = this.subjects.a1.asObservable()
const a2_observable = this.subjects.a2.asObservable()
const example_payload_1 = {
a1: "first",
a2: "second",
a10: "useless"
}
const example_payload_2 = {
a1: "first-second",
a2: "second-second",
a10: "useless-second"
}
a1_observable.subscribe((a1_new_value: any) => {
const i_also_want_the_last_value_emitted_by_a2 = a2_observable.last_value() // of course, this doesn't exist
console.log(a1_new_value)
console.log(i_also_want_the_last_value_emitted_by_a2)
})
this.treatPayload(example_payload_1)
this.treatPayload(example_payload_2)
}
}
So, is there a way to retrieve the correct value of i_also_want_the_last_value_emitted_by_a2 without a pipe operator? I think it would be a problem to emit all values I could possibly use in a subscriber within a pipe of the a2_observable.
You could use BehaviorSubject.value, where you could store your server data.

How to trigger visitInputObject method on custom directive?

I'm building a custom directive in which I'm hoping to validate entire input objects. I'm using the INPUT_OBJECT type with the visitInputObject method on SchemaDirectiveVisitor extended class.
Every time I run a mutation using the input type then visitInputObject does not run.
I've used the other types/methods like visitObject and visitFieldDefinition and they work perfectly. But when trying to use input types and methods they will not trigger.
I've read all the available documentation I can find. Is this just not supported yet?
Some context code(Not actual):
directive #validateThis on INPUT_OBJECT
input MyInputType #validateThis {
id: ID
someField: String
}
type Mutation {
someMutation(myInput: MyInputType!): SomeType
}
class ValidateThisDirective extends SchemaDirectiveVisitor {
visitInputObject(type) {
console.log('Not triggering');
}
}
All the visit methods of a SchemaDirectiveVisitor are ran at the same time -- when the schema is built. That includes visitFieldDefinition and visitFieldDefinition. The difference is that when we use visitFieldDefinition, we often do it to modify the resolve function for the visited field. It's this function that's called during execution.
You use each visit methods to modify the respective schema element. You can use visitInputObject to modify an input object, for example to add or remove fields from it. You cannot use it to modify the resolution logic of an output object's field. You should use visitFieldDefinition for that.
visitFieldDefinition(field, details) {
const { resolve = defaultFieldResolver } = field
field.resolve = async function (parent, args, context, info) {
Object.keys(args).forEach(argName => {
const argDefinition = field.args.find(a => a.name === argName)
// Note: you may have to "unwrap" the type if it's a list or non-null
const argType = argDefinition.type
if (argType.name === 'InputTypeToValidate') {
const argValue = args[argName]
// validate here
}
})
return resolve.apply(this, [parent, args, context, info]);
}
}

Ordered list of redux-form fields

Do you know how can I get the ordered list of field names from given form? Instance API has a property called "fieldList" and it's an array but it's not in correct order. (ordered list = [firstFieldName, secondFieldName, ...] so what I need is a list of field names in order they appear in my form - top to bottom)
Also the redux-form' action '##redux-form/REGISTER_FIELD' is dispatching out of correct form order so I guess it's not what I need here...
(My redux-form version: 7.3.0)
I have experience with redux-form and also have checked its API, but didn't find a documented way for getting the fields in the way they appear in the form.
However, here's how I would do it:
I'll create a Reducer, that will keep track of the fields in the order,
they are registered (appear in the form).
We have very detailed action. As you already mentioned - ##redux-form/REGISTER_FIELD action is dispatching out all the fields in process of being registered in the correct order. This action has the following payload:
{
type: '##redux-form/REGISTER_FIELD',
meta: {
form: 'user'
},
payload: {
name: 'firstName',
type: 'Field'
}
}
Create a reducer. So I'll just create a Reducer, that will listen for all ##redux-form/REGISTER_FIELD actions. Something like that:
// The reducer will keep track of all fields in the order they are registered by a form.
// For example: `user` form has two registered fields `firstName, lastName`:
// { user: ['firstName', 'lastName'] }
const formEnhancedReducer = (state = {}, action) {
switch (action.type) {
case '##redux-form/REGISTER_FIELD':
const form = action.meta.form
const field = action.payload.name
return { ...state, [form]: [...state[form], field] }
default:
return state
}
}
Usage. In order to get ordered fields by a form, you just have access the Store (state) formEnhancer property with the form name: state.formEnhanced.user.
Keep in mind that you have to consider some cases as ##redux-form/DESTROY, but I think it's a pretty straightforward implementation.
I would prefer to keep things simple and just subscribed to ##redux-form/REGISTER_FIELD and just change the reducer implementation a little bit, in order to prevent form fields duplication. So I will just validate if the form field is already registered and won't care for supporting ##redux-form/DESTROY.
Hope it helps.
One way that I have been able to retrieve an ordered list of form field names from a given form is via the registered fields stored in the redux form state using the connect HOC (Higher Order Component) from 'react-redux':
import React, { Component } from 'react';
import { connect } from 'react-redux';
import _ from 'lodash';
class Foo extends Component {
render() {
const {
registeredFields,
} = this.props;
...
...
...
}
}
const mapStateToProps = (state, props) => {
// retrieve the registered fields from the form that is stored in redux state; using lodash 'get' function
const registeredFields = _.get(state, 'form.nameOfYourForm.registeredFields');
// creating an object with the field name as the key and the position as the value
const registeredFieldPositions = _.chain(registeredFields).keys().reduce((registeredFieldPositions, key, index) => {
registeredFieldPositions[key] = index;
return registeredFieldPositions;
}, {}).value();
return({
registeredFieldPositions,
});
};
// registeredFieldPositions will now be passed as a prop to Foo
export default connect(mapStateToProps)(Foo);

Using Config.skip with a React-Apollo Query

I'm having some trouble making use of the Config.skip property inside of my graphql() wrapper.
The intent is for the query to be fired with an argument of currentGoalID, only after a user has selected an item from the drop-down (passing the associated currentGoalID) , and the (Redux) state has been updated with a value for currentGoalID.
Otherwise, I expect (as per Apollo documentation) that:
... your child component doesn’t get a data prop at all, and the options or props methods are not called.
In this case though, it seems that my skip property is being ignored based upon the absence of a value for currentGoalID, and the option is being called because the webpack compiler/linter throws on line 51, props is not defined...
I successfully console.log the value of currentGoalID without the graphql()
wrapper. Any idea why config.skip isn't working? Also wish to be advised on the proper use of this in graphql() function call. I've excluded it here, but am unsure of the context, thanks.
class CurrentGoal extends Component {
constructor(props) {
super(props)
}
render (){
console.log(this.props.currentGoalID);
return( <p>Current Goal: {null}</p>
)
}
}
const mapStateToProps = (state, props) => {
return {
currentGoal: state.goals.currentGoal,
currentGoalID: state.goals.currentGoalID,
currentGoalSteps: state.goals.currentGoalSteps
}
}
const FetchGoalDocByID = gql `
query root($varID:String) {
goalDocsByID(id:$varID) {
goal
}
}`;
const CurrentGoalWithState = connect(mapStateToProps)(CurrentGoal);
const CurrentGoalWithData = graphql(FetchGoalDocByID, {
skip: (props) => !props.currentGoalID,
options: {variables: {varID: props.currentGoalID}}
})(CurrentGoalWithState);
// export default CurrentGoalWithState
export default CurrentGoalWithData
See the answer here: https://stackoverflow.com/a/47943253/763231
connect must be the last decorator executed, after graphql, in order for graphql to include the props from Redux.

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)
}
}
}

Resources