I am learning REACT and am having some trouble.
I have multiple pages/components each of these pages has a form in it and I have written a function to handle the form input once submitted.
However i would like to move this function to its own component this way each of the pages simply calls this single component when the form is submitted.
My question is how to do accomplish this using REACT, I have been searching for hours and just cannot figure it out.
Here are two example components:
Form handler/form processing
'use strict';
import React from 'react/addons';
const FormProcessing = React.createClass({
_test: function() {
console.log('test');
//do something with form values here
},
render(){
}
});
export default FormProcessing;
Example component with form, that needs to call the 'test' function from the 'FormProcessing' component
'use strict';
import React from 'react/addons';
import {Link} from 'react-router';
import DocumentTitle from 'react-document-title';
const BreakingNewsPage = React.createClass({
propTypes: {
currentUser: React.PropTypes.object.isRequired
},
_inputFields: function() {
return (
<form id="breakingNewsForm" action="" onclick={call test function from form processing component}>
<fieldset>
<legend>Headline</legend>
<div className="form-group">
<input type="text" className="form-control input-size-md" id="inputHeadline" placeholder="Headline (Example: Budget Cuts)"/>
</div>
<legend>Breaking News</legend>
<div class="form-group">
<input type="text" className="form-control input-size-lg" id="inputDescription1" placeholder="Breaking News Description"/>
<input type="text" className="form-control input-size-lg" id="inputDescription2" placeholder="Breaking News Description"/>
</div>
</fieldset>
</form>
);
},
_formButtons: function() {
return (
<div className="form-buttons">
<button form="breakingNewsForm" type="reset" className="btn-lg">Clear</button>
<button form="breakingNewsForm" type="submit" classn="btn-lg">Save</button>
</div>
);
},
render() {
return (
<DocumentTitle title="Breaking News">
<section className="ticker-page page container-fluid">
<div className="page-title">
<h1>Breaking News</h1>
</div>
<div className="com-md-6">
{this._inputFields()}
{this._formButtons()}
</div>
</section>
</DocumentTitle>
);
}
});
export default BreakingNewsPage;
Updated code using example provided by: ycavatars
This class renders the form and a few other bits, I have now required the 'FormProcessing class' and have assigned it to _formHandler: FormProcessing,
I then try and call the function handleForm from the formProcessing class using this._formHandler.handleForm(); but i receive this error: Uncaught TypeError: this._formHandler.handleForm is not a function
'use strict';
import React from 'react/addons';
import {Link} from 'react-router';
import DocumentTitle from 'react-document-title';
var FormProcessing = require ('../components/FormProcessing.js');
const IntroPage = React.createClass({
propTypes: {
currentUser: React.PropTypes.object.isRequired
},
_formHandler: FormProcessing,
// initial state of form
getInitialState: function() {
return {
type: 'info',
message: ''
};
},
// handles the form callback
_handleSubmit: function (event) {
event.preventDefault();
// Scroll to the top of the page to show the status message.
this.setState({ type: 'info', message: 'Saving...' }, this._sendFormData);
},
// sends form data to server
_sendFormData: function () {
this._formHandler._handleForm();
this.setState({ type: 'success', message: 'Form Successfully Saved' });
return;
},
_inputFields: function() {
var rows = [];
for(var i = 1; i <= 12; i++) {
var placeHolder = 'Intro text line ' + i;
var inputID = 'inputIntroText ' + i;
rows.push(<input type="text" className="form-control input-size-lg" name="formInput" id={inputID} placeholder={placeHolder}/>);
}
return (
<form id="introForm" action="" onSubmit={this._handleSubmit}>
<fieldset>
<legend>Intro Title</legend>
<div className="form-group">
<input type="text" className="form-control input-size-md" name="formInput" id="introTitle" placeholder="Intro Title"/>
</div>
<legend>Intro Text</legend>
<div className="form-group">
{rows}
</div>
</fieldset>
</form>
);
},
_formButtons: function() {
return (
<div className="form-buttons">
<button form="introForm" type="reset" className="btn-lg">Clear</button>
<button form="introForm" type="submit" className="btn-lg">Save</button>
</div>
);
},
render() {
// checks and displays the forms state
if (this.state.type && this.state.message) {
var classString = 'alert alert-' + this.state.type;
var status = <div id="status" className={classString} ref="status">
{this.state.message}
</div>;
}
return (
<DocumentTitle title="Intro">
<section className="intro-page page container-fluid">
<div className="page-title">
<h1>Intro</h1>
</div>
<div className="com-md-6">
{status}
{this._inputFields()}
{this._formButtons()}
</div>
</section>
</DocumentTitle>
);
}
});
export default IntroPage;
This is the FormProcessing class
'use strict';
import React from 'react/addons';
var IntroPage = require ('../pages/IntroPage.js')
class FormProcessing extends React.Component {
_handleForm() {
console.log('you caled me!');
}
};
export default FormProcessing;
You want to invoke FormProcessing._test from BreakingNewsPage component. You have to know the difference between class and instance. React.createClass returns a class which describes your component, and the render method of your component returns a virtual DOM element.
In order to call FormProcessing._test, you have to get the reference to the backing instance of FormProcessing class. That's why react provides a ref. The official tutorial explains the details.
There's an open source project, how to center in css, uses many ref. You could take a look.
Related
I'm trying to create a basic form that, once input is submitted, sends data to the parent and is then rendered in a list as a card. Documentation has been pointing me towards using the Event Bus, but it all seems a little too over engineered for such a simple task. Not to mention its not working xD. Am I on the right track here? or missing the whole idea?
The Data seems to be updating on submit, but I'm not seeing a card render. I'm also seeing the follow error,
Property or method "initiativeList" is not defined on the instance but referenced during render.
I do, however, notice a particularly odd change in the render. Instead of a child being rendered in EncounterList.js the child's attributes are merging into the parent .
Any help is greatly appreciated.
EncounterDashboard.js
<template>
<div>
<NewCharacterForm #add-char="addChar" />
<EncounterList v-bind="encounterList" #add-char="addChar" />
</div>
</template>
<script>
import Character from "../classes/Encounter";
import NewCharacterForm from "./NewCharacterForm/NewCharacterForm.vue";
import EncounterList from "./EncounterList/EncounterList";
import EventBus from "./EventBus.js";
export default {
name: "EncounterDashboard",
components: { NewCharacterForm, EncounterList },
data() {
return {
newChar: {},
encounterList: []
};
},
methods: {
addChar(newChar) {
this.newChar = newChar;
this.encounterList.push(newChar);
EventBus.$emit("add-to-list", this.encounterList);
}
}
};
</script>
NewCharacterForm.js
<template>
<div class="new-char-wrapper">
<form class="char-form" ref="form" v-on:submit.prevent="handleSubmit">
<NewCharInput class="name-input" label="NAME" name="name" v-model="name" />
<div class="stat-wrapper">
<NewCharInput
class="init-input"
label="INITIATIVE"
name="initiative"
v-model="initiative"
type="number"
/>
<NewCharInput class="hp-input" label="HP" name="hp" v-model="hp" type="number" />
</div>
<div class="submit-row">
<button class="submit">SUBMIT</button>
</div>
</form>
</div>
</template>
<script>
import NewCharInput from "./NewCharInput";
import Character from "../../classes/Character";
import { uuid } from "vue-uuid";
export default {
name: "NewCharacterForm",
components: { NewCharInput },
data() {
return {
name: "",
initiative: "",
hp: 0
};
},
props: ["addChar"],
methods: {
handleSubmit() {
const charName = this.$refs.form.name.value;
const charInitiative = this.$refs.form.initiative.value;
const charHp = this.$refs.form.hp.value;
const charId = this.$uuid.v4();
const newChar = new Character(charName, charInitiative, charId, charHp);
this.$emit("add-char", newChar);
}
}
};
</script>
EncounterList.js
<template>
<div class="encounter-list">
<div class="header-row">
<h2 class="header col-init">INIT</h2>
<h2 class="header col-name">NAME</h2>
<h2 class="header col-hp">HP</h2>
</div>
<EncounterCard
v-for="character in initiativeList"
v-bind:key="character.id"
v-bind:hp="character.hp"
v-bind:name="character.name"
v-bind:initiative="character.initiative"
/>
</div>
</template>
<script>
import EncounterCard from "../EncounterCard/EncounterCard";
import EventBus from "../EventBus";
export default {
name: "EncounterList",
components: { EncounterCard },
data() {
return {
data: {
initiativeList: []
}
};
},
methods: {
populateList(charList) {
this.initiativeList = charList;
}
},
mounted() {
EventBus.$on("add-to-list", charList => {
this.populateList(charList);
});
}
};
</script>
EncounterCard.js
<template>
<div class="encounter-card-wrapper">
<h1 class="char-init">{{character.initiative}}</h1>
<h1 class="char-name">{{character.name}}</h1>
<h1 class="char-hp">{{character.hp}}</h1>
</div>
</template>
<script>
export default {
name: "EncounterCard",
props: ["character"]
};
</script>
data() {
return {
data: { //Is this what you're trying to do?
initiativeList: []
}
};
},
If the data attribute is intended, "initiativeList" should be changed to "data.initiativeList".
<EncounterCard
v-for="character in data.initiativeList"
v-bind:key="character.id"
v-bind:hp="character.hp"
v-bind:name="character.name"
v-bind:initiative="character.initiative"
/>
and
populateList(charList) {
this.data.initiativeList = charList;
}
I am looking to create a form with a datepicker in my React component with materialize-css. I don't have many fields this form is capturing and the structure is fairly simple. The form returned looks like this:
<form onSubmit={this.handleSubmit.bind(this)}>
<div className="container">
<div className="card grey lighten-3">
<div className="card-content black-text">
<span className="card-title">
<input placeholder="Event Name"
name="name" value={this.state.name}
onChange={this.handleStateChange.bind(this)}/>
</span>
<input name="description" placeholder="Description"
value={this.state.description}
onChange={this.handleStateChange.bind(this)}/>
<input name="image" placeholder="Image URL"
value={this.state.image}
onChange={this.handleStateChange.bind(this)}/>
<input placeholder="Start Date"
className="datepicker" name="startDate" value={this.state.startDate}
onSelect={this.handleStateChange.bind(this)}/>
</div>
<div class="card-action">
<div className="row">
<span>
<div className="col s3">
<input className="btn light-blue accent-1" type="submit" value="Submit"/>
</div>
<div className="col s3">
<a className="btn grey" onClick={this.handleExpand.bind(this)}>Cancel</a>
</div>
</span>
</div>
</div>
</div>
</div>
</form>
The state change is handled with
handleStateChange(item) {
this.setState({[item.target.name]: item.target.value});
}
and I have called the AutoInit to initialize my datepicker
M.AutoInit();
I've tried using onChange instead of onSelect to manage the datepicker state change, but it doesn't seem to capture that event. With onSelect used, the date sometimes gets capture if I pick a date then re-open the datepicker.
I have also tried using some of the alternate initialization methods for the datepicker to no avail.
How do I correctly capture the input change with my given setup?
Hi hopefully this will help somebody -
What happens with the <DatePicker /> component is that the default onChange method returns the date (2019-08-01) and not the the element's event handler object. In order to counter this we need to create an object inside the onChange method that mimics the eventhandler's target.id and target.value
Other components like the <input /> works as per normal. Check it out:
This is what the component should look like:
<DatePicker
label="myDate"
value={state.myDate}
id="myDate"
onChange={(newDate) => {
handleChange({
target: {
id: "myDate",
value: newDate
}
})
}} />
Here is the full code:
import React, { useState, useEffect } from "react";
import "materialize-css/dist/css/materialize.min.css";
import "materialize-css/dist/js/materialize.min.js";
import { DatePicker } from "react-materialize";
const PickDate = (props) => {
const [state, setState] = useState({myName: "Mags", myDate: "2019-08-01"});
const handleChange = (e) => {
const key = e.target.id;
const val = e.target.value;
const newState = {...state};
newState[key] = val;
setState(newState);
}
return (
<React.Fragment>
<input type="text" value={state.myName} id="myName" onChange={handleChange} />
<DatePicker
label="myDate"
value={state.myDate}
id="myDate"
onChange={(newDate) => {
handleChange({
target: {
id: "myDate",
value: newDate
}
})
}} />
</React.Fragment>
);
}
export default PickDate;
PS. this uses React Hooks - but it will work on normal classes too.
After studing all the saturday about the lifecycle of React. I got this solution for this:
The use of the component:
<DatePicker label="Fecha" value={this.state.formSchedule.start}
onChange={(date) => {
this.state.formSchedule.start = date;
this.setState({ formSchedule: this.state.formSchedule });
}}/>
The Class DatePicker.tsx:
import * as React from 'react';
import Materialize from 'materialize-css';
import moment from 'moment'
import 'moment/locale/es'
import { any } from 'prop-types';
interface IState {
value: any;
}
interface IProps {
label: any;
format: any;
onChange: any;
formatMoment: any;
}
export default class DatePicker extends React.Component<IProps, IState> {
static defaultProps = {
label: "Fecha",
value: new Date(),
format: 'ddd d, mmm',
formatMoment: 'ddd D, MMM'
}
constructor(props: any) {
super(props);
this.componentWillReceiveProps(props);
}
componentWillReceiveProps(props) {
this.state = {
value: props.value
};
}
render() {
return <div className="input-field col s6">
<i className="material-icons prefix">date_range</i>
<input id="date" type="text" className="datepicker queso"
value={moment(this.state.value).locale('es').format(this.props.formatMoment)}
/>
<label className="active" htmlFor="date">{this.props.label}</label>
</div>;
}
componentDidMount() {
var context = this;
var elems = document.querySelectorAll('.queso');
Materialize.Datepicker.init(elems, {
defaultDate: new Date(),
format: this.props.format,
container: 'body',
onSelect: function (date) {
context.setState({ value: context.state.value });
context.props.onChange(date);
},
autoClose: true
} as Partial<any>);
}
}
Thanks Lucas, your answer also helped me.
Here is my solution -without redux or props, just the class component with its own state
import React, { Component } from "react";
import "./calendar.css";
import Materialize from "materialize-css";
import moment from "moment";
class Calendar extends Component {
componentDidMount() {
var context = this;
var elems = document.querySelectorAll(".dateset");
Materialize.Datepicker.init(elems, {
defaultDate: new Date(),
format: this.state.format,
container: "body",
onSelect: function(date) {
context.setState({ value: context.state.value });
console.log(date); // Selected date is logged
},
autoClose: true
});
}
state = {
value: new Date(),
format: "ddd d, mmm",
formatMoment: "ddd D, MMM"
};
render() {
return (
<div className="input-field col s6">
<i className="material-icons prefix">date_range</i>
<input
id="date"
type="text"
className="datepicker dateset"
defaultValue={moment(this.state.value).format(
this.state.formatMoment
)}
/>
</div>
);
}
}
export default Calendar;
None of the already added answers helped me, so I decided to do it in my way.
I did not install Materialize and jQuery into my modules, just added them in index.html
When you click on input field, then datepicker modal window opens and there two buttons CANCEL and OK. I find that OK button by jQuery and add click function, which takes value from input field and updates the state;
componentDidMount(){
window.$(".datepicker-done").click(() => {
var datepickerValue = window.$("#date-input").val(); // date-input it's 'id' on datepicker input
this.setState({ approxLastDay: datepickerValue });
});
}
NOTE: datepickerValue will be in string format (e.g. "Aug 6, 2019")
I having the same problem, here is my solution.
import React, { Component } from 'react'
import M from 'materialize-css';
class CreateReport extends Component {
constructor(props) {
super(props);
this.state = {
startDate: '',
endDate: ''
};
// refs for startDate, endDate
this.startDate = React.createRef();
this.endDate = React.createRef();
}
componentDidMount() {
var context = this;
document.addEventListener('DOMContentLoaded', function () {
var start = document.querySelectorAll('.datepicker');
M.Datepicker.init(start, {
format: "mm/dd/yyyy",
autoClose: true,
onClose: context.handleDate
});
});
}
handleDate = () => {
this.setState({
startDate: this.startDate.current.value,
endDate: this.endDate.current.value,
})
// console.log(this.state.startDate)
// console.log(this.state.endDate)
}
handleChange = (e) => {
this.setState({
[e.target.id]: e.target.value
})
}
handleSumbit = (e) => {
e.preventDefault();
console.log(this.state)
}
render() {
return (
<div>
<div className="container">
<form onSubmit={this.handleSumbit} className="white col s12 m6">
<div className="row">
<div className="input-field col s3 offset-s2">
<label htmlFor="date">Start Date</label>
<input
type="text"
className="datepicker"
id="startDate"
onChange={this.handleChange}
value={this.state.startDate}
ref={this.startDate}
/>
</div>
<div className="input-field col s3 offset-s2">
<label htmlFor="date">End Date</label>
<input
type="text"
className="datepicker"
id="endDate"
onChange={this.handleChange}
value={this.state.endDate}
ref={this.endDate} />
</div>
</div>
<div className="col s8 offset-s2 center-align">
<button className="btn pink lighten-1 z-depth-0 ">Sign Up</button>
</div>
</form>
</div>
</div >
)
}
}
export default CreateReport
I made a less elegant solution, but it's also quite a few lines of code. And it seems to be working (after hours of debugging!).
I made a css-class called trouble and applied it to all the fields dealing with DatePicker and TimePicker. In the intialization I created eventlisteners to check for changes. When a change had been detected it calles the _valueTracker, and causes it to send an update event that redux notices.
One very strange thing is that if I pass in the exactly the same value as on e.target.value, it does nothing. It needs to be a string of something random such as "UPDATE!" in this case. I use this code along with the rest of the materialize initialization.
const troubles = document.querySelectorAll(".trouble");
troubles.forEach((item) => {
item.addEventListener("change", (e) => {
e.target._valueTracker.setValue("UPDATE!");
});
});
I am working with redux-form and it seems confusing me to load initial data for edit form but the data is not being validated on submit. I have managed to pass and load data into the fields but that seems not loading into the form props etc. Please see the following piece of code and let me know if need something more.
Form_Bayan.js
import React, {Component, PropTypes} from "react";
import {browserHistory} from "react-router";
import {reduxForm, Field} from "redux-form";
import {MyCustomInput, MySimpleInput, MyCustomSelect} from "./__form_field_components";
import {connect} from "react-redux";
import {bindActionCreators} from "redux";
import {
ADMIN_FETCH_AUTOSUGGESTS_Lbl,
adminFetchAutoSuggestCats_act,
ADMIN_GETCATID_BYNAME_Lbl,
adminGetCatIdByName_act,
ADMIN_ADDNEWBAYAAN_Lbl,
adminAddNewBayaan_act,
adminFetchArticlesByCat_act,
adminUpdateBayaan_act
} from "../../actions/adminActionCreators";
import _ from "lodash";
class NewBayanForm extends Component {
constructor(props) {
super(props);
this.state = {
submitButtonMeta: {
btnTitle: "Save",
btnClass: "btn btn-default",
btnIcon: null,
disabled: false
},
globalMessage: { // set when an action is performed by ActionCreation+Reducer and a message is returned
message: "",
className: ""
},
tempData: {
the_bayaansMainCat_id: this.props.associatedMainCatId, // being passed from parent component to avoide redundent declaration
the_autoSuggestCatList: [],
slug: "",
the_catId: null
}
};
}
resetMessageState() {
var noMsg = {message: "", className: ""};
this.setState({globalMessage: noMsg});
}
componentDidMount() {
this.props.adminFetchAutoSuggestCats_act(this.state.tempData.the_bayaansMainCat_id);
}
doSubmit(props) {
var newBayanObj = {
item_title: props.titleTxt,
item_slug: this.state.tempData.slug,
content: props.videoIdTxt,
picture: "",
attachment: "",
media_path: "https://www.youtube.com/watch?v=" + props.videoIdTxt,
reference: "",
tag_keywords: props.keywordsTxt,
author_name: props.authorTxt,
cat_id: this.state.tempData.the_catId
};
this.props.adminUpdateBayaan_act(newBayaanObj)
.then(() => {
this.props.adminFetchArticlesByCat_act(this.props.associatedMainCatId)
.then(() => {
this.props.toggleViewFunction(); // comming from Parent Class (bayaansPage)
});
});
}
fetchCategoryId(value) {
// make API call to fetch / generate category ID for this post
this.props.adminGetCatIdByName_act(value, this.state.tempData.the_bayaansMainCat_id); // '1': refers to look up under 'Bayaans' parent category for the specified category name
}
// will always receive and triggers when there are 'new props' and not old/same props
render() { // rendering Edit form
const {handleSubmit} = this.props;
console.log('<NewBayanForm> (render_editForm) this.props:', this.props);
return (
<div className="adminForm">
<form onSubmit={handleSubmit(this.doSubmit.bind(this))}>
<div className="col-sm-6">
<div className="row">
<div className="col-sm-5"><label>Title:</label></div>
<div className="col-sm-7"><Field name="titleTxt" component={MySimpleInput}
defaultValue={this.props.name} type="text"
placeholder="Enter Title"/></div>
</div>
<div className="row">
<div className="col-sm-5"><label>Slug:</label></div>
<div className="col-sm-7">{this.state.tempData.slug || this.props.slug} <input
type="hidden" name="slugTxt" value={this.state.tempData.slug}/></div>
</div>
<div className="row">
<div className="col-sm-5"><label>Select Category:</label></div>
<div className="col-sm-7"><Field name="catTxt" component={MyCustomSelect}
defaultValue={this.props.category_name} type="text"
placeholder="Select or Type a New"
selectableOptionsList={this.state.tempData.the_autoSuggestCatList}
onSelectionDone={ this.fetchCategoryId.bind(this) }/>
<input type="hidden" name="catIdTxt"
value={this.state.tempData.the_catId || this.props.category_id}/>
</div>
</div>
</div>
<div className="col-sm-6">
<div className="row">
<div className="col-sm-5"><label>Youtube Video ID:</label></div>
<div className="col-sm-7"><Field name="videoIdTxt" component={MySimpleInput}
defaultValue={this.props.content} type="text"
placeholder="TsQs9aDKwrw"/></div>
<div className="col-sm-12 hint"><b>Hint: </b> https://www.youtube.com/watch?v=<span
className="highlight">TsQs9aDKwrw</span></div>
</div>
<div className="row">
<div className="col-sm-5"><label>Author/Speaker:</label></div>
<div className="col-sm-7"><Field name="authorTxt" component={MySimpleInput}
defaultValue={this.props.author} type="text"/></div>
</div>
<div className="row">
<div className="col-sm-5"><label>Tags/Keywords:</label></div>
<div className="col-sm-7"><Field name="keywordsTxt" component={MySimpleInput}
defaultValue={this.props.tag_keywords} type="text"/>
</div>
</div>
</div>
<div className="row">
<div className={this.state.globalMessage.className}>{this.state.globalMessage.message}</div>
</div>
<div className="buttonControls">
<a className="cancelBtn" onClick={this.props.toggleViewFunction}>Cancel</a>
<button className={this.state.submitButtonMeta.btnClass}
disabled={this.state.submitButtonMeta.disabled}>
{this.state.submitButtonMeta.btnTitle}</button>
</div>
</form>
</div>
);
}
}
function validate(values) { // Validate function being called on Blur
const errors = {};
if (!values.titleTxt)
errors.titleTxt = "Enter Title";
if (!values.catTxt)
errors.catTxt = "Select/Enter a Category";
if (!values.videoIdTxt)
errors.videoIdTxt = "Enter youtube video ID (follow the provided hint)";
if (!values.keywordsTxt)
errors.keywordsTxt = "Enter keywords (will help in search)";
return errors;
}
// ReduxForm decorator
const newBayanFormAdmin_reduxformObj = reduxForm({
form: "newBayanFormAdmin", // any unique name of our form
validate // totally equivelent to--> validate: validate
});
function mapStateToProps({siteEssentials}, ownProps) {
// 1st param is related to our Redux State, 2nd param relates to our own component props
var initialValues = {
titleTxt: ownProps.name,
slugTxt: ownProps.slug,
catTxt: ownProps.category_name,
catIdTxt: ownProps.category_id,
videoIdTxt: ownProps.content,
authorTxt: ownProps.author,
keywordsTxt: ownProps.tag_keywords
};
return ({siteEssentials}, initialValues);
};
function mapDispatchToProps(dispatch) {
return bindActionCreators({
adminFetchAutoSuggestCats_act,
adminGetCatIdByName_act,
adminAddNewBayaan_act,
adminFetchArticlesByCat_act
}, dispatch);
};
NewBayanForm = connect(mapStateToProps, mapDispatchToProps) (newBayanFormAdmin_reduxformObj(NewBayanForm));
export default NewBayanForm;
try initialValues for more details
inside reduxform :
export default NewBayanForm = reduxForm({ form: 'NewBayanForm', initialValues: {
name: "abc",
email: "abc#gmail.com",
phoneNo: "1234567890"
} })(NewBayanForm)
or
<NewBayanForm initialValues={yourObject} />
I found this tutorial which is quite simple and helpful.
https://www.davidmeents.com/?p=38
I am working on a "settings" page where a logged in user can change their profile picture. However it seems that Meteor is having trouble finding the profile attribute for a user.
signup.js (here is where I create the user on signup and create the profile attribute)
import React, { Component } from 'react';
import { browserHistory } from 'react-router';
export default class Signup extends Component {
handleSubmit(event) {
event.preventDefault();
var signupEmail = event.target.signupEmail.value;
var signupPassword = event.target.signupPassword.value;
if (signupPassword !== '') {
Accounts.createUser({
email: signupEmail,
password: signupPassword,
profile: {
avatar: "/user-default.svg"
}
}, (err) => {
err ? (console.log(err.reason)) : browserHistory.push("/app/profile");
});
}
}
render() {
return (
<div className="login-form">
<form onSubmit={this.handleSubmit}>
<div className="input-options">
<input type="text" placeholder="Email" name="signupEmail" />
</div>
<div className="input-options">
<input type="password" placeholder="Password" name="signupPassword" />
</div>
<button className="login-submit bold">Sign me up!</button>
</form>
</div>
);
}
}
profile_settings.js
import React, { Component } from 'react';
import { Link } from 'react-router';
import reactMixin from 'react-mixin';
import ReactMeteorData from 'meteor/react-meteor-data';
export default class ProfileSettings extends Component {
constructor(props) {
super(props);
this.state = {
avatar: this.props.user.profile.avatar
}
}
getMeteorData(){
return{
user: Meteor.user()
}
}
componentWillMount(){
// we create this rule both on client and server
Slingshot.fileRestrictions("avatar", {
allowedFileTypes: ["image/png", "image/jpeg", "image/gif"],
maxSize: 2 * 500 * 500
});
}
upload(){
var userId = Meteor.user()._id;
var metaContext = {avatarId: userId};
var uploader = new Slingshot.Upload("UsersAvatar", metaContext);
uploader.send(document.getElementById('input').files[0], function (error, downloadUrl) { // you can use refs if you like
if (error) {
// Log service detailed response
console.error('Error uploading', uploader.xhr.response);
alert (error); // you may want to fancy this up when you're ready instead of a popup.
}
else {
// we use $set because the user can change their avatar so it overwrites the url :)
Meteor.users.update(Meteor.userId(), {$set: {"profile.avatar": downloadUrl}});
}
// you will need this in the event the user hit the update button because it will remove the avatar url
this.setState({avatar: downloadUrl});
}.bind(this));
}
formSubmit(){
let avatarUrl = this.state.avatar;
Meteor.users.update( {_id: Meteor.userId() }, {
$set: {profile: avatarUrl}
});
}
render() {
return (
<div>
<div className="sticky-header">
<h3>Settings</h3>
</div>
<form>
<div className="row well">
<div className="col-md-6">
<div className="form-group">
<label htmlFor="exampleInputFile">File input</label>
<input type="file" id="input" onChange={this.upload.bind(this)} />
<p className="help-block">Image max restriction: 2MB, 500x500. Cropped: 200x200</p>
</div>
</div>
<div className="col-md-6 utar-r">
<img src={this.state.avatar} height="200" width="200" alt="..." className="img-rounded" />
</div>
<div className="form-group">
<button className="btn btn-lg btn-primary btn-block" type="submit" onClick={this.formSubmit.bind(this)}>Update Profile</button>
</div>
</div>
</form>
<footer className="sticky-footer">
<Link to="/app/profile">
<button className="profile-edit bg-black">
<h3>Cancel</h3>
</button>
</Link>
<Link to="">
<button className="profile-edit">
<h3>Save Changes</h3>
</button>
</Link>
</footer>
</div>
);
}
}
reactMixin(ProfileSettings.prototype, ReactMeteorData);
Here is the error I am getting: TypeError: Cannot read property 'profile' of undefined
The error is not failing to find an profile attribute, but says that there's no user (or that user is undefined) This is exactly what TypeError: Cannot read property 'profile' of undefined means.
There are a few errors in your code:
the return of getMeteorData is available under this.data and not this.props
getMeteorData will run after constructor so there's no way to get Meteor's data in the constructor
getMeteorData returns data reactively, and is likely not to have the data you want when you instantiate the class anyway
So I'd recommend using the container/component approach that is like:
export default class ProfileSettingsContainer extends Component {
getMeteorData(){
return{
user: Meteor.user()
}
}
render() {
const user = this.data.user;
if (user) {
return <ProfileSettings user={user} />
} else {
return null; // or what ever placeholder you want while the data is being loaded
}
}
}
class ProfileSettings extends Component {
constructor(props) {
super(props);
this.state = {
avatar: props.user.profile.avatar
}
}
}
with this structure, the ProfileSettings will instantiate with something under props.user
Finnaly,
Meteor.users.update( {_id: Meteor.userId() }, {
$set: {profile: avatarUrl}
});
should be
Meteor.users.update( {_id: Meteor.userId() }, {
$set: {'profile.avatar': avatarUrl}
});
but that's unrelated
Using the example of initializingFromState within Redux-Form, I am trying to set this up dynamically. This is to edit a particular book in a list of books, and is using a simple api set up in express.js.
The full container is below. I somehow need to pass in initialValues, within the mapStateToProps function. In the example, it is done via a static object, but I can't work out how to use the information I have pulled in via fetchBook, and pass it to initialValues.
Container:
import React, { Component, PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import { fetchBook, editBook } from '../actions/index';
class BookEdit extends Component {
componentWillMount() {
this.props.fetchBook(this.props.params.id);
}
static contextTypes = {
router: PropTypes.object
}
onSubmit(props) {
this.props.editBook(this.props.book.id, props)
.then(() => {
this.context.router.push('/');
});
}
const data = {
title: {this.props.book.title},
description: {this.props.author}
}
render() {
const { fields: { title, author }, handleSubmit } = this.props;
const { book } = this.props;
if (!book) {
return (
<div>
<p>Loading...</p>
</div>
)
}
return (
<div>
<Link to="/">Back</Link>
<form onSubmit={handleSubmit(this.onSubmit.bind(this))}>
<h2>Add a new book</h2>
<label>Title</label>
<input type="text" {...title} />
<div className="text-help">{title.touched ? title.error : ''}</div>
<label>Author</label>
<input type="text" {...author} />
<div className="text-help">{author.touched ? author.error : ''}</div>
<button type="submit">Add</button>
<Link to="/" className="button">Go back</Link>
</form>
</div>
);
}
}
function mapStateToProps(state) {
return {
book: state.books.book,
initialValues: // how do I pass in the books here?
};
}
export default reduxForm({
form: 'EditBookForm',
fields: ['title', 'author']
}, mapStateToProps, { fetchBook, editBook })(BookEdit);
Thank you.
Your form values aren't what's in state.books.book? I think this is all you're looking for:
function mapStateToProps(state) {
return {
book: state.books.book,
initialValues: state.books.book
};
}
Since you're only really looking at this.props.book to know if it's loaded or not, it might be more explicit to do something like:
function mapStateToProps(state) {
return {
loaded: !!state.books.book,
initialValues: state.books.book
};
}
Hope that helps.
In related to above question, Erik. I have following form and not sure why it is not Validating on submit. It loads the data into fields but when I hit submit the validation fails.
Form_Bayan.js
import React, {Component, PropTypes} from "react";
import {browserHistory} from "react-router";
import {reduxForm, Field} from "redux-form";
import {MyCustomInput, MySimpleInput, MyCustomSelect} from "./__form_field_components";
import {connect} from "react-redux";
import {bindActionCreators} from "redux";
import {
ADMIN_FETCH_AUTOSUGGESTS_Lbl,
adminFetchAutoSuggestCats_act,
ADMIN_GENERATESLUG_Lbl,
adminGenerateSlug_act,
ADMIN_GETCATID_BYNAME_Lbl,
adminGetCatIdByName_act,
ADMIN_ADDNEWBAYAAN_Lbl,
adminAddNewBayaan_act,
adminFetchArticlesByCat_act,
adminUpdateBayaan_act
} from "../../actions/adminActionCreators";
import _ from "lodash";
class NewBayanForm extends Component {
constructor(props) {
super(props); // this component inherits "toggleViewFunction" function through props for redirection
this.generateSlug = this.generateSlug.bind(this);
this.state = {
submitButtonMeta: {
btnTitle: "Save",
btnClass: "btn btn-default",
btnIcon: null,
disabled: false
},
globalMessage: { // set when an action is performed by ActionCreation+Reducer and a message is returned
message: "",
className: ""
},
tempData: {
//the_bayaansMainCat_id : 1, // '1' refers to the 'Bayaans' parent category in admin , this ID is used here for different sort of lookups i.e. fetch available subcats for autosuggest, fetch cat ID by name under parent catID
the_bayaansMainCat_id: this.props.associatedMainCatId, // being passed from parent component to avoide redundent declaration
the_autoSuggestCatList: [],
slug: "",
the_catId: null
}
};
}
resetMessageState() {
var noMsg = {message: "", className: ""};
this.setState({globalMessage: noMsg});
}
componentDidMount() {
console.log("<NewBayanForm> (componentDidMount)");
this.props.adminFetchAutoSuggestCats_act(this.state.tempData.the_bayaansMainCat_id);
}
doSubmit(props) {
//console.log("----- submitting form -----");
//console.log(props);
this.disableSubmitButton();
// prepare data for submit request
// item_title, item_slug, content, picture, attachment, media_path, reference, tag_keywords, author_name, cat_id, date_created
var newBayanObj = {
item_title: props.titleTxt,
item_slug: this.state.tempData.slug,
content: props.videoIdTxt,
picture: "",
attachment: "",
media_path: "https://www.youtube.com/watch?v=" + props.videoIdTxt,
reference: "",
tag_keywords: props.keywordsTxt,
author_name: props.authorTxt,
cat_id: this.state.tempData.the_catId
};
this.props.adminUpdateBayaan_act(newBayaanObj)
.then(() => {
console.log("%c <NewBayanForm> (doSubmit) Updated bayaan, refetching updated bayaans list...", "color:blue;font-weight:bold;");
this.props.adminFetchArticlesByCat_act(this.props.associatedMainCatId)
.then(() => {
console.log("%c <NewBayanForm> (doSubmit) Redirecting to Gallery after update...", "color:blue;font-weight:bold;");
this.props.toggleViewFunction(); // comming from Parent Class (bayaansPage)
});
});
}
disableSubmitButton() {
console.log("<NewBayanForm> (disableSubmitButton)");
// Ref: http://stackoverflow.com/questions/18933985/this-setstate-isnt-merging-states-as-i-would-expect
var newButtonState = {
btnTitle: "Please wait... ",
btnClass: "btn btn-disabled",
btnIcon: null,
disabled: true
};
this.setState({submitButtonMeta: newButtonState});
this.resetMessageState(); // Need to reset message state when retrying for form submit after 1st failure
}
enableSubmitButton() {
console.log("<NewBayanForm> (enableSubmitButton)");
// Ref: http://stackoverflow.com/questions/18933985/this-setstate-isnt-merging-states-as-i-would-expect
var newButtonState = {btnTitle: "Save", btnClass: "btn btn-default", btnIcon: null, disabled: false};
this.setState({submitButtonMeta: newButtonState});
}
fetchCategoryId(value) {
console.log('<NewBayanForm> (fetchCategoryId) input-Value:', value); // make API call to fetch / generate category ID for this post
this.props.adminGetCatIdByName_act(value, this.state.tempData.the_bayaansMainCat_id); // '1': refers to look up under 'Bayaans' parent category for the specified category name
}
// will always receive and triggers when there are 'new props' and not old/same props
componentWillReceiveProps(nextProps) { // required when props are passed/changed from parent source. And we want to do some operation as props are changed (Ref: http://stackoverflow.com/questions/32414308/updating-state-on-props-change-in-react-form)
console.log("<NewBayanForm> (componentWillReceiveProps) nextProps: ", nextProps); // OK
//console.log("this.props : ", this.props); // OK
//console.log("nextProps.siteEssentials.actionsResult : ", nextProps.siteEssentials.actionsResult); // OK
if (nextProps.hasOwnProperty("siteEssentials")) { // if action status appeared as Done!
if (nextProps.siteEssentials.hasOwnProperty("actionsResult")) { // if action status appeared as Done!
if (nextProps.siteEssentials.actionsResult[ADMIN_GETCATID_BYNAME_Lbl] !== "FAILED") {
var clonedState = this.state.tempData;
clonedState.the_catId = nextProps.siteEssentials.actionsResult[ADMIN_GETCATID_BYNAME_Lbl];
//var newTempState = {slug: this.state.tempData.slug, the_catId: nextProps.siteEssentials.actionsResult[ADMIN_GETCATID_BYNAME_Lbl] };
this.setState({tempData: clonedState});
}
if (nextProps.siteEssentials.actionsResult[ADMIN_FETCH_AUTOSUGGESTS_Lbl] !== "FAILED") {
var clonedState = this.state.tempData;
clonedState.the_autoSuggestCatList = nextProps.siteEssentials.actionsResult[ADMIN_FETCH_AUTOSUGGESTS_Lbl];
this.setState({tempData: clonedState});
}
console.log("<NewBayanForm> (componentWillReceiveProps) new-State:", this.state);
}
}
}
render() { // rendering Edit form
const {handleSubmit} = this.props;
console.log('<NewBayanForm> (render_editForm) this.props:', this.props);
return (
<div className="adminForm">
<form onSubmit={handleSubmit(this.doSubmit.bind(this))}>
<div className="col-sm-6">
<div className="row">
<div className="col-sm-5"><label>Title:</label></div>
<div className="col-sm-7"><Field name="titleTxt" component={MySimpleInput}
defaultValue={this.props.name} type="text"
placeholder="Enter Title"/></div>
</div>
<div className="row">
<div className="col-sm-5"><label>Slug:</label></div>
<div className="col-sm-7">{this.state.tempData.slug || this.props.slug} <input
type="hidden" name="slugTxt" value={this.state.tempData.slug}/></div>
</div>
<div className="row">
<div className="col-sm-5"><label>Select Category:</label></div>
<div className="col-sm-7"><Field name="catTxt" component={MyCustomSelect}
defaultValue={this.props.category_name} type="text"
placeholder="Select or Type a New"
selectableOptionsList={this.state.tempData.the_autoSuggestCatList}
onSelectionDone={ this.fetchCategoryId.bind(this) }/>
<input type="hidden" name="catIdTxt"
value={this.state.tempData.the_catId || this.props.category_id}/>
</div>
</div>
</div>
<div className="col-sm-6">
<div className="row">
<div className="col-sm-5"><label>Youtube Video ID:</label></div>
<div className="col-sm-7"><Field name="videoIdTxt" component={MySimpleInput}
defaultValue={this.props.content} type="text"
placeholder="TsQs9aDKwrw"/></div>
<div className="col-sm-12 hint"><b>Hint: </b> https://www.youtube.com/watch?v=<span
className="highlight">TsQs9aDKwrw</span></div>
</div>
<div className="row">
<div className="col-sm-5"><label>Author/Speaker:</label></div>
<div className="col-sm-7"><Field name="authorTxt" component={MySimpleInput}
defaultValue={this.props.author} type="text"/></div>
</div>
<div className="row">
<div className="col-sm-5"><label>Tags/Keywords:</label></div>
<div className="col-sm-7"><Field name="keywordsTxt" component={MySimpleInput}
defaultValue={this.props.tag_keywords} type="text"/>
</div>
</div>
</div>
<div className="row">
<div className={this.state.globalMessage.className}>{this.state.globalMessage.message}</div>
</div>
<div className="buttonControls">
<a className="cancelBtn" onClick={this.props.toggleViewFunction}>Cancel</a>
<button className={this.state.submitButtonMeta.btnClass}
disabled={this.state.submitButtonMeta.disabled}>
{this.state.submitButtonMeta.btnTitle}</button>
</div>
</form>
</div>
);
}
}
function validate(values) { // Validate function being called on Blur
const errors = {};
if (!values.titleTxt)
errors.titleTxt = "Enter Title";
if (!values.catTxt)
errors.catTxt = "Select/Enter a Category";
if (!values.videoIdTxt)
errors.videoIdTxt = "Enter youtube video ID (follow the provided hint)";
if (!values.keywordsTxt)
errors.keywordsTxt = "Enter keywords (will help in search)";
return errors;
}
// ReduxForm decorator
const newBayanFormAdmin_reduxformObj = reduxForm({
form: "newBayanFormAdmin", // any unique name of our form
validate // totally equivelent to--> validate: validate
});
function mapStateToProps({siteEssentials}, ownProps) {
console.log("<NewBayanForm> (mapStateToProps) siteEssentials:", siteEssentials);
// 1st param is related to our Redux State, 2nd param relates to our own component props
var initialValues = {
titleTxt: ownProps.name,
slugTxt: ownProps.slug,
catTxt: ownProps.category_name,
catIdTxt: ownProps.category_id,
videoIdTxt: ownProps.content,
authorTxt: ownProps.author,
keywordsTxt: ownProps.tag_keywords
};
console.log("<NewBayanForm> (mapStateToProps) initialValues: ", initialValues);
return ({siteEssentials}, initialValues);
};
function mapDispatchToProps(dispatch) {
return bindActionCreators({
adminFetchAutoSuggestCats_act,
adminGenerateSlug_act,
adminGetCatIdByName_act,
adminAddNewBayaan_act,
adminFetchArticlesByCat_act
}, dispatch);
};
NewBayanForm = connect(mapStateToProps, mapDispatchToProps) (newBayanFormAdmin_reduxformObj(NewBayanForm));
export default NewBayanForm;