How to stub document method with sinon - React - reactjs

import React, { PropTypes, Component } from 'react';
import classNames from 'classnames/bind';
import { get, includes } from 'lodash';
import { Link } from 'react-router';
import * as styles from '../CAMNavPanel.css';
const cx = classNames.bind(styles);
class CAMNavPanelListItem extends Component {
static propTypes = {
navData: PropTypes.shape({
title: PropTypes.string,
isRedirect: PropTypes.bool,
url: PropTypes.string,
}).isRequired,
location: PropTypes.shape({ pathname: PropTypes.string.isRequired,
query: PropTypes.objectOf(PropTypes.object).isRequired,
search: PropTypes.string.isRequired,
}).isRequired,
};
constructor() {
super();
this.state = { currentView: '' };
this.getClasses.bind(this);
}
// in case of url being manually set, figure out correct tab to highlight
componentWillMount() {
this.changeLocation();
}
// give correct tab the 'active' class
getClasses(navData) {
const { location } = this.props;
const activeClass = 'active';
let isContainedInOtherUrls = false;
if (get(navData, 'otherUrls') && includes(navData.otherUrls, location.pathname)) {
isContainedInOtherUrls = true;
}
if ((this.state.currentView === navData.url) || isContainedInOtherUrls) {
return activeClass;
}
return '';
}
getActiveClass(e, navData) {
const elements = document.getElementsByClassName('CAMNavPanel-rewardsMenu')[0].getElementsByTagName('li');
for (let i = 0; i < elements.length; i += 1) {
elements[i].className = '';
}
this.setState({ currentView: navData.url }, () => {
if (get(navData, 'scrollIntoView')) {
document.getElementsByClassName(navData.scrollIntoView)[0].scrollIntoView();
}
});
}
// update state based on the URL
changeLocation() {
const { location } = this.props;
const currentView = location.pathname;
this.setState({ currentView });
}
render() {
const { navData } = this.props;
let target = '';
if (navData.isExternalLink) {
target = '_blank';
}
return (
<li className={cx(this.getClasses(navData))} key={navData.title}>
{ navData.isRedirect ? <a href={navData.url} target={target}>
{navData.title}</a> :
<Link to={navData.url} onClick={e => this.getActiveClass(e, navData)}>{navData.title}</Link> }
</li>
);
}
}
export default CAMNavPanelListItem;
Test case:
describe('CAMNavPanelListItem with isRedirect false plus highlight li', () => {
let wrapper;
const navData = {
title: 'My Orders',
isRedirect: false,
isExternalLink: false,
url: '/orders',
};
const location = {
pathname: '/orders',
};
beforeEach(() => {
documentObj = sinon.stub(document, 'getElementsByClassName');
const li = {
getElementsByTagName: sinon.stub(),
};
documentObj.withArgs('CAMNavPanel-rewardsMenu').returns([li]);
wrapper = shallow(
<CAMNavPanelListItem
navData={navData}
location={location}
/>,
);
wrapper.setState({ currentView: navData.url });
});
it('should render CAMNavPanelListItem with Link as well', () => {
expect(wrapper.find('li')).to.have.length(1);
expect(wrapper.find('li').hasClass('active')).to.equal(true);
expect(wrapper.find('Link')).to.have.length(1);
});
it('should click and activate activeClass', () => {
wrapper.find('Link').simulate('click', { button: 0 });
});
afterEach(() => {
wrapper.unmount();
documentObj.restore();
});
});
Errors I am getting:
const elements = document.getElementsByClassName('CAMNavPanel-rewardsMenu')[0].getElementsByTagName('li');
console.log('Elements of getElementsByTagName', elements);
The elements I am getting as undefined.
Please help. How do I stub after the click of the Link element.

You might want to look into something like jsdom to mock out an entire DOM instead of manually mocking a couple of the DOM functions. Then, you wouldn't have to implement the document.getElementsByClassName() function and it would make the test suite a bit more robust to changes in the component's implementation. You would just test for certain elements using functions on the jsdom itself.
You could also try mocha-jsdom to automatically setup and teardown the test DOM for each describe() block.

From what it seems, you made a stub to getElementsByTagName, but did not define to return anything, so you are getting undefined.
Add the following code:
const li = {
getElementsByTagName: sinon.stub(),
};
// add the code below after the above code lines.
const elements = [];
li.getElementsByTagName.withArgs('li').returns(elements);

This is what worked for me.
global.window.document.querySelectorAll = sinon.stub();
global.window.document.getElementsByClassName = sinon.stub();
const scrollIntoView = sinon.stub();
global.window.document.querySelectorAll.returns([{ className: '' }]);
global.window.document.getElementsByClassName.returns([{
scrollIntoView,
}]);
and I changed my original code to:
const elements = document.querySelectorAll('.CAMNavPanel-rewardsMenu li');
Used the global window object

Related

infinite loop when updating context in react

struggling with passing some values from a child to global context so i can use around my app.
After some research, I've been trying to update context on componentDidUpdate:
componentDidUpdate() {
this.refs.input.scrollIntoView();
if (this.state.history !== this.context.globalHistory) {
console.log(this.state.history);
console.log(this.context.globalHistory);
// this.context.setGlobalHistory(this.state.history);
console.log(this.context.globalHistory);
}
}
If I uncomment the line this.context.setGlobalHistory(this.state.history). I get an infinite loop.
I saw some answers saying to use useEffect but I get this error when trying to do so:
Line 67:9: React Hook "useEffect" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks
Any ideas how to get around this? Newbie to react so any help is appreciated.
Full code of the component
import React, { Component, useContext, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import * as BaseCommands from './commands';
import Bash from './bash';
import Styles from './styles';
import HistoryContext from "./HistoryContext";
const CTRL_CHAR_CODE = 17;
const L_CHAR_CODE = 76;
const C_CHAR_CODE = 67;
const UP_CHAR_CODE = 38;
const DOWN_CHAR_CODE = 40;
const TAB_CHAR_CODE = 9;
const noop = () => {};
export default class Terminal extends Component {
static contextType = HistoryContext;
constructor({ history, structure, extensions, prefix }) {
super();
this.Bash = new Bash(extensions);
this.ctrlPressed = false;
this.state = {
settings: { user: { username: prefix.split('#')[1] } },
history: history.slice(),
structure: Object.assign({}, structure),
cwd: '',
};
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
}
componentDidMount() {
this.refs.input.focus();
}
componentWillReceiveProps({ extensions, structure, history }) {
const updatedState = {};
if (structure) {
updatedState.structure = Object.assign({}, structure);
}
if (history) {
updatedState.history = history.slice();
}
if (extensions) {
this.Bash.commands = Object.assign({}, extensions, BaseCommands);
}
this.setState(updatedState);
}
/*
* Utilize immutability
*/
shouldComponentUpdate(nextProps, nextState) {
return (this.state !== nextState) || (this.props !== nextProps);
}
/*
* Keep input in view on change
*/
componentDidUpdate() {
this.refs.input.scrollIntoView();
if (this.state.history !== this.context.globalHistory) {
console.log(this.state.history);
console.log(this.context.globalHistory);
console.log('doesnt match');
// this.context.setGlobalHistory(this.state.history);
console.log(this.context.globalHistory);
}
}
/*
* Forward the input along to the Bash autocompleter. If it works,
* update the input.
*/
attemptAutocomplete() {
const input = this.refs.input.value;
const suggestion = this.Bash.autocomplete(input, this.state);
if (suggestion) {
this.refs.input.value = suggestion;
}
}
/*
* Handle keydown for special hot keys. The tab key
* has to be handled on key down to prevent default.
* #param {Event} evt - the DOM event
*/
handleKeyDown(evt) {
if (evt.which === CTRL_CHAR_CODE) {
this.ctrlPressed = true;
} else if (evt.which === TAB_CHAR_CODE) {
// Tab must be on keydown to prevent default
this.attemptAutocomplete();
evt.preventDefault();
}
}
/*
* Handle keyup for special hot keys.
* #param {Event} evt - the DOM event
*
* -- Supported hot keys --
* ctrl + l : clear
* ctrl + c : cancel current command
* up - prev command from history
* down - next command from history
* tab - autocomplete
*/
handleKeyUp(evt) {
if (evt.which === L_CHAR_CODE) {
if (this.ctrlPressed) {
this.setState(this.Bash.execute('clear', this.state));
}
} else if (evt.which === C_CHAR_CODE) {
if (this.ctrlPressed) {
this.refs.input.value = '';
}
} else if (evt.which === UP_CHAR_CODE) {
if (this.Bash.hasPrevCommand()) {
this.refs.input.value = this.Bash.getPrevCommand();
}
} else if (evt.which === DOWN_CHAR_CODE) {
if (this.Bash.hasNextCommand()) {
this.refs.input.value = this.Bash.getNextCommand();
} else {
this.refs.input.value = '';
}
} else if (evt.which === CTRL_CHAR_CODE) {
this.ctrlPressed = false;
}
}
handleSubmit(evt) {
evt.preventDefault();
// Execute command
const input = evt.target[0].value;
const newState = this.Bash.execute(input, this.state);
this.setState(newState);
this.refs.input.value = '';
console.log(this.context)
//const newHist = this.state.history
// this.context.setGlobalHistory(this.state.history)
//console.log(newHist)
}
renderHistoryItem(style) {
return (item, key) => {
const prefix = item.hasOwnProperty('cwd') ? (
<span style={style.prefix}>{`${this.props.prefix} ~${item.cwd} $`}</span>
) : undefined;
return <div data-test-id={`history-${key}`} key={key} >{prefix}{item.value}</div>;
};
}
render() {
const { onClose, onExpand, onMinimize, prefix, styles, theme } = this.props;
const { history, cwd } = this.state;
const style = Object.assign({}, Styles[theme] || Styles.light, styles);
//console.log(history)
//this.context.setGlobalHistory(history)
return (
<div className="ReactBash" style={style.ReactBash}>
<div style={style.header}>
<span style={style.redCircle} onClick={onClose}></span>
<span style={style.yellowCircle} onClick={onMinimize}></span>
<span style={style.greenCircle} onClick={onExpand}></span>
</div>
<div style={style.body} onClick={() => this.refs.input.focus()}>
{history.map(this.renderHistoryItem(style))}
<form onSubmit={evt => this.handleSubmit(evt)} style={style.form} >
<span style={style.prefix}>{`${prefix} ~${cwd} $`}</span>
<input
autoComplete="off"
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyUp}
ref="input"
style={style.input}
/>
</form>
</div>
</div>
);
}
}
Terminal.Themes = {
LIGHT: 'light',
DARK: 'dark',
};
Terminal.propTypes = {
extensions: PropTypes.object,
history: PropTypes.array,
onClose: PropTypes.func,
onExpand: PropTypes.func,
onMinimize: PropTypes.func,
prefix: PropTypes.string,
structure: PropTypes.object,
styles: PropTypes.object,
theme: PropTypes.string,
};
Terminal.defaultProps = {
extensions: {},
history: [],
onClose: noop,
onExpand: noop,
onMinimize: noop,
prefix: 'hacker#default',
structure: {},
styles: {},
theme: Terminal.Themes.LIGHT,
};
After reading this for a while, I think the "correct" answer is to lift up state I am messing with to the parent completely.
I wanted a lazier route since I didnt write the child component, but after tinkering this long, it would have faster to do this surgery from the start.

React-Router: How do I add a new component and route to the onboarding steps on a wizard?

This project I am working with has an onboarding Wizard, basically some code to deal with the step by step onboarding process almost similar to what you see here:
https://medium.com/#l_e/writing-a-wizard-in-react-8dafbce6db07
except this one supposedly has a function to convert a component or step into a route:
convertStepToRoute = step => {
const Component = StepComponents[step.component || ''];
return Component
? <Route
key={step.key}
path={`${WizardLayout.pathname}/${step.url}`}
render={this.renderRouteComponent(Component)}
/>
: null;
};
StepComponents comes from import StepComponents from '../Steps'; which is a directory with all the components, they were six now seven of them that are supposed to walk the user through the onboarding process.
And its my understanding that they are pulled from the index.js file inside of Steps/ directory similar to how there would be a root reducer file in a reducers folder to export all of them, the steps component in this case like so:
import glamorous from "glamorous";
import ThemedCard from "../../ThemedCard";
import BusinessAddress from "./BusinessAddress";
import CreatePassword from "./CreatePassword";
import GetInvolved from "./GetInvolved";
import Representatives from "./Representatives";
import Topics from "./Topics";
import MemberBenefits from "./MemberBenefits";
export const StepHeader = glamorous.div({
marginBottom: 20,
marginTop: 20,
fontSize: "2rem",
color: "#757575"
});
const OnboardingCompleted = glamorous(ThemedCard)({
textAlign: "center",
boxShadow: "none !important"
});
export default {
CreatePassword,
BusinessAddress,
Completed: OnboardingCompleted,
GetInvolved,
MemberBenefits,
Topics,
Representatives
};
Well, I added mine MemberBenefits and it does not seem to work, its not rendering with its corresponding route. Where could it not be registering this new step or component?
Okay so the magic is not happening inside of Onboarding/OnBoardingWizard/index.js, its happening inside of Wizard/WizardEngine.js:
import React from "react";
import PropTypes from "prop-types";
import objectToArray from "../../../../common/utils/object-to-array";
// TODO: figure out how to use this without making children of wizard engine tied to wizardStep
// eslint-disable-next-line no-unused-vars
class WizardStep {
constructor({ component, color, order, render }, stepComponents) {
if (!component || !render) {
throw new Error("Component or render must be provided.");
}
let componentValue;
if (component) {
componentValue = this.resolveComponent(component, stepComponents);
if (!!componentValue && !React.isValidElement(componentValue)) {
throw new Error(
"wizard step expected component to be a valid react element"
);
}
} else if (render && typeof render === "function") {
throw new Error("wizard step expected render to be a function");
}
this.Component = componentValue;
this.color = color;
this.order = order;
this.render = render;
}
resolveComponent = (component, stepComponents) => {
const componentValue = component;
if (typeof component === "string") {
const componentValue = stepComponents[component];
if (!componentValue) {
throw new Error("component doesnt exist");
}
}
return componentValue;
};
}
export default class WizardEngine extends React.Component {
static propTypes = {
steps: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
initActiveIndex: PropTypes.oneOfType([PropTypes.func, PropTypes.number]),
stepComponents: PropTypes.object
};
constructor(props) {
super(props);
this.state = {
activeIndex: this.resolveInitActiveIndex(props),
steps: this.buildStepsFromConfig(props)
};
}
componentWillReceiveProps(nextProps) {
this.setState({ steps: this.buildStepsFromConfig(nextProps) });
}
resolveInitActiveIndex = props => {
const { initActiveIndex } = props;
let activeIndex = 0;
if (typeof initActiveIndex === "function") {
activeIndex = initActiveIndex(props);
}
if (typeof initActiveIndex === "number") {
activeIndex = initActiveIndex;
}
return activeIndex;
};
buildStepsFromConfig = props => {
const { steps } = props;
let stepArr = steps;
// validate stepList
if (typeof steps === "object" && !Array.isArray(steps)) {
stepArr = objectToArray(steps);
}
if (!Array.isArray(stepArr)) {
throw new Error(
`Unsupported Parameter: Wizard Engine(steps) expected either (object, array); got ${typeof stepArr}`
);
}
return stepArr;
// return stepArr.map(step => new WizardStep(step));
};
setActiveIndex = activeIndex => {
this.setState({ activeIndex });
};
goForward = () => {
this.setState(prevState => ({
activeIndex: prevState.activeIndex + 1
}));
};
goBack = () => {
this.setState(prevState => ({
activeIndex: prevState.activeIndex - 1
}));
};
render() {
const { children } = this.props;
const childProps = {
...this.state,
setActiveIndex: this.setActiveIndex,
goForward: this.goForward,
goBack: this.goBack,
currentStep: this.state.steps[this.state.activeIndex]
};
if (Array.isArray(children)) {
return (
<div>
{children.map((child, i) => {
if (typeof child === "function") {
return child(childProps);
}
childProps.key = `${child.type.name}_${i}`;
return React.cloneElement(child, childProps);
})}
</div>
);
}
if (typeof children === "function") {
return children(childProps);
}
return children;
}
}
I think the first method load the element only when it needed.
The second method load all methods everytime. Why to load Home when you are in /Products?
The path URL is being mapped on the backend utilizing the Entity Framework similar to the setup you can view here in this documentation:
https://dzone.com/articles/aspnet-core-crud-with-reactjs-and-entity-framework
except it is being done in Express.
So it's not using React-Router in the traditional sense where Express allows it to control the whole mapping route paths to components, but instead the path to the onboarding component is being mapped here inside the Express src/app-server/apiConfig.js like so:
"get-involved-onboarding": {
title: "Get Involved",
url: "/account/onboarding/get-involved",
icon: "explore",
component: "GetInvolved",
progress: {
stepType: "GetInvolved",
hasCompleted: true
}
},

Lot of repetition in React component

I have a rather large React component that manages the display of a detail for a job on my site.
There are a few things that I would like to do smarter
The component has a few options for opening Dialogs. For each dialog I have a separate Open and Close function. For example handleImageGridShow and handleImageGridClose. Is there any way to be more concise around this?
I have many presentational components (e.g. ViewJobDetails) that shows details about the job. My issue is that I have to pass them down into each Component as a prop and I'm passing the same props over and over again
As I'm loading my data from firebase I often have to do similar checks to see if the data exists before I render the component (e.g.this.state.selectedImageGrid && <ImageGridDialog />). Is there any more clever way of going about this?
import React, { Component } from 'react';
import { withStyles } from 'material-ui/styles';
import ViewJobAttachment from "../../components/jobs/viewJobAttachment";
import ViewJobDetails from "../../components/jobs/viewJob/viewJobDetails";
import ViewJobActions from "../../components/jobs/viewJob/viewJobActions";
import ViewCompanyDetails from "../../components/jobs/viewJob/viewCompanyDetails";
import ViewClientsDetails from "../../components/jobs/viewJob/viewClientsDetails";
import ViewProductsDetails from "../../components/jobs/viewJob/viewProductsDetails";
import ViewAttachmentDetails from "../../components/jobs/viewJob/viewAttachmentDetails";
import ViewEventLogDetails from "../../components/jobs/viewJob/viewEventLogDetails";
import ViewSummaryDetails from "../../components/jobs/viewJob/viewSummary";
import {FirebaseList} from "../../utils/firebase/firebaseList";
import SimpleSnackbar from "../../components/shared/snackbar";
import {calculateTotalPerProduct} from "../../utils/jobsService";
import BasicDialog from "../../components/shared/dialog";
import ImageGrid from "../../components/shared/imageGrid";
import Spinner from "../../components/shared/spinner";
import ViewPinnedImageDialog from "../../components/jobs/viewEntry/viewPinnedImage";
import {
Redirect
} from 'react-router-dom';
const styles = theme => ({
wrapper: {
marginBottom: theme.spacing.unit*2
},
rightElement: {
float: 'right'
}
});
const ImageGridDialog = (props) => {
return (
<BasicDialog open={!!props.selectedImageGrid}
handleRequestClose={props.handleRequestClose}
fullScreen={props.fullScreen}
title={props.title}
>
<ImageGrid selectedUploads={props.selectedImageGrid}
handleClickOpen={props.handleClickOpen}/>
</BasicDialog>
)
};
class ViewJob extends Component {
constructor() {
super();
this.state = {
currentJob: null,
entries: [],
promiseResolved: false,
attachmentDialogOpen: false,
openAttachment: null,
selectedImageGrid: false,
selectedPinnedImage: false,
showSnackbar: false,
snackbarMsg: '',
markedImageLoaded: false,
loading: true,
redirect: false
};
this.firebase = new FirebaseList('jobs');
this.handleJobStatusChange = this.handleJobStatusChange.bind(this);
this.handleImageGridShow = this.handleImageGridShow.bind(this);
this.handleImageGridClose = this.handleImageGridClose.bind(this);
this.handlePinnedImageClose = this.handlePinnedImageClose.bind(this);
this.handlePinnedImageShow = this.handlePinnedImageShow.bind(this);
this.handleMarkedImageLoaded = this.handleMarkedImageLoaded.bind(this);
this.handleRemove = this.handleRemove.bind(this);
this.pushLiveToClient = this.pushLiveToClient.bind(this);
}
componentDidMount() {
this.firebase.db().ref(`jobs/${this.props.id}`).on('value', (snap) => {
const job = {
id: snap.key,
...snap.val()
};
this.setState({
currentJob: job,
loading: false
})
});
const previousEntries = this.state.entries;
this.firebase.db().ref(`entries/${this.props.id}`).on('child_added', snap => {
previousEntries.push({
id: snap.key,
...snap.val()
});
this.setState({
entries: previousEntries
})
});
}
handleRemove() {
this.firebase.remove(this.props.id)
.then(() => {
this.setState({redirect: true})
})
};
pushLiveToClient() {
const updatedJob = {
...this.state.currentJob,
'lastPushedToClient': Date.now()
};
this.firebase.update(this.state.currentJob.id, updatedJob)
.then(() => this.handleSnackbarShow("Job pushed live to client"))
}
handleJobStatusChange() {
const newState = !this.state.currentJob.completed;
const updatedJob = {
...this.state.currentJob,
'completed': newState
};
this.firebase.update(this.state.currentJob.id, updatedJob)
}
handleSnackbarShow = (msg) => {
this.setState({
showSnackbar: true,
snackbarMsg: msg
});
};
handleSnackbarClose= (event, reason) => {
if (reason === 'clickaway') {
return;
}
this.setState({ showSnackbar: false });
};
handleAttachmentDialogClose =() => {
this.setState({attachmentDialogOpen: false})
};
handleClickOpen = (file) => {
this.setState({
attachmentDialogOpen: true,
openAttachment: file
});
};
handleImageGridShow(imageGrid) {
this.setState({selectedImageGrid: imageGrid})
}
handleImageGridClose() {
this.setState({selectedImageGrid: false})
}
handlePinnedImageShow(pinnedImage) {
this.setState({selectedPinnedImage: pinnedImage})
}
handlePinnedImageClose() {
this.setState({selectedPinnedImage: false})
}
handleMarkedImageLoaded() {
this.setState({markedImageLoaded: true})
}
render() {
const {classes} = this.props;
let {_, costPerItem} = calculateTotalPerProduct(this.state.entries);
if (this.state.redirect) {
return <Redirect to='/jobs' push/>
} else {
if (this.state.loading) {
return <Spinner/>
} else {
return (
<div className={styles.wrapper}>
{this.state.currentJob &&
<div>
<ViewJobActions currentJob={this.state.currentJob}
handleJobStatusChange={this.handleJobStatusChange}
pushLiveToClient={this.pushLiveToClient}
/>
<ViewJobDetails currentJob={this.state.currentJob}/>
<ViewCompanyDetails currentJob={this.state.currentJob}/>
<ViewClientsDetails currentJob={this.state.currentJob}/>
<ViewProductsDetails currentJob={this.state.currentJob}/>
{this.state.currentJob.selectedUploads && this.state.currentJob.selectedUploads.length > 0
? <ViewAttachmentDetails currentJob={this.state.currentJob} handleClickOpen={this.handleClickOpen}/>
: null}
<ViewEventLogDetails jobId={this.state.currentJob.jobId}
jobKey={this.state.currentJob.id}
entries={this.state.entries}
handlePinnedImageShow={this.handlePinnedImageShow}
handleImageGridShow={this.handleImageGridShow}/>
<ViewSummaryDetails stats={costPerItem}/>
<ViewJobAttachment open={this.state.attachmentDialogOpen}
handleRequestClose={this.handleAttachmentDialogClose}
attachment={this.state.openAttachment}
/>
{this.state.selectedImageGrid &&
<ImageGridDialog selectedImageGrid={this.state.selectedImageGrid}
handleRequestClose={this.handleImageGridClose}
handleClickOpen={this.handleClickOpen}
title="Pictures for job"
fullScreen={false}/>}
{this.state.selectedPinnedImage &&
<ViewPinnedImageDialog attachment={this.state.selectedPinnedImage}
open={!!this.state.selectedPinnedImage}
markedImageLoaded={this.state.markedImageLoaded}
handleMarkedImageLoaded={this.handleMarkedImageLoaded}
handleRequestClose={this.handlePinnedImageClose}
otherMarkedEntries={this.state.entries}
/>
}
<SimpleSnackbar showSnackbar={this.state.showSnackbar}
handleSnackbarClose={this.handleSnackbarClose}
snackbarMsg={this.state.snackbarMsg}/>
</div>}
</div>
);
}
}
}
}
export default withStyles(styles)(ViewJob);
You can define a regular component method and bind it in handler like this onSomething={this.handler.bind(this, index)} assuming you have some distinguishable thing in the index var
function should look like this
handler(index) {
...
}

Dynamic React-Router 1.0.0-rc3

Regardless of what I have read from the folks at react-router, I
prefer my app's router have dynamic data, and I have been successful
in doing it, with one caveat: I cannot loop recursively for child
routes.
Here is a working dynamic react-router:
export default class Index extends React.Component {
constructor() {
super();
this.state = {
navItems: [] };
}
componentWillMount() {
NavMenuAPI.getAll()
.then((response) => {
const data = response.data;
this.setState({ navItems: data });
});
}
fetchMenuSystem(data) {
const routes = [];
data.map(function(route) {
let routePaths = [];
let component = {};
if (route.linkTo === '/') {
const pageApp = route.title.replace(/ /g, '-').toLocaleLowerCase();
component = require('./components/pages/' + pageApp);
} else {
const pageApp = route.title.replace(/ /g, '-').toLocaleLowerCase();
component = require('./components/pages/' + pageApp);
}
if (route.paths === undefined) {
routePaths.push(route.linkTo);
} else {
routePaths = JSON.parse(JSON.stringify(route.paths));
}
routePaths.map(function(path) {
const props = { key: path, path, component };
// Static `onEnter` is defined on
// component, we should pass it to route props
// if (component.onEnter) props.onEnter = component.onEnter;
routes.push(<Route { ...props } />);
});
//////////////////////////////////////////////
// PROBLEM !!!!!!!!!!
// if (route.childNodes !== undefined) {
// this.fetchMenuSystem(route.childNodes);
// }
});
return routes;
}
fetchRoutes() {
const data = this.state.navItems;
const result = this.fetchMenuSystem(data);
return (
<Route component={ require('./components/APP') }>
{ result }
<Route path="*" component={ require('./components/pages/not-found') }/>
</Route>
);
}
render() {
if (this.state.navItems.length === 0) return <div>Loading ...</div>;
const routerProps = {
routes: this.fetchRoutes(),
history: createHistory({
queryKey: false
}),
createElement: (component, props) => {
return React.createElement(component, { ...props });
}
};
return (
<Router { ...routerProps } />
);
}
}
ReactDOM.render(<Index />, document.getElementById('reactRoot'));
As I said, this works, however, only for the first level, when I try to recursively loop through for any childNodes, I receive the error:
TypeError: Cannot read property 'fetchMenuSystem' of undefined
I tried to bind the call to the fetch function and bind the mapping, none of which worked.
I would greatly appreciate assistance on this.
OK, I solved this. The problem lied with the routes state & the 'this' keyword.
Here are the changes:
export default class Index extends React.Component {
constructor() {
super();
this.state = {
navItems: [],
routes: []
};
}
................
fetchMenuSystem(data) {
const currRoutesState = this.state.routes;
const self = this;
data.map(function(route) {
................
routePaths.map(function(path) {
................
currRoutesState.push(<Route { ...props } />);
});
if (route.childNodes !== undefined) {
self.fetchMenuSystem(route.childNodes);
}
});
return currRoutesState;
}
................
}
Final thoughts if you plan on using this:
[Re: npm install --save history]
I had to opt for 100% client side routing,
(not isomorphic/universal) so in my imports ....
import createHistory from 'history/lib/createHashHistory';
// import createBrowserHistory from 'history/lib/createBrowserHistory';

Recursive data & components, later fetches throwing an error

First off my graphql data model:
type Human {
id: !String,
name: !String,
children: [Human]
}
The only route (relay route config) I'm atm using:
class extends Relay.Route {
static queries = {
human: () => Relay.QL`query RootQuery { viewer }`
};
static routeName = 'AppHomeRoute';
}
The list component:
class HumanList extends Component {
render() {
let {children} = this.props.human;
let subListsHTML = human ? children.map(child => (
<HumanListItem key={child.id} human={child}/>
)) : '';
return <ul>{subListsHTML}</ul>;
}
}
export default Relay.createContainer(HumanList, {
fragments: {
human: () => Relay.QL`
fragment on Human {
children {
id,
${HumanListItem.getFragment('human')}
}
}
`
}
});
The list item component:
class HumanListItem extends Component {
state = {expanded: false};
render() {
let {human} = this.props;
let sublistHTML = '';
if (this.state.expanded) {
sublistHTML = <ul><HumanList human={human}/></ul>;
}
return (
<li>
<div onClick={this.onClickHead.bind(this)}>{human.name}</div>
{sublistHTML}
</li>
);
}
onClickHead() {
this.props.relay.setVariables({expanded: true});
this.setState({expanded: true});
}
}
HumanListItem.defaultProps = {viewer: {}};
export default Relay.createContainer(HumanListItem, {
initialVariables: {
expanded: false
},
fragments: {
human: (variables) => Relay.QL`
fragment on Human {
name,
${HumanList.getFragment('human').if(variables.expanded)}
}
`
}
});
Which runs fine for the root list. But as soon as I click on a ListItem and it is expanded, I get the following error:
Warning: RelayContainer: Expected prop 'human' supplied 'HumanList' to be data fetched by Relay. This is likely an error unless you are purposely passing in mock data that conforms to the shape of this component's fragment.
I can't make much sense of it, since the data I'm passing is not mocked but directly fetched by Relay as can be seen in the HumanList comp.
The error indicates that the <HumanList> is being rendered before its data is ready.
class HumanListItem extends Component {
onClickHead() {
this.props.relay.setVariables({expanded: true});
this.setState({expanded: true}); // <-- this causes the component to re-render before data is ready
}
Rather than using state, you can instead look at the current value of the variables:
class HumanListItem extends Component {
// no need for `state.expanded`
render() {
let {human} = this.props;
let sublistHTML = '';
if (this.props.relay.variables.expanded) {
// `variables` are the *currently fetched* data
// if `variables.expanded` is true, expanded data is fetched
sublistHTML = <ul><HumanList human={human}/></ul>;
}
return (
<li>
<div onClick={this.onClickHead.bind(this)}>{human.name}</div>
{sublistHTML}
</li>
);
}
onClickHead() {
this.props.relay.setVariables({expanded: true});
// no need for `setState()`
}
}
HumanListItem.defaultProps = {viewer: {}};
export default Relay.createContainer(HumanListItem, {
initialVariables: {
expanded: false
},
fragments: {
human: (variables) => Relay.QL`
fragment on Human {
name,
${HumanList.getFragment('human').if(variables.expanded)}
}
`
}
});

Resources