Reactjs controlling state in parent from grand child - javascript

I have at my top level:
import React from 'react';
import JobList from './JobList';
import RightPanel from './RightPanel';
import JobStore from '../../stores/JobStore';
import LoadJobsScreen from '../../actions/jobs-screen/LoadJobsScreen';
import Modal from '../global/Modal';
export default class JobScreen extends React.Component {
static contextTypes = {
executeAction: React.PropTypes.func.isRequired
};
componentWillMount() {
this.toggleModal = this.toggleModal.bind(this);
this.state = {open: false}
this.context.executeAction(LoadJobsScreen, this);
}
toggleModal() {
this.setState({
open: !this.state.open
});
console.log(this.state.open);
}
render() {
return (
<div className="jobs-screen">
<div className="col-xs-12 col-sm-10 job-list"><JobList /></div>
<div className="col-xs-12 col-sm-2 panel-container">
<div className="right-panel pull-right"><RightPanel /></div>
</div>
<Modal open={this.state.open} toggleModal={this.toggleModal} />
</div>
);
}
}
Modal is:
import React from 'react';
class Modal extends React.Component {
constructor() {
super();
}
render() {
let open = this.props.open;
return (
<div className={'modal fade'+(open ? '' : ' hide')} tabindex="-1" role="dialog">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
<h4 className="modal-title">{this.props.title}</h4>
</div>
<div className="modal-body">
{this.props.children}
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" className="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
)
}
}
export default Modal;
But I want to open and close it (as well as send data to it later) from within a deeper component:
import React from 'react';
import UrgencyToggle from './UrgencyToggle';
import ApproveButton from './ApproveButton';
import ShippingTable from './ShippingTable';
import DropdownButtonList from '../global/DropdownButtonList';
export default class Job extends React.Component {
constructor(props) {
super(props);
}
setUrgency(urgency) {
actionContext.dispatch('SET_JOB_URGENCY', {
data: urgency
})
};
render() {
return ( <
span className = "name" > < img src = "/images/system-icons/pencil.png"
onClick = {
this.toggleModal
}
width = "13" / > < /span>
)
}
};
Obviously this doesn't work because toggleModal is all the way up in JobScreen. How can I execute the function in the grandparent from this depth?

If your JobScreen, JobList, Joband Modal components are designed to be tightly coupled, i.e not meant to be separated from each other in future, you could use the JobScreen as a High Order Component to store the state of your modal and passing down the tree as prop a callback function to update this state (I simplified a bit and made some assumptions on missing components) :
export default class JobScreen extends React.Component {
constructor(props) {
super(props);
this.displayName = 'JobScreen'
this.state = {
modalOpened: false,
modalTitle: "",
}
}
componentWillMount() {
this.context.executeAction(LoadJobsScreen, this);
}
toggleModal() {
this.setState({
modalOpened: !this.state.modalOpened
});
}
editModalTitle(title) {
this.setState({
modalTitle: title
})
}
render() {
return (
<div className="jobs-screen">
<div className="col-xs-12 col-sm-10 job-list">
<JobList
toggleModal={() => this.toggleModal() /* auto binding with arrow func */}
editModalTitle={(title) => this.editModalTitle(title)} />
</div>
<Modal
open={this.state.modalOpened}
title={this.state.modalTitle}/>
</div>
);
}
}
const JobList = (props) => {
const jobs = [1,2,3]
return (
<ul>
{jobs.map(key => (
<li key={key}>
<Job
toggleModal={props.toggleModal}
editModalTitle={props.editModalTitle}/>
</li>
))}
</ul>
);
}
const Job = (props) => {
return (
<span className="name">
<img
src="/images/system-icons/pencil.png"
width="13"
onClick={(e) => {
props.toggleModal(e)
props.editModalTitle("new title") //not very efficient here cause we're updating state twice instead of once, but it's just for the sake of the example
}}/>
</span>
);
}
I deliberately not mentions how to modify the modal children this way cause it's an absolute anti-pattern. So, you should definitively look at something like Redux which provides a way to manage the state of your application and dispatch action from wherever you want to update in a 'one way data binding' way. I've the impression you're trying to bypass the React internal mechanisms by using context as an action dispatcher. So, Redux (or another Flux library) will be your best bet here.

Related

Change react-modal data dynamically

I have a Parent component, App.js and a Child component, MealModal.js. When a user click on a specific meal card, it raises a modal that should display further information about the meal.
Hence I try to find a way to dynamically change the modals's data, depending on which meal card is clicked
I have tried to pass the id of the meal to the onClick={this.openModal} function and set the state of modalId is the function. But I got the following error:
Warning: Cannot update during an existing state transition (such as
within render or another component's constructor). Render methods
should be a pure function of props and state; constructor side-effects
are an anti-pattern, but can be moved to 'componentWillMount'.
Here are my components so far:
App.js:
import React from 'react';
import MealCard from './MealCard';
import MealsMap from './MealsMap';
import MealsFilters from './MealsFilters';
import MealModal from './MealModal';
export default class App extends React.Component {
constructor() {
super();
this.state = {
modalIsOpen: false,
modalId: 0
}
this.openModal = this.openModal.bind(this);
this.closeModal = this.closeModal.bind(this);
};
openModal() {
this.setState({modalIsOpen: true});
};
closeModal() {
this.setState({modalIsOpen: false});
};
render() {
return (
<div>
<MealsFilters/>
<div className="app-wrapper" style={{display: 'flex'}}>
<div className="container">
<div className="row">
{[...Array(20)].map((x, i) =>
<div className="col-sm-6 col-xs-12 " key={i} onClick={this.openModal}>
<MealCard />
</div>
)}
</div>
</div>
<MealsMap/>
</div>
<MealModal modalIsOpen={this.state.modalIsOpen} closeModal={this.closeModal} modalId={this.state.modalId}/>
</div>
);
}
}
MealModal.js
import React from 'react';
import Modal from 'react-modal';
const customStyles = {
content : {
}
};
Modal.setAppElement('#app')
export default class MealModal extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Modal
isOpen={this.props.modalIsOpen}
onRequestClose={this.props.closeModal}
style={customStyles}
contentLabel="Meal Modal"
>
<div className="modal-wrapper">
<div className="container text-center">
<h1>Hello</h1>
<h2>ID of this modal is {this.props.modalId}</h2>
<h3>This is an awesome modal.</h3>
<button onClick={this.props.closeModal}>close</button>
</div>
</div>
</Modal>
)
}
}
Any idea on how I could do this ?
Ok so I found the solution:
First, I changed onClick={this.openModal} in the parent comoponent to onClick= () => {this.openModal}
Second, I add the id as a parameter:
onClick= () => {this.openModal(i)}
Finally: update the openModal function:
openModal(modalId) {
this.setState({modalIsOpen: true,
modalId});
};
And it works.
openModal(modalId) {
this.setState({
modalId,
modalIsOpen: true
});
};
and modify the call function as
<div className="col-sm-6 col-xs-12" key={i} onClick={() => this.openModal(x) } >
<MealCard/>
</div>

Converting stateless to a class component react js

I have a component here that fills up a data for movies:
const MovieList = (props) => {
const movieItems = props.movies.map((movie) => {
return <MovieListItem key={movie._id} movie={movie} />
});
return(
<div className="container">
<h2 className="latest">MOVIE RESULTS</h2>
<div className="row">
{ movieItems }
</div>
</div>
);
};
Basically, I was planning to convert my next component where I am filling up my movieItems to a class component. This class has some helpers function along with a new function onClickImgMoreBtn whenever it was click it must console log something. But it doesnt work any more when I tried to convert it class component. Even my helper classes wont work either.
Here's my original stateless function:
import React from 'react';
const helper = require('../../helpers/helper_movies')
const MovieListItem = ({movie}) => {
return (
<div>
{helper.mappingComma(movie.genre)}
{helper.getExcerpt(movie.overview)}
<button className="btn btn-secondary float-right btn-sm" href={movie._id} onClick={this.onClickImgMoreBtn}>MORE</button>
</div>
);
};
export default MovieListItem;
And here's how i change it along with the new function:
import React, { Component } from 'react';
const helper = require('../../helpers/helper_movies');
class MovieListItem extends Component{
constructor(props){
super(props);
}
onClickImgMoreBtn(){
console.log('hi there!');
}
render(){
return (
<div>
{helper.mappingComma(this.props.genre)}
{helper.getExcerpt(this.props.overview)}
<button className="btn btn-secondary float-right btn-sm" href={this.props._id} onClick={this.onClickImgMoreBtn}>MORE</button>
</div>
);
}
}
export default MovieListItem;
Any idea what am i missing? did i properly converted it?
In your stateless component you destructure movie from the props and use movie.genre, movie._id and movie.overview and hence in the component you need to use
this.props.movie.genre, this.props.movie._id and this.props.movie.overview
render(){
return (
<div>
{helper.mappingComma(this.props.movie.genre)}
{helper.getExcerpt(this.props.movie.overview)}
<button className="btn btn-secondary float-right btn-sm" href={this.props.movie._id} onClick={this.onClickImgMoreBtn}>MORE</button>
</div>
);
}
or better, destructure movie form props in render
render(){
const { movie } = this.props;
return (
<div>
{helper.mappingComma(movie.genre)}
{helper.getExcerpt(movie.overview)}
<button className="btn btn-secondary float-right btn-sm" href={movie._id} onClick={this.onClickImgMoreBtn}>MORE</button>
</div>
);
}
Also since you are not doing anything in the constructor, you need to have the constructor defined.

React Help Needed - Components not updating when index increases

I cannot get the component displayed to update when the index increases. I am able to console the proper component now but because the onClick is below the component that needs to update, it isn't changing. Can someone help me fix my code? i think I am close but cannot figure it out for the life of me.
This sign up page is where I would like to update the component. Essentially I want to display each component in the array once the next button is clicked. Currently the function console logs everything as I want it to, it's just a matter of getting it to appear in the
it is returning an error "cannot read property 'count' of null":
import React from 'react';
import Q1Name from './questions/Q1Name';
import Q2Birthday from './questions/Q2Birthday';
import Q3City from './questions/Q3City';
import Q4YouReady from './questions/Q4YouReady';
import Q5Setting from './questions/Q5Setting';
import Q6Length from './questions/Q6Length';
import Q7Email from './questions/Q7Email';
class SignUpPage extends React.Component {
constructor(props) {
super(props);
this.state = {
i: 0
}
}
_handleClick() {
const components = [Q1Name, Q2Birthday, Q3City, Q4YouReady, Q5Setting, Q6Length, Q7Email];
if(this.state.i < components.length) this.setState({ i : this.state.i + 1});
}
// handleIncrement() {
// this.setState({ count: this.state.count + 1});
// }}
render() {
const components = [Q1Name, Q2Birthday, Q3City, Q4YouReady, Q5Setting, Q6Length, Q7Email];
const componentsToRender = components.map((Component, i) => (
<Component key={i} />
));
return (
<div className = "container-fluid signup-page">
<div className = "question-box">
{componentsToRender[this.state.i]}
<button type="submit" className="btn btn-custom btn-lg" onClick={() => this._handleClick}>Next Question!</button>
</div>
</div>
);
}
}
export default SignUpPage;
There are a few component types I am bringing in, age, birthday, email, and a few button clicks, etc.
import React from 'react';
class Q1Name extends React.Component {
handleSubmit(event) {
event.preventDefault();
this.props.onNext();
}
render() {
return (
<div className="questions q1" style={this.props.style}>
<h1 id="question-h1">What is your name?</h1>
<form>
<div className="form-group">
<input type="name" className="form-control text-form custom-form" id="nameInput" aria-describedby="name" placeholder="" />
</div>
{/* <button type="submit" className="btn btn-custom btn-lg" onSubmit={this.handleSubmit}>Next Question!</button> */}
</form>
</div>
);
}
}
export default Q1Name;
Here is an example of the button option component:
import React from 'react';
class Q5Setting extends React.Component {
render() {
return (
<div className="questions">
<h1 id="question-h1">What is your ideal setting?</h1>
<button type="button" className="btn btn-custom-select btn-lg">Take me to the beach!</button>
<button type="button" className="btn btn-custom-select btn-lg">Anywhere outdoors!</button>
<button type="button" className="btn btn-custom-select btn-lg">All about the city!</button>
</div>
);
}
}
export default Q5Setting;
Any help in figuring this out would be greatly appreciated!!
In your constructor initialise state
constructor(props) {
super(props)
this.state = { i: 0 }
}
Write helper method handleClick
_handleClick() {
if(this.state.i < components.length) this.setState({ i : this.state.i + 1});
}
Now reference componentsToRender using i in state
`componentsToRender[this.state.i]
Don't forget to call your helper function on click.
onClick = {() => this._handleClick()}
The idea is your app will only re-render when your state object changes. Follow that rule for your components you wish to re-erender on the fry.

React js: Accessing state of other components

I have a component built using the below code. The aim is to add a class on the card to highlight it when the button inside it is clicked. However, the below code works on the first click but doesn't work for the subsequent clicks.
I understood that I have to set the clicked state of other elements to false when I remove the class. How can this be done?
import React, { Component } from 'react';
import './PricingCard.css';
class PricingCard extends Component {
constructor(){
super();
this.state = {
clicked : false
}
}
makeSelection(){
let elems = document.getElementsByClassName('Card');
for(var i=0;i<elems.length;i++){
elems[i].classList.remove("active");
}
this.setState({clicked: true});
}
render() {
var activeClass = this.state.clicked ? 'active' : '';
return (
<div className= {"categoryItem Card " + this.props.planName + " " +activeClass}>
<div className="cardDetails">
<div> {this.props.planName} </div>
<div className="pricing"> {this.props.price} </div>
<button onClick={this.makeSelection.bind(this)} className="buttonPrimary"> Select this plan </button>
<div className="subtitle"> {this.props.footerText} </div>
</div>
</div>
);
}
}
export default PricingCard;
Wouldn't it be easier to have the logic in a parent component? Since it is "aware" of all the child Card components.
Have something like...
this.state = { selectedComponent: null };
onClick(card_id) {
this.setState({ selectedComponent: card_id });
}
...in render:
const cards = smth.map((card) =>
<Card onClick={this.onClick.bind(this, card.id)}
isActive={map.id === this.state.selectedComponent} />
Would this work?
Best way will be to lift lift the state up. Like this:
class PricingCardContainer extends React.Component {
constructor(props){
super(props);
this.state = {
selectedCard: NaN,
}
}
handleCardClick(selectedCard){ this.setState({ selectedCard }); }
render() {
return (
<div>{
this.props.dataArray.map((data, i) =>
<PricingCard
key={i}
className={this.state.selectedCard === i ? 'active': ''}
price={data.price}
onClick={() => this.handleCardClick(i)}
footerText={data.footerText}
planName={data.planName}
plan={data.plan}
/>
)
}</div>
)
}
}
const PricingCard = ({ className = '', planName, price, onClick, footerText }) => (
<div className= {`categoryItem Card ${planName} ${className}`}>
<div className="cardDetails">
<div> {planName} </div>
<div className="pricing"> {price} </div>
<button onClick={onClick} className="buttonPrimary"> Select this plan </button>
<div className="subtitle"> {footerText} </div>
</div>
</div>
);
export default PricingCard;
Although it would be better to use some data id than index value.

Rendering A List in React

I am creating a blog using React and Firebase. I have a component called Blogger that creates blog posts and then saves them in firebase. Now, I am trying to render a list of all of the blog posts that have been saved into firebase. I can't seem to get a list to render.
So far, I have created a parent component called Blogger and a child component called List. I want List to render a list of blog post titles within the Blogger component. I've passed the prop title to List like this
What am I doing wrong?
My stack is webpack + React + React Router + Flux + Firebase
I get this error:
The error message I recieve
This is my parent object in which the list is created:
import AltContainer from 'alt-container';
import React from 'react';
import { Link } from 'react-router';
import List from './List.jsx'
import Firebase from 'firebase'
const rootURL = 'https://incandescent-fire-6143.firebaseio.com/';
export default class Blogger extends React.Component {
constructor(props) {
super(props);
this.firebaseRef = new Firebase(rootURL + 'items/');
this.state = {
title: '',
text: ''
};
this.firebaseRef.on('value', function(snapshot) {
console.log(snapshot.val());
});
}
handleInputChange = () => {
this.setState({
title: this.refs.title.value,
text: this.refs.text.value});
}
handleClick = () => {
this.firebaseRef.push({
title: this.state.title,
text: this.state.text,
done: false
})
this.setState({title: '',
text: ''
});
}
render() {
return (
<div>
<div className="row panel panel-default">
<div className="col-md-8 col-md-offset-2">
<h2>
Create a New Blog Post
</h2>
</div>
</div>
<h2>Blog Title</h2>
<div className="input-group">
<input
ref="title"
value={this.state.title}
onChange = {this.handleInputChange}
type="text"
className="form-control"/>
<span className="input-group-btn">
</span>
</div>
<h2>Blog Entry</h2>
<div className="input-group">
<textarea
ref="text"
value={this.state.text}
onChange = {this.handleInputChange}
type="text"
className="form-control"/>
</div>
<div className="blog-submit input-group-btn">
<button onClick={this.handleClick}
className="btn btn-default" type="button">
Publish Blog Post
</button>
</div>
<List title={this.state.title} />
</div>
);
}
}
This is the child object to which I want to pass the props:
import AltContainer from 'alt-container';
import React from 'react';
import { Link } from 'react-router';
import Blogger from './Blogger'
export default class List extends React.Component {
constructor(props) {
super(props);
console.log(Object.keys(this.props.title));
}
render: () => {
return (
if(this.props.title && Object.keys(this.props.title).length === 0) {
return <h4>enter a blog entry to get started</h4>
} else {
var children = [];
for(var key in this.props.title) {
children.push(
<li>
{title.text}
</li>
)
}
}
);
}
}
The error in your screenshot is quite clear. It's a syntax error.
The following is not legal JavaScript:
function foo () {
return ( if (true) return 'hello )
}
Nesting return statements like this will crash.
The pattern you are looking for is more like this:
function foo () {
if (cond) {
return <List />
}
return <SomethingElse />
}
Additionally the way you are writing render is incorrect. Class functions should just be:
render() {
// return stuff
}
Finally your render method should something like this:
render() {
if (this.props.title && Object.keys(this.props.title).length === 0) {
return <h4>enter a blog entry to get started</h4>
}
return Object.keys(this.props.title).map(key =>
<li>{this.props.title[key]}</li>
)
}
Here is the solution
Blogger.jsx
import AltContainer from 'alt-container';
import React from 'react';
import { Link } from 'react-router';
import List from './List.jsx'
import Firebase from 'firebase'
const rootURL = 'https://incandescent-fire-6143.firebaseio.com/';
export default class Blogger extends React.Component {
constructor(props) {
super(props);
this.firebaseRef = new Firebase(rootURL + 'items/');
this.state = {
title: '',
text: ''
};
this.firebaseRef.on('value', function(snapshot) {
console.log(snapshot.val());
});
}
handleInputChange = () => {
this.setState({
title: this.refs.title.value,
text: this.refs.text.value});
}
handleClick = () => {
this.firebaseRef.push({
title: this.state.title,
text: this.state.text,
done: false
})
this.setState({title: '',
text: ''
});
}
render() {
return (
<div>
<div className="row panel panel-default">
<div className="col-md-8 col-md-offset-2">
<h2>
Create a New Blog Post
</h2>
</div>
</div>
<h2>Blog Title</h2>
<div className="input-group">
<input
ref="title"
value={this.state.title}
onChange = {this.handleInputChange}
type="text"
className="form-control"/>
<span className="input-group-btn">
</span>
</div>
<h2>Blog Entry</h2>
<div className="input-group">
<textarea
ref="text"
value={this.state.text}
onChange = {this.handleInputChange}
type="text"
className="form-control"/>
</div>
<div className="blog-submit input-group-btn">
<button onClick={this.handleClick}
className="btn btn-default" type="button">
Publish Blog Post
</button>
</div>
<List title={this.state.title} />
</div>
);
}
}
List.jsx
import AltContainer from 'alt-container';
import React from 'react';
import { Link } from 'react-router';
import Blogger from './Blogger'
export default class List extends React.Component {
constructor(props) {
super(props);
// console.log(this.props.blog);
}
render() {
console.log(this.props.blog)
// Object.keys(this.props.title[key]).map(key) =>
// <li>{this.props.title}</li>
// )
return (
<div>
<li>{this.props.blog.title}</li>
</div>
)
}
}

Categories