I'm using Meteor with React. I have a really simple goal, but i have tried a lot and can't solve it for myself. I will show you my attemps below.
I want to create a form for the Ingredients. At the first moment there is only one input (for only one ingredient) and 2 buttons: Add Ingredient and Submit.
class IngredientForm extends Component {
render() {
return(
<form onSubmit={this.handleSubmit.bind(this)}>
<input type="text"/>
{ this.renderOtherInputs() }
<input type="button" value="Add Ingredient" onClick={this.addIngredient.bind(this)}>
<input type="submit" value="Submit Form">
</form>
);
}
}
So when I click Submit then all the data goes to the collection. When I click Add Ingredient then the another text input appears (in the place where renderOtherInputs() ).
I know, that Meteor is reactive - so no need to render something directly. I should underlie on the reactive data storage.
And I know from the tutorials the only one way to render something - I should have an array (that was based on collection, which is always reactive) and then render something for each element of that array.
So I should have an array with number of elements = number of additional inputs. that is local, so I can't use Collection, let's use Reactive Var instead of it.
numOfIngredients = new ReactiveVar([]);
And when I click Add button - the new element should be pushed to this array:
addIngredient(e) {
e.preventDefault();
let newNumOfIngredients = numOfIngredients.get();
newNumOfIngredients.push('lalal');
numOfIngredients.set(newNumOfIngredients);
}
And after all I should render additional inputs (on the assumption of how many elements I have in the array):
renderOtherInputs() {
return numOfIngredients.get().map((elem) => {
return(
<input type="text"/>
);
}
}
The idea is: when I click Add button then new element is pushed to the ReactiveVar (newNumOfIngredients). In the html code I call this.renderOtherInputs(), which return html for the as many inputs as elements I have in my ReactiveVar (newNumOfIngredients). newNumOfIngredients is a reactive storage of data - so when I push element to it, all things that depends on it should re-render. I have no idea why that is not working and how to do this.
Thank you for your help.
Finally I got the solution. But why you guys don't help newbie in web? It is really simple question for experienced developers. I read that meteor and especially react have powerful communities, but...
the answer is: we should use state!
first let's define our state object in the constructor of react component:
constructor(props) {
super(props);
this.state = {
inputs: [],
}
}
then we need a function to render inputs underlying our state.inputs:
renderOtherInputs() {
return this.state.inputs.map( (each, index) => {
return (
<input key={ index } type="text" />
);
});
}
and to add an input:
addInput(e) {
e.preventDefault();
var temp = this.state.inputs;
temp.push('no matter');
this.setState({
inputs: temp,
});
}
p.s. and to delete each input:
deleteIngredient(e) {
e.preventDefault();
let index = e.target.getAttribute('id');
let temp = this.state.inputs;
delete temp[index];
this.setState({
inputs: temp,
});
}
Related
I have a master and a child component. The child component persists the data for the create mode as well as the edit mode. The child has a data section as follows which is being used when the component is in create mode
data() {
return {
title: '',
description: '',
organizer: '',
startdate: '',
enddate: '',
email: '',
phone: ''
}
},
and my inputs in create mode are as follows
<input type="text" placeholder="enter event title here" class="form-control" v-model="title">
In the edit mode, I am updating a prop value on the client as follows, which is
props:['currentevent']
The value of the currentevent is being passed from the master component to the child component and is also the value that is currently being edited.
so, the complete code for handling an input value looks like as follows
<input type="text" placeholder="enter event title here" class="form-control" v-if="currentevent" :value="currentevent.title">
<input type="text" placeholder="enter event title here" class="form-control" v-else v-model="title">
and in my save method (in the child component), I am checking if currentevent is empty or not. If it is empty then I trigger the add code otherwise, I trigger the update code.
Question : This works , but I have a large form and having to do this for each and every component is not a clean design . Can you please let me know what should I be doing ?
I totally appreciate your predicament. The best way to handle form data is to make it create/update agnostic. Here's what I'd recommend you try:
Instead of maintaining all the data fields as disparate properties, contain them in a single object, in this case I'm calling it eventObj for clarity:
data () {
return {
eventObj: {}
}
}
Then in your markup you'd reference them via the object:
<input type="text" placeholder="..." class="form-control" v-model="eventObj.title">
You'd then need to define a prop for passing in the data (as an object) from the parent component if you are editing:
props: {
currentevent: Object
}
And then you'd just need to map the incoming prop to the child component's data:
created() {
Object.assign(this.eventObj, this.currentevent || {})
}
Now when your inputs like <input v-model="eventObj.title"> are processed, if there is a saved title (that was passed in with currentevent) the field will be prepopulated with it, otherwise it will be blank.
I think this should help you in the right direction toward solving the complexity you're trying to figure out. There are other logistical issues involved with this kind of stuff in general, but I won't drone on. :)
The issue I see is you want to remove the v-if/else in the form. I will recommend here is keep your local data of child to be in sync with the props passed and only use local variable in the form.
One way to do this can be put a watcher on props and whenever props changes, update local variables and only use those variables in form.
watch: {
currentevent: function(newVal){
title = newVal.title,\
description = newVal.description
...
}
}
I'd like to call some callback in a React component whenever the input's value has changed.
simplified example should explain:
in a jsx file:
<input onKeyDown={this.props.changeCallback( VALUE_OF_INPUT )} ></input>
what should VALUE_OF_INPUT be?
Thanks!
There's not much React-specific about this. Use the event which is passed to the callback as the first argument:
onKeyDown={event => this.props.changeCallback(event.target.value)}
This approach (using a fat arrow function) also handles binding this, at the expense of creating a new anonymous function once per render. Note that calling bind essentially does the same thing, so this is my preferred option out of the two.
It's important to make the distinction between assigning a function to the onKeyDown attribute (as is done here) and calling the function (what you did).
<input value={this.state.value} onKeyDown={this.props.changeCallback.bind( this, this.state.value)} ></input>
Would do the job.
Another way to do it dynamic is give it a value callback this works well if pre populating the "value" from some var.
/* Better function for larger forms */
handleChange = (event) => {
var name = event.target.name;
var value = event.target.value;
this.setState({[name]: value});
}
/* set Default and callback to state */
valueCallback = (value, name) => {
if (this.state[name]) {
return this.state[name]
}
else {
return value;
}
}
then you can go ...
var defaut = '1234'; // or this.props.something.something;
<input type={type} name={name} value={this.valueCallback(defaut, name)} onChange={this.handleChange} />
I'm learning how to use meteor with react so forgive the basic question.
I want to create a form that populates when the page loads if the data has already been submitted. I've been trying to use getInitialState however I'm not getting anywhere. Some help would be really appreciated.
Path: MyResolutions.jsx
export default class MyResolutions extends Component {
getInitialState() {
return {
resolution: Resolutions.find().fetch(),
timeToComplete: Resolutions.find().fetch(),
};
}
render() {
return (
<form onSubmit={this.addResolutions.bind(this)}>
<input
type="text"
ref="resolution"
placeholder="Resolution title"
value={this.state.resolution} />
<input
type="text"
ref="timeToComplete"
placeholder="Time To Complete"
value={this.state.timeToComplete} />
<button type="submit">Submit</button>
</form>
)
}
}
This depends on the shape of your data coming from your initial state:
getInitialState() {
return {
resolution: Resolutions.find().fetch(),
timeToComplete: Resolutions.find().fetch(),
};
}
Assuming that this.state.resolution returns something like:
{
value: 'some string'
}
You would actually have to do something like this.state.resolution.value . So maybe try console.log(this.state.resolution) to get the shape of your data and then use dot notation to display the keys you need.
Although you don t need to look at the code below to understand the question, I added it in case you need to visualize the scenario. Whenever the form submits, addList method is called.
And the component updates itself. But I didn t expect this behaviour, that s why at first I did try to assign my lists to state, so that when the state changed the component would update itself as I wanted.
Anyway it already updates itself, but why ? Which way is more efficient ?
import React,{Component} from 'react';
import TrackerReact from 'meteor/ultimatejs:tracker-react';
import {Lists} from '../lib/collections/lists.js';
export default class App extends TrackerReact(Component) {
constructor(){
super();
// this.state = {lists : this.lists()}
}
lists(){
return Lists.find().fetch();
}
addList(e){
e.preventDefault();
//let text = this.refs.list.value ;
let text = this._inputList.value ;
console.log(this._inputList.value);
Lists.insert({
title : text
});
this._inputList.value = "";
//this.setState({lists : this.lists()});
}
render() {
return (
<div>
<h2>Lists</h2>
<ul>
{this.lists().map((a,b)=>(
<List key={a._id} title={a.title} />
))}
</ul>
<form onSubmit={this.addList.bind(this)}>
<input
type="text"
ref={(input)=>{
this._inputList = input ;
}}
placeholder="add list bro"
/>
</form>
</div>
)
}
}
So if we add a componentWillUpdate react life-cycle method to see if it rerenders ;
componentWillUpdate() {
console.log('will update');
}
When form submitted "will update" is logged on console as expected. However if we update addList as ;
addList(e){
e.preventDefault();
// nothing else.
}
We don t see the output "will update" on console. Which means method being called doesn t require the component to be rerendered. There is no such a rule. In this case , it is probably about TrackerReact. TrackerReact might force the component to rerender.
I am using Meteor with React. Consider this simple component below. There is a local mini-Mongo collection CommentsCollection. The component will insert a row in it when componentWillMount will be called. getMeteorData will return the first record in the collection and we'll be able to modify the title. Problem: if I place my cursor at the start of the title and start typing, after the first character update the cursor will jump to the end of the string and the rest of my typing will be placed there. How do I work around this?
CommentsCollection = new Meteor.Collection(null); // Local mini-mongo collection
EventTestComponent = React.createClass({
mixins : [ReactMeteorData],
componentWillMount(){
CommentsCollection.insert({title:"test title", message:"some test message"});
},
getMeteorData(){
return {
comment: CommentsCollection.findOne()
};
},
handleTitleChange(e){
CommentsCollection.update({_id: this.data.comment._id}, {$set:{title: e.target.value}});
},
render(){
if(this.data.comment) {
return (
<div>
<input type="text" value={this.data.comment.title} onChange={this.handleTitleChange}/>
</div>
);
}else{
return <div>Loading...</div>
}
}
});
I came up with this solution right after I posted the question:
<input type="text"
defaultValue={this.data.comment.title}
onKeyUp={this.handleTitleChange}/>
So: change value to defaultValue, and onChange to onKeyUp. Works like a charm!