Reuse selector in other selectors in NGXS - ngxs

I have two classes PizzasState and ToppingsState.
PizzaState already has selector to get selected pizza.
#State<PizzaStateModel>({
name: 'pizzas',
defaults: initialState
})
export class PizzasState {
constructor(private pizzaService: PizzasService) {
}
#Selector([RouterState])
static getSelectedPizza(
state: PizzaStateModel,
routerState: RouterStateModel<RouterStateParams>
): Pizza {
const pizzaId = routerState.state && routerState.state.params.pizzaId;
return pizzaId && state.entities[pizzaId];
}
#Selector()
getPizzaVisualized(state: PizzaStateModel): Pizza {
//
// what is here?
//
}
}
and ToppingsState has selectedToppings
#State({
name: 'toppings',
defaults: initialState
})
export class ToppingsState {
constructor(private toppingsService: ToppingsService) {
}
#Selector()
static selectedToppings(state: ToppingsStateModel): number[] {
return state.selectedToppings;
}
Now I want to join my selected pizza with selected toppings and get my visualized pizza.
How can I reuse getSelectedPizza and getSelectedToppings correctly?
Thank you

It looks like you need to use a joining selector.
#Selector([ToppingsState.selectedToppings])
getPizzaVisualized(state: PizzaStateModel, selectedToppings: number[]): Pizza {
// User data from both states.
}

Related

Object still frozen when using Immer and NGXS

I am implementing ngxs into an existing codebase.
I have imported Immer and then ngxs bridge in hopes to handle side effects easier.
I've followed every example that I can find through google, I always get:
core.js:6014 ERROR TypeError: Cannot assign to read only property 'savedPrograms' of object '[object Object]'
I've tried using the #ImmutableContext() decorator to accont for this, but I get the exact same error. I also tried using the produce method, but when i give draft.savedPrograms a new value, it throws the error above.
#Action(UserActions.AddProgram)
#ImmutableContext()
public addProgram(ctx: StateContext<UserStateModel>, action) {
ctx.setState(
produce((draft: UserStateModel) => {
draft.user.savedPrograms = action.payload;
})
);
}
The only way i can get this to work is if i use JSON parse/stringify to create a copy of the user and then update the user object.
#Action(UserActions.AddProgram)
public addProgram(ctx: StateContext<UserStateModel>, action) {
const state = produce(draft => {
const copy = JSON.parse(JSON.stringify(draft));
copy.user.savedPrograms.push(action.payload);
draft = copy;
});
ctx.setState(state);
}
I'm not quite sure why ImmutableContext doesn't work out of the box
From immer-adapter documentation:
import { ImmutableContext, ImmutableSelector } from '#ngxs-labs/immer-adapter';
#State<AnimalsStateModel>({
name: 'animals',
defaults: {
zebra: {
food: [],
name: 'zebra'
},
panda: {
food: [],
name: 'panda'
}
}
})
export class AnimalState {
#Selector()
#ImmutableSelector()
public static zebraFood(state: AnimalsStateModel): string[] {
return state.zebra.food.reverse();
}
#Action(Add)
#ImmutableContext()
public add({ setState }: StateContext<AnimalsStateModel>, { payload }: Add): void {
setState((state: AnimalsStateModel) => ({
state.zebra.food.push(payload);
return state;
}));
}
}
As I understand you need to remove produce from your code:
#Action(UserActions.AddProgram)
#ImmutableContext()
public addProgram(ctx: StateContext<UserStateModel>, action) {
ctx.setState(
(draft: UserStateModel) => {
draft.user.savedPrograms = action.payload;
return draft;
}
);
}

How do I pass data back to previous page using `navigateBack`?

I am using navigateTo to open a page with listview and would like to pass the results back using navigateBack but unable to achieve that. Any idea?
With Service class and Observable, you can achieve this.
notify.service.ts
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs-compat/Subject';
#Injectable({
providedIn: 'root'
})
export class NotifyService {
private refreshDataForView = new Subject<any>();
refreshDataForParentViewObservable$ = this.refreshDataForView.asObservable();
public relaodDataForParentView(data: any) {
if (data) {
this.refreshDataForView.next(data);
}
}
}
Second componenet.ts
constructor(
private notifyService: NotifyService
) { }
goBack() {
this.notifyService.relaodDataForParentView({ data: 'any data you wanrt to pass here ' });
this.router.back();
}
First component.ts
reloadDataSubscription: any;
constructor(
private notifyService: NotifyService
) {}
ngOnInit() {
this.reloadDataSubscription = this.notifyService.refreshDataForParentViewObservable$
.subscribe((res) => {
console.log('======', res);
// do what you want to do with the data passed from second view
});
}

Relay Modern node does not contain fragment properties

I have the following setup in my React Project:
export default class OverviewScreen extends React.Component<any, any> {
public render() {
return (
<QueryRenderer
environment={environment}
query={OverviewScreenQuery}
render={this.queryRender}/>
);
}
protected queryRender({error, props}): JSX.Element {
if (error) {
return <div>{error.message}</div>;
} else if (props) {
return (
<div>
<div>
<ActivityOfferList viewer={props.viewer} title="Titel"/>
<ActivityTypeListsFragment viewer={props.viewer}/>
</div>
</div>
);
}
return <div>Loading...</div>;
}
}
const OverviewScreenQuery = graphql`
query OverviewScreenQuery {
viewer {
...HorizontalOfferList_viewer
...ActivityTypeLists_viewer
}
}`;
class ActivityTypeLists extends React.Component<IHorizontalOfferListProps, any> {
public render() {
return (
<div>
{this.props.viewer.allActivityTypes.edges.map((typeEdge) => {
let typeNode = typeEdge.node;
return this.getCardListForActivityType(typeNode);
})}
</div>
);
}
private getCardListForActivityType(typeNode: any) {
console.log(typeNode);
return (
<CardList key={typeNode.__id} title={typeNode.title}>
{typeNode.activities.edges.map(({node}) => {
return (
<RelayPicturedTypeActivityCard key={node.__id} offer={node} activityType={typeNode}/>
);
})}
</CardList>
);
}
}
export const ActivityTypeListsFragment = createFragmentContainer(ActivityTypeLists, graphql`
fragment ActivityTypeLists_viewer on Viewer {
allActivityTypes(first: 5) {
edges {
node {
...PicturedTypeActivityCard_offer
}
}
}
}
`);
export class PicturedTypeActivityCard extends React.Component<any, any> {
public render() {
return (
<PicturedCard title={this.props.offer.title} subtitle={this.props.activityType.title} width={3}/>
);
}
}
export const RelayPicturedTypeActivityCard = createFragmentContainer(PicturedTypeActivityCard, graphql`
fragment PicturedTypeActivityCard_offer on ActivityType {
title
activities(first: 4) {
edges {
node {
id
title
}
}
}
}
`);
Which should work and give me the correct result from the graphcool relay endpoint.
The Network call to the relay endpoint is indeed correct and I receive all the ActivityTypes and their activities and titles from my endpoint.
But somehow in the function getCardListForActivityType() the typeNode only contains the __id of the node as data and no title at all:
If I insert title and activities directly instead of using
...PicturedTypeActivityCard_offer
then the data also gets passed down correctly. So something with the Fragment must be off.
Why is it that the network call is complete and uses the fragment correctly to fetch the data, but the node object never gets the fetched data?
This is indeed correct behavior.
Your components must, individually, specify all their own data dependencies, Relay will only pass to the component the data it asked for. Since your component is not asking any data, it's receiving an empty object.
That __id you see is used internally by Relay and you should not rely on it (that is why it has the __ prefix).
Basically, the prop viewer on ActivityTypeLists component will have exactly the same format than the query requested on the ActivityTypeLists_viewer fragment, without any other fragments from other components that you are referencing there.
This is known as data masking, see more in the following links:
https://facebook.github.io/relay/docs/en/thinking-in-relay.html#data-masking
https://facebook.github.io/relay/docs/en/graphql-in-relay.html#relaymask-boolean

Relay: Optimistic update not causing a component rerender

I'm using PostgraphQL (https://github.com/calebmer/postgraphql) with Relay and wired a UpdateQuestionMutation into my app. However, I do not get optimistic updating to work.(When I enable network throttling in chrome I can see that the the optimistic update gets handled but the component still shows the old title).Do I miss something? I have following pieces :
class QuestionClass extends Component<IQuestion, void> {
save = (item) => {
this.props.relay.commitUpdate(
new UpdateQuestionMutation({store: this.props.store, patch: item})
);
this.isEditing = false;
};
public render(): JSX.Element {
const item = this.props.store;
console.log(item);
...
const Question = Relay.createContainer(QuestionClass, {
fragments: {
// The property name here reflects what is added to `this.props` above.
// This template string will be parsed by babel-relay-plugin.
store: () => Relay.QL`
fragment on Question {
${UpdateQuestionMutation.getFragment('store')}
title
description
userByAuthor {
${User.getFragment('store')}
}
}`,
},
});
...
export default class UpdateQuestionMutation extends Relay.Mutation<any, any> {
getMutation() {
return Relay.QL `mutation { updateQuestion }`
}
getVariables() {
console.log(this.props);
return {
id: this.props.store.id,
questionPatch: this.props.patch
}
}
getFatQuery() {
return Relay.QL `fragment on UpdateQuestionPayload { question }`
}
getConfigs() {
return [{
type: "FIELDS_CHANGE",
fieldIDs: {
question: this.props.store.id
}
}]
}
getOptimisticResponse() {
return {
store: this.props.patch
}
}
// This mutation has a hard dependency on the question's ID. We specify this
// dependency declaratively here as a GraphQL query fragment. Relay will
// use this fragment to ensure that the question's ID is available wherever
// this mutation is used.
static fragments = {
store: () => Relay.QL`
fragment on Question {
id
}
`,
};
}
Edit: That's what I see in the postgraphql logs:
mutation UpdateQuestion($input_0: UpdateQuestionInput!) { updateQuestion(input: $input_0) { clientMutationId ...F1 } } fragment F0 on Question { id rowId title description userByAuthor { id rowId username } } fragment F1 on UpdateQuestionPayload { question { id ...F0 } }

only allow children of a specific type in a react component

I have a Card component and a CardGroup component, and I'd like to throw an error when CardGroup has children that aren't Card components. Is this possible, or am I trying to solve the wrong problem?
For React 0.14+ and using ES6 classes, the solution will look like:
class CardGroup extends Component {
render() {
return (
<div>{this.props.children}</div>
)
}
}
CardGroup.propTypes = {
children: function (props, propName, componentName) {
const prop = props[propName]
let error = null
React.Children.forEach(prop, function (child) {
if (child.type !== Card) {
error = new Error('`' + componentName + '` children should be of type `Card`.');
}
})
return error
}
}
You can use the displayName for each child, accessed via type:
for (child in this.props.children){
if (this.props.children[child].type.displayName != 'Card'){
console.log("Warning CardGroup has children that aren't Card components");
}
}
You can use a custom propType function to validate children, since children are just props. I also wrote an article on this, if you want more details.
var CardGroup = React.createClass({
propTypes: {
children: function (props, propName, componentName) {
var error;
var prop = props[propName];
React.Children.forEach(prop, function (child) {
if (child.type.displayName !== 'Card') {
error = new Error(
'`' + componentName + '` only accepts children of type `Card`.'
);
}
});
return error;
}
},
render: function () {
return (
<div>{this.props.children}</div>
);
}
});
For those using a TypeScript version.
You can filter/modify components like this:
this.modifiedChildren = React.Children.map(children, child => {
if (React.isValidElement(child) && (child as React.ReactElement<any>).type === Card) {
let modifiedChild = child as React.ReactElement<any>;
// Modifying here
return modifiedChild;
}
// Returning other components / string.
// Delete next line in case you dont need them.
return child;
});
Use the React.Children.forEach method to iterate over the children and use the name property to check the type:
React.Children.forEach(this.props.children, (child) => {
if (child.type.name !== Card.name) {
console.error("Only card components allowed as children.");
}
}
I recommend to use Card.name instead of 'Card' string for better maintenance and stability in respect to uglify.
See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name
One has to use "React.isValidElement(child)" along with "child.type" if one is working with Typescript in order to avoid type mismatch errors.
React.Children.forEach(props.children, (child, index) => {
if (React.isValidElement(child) && child.type !== Card) {
error = new Error(
'`' + componentName + '` only accepts children of type `Card`.'
);
}
});
You can add a prop to your Card component and then check for this prop in your CardGroup component. This is the safest way to achieve this in React.
This prop can be added as a defaultProp so it's always there.
class Card extends Component {
static defaultProps = {
isCard: true,
}
render() {
return (
<div>A Card</div>
)
}
}
class CardGroup extends Component {
render() {
for (child in this.props.children) {
if (!this.props.children[child].props.isCard){
console.error("Warning CardGroup has a child which isn't a Card component");
}
}
return (
<div>{this.props.children}</div>
)
}
}
Checking for whether the Card component is indeed a Card component by using type or displayName is not safe as it may not work during production use as indicated here: https://github.com/facebook/react/issues/6167#issuecomment-191243709
I made a custom PropType for this that I call equalTo. You can use it like this...
class MyChildComponent extends React.Component { ... }
class MyParentComponent extends React.Component {
static propTypes = {
children: PropTypes.arrayOf(PropTypes.equalTo(MyChildComponent))
}
}
Now, MyParentComponent only accepts children that are MyChildComponent. You can check for html elements like this...
PropTypes.equalTo('h1')
PropTypes.equalTo('div')
PropTypes.equalTo('img')
...
Here is the implementation...
React.PropTypes.equalTo = function (component) {
return function validate(propValue, key, componentName, location, propFullName) {
const prop = propValue[key]
if (prop.type !== component) {
return new Error(
'Invalid prop `' + propFullName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
};
}
You could easily extend this to accept one of many possible types. Maybe something like...
React.PropTypes.equalToOneOf = function (arrayOfAcceptedComponents) {
...
}
static propTypes = {
children : (props, propName, componentName) => {
const prop = props[propName];
return React.Children
.toArray(prop)
.find(child => child.type !== Card) && new Error(`${componentName} only accepts "<Card />" elements`);
},
}
I published the package that allows to validate the types of React elements https://www.npmjs.com/package/react-element-proptypes :
const ElementPropTypes = require('react-element-proptypes');
const Modal = ({ header, items }) => (
<div>
<div>{header}</div>
<div>{items}</div>
</div>
);
Modal.propTypes = {
header: ElementPropTypes.elementOfType(Header).isRequired,
items: React.PropTypes.arrayOf(ElementPropTypes.elementOfType(Item))
};
// render Modal
React.render(
<Modal
header={<Header title="This is modal" />}
items={[
<Item/>,
<Item/>,
<Item/>
]}
/>,
rootElement
);
To validate correct children component i combine the use of react children foreach and the Custom validation proptypes, so at the end you can have the following:
HouseComponent.propTypes = {
children: PropTypes.oneOfType([(props, propName, componentName) => {
let error = null;
const validInputs = [
'Mother',
'Girlfried',
'Friends',
'Dogs'
];
// Validate the valid inputs components allowed.
React.Children.forEach(props[propName], (child) => {
if (!validInputs.includes(child.type.name)) {
error = new Error(componentName.concat(
' children should be one of the type:'
.concat(validInputs.toString())
));
}
});
return error;
}]).isRequired
};
As you can see is having and array with the name of the correct type.
On the other hand there is also a function called componentWithName from the airbnb/prop-types library that helps to have the same result.
Here you can see more details
HouseComponent.propTypes = {
children: PropTypes.oneOfType([
componentWithName('SegmentedControl'),
componentWithName('FormText'),
componentWithName('FormTextarea'),
componentWithName('FormSelect')
]).isRequired
};
Hope this help some one :)
Considered multiple proposed approaches, but they all turned out to be either unreliable or overcomplicated to serve as a boilerplate. Settled on the following implementation.
class Card extends Component {
// ...
}
class CardGroup extends Component {
static propTypes = {
children: PropTypes.arrayOf(
(propValue, key, componentName) => (propValue[key].type !== Card)
? new Error(`${componentName} only accepts children of type ${Card.name}.`)
: null
)
}
// ...
}
Here're the key ideas:
Utilize the built-in PropTypes.arrayOf() instead of looping over children
Check the child type via propValue[key].type !== Card in a custom validator
Use variable substitution ${Card.name} to not hard-code the type name
Library react-element-proptypes implements this in ElementPropTypes.elementOfType():
import ElementPropTypes from "react-element-proptypes";
class CardGroup extends Component {
static propTypes = {
children: PropTypes.arrayOf(ElementPropTypes.elementOfType(Card))
}
// ...
}
An easy, production friendly check. At the top of your CardGroup component:
const cardType = (<Card />).type;
Then, when iterating over the children:
React.children.map(child => child.type === cardType ? child : null);
The nice thing about this check is that it will also work with library components/sub-components that don't expose the necessary classes to make an instanceof check work.
Assert the type:
props.children.forEach(child =>
console.assert(
child.type.name == "CanvasItem",
"CanvasScroll can only have CanvasItem component as children."
)
)
Related to this post, I figured out a similar problem I had. I needed to throw an error if a child was one of many icons in a Tooltip component.
// icons/index.ts
export {default as AddIcon} from './AddIcon';
export {default as SubIcon} from './SubIcon';
...
// components/Tooltip.tsx
import { Children, cloneElement, isValidElement } from 'react';
import * as AllIcons from 'common/icons';
...
const Tooltip = ({children, ...rest}) => {
Children.forEach(children, child => {
// ** Inspired from this post
const reactNodeIsOfIconType = (node, allIcons) => {
const iconTypes = Object.values(allIcons);
return iconTypes.some(type => typeof node === 'object' && node !== null && node.type === type);
};
console.assert(!reactNodeIsOfIconType(child, AllIcons),'Use some other component instead...')
})
...
return Children.map(children, child => {
if (isValidElement(child) {
return cloneElement(child, ...rest);
}
return null;
});
}

Resources