React.js insert into ref of another component - javascript

I'm starting to learn react.js, and I've face a situation that I can't get an answer.
This is my components structure(this is just to show hierarchy):
-- ItemContainer
----- ItemList
-------- ItemSingle
----- ItemForm
Where ItemList and ItemForm are siblings, chilren of ItemContainer. And ItemSingle is a child from ItemList.
On each ItemSingle I have a "edit" button, that should update the form with its content, but I'm not able to send from ItemSingle to ItemForm.refs.
This is what I've tried
ItemSingle Component:
var ItemSingle = React.createClass({
insertEdit: function(edit_item, e){
e.preventDefault();
React.findDOMNode(ItemForm.refs.title).value = edit_item.title;
React.findDOMNode(ItemForm.refs.content).value = edit_item.content;
},
render: function() {
return (
<div className="box">
<div className="box-body bigger-box">
<h4>
<strong>#{this.props.index + 1}</strong>
<span className="title">{this.props.title}</span>
<span className="float-right box-option">
<a href="#" onClick={this.insertEdit.bind(this, this.props)}>Edit</a>
</span>
</h4>
<div className="text" dangerouslySetInnerHTML={{__html: this.props.content}} />
</div>
</div>
);
}
});
ItemForm Component:
var ItemForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var title = React.findDOMNode(this.refs.title).value.trim();
var content = React.findDOMNode(this.refs.content).value.trim();
if(!title || !content){ return false; }
this.props.onItemsAdd({title: title, content: content});
React.findDOMNode(this.refs.title).value = '';
React.findDOMNode(this.refs.content).value = '';
$('textarea').trumbowyg('empty');
return true;
},
componentDidMount: function() {
$('textarea').trumbowyg();
},
render: function() {
return (
<div className="form-container">
<form className="form" method="POST">
<div className="form-group">
<input type="text" className="form-control" ref="title"/>
</div>
<div className="form-group">
<textarea ref="content"></textarea>
</div>
<div className="form-group">
<a className="btn btn-success btn-flat btn-block btn-lg" onClick={this.handleSubmit}><i className="fa fa-save"></i> Save</a>
</div>
</form>
</div>
);
}
});
And this is the error I got on ItemSingle line number 4:
Uncaught TypeError: Cannot read property 'title' of undefined

Refs in React are not meant to be used like this.
If you have not programmed several apps with React, your first inclination is usually going to be to try to use refs to "make things happen" in your app. If this is the case, take a moment and think more critically about where state should be owned in the component hierarchy. Often, it becomes clear that the proper place to "own" that state is at a higher level in the hierarchy. Placing the state there often eliminates any desire to use refs to "make things happen" – instead, the data flow will usually accomplish your goal.
In your app you will need state, probably in ItemList component, and methods to change this state should be passed to the ItemSingle components.
https://facebook.github.io/react/docs/more-about-refs.html

Related

With React, what is the best way to pass a useState array back and forth between components?

I had some help with a previous issue with my little project, but I have a new problem I can't seem to understand. My program takes an array of objects (call them cards), and displays an on-screen card for each element in the array. I have an edit button for each card, which should open the edit form for the chosen item, and pre-populate it with its current state - this all works.
I want to be able to edit the item, save it back in place into the array, and have that 'card' updated. This is the main component:
import React from "react";
import ReactFitText from "react-fittext";
import Editform from "./Editform";
function Displaycards({ lastid }) {
// dummy data hardcoded for now
const [cards, setCards] = React.useState([
{
id: 1,
gamename: "El Dorado",
maxplayers: 4,
freespaces: 1,
tablenum: 5,
},
{
id: 2,
gamename: "Ticket to Ride",
maxplayers: 4,
freespaces: 2,
tablenum: 3,
},
]);
const [showForm, setShowForm] = React.useState(false);
const [currentid, setCurrentid] = React.useState(0);
return (
<div className="cardwrapper">
{cards.map(({ id, gamename, maxplayers, freespaces, tablenum }) => {
return (
<div key={id}>
<div>
<div className="card">
<ReactFitText compressor={0.8}>
<div className="gamename">{gamename}</div>
</ReactFitText>
<div className="details">
<p>Setup for: </p>
<p className="bignumbers">{maxplayers}</p>
</div>
<div className="details">
<p>Spaces free:</p>
<p className="bignumbers">{freespaces}</p>
</div>
<div className="details">
<p>Table #</p>
<p className="bignumbers">{tablenum}</p>
</div>
<button type="button" className="playbutton">
I want to play
</button>
<br />
</div>
<div className="editbuttons">
<button
type="button"
className="editbutton"
onClick={() => {
setShowForm(!showForm);
setCurrentid(id);
}}
>
Edit
</button>
<button type="button" className="delbutton">
X
</button>
</div>
</div>
</div>
);
})}
{showForm && (
<div>
<Editform cards={cards} setCards={setCards} id={currentid} />
</div>
)}
</div>
);
}
export default Displaycards;
and this is the Editform.js which it calls at the bottom. As far as I can tell I'm passing my array, setter function, and id of the card I want to edit:
function Editform({ cards, setCards, id }) {
const thisCard = cards.filter((card) => card.id === id)[0];
const editThisCard = thisCard.id === id; // trying to match id of passed card to correct card in 'cards' array.
console.log(editThisCard);
function saveChanges(cardtochange) {
setCards(
cards.map(
(
card // intention is map back over the original array, and if the id matches that
) =>
card.id === id // of the edited card, write the changed card back in at its ID
? {
id: id,
gamename: cardtochange.gamename,
maxplayers: cardtochange.maxplayers,
freespaces: cardtochange.freespaces,
tablenum: cardtochange.tablenum,
}
: card // ... or just write the original back in place.
)
);
}
return (
<>
{editThisCard && ( // should only render if editThisCard is true.
<div className="form">
<p>Name of game:</p>
<input type="text" value={thisCard.gamename}></input>
<p>Max players: </p>
<input type="text" value={thisCard.maxplayers}></input>
<p>Free spaces: </p>
<input type="text" value={thisCard.freespaces}></input>
<p>Table #: </p>
<input type="text" value={thisCard.tablenum}></input>
<p></p>
<button
type="button"
className="playbutton"
onClick={saveChanges(thisCard)} //remove to see edit form - leave in for perpetual loop.
>
Save changes
</button>
</div>
)}
</>
);
}
export default Editform;
If I comment out the onClick for the button, the page renders. If it's in there, the page gets stuck in an infinite loop that even React doesn't catch.
The way I'm trying to recreate my array is based on advice I've read here, when searching, which said to take the original array, rebuild it item-for-item unless the ID matched the one I want to change, and to then write the new version in.
I suspect there might be a way to do it with my setter function (setCards), and I know there's an onChange available in React, but I don't know a) how to get it to work, or b) how - or even if I need to - pass the changed array back to the calling component.
Your function is invoked directly upon components render:
{saveChanges(thisCard)}
Rename it to a callback style signature:
{() => saveChanges(thisCard)}
Also do add a jsfiddle/ runnable snippet for answerers to test ✌️.
Edit:
About the array of objects passing and updates, at your part the code is good where filter is used. We can apply idea of moving update logic to parent where data is located.
Now id + updated attributes could be passed to the update callback in child.
To give you hint, can use spread operator syntax to update items out of existing objects.

How to submit a dynamic form in React.js

I have a dynamically created from in React, and I'd like to be able to submit the values of all the input fields, but I can't add seperate on change handlers for each input elment, as they are created dynamically:
extract from the form js:
const FormElements = ({formFields}) => ( <div> {
formFields.map(formField => ( <FormElement name={formField.name} type={formField.fieldType} />)
)} </div> );
console.log(formFields);
return (
<div class="col-md-12">
<div class="panel panel-primary">
<div class="panel-heading">
<h4 class="panel-title">
{title} - {id}
</h4>
</div>
<div class="panel-body">
<form >
<FormElements formFields={formFields} />
<a
class="btn btn-primary"
onClick={this.handleSubmitButton}//what do I do with this function?
href="#">Submit</a>
</form>
</div>
</div>
</div>
);
form element js:
export default class FormElement extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div class="form-group">
<label for="{this.props.name}">{this.props.name}</label>
<input type="{this.props.type}}" class="form-control" id="{this.props.name}" placeholder="blah blah" />
</div>
);
}
}
Since they are controlled inputs there is not a react way to that, and even if there is I would not recommend it, React is all about declarative code.
There are two ways to solve this, one is to use make a property onChange on your FormElement and pass a function with ids, something like this:
<FormElements onChange={(key, value) => this.setState({ [key]: value })
The other way is to send give all the not defined props to the input:
export default class FormElement extends React.Component {
constructor(props) {
super(props);
}
render() {
const { name, type, ...other } = this.props
return (
<div class="form-group">
<label for="{name}">{name}</label>
<input type="{type}}" class="form-control" {...other} id="{this.props.name}" placeholder="blah blah" />
</div>
);
}
}
(the { [key]: value } and {...other} is ES6)
I actually managed this in a quite convoluted, and probably not recommended way, but it works! I've also never seen this done elsewhere...probably for good reason:
Form element:
export default class FormElement extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
onChange(e) {
this.props.handleChange(e.target.id, e.target.value);
}
render() {
return (
<div class="form-group">
<label for={this.props.id}>{this.props.name}</label>
<input type="{this.props.type}}" class="form-control" id={this.props.id} value={this.props.value} placeholder="blah blah" onChange={this.onChange}/>
</div>
);
}
}
form:
handleFormElementChange(id, value) {
console.log("changing: " + id + " = "+ value);
var frm = this.state.formData;
var index=-1;
for(var i=0;i<frm.length;i++) {
if(frm[i].id==id) {
index=i;
break;
}
}
frm[index].value = value;
this.setState({formData: frm});
}
const FormElements = ({formFields}) => ( <div> {
formFields.map(formField => ( <FormElement name={formField.name} key={formField.id} value={formField.value} id={formField.id} type={formField.fieldType} handleChange={this.handleFormElementChange.bind(this)}/>)
)} </div> );
What's happening is the actual full form data is being updated in the form component, and each time a change is made to one of the form elements, it passes it back to the parent form, update's the form's sate and then re-renders the whole form.
The complication here was actually finding the correct form element in the overall form status, by searching through the array for the key, and updating the value.
While I see this working with small forms, I can see how it would start to significantly slow down rendering on large form applications.

React not updating text (issue with .getDOMNode())

I'm using React to make a Markdown previewer and trying to make sense of what was used here to make the right box update with a live preview as soon as the text is changed on the left. They have this code at the bottom:
var RawInput = React.createClass({
update:function(){
var modifiedValue=this.refs.inputValue.getDOMNode().value;
this.props.updateValue(modifiedValue);
},
render:function(){
return (<textarea rows="22" type="text" ref="inputValue" value={this.props.value} onChange={this.update} className="form-control" />)
}
});
I implementing this in my code:
const InputText = React.createClass ({
update() {
let newValue = this.refs.inputValue.getDOMNode().value;
this.props.updateValue(newValue);
},
render() {
return (
<div>
<textarea
id="input-text"
rows="18"
type="text"
ref="inputValue"
value={this.props.value}
onChange={this.update}
/>
</div>
);
}
});
The app runs fine except that there is no live preview and the text on the right doesn't update. In the console I get this error: this.refs.inputValue.getDOMNode is not a function .
Here is the full code:
import React from 'react';
import Banner from './components/Banner.jsx';
import ContainerHeader from './components/ContainerHeader.jsx';
import marked from 'marked';
const App = React.createClass ({
updateValue(newValue) {
this.setState ({
value: newValue
})
},
getInitialState() {
return {
value: 'Heading\n=======\n\nSub-heading\n-----------\n \n### Another deeper heading\n \nParagraphs are separated\nby a blank line.\n\nLeave 2 spaces at the end of a line to do a \nline break\n\nText attributes *italic*, **bold**, \n`monospace`, ~~strikethrough~~ .\n\nShopping list:\n\n * apples\n * oranges\n * pears\n\nNumbered list:\n\n 1. apples\n 2. oranges\n 3. pears\n'
}
},
markup(value) {
return {__html: value}
},
render() {
return (
<div>
<Banner />
<div className="container">
<div className="row">
<div className="col-xs-12 col-sm-6">
<ContainerHeader text="I N P U T" />
<InputText
value={this.state.value}
updateValue={this.updateValue} />
</div>
<div className="col-xs-12 col-sm-6">
<ContainerHeader text="O U T P U T" />
<div id="output-text">
<span dangerouslySetInnerHTML={this.markup(marked(this.state.value))}></span>
</div>
</div>
</div>
</div>
</div>
);
}
});
const InputText = React.createClass ({
update() {
let newValue = this.refs.inputValue.getDOMNode().value;
this.props.updateValue(newValue);
},
render() {
return (
<div>
<textarea
id="input-text"
rows="18"
type="text"
ref="inputValue"
value={this.props.value}
onChange={this.update}
/>
</div>
);
}
});
export default App;
Any help welcome on solving this, and thanks!
Since version 15.* in React this.refs.inputValue refers to DOMElement, so you don't need use getDOMNode;
this.refs.inputValue.value
However, I think in this case you don't need use refs, as you call update inside onChange event, you can get target(refer to textarea) from event object, and from target get value
update(e) {
this.props.updateValue(e.target.value);
},
The example is using React v0.14.x which was one combined module.
From React v15 (0.15, but they changed to use it as the major) the methods and classes were split into two modules, React and ReactDOM.
You need to import and use ReactDOM.findDOMNode(component), documentation for which can be found here.
You actually do not need this.refs.inputValue
update(e) {
this.props.updateValue(e.target.value);
},

Checkbox keeps state across components in React JS

I'm new in React JS, but I read about <input> that you have to save actual state in onChange like described here: React DOC - Forms
I have a list with a checkbox and I applied same behavior here in CampaignsRow
var campaignsData = [{Name: "First"}, {Name: "Second"}, {Name: "Third"}];
var CampaignsRow = React.createClass({
getInitialState: function() {
return {checked: false};
},
checkedChange: function (e) {
this.setState({checked: e.target.checked});
},
render: function() {
console.log(this.props.data.Name, this.state.checked);
return (
<div className="row">
<div className="cell checkbox">
<input type="checkbox" checked={this.state.checked} onChange={this.checkedChange}/>
</div>
<div className="cell campaignName">{this.props.data.Name}</div>
</div>
);
}
});
var CampaignsTable = React.createClass({
render: function() {
var rows = this.props.campaigns.map(function(campaign) {
return (
<CampaignsRow data={campaign}/>
);
});
return <div className="table">
<div className="row header">
<div className="cell checkbox"><input type="checkbox"/></div>
<div className="cell campaignName">Name</div>
</div>
{rows}
</div>
}
});
ReactDOM.render(<CampaignsTable campaigns={campaignsData} /> ,document.getElementById('reactContainer'));
My problem is, if I check the checkbox at the campaign with name First and then I remove first item by campaignsData.shift() (to simulate downloading new data from Server) and then render again, checkbox at Second campaign is checked.
What is the purpose of this.state when it is not attached to the instance. Render works fine, because in the console is printed Second true, so this.state.checked was moved from First to Second campaign.
You should add unique key property to multiple components, so that React can keep track of identities:
var rows = this.props.campaigns.map(function(campaign) {
return (
<CampaignsRow key={campaign.name} data={campaign}/>
);
});

React state nested attributes

I'm playing around with a Fluxxor tutorial (example at the very top) and there's a simple todo list built with React. It's very basic and I wanted to add a simple validation to get better understand of data binding.
var Application = React.createClass({
mixins: [FluxMixin, StoreWatchMixin("TodoStore")],
getInitialState: function() {
return { newTodoText: "" };
},
getStateFromFlux: function() {
var flux = this.getFlux();
return flux.store("TodoStore").getState();
},
render: function() {
var todos = this.state.todos;
return (
<div>
<ul>
{Object.keys(todos).map(function(id) {
return <li key={id}><TodoItem todo={todos[id]} /></li>;
})}
</ul>
<form onSubmit={this.onSubmitForm}>
<input type="text" size="30" placeholder="New Todo"
value={this.state.newTodoText}
onChange={this.handleTodoTextChange} />
<input type="submit" value="Add Todo" />
</form>
<button onClick={this.clearCompletedTodos}>Clear Completed</button>
</div>
);
},
So if I want to disable submit if the input field is empty, I can compare the value of input with the initial state:
<input type="submit" value="Add Todo" disabled={!#state.newTodoText}/>
Now I want to disable Clear Completed until at least one TodoItem is marked as completed. When this happens this.state.todos is updated with a with a completed key set to true and it's available in my application component:
Object {todo: Object}
todo: Object
complete: true
id: 1
text: "text"
How I should handle this logic? Thanks in advance.
You can write a function which returns the number of completed todos in your object.
countCompleted: function(todos) {
var todoKeys = Object.keys(todos);
var completed = todoKeys.filter(function(key) {
var todo = todos[key];
return todo.complete;
});
return completed.length;
}
Then call that function from render to conditionally disable the button.
<button
onClick={this.clearCompletedTodos}
disabled={this.countCompleted(todos) > 0}>
Clear Completed
</button>

Categories

Resources