I have a list of checkboxes that receive a handler function, which changes the state when a checkbox is checked/unchecked. When I click on the checkbox, I am getting the following error:
TypeError: onChangePoll is not a function
I've tried:
Changing the handler function to an arrow function
Moving the checkbox component from it's modular file to place it inside the file where the function resides
Changed onChange to onClick
Traced it through the DOM as it renders
It seems that the handler function is being passed to the component correctly when rendering, but when I make the click, it seems to disappear.
For the code below, affiliates is an array that looks like this:
[
{
shortName: `a`
},
{
shortName: `b`
},
{
shortName: `c`
}
];
NewPoll.js
import React, { useState } from 'react';
import { Redirect } from "react-router";
import { AffiliateSelects } from './../helpers/FormElements';
import { affiliates } from './../helpers/Config';
import { initialNewPoll } from './../helpers/Config';
import { isAffiliates, validateNewPollModal } from './../helpers/FormValidator';
function NewPoll(props) {
const {
pollData,
type,
onChangePoll
} = props;
// Set headline for modal
const headline = type === `new` ? `Add a New Poll` : `Edit “${pollData.pollName.value}”`;
return(
<div className="modal-content">
<h2>{headline}</h2>
<form className="form__inputs form__inputs--modal">
<div className="form__inputs-row">
<label
className={pollData.affiliates.error ? `form__input-label form__input-label--error` : `form__input-label`}
htmlFor="affiliates"
>
{pollData.affiliates.error ? `Affiliates (${pollData.affiliates.error})` : `Affiliates`}
</label>
<AffiliateSelects
type="checkbox"
name="affiliates"
state={pollData.affiliates.value}
onChangePoll={onChangePoll}
/>
</div>
<div className="form__buttons-row">
<ul className="form__buttons-row-list">
<li className="form__buttons-row-item">
<button
className="button"
type="reset"
name="clear-filters"
onClick={(e) => onChangePoll({
name: `clearFields`
})}
>
Clear Fields
</button>
</li>
<li className="form__buttons-row-item">
<button
className="button"
type="reset"
name="create-poll"
onClick={(e) => onChangePoll({
name: `createPoll`
})}
>
{type === `new` ? `Create Poll` : `Save Edits`}
</button>
</li>
</ul>
</div>
</form>
</div>
)
}
export const NewPollModal = (props) => {
const {
pollData,
setPollData,
type
} = props;
const onChangePoll = (el) => {
// Validate the poll data being sent through
let validPoll;
switch(el.name) {
case `affiliates`:
validPoll = isAffiliates(el.value, pollData.affiliates.value);
break;
case `clearFields`:
validPoll = initialNewPoll;
break;
default:
break;
}
if (el.name === `createPoll`) {
// Check to make sure all of the fields are valid
const isPollValid = validateNewPollModal(pollData);
if (isPollValid.valid) {
// If they are valid, send the state to the poll edit page
setPollData({
...pollData,
valid: true
});
} else {
// If they are not valid, create errors on the form
setPollData(isPollValid.validPoll);
}
} else if (el.name === `clearFields`) {
setPollData(validPoll);
} else {
setPollData({
...pollData,
[`${el.name}`]: {
valid: validPoll.valid,
error: validPoll.error,
value: validPoll.value
}
});
}
}
if (pollData.valid) {
return(
<Redirect
to={{
pathname: "/poll",
state: {
affiliates: pollData.affiliates.value
}
}}
/>
)
} else {
return(
<NewPoll
pollData={pollData}
type={type}
onChangePoll={onChangePoll}
/>
)
}
}
FormElements.js
import React from 'react';
import { affiliates } from './Config';
function ListItem(props) {
const {
aff,
type,
state,
onChangePoll
} = props;
// If we are in edit mode, add the checkmark if the affiliate is in state
const checked = state.includes(aff) ? true : false;
const affiliates = {
name: `affiliates`,
value: aff
};
if (type === `checkbox`) {
console.log(onChangePoll);
return(
<li className="affiliate-selects__items">
<input
className="affiliate-selects__checkbox"
type={type}
name="affiliates"
id={`affiliates-${aff}`}
checked={checked}
onChange={() => onChangePoll(affiliates)}
/>
<label className="affiliate-selects__label" htmlFor={`affiliates-${aff}`}>{aff}</label>
</li>
)
} else if (type === `list`) {
return(
<li className="affiliate-selects__items">{aff}</li>
)
}
}
function List(props) {
const {
type,
state,
affList,
onChangePoll
} = props;
let listClass;
if (type === `checkbox`) {
listClass = `affiliate-selects affiliate-selects--checkbox`;
} else if (type === `list`) {
listClass = `affiliate-selects affiliate-selects--list`;
}
return(
<ul className={listClass}>
{affList.map((aff) =>
<ListItem
key={`affiliate-selects-${aff.shortName}`}
aff={aff.shortName}
type={type}
state={state}
onChangePoll={onChangePoll}
/>
)}
</ul>
)
}
/**
* Displays a list of affiliates, eiter as a checkbox list, or as a regular list.
* #param {String} props.type Will display the affiliates as a list of checkboxes for a display * list
* Ex: `checkbox` || `list`
* #param {Function} props.handleOnChange Event handler function that changes the items that are filtered
* (optional)
* #param {String} props.prefix Will prefix all input IDs so form inputs will work correctly
* (optional)
* #param {Array} props.affArray Array of affiliate string names
* Ex: [`nj`, `oregonlive`, `masslive`]
* (optional)
*/
export function AffiliateSelects(props) {
const type = props.type || `list`;
const state = props.state || [];
const {
affArray,
onChangePoll
} = props;
// If the user doens't pass in an array, list all of the affiliates from the config
let affList;
if (affArray === undefined || affArray.length === 0) {
affList = affiliates;
} else {
affList = affArray.reduce((accum, aff) => {
accum.push({shortName: aff});
return accum;
}, []);
}
return(
<List
type={type}
state={state}
affList={affList}
onChangePoll={onChangePoll}
/>
)
}
The expected behavior would be that the user clicks the checkbox next to an affiliate, and that affiliate gets validated by the handler and saved to state.
Related
I have tried and I keep running into react 302 errors with multiple instances of react and issues with state. I am still learning react and I always get stuck when it comes to class and function components. I am trying to convert this into a class based component since that is what I am familiar with using and need to integrate into another class based component.
const { useState, useEffect, } = React;
const { useSelector } = ReactRedux;
import { React, AllWidgetProps, getAppStore, appActions, ReactRedux, WidgetProps, WidgetManager, IMState } from 'jimu-core';
import { Button, Label, Row, Col, Select, Option } from 'jimu-ui';
import defaultMessages from './translations/default';
/**
* This widget will show how to control widget state for a collapsible sidebar widget and a widget within the widget controller widget.
*/
export default function Widget(props: AllWidgetProps<{}>) {
// Establish state properties, initial values and their corresponding set state actions
const [sidebarWidgetId, setSidebarWidgetId] = useState(null as string);
const [openCloseWidgetId, setOpenCloseWidgetId] = useState(null as string);
const [sidebarVisible] = useState(true as boolean);
const [openness, setOpenness] = useState(false as boolean);
const [appWidgets, setAppWidgets] = useState({} as Object);
const [widgetsArray, setWidgetsArray] = useState([] as Array<any>);
const [sidebarWidgetsArray, setSidebarWidgetsArray] = useState([] as Array<any>);
// Get the widget state - because the sidebar state may change in the runtime, via Redux's useSelector hook
const widgetState = useSelector((state: IMState) => {
const widgetState = state.widgetsState[sidebarWidgetId];
return widgetState;
});
// Update the appWidgets property once, on page load
useEffect(() => {
const widgets = getAppStore().getState().appConfig.widgets;
setAppWidgets(widgets);
}, []);
// Update the widgetsArray and sidebarWidgetsArray properties every time appWidgets changes
useEffect(() => {
if (appWidgets) {
const widgetsArray = Object.values(appWidgets);
setWidgetsArray(widgetsArray);
setSidebarWidgetsArray(widgetsArray.filter(w => w.uri === 'widgets/layout/sidebar/'));
}
}, [appWidgets]);
// Toggle the sidebar widget
const handleToggleSidebar = (): void => {
// If widget state's collapse property is true, collapse
if (widgetState && widgetState.collapse === true) {
getAppStore().dispatch(appActions.widgetStatePropChange(
sidebarWidgetId,
'collapse',
!sidebarVisible
));
}
// If widget state's collapse property is false, expand
else if (widgetState && widgetState.collapse === false) {
getAppStore().dispatch(appActions.widgetStatePropChange(
sidebarWidgetId,
'collapse',
sidebarVisible
));
}
else {
alert(
defaultMessages.sidebarAlert
)
}
};
// Load the widget class prior to executing the open/close actions
const loadWidgetClass = (widgetId: string): Promise<React.ComponentType<WidgetProps>> => {
if (!widgetId) return;
const isClassLoaded = getAppStore().getState().widgetsRuntimeInfo?.[widgetId]?.isClassLoaded;
if (!isClassLoaded) {
return WidgetManager.getInstance().loadWidgetClass(widgetId);
} else {
return Promise.resolve(WidgetManager.getInstance().getWidgetClass(widgetId));
}
};
// Open widget method
const handleOpenWidget = (): void => {
// Construct the open action, then run the loadWidgetClass method, dipatch the open action
// and, finally, set the openness to true
const openAction = appActions.openWidget(openCloseWidgetId);
loadWidgetClass(openCloseWidgetId).then(() => {
getAppStore().dispatch(openAction);
}).then(() => { setOpenness(true) });
};
// Close widget method
const handleCloseWidget = (): void => {
// Construct the close action, then run the loadWidgetClass function, dipatch the close action
// and, finally, set the openness to false
const closeAction = appActions.closeWidget(openCloseWidgetId);
loadWidgetClass(openCloseWidgetId).then(() => {
getAppStore().dispatch(closeAction);
}).then(() => { setOpenness(false) });
};
// Handler for the openness toggle button
const handleToggleOpennessButton = (): void => {
// Check the openness property value and run the appropriate function
if (openness === false) { handleOpenWidget(); }
else if (openness === true) { handleCloseWidget(); }
else { console.error(defaultMessages.opennessError) }
};
// Handler for the sidebar selection
const handleSidebarSelect = evt => {
setSidebarWidgetId(evt.currentTarget.value);
};
// Handler for the open/close selection
const handleOpenCloseSelect = evt => {
setOpenCloseWidgetId(evt.currentTarget.value);
};
return (
<div className='widget-control-the-widget-state jimu-widget m-2' style={{ width: '100%', height: '100%', maxHeight: '800px', padding: '0.5em' }}>
<h6
title={defaultMessages.title}
>
{defaultMessages.title}
</h6>
{sidebarWidgetsArray && sidebarWidgetsArray.length > 0 &&
<Row className='p-2 justify-content-between align-items-center'>
<Col className='col-sm-6'>
<Label
title={defaultMessages.sidebarLabel}
>
{defaultMessages.sidebarLabel}
</Label>
<Select
defaultValue=''
onChange={handleSidebarSelect}
placeholder={defaultMessages.sidebarPlaceholder}
title={defaultMessages.sidebarPlaceholder}
>
{/* Use the sidebarWidgetsArray to populate the select's options */}
{
sidebarWidgetsArray.map((w) => <Option
value={w.id}
>
{w.label}
</Option>)
}
</Select>
</Col>
{sidebarWidgetId &&
<Col className='col-sm-6'>
<Button
onClick={handleToggleSidebar}
htmlType='submit'
type='primary'
title={defaultMessages.sidebarButtonLabel}
>
{defaultMessages.sidebarButtonLabel}
</Button>
</Col>
}
</Row>
}
{widgetsArray && widgetsArray.length > 0 &&
<Row className='p-2 justify-content-between align-items-center'>
<Col className='col-sm-6'>
<Label
title={defaultMessages.widgetControllerWidgetLabel}
>
{defaultMessages.widgetControllerWidgetLabel}
</Label>
<Select
defaultValue=''
onChange={handleOpenCloseSelect}
placeholder={defaultMessages.widgetControllerWidgetPlaceholder}
title={defaultMessages.widgetControllerWidgetPlaceholder}
>
{/* Use the widgetsArray to populate the select's options */}
{
widgetsArray.map((w) => (
<Option
value={w.id}
>
{w.label}
</Option>
))
}
</Select>
</Col>
{openCloseWidgetId &&
<Col className='col-sm-6'>
<Button
onClick={handleToggleOpennessButton}
htmlType='submit'
type='primary'
title={defaultMessages.widgetControllerWidgetButton}
>
{defaultMessages.widgetControllerWidgetButton}
</Button>
</Col>
}
</Row>
}
</div>
);
};
I'm pulling countries from the Restcountries API and if the current state of the array has more than one or less than or equal to ten countries, I want to list the country names along with a 'show' button next to each one. The show button should display what's in the return (render) of my Country function. In the App function, I wrote a handler for the button named handleViewButton. I'm confused on how to filter the element in the Countries function in the else conditional statement in order to display the Country. I tried passing handleViewButton to the Button function, but I get an error 'Uncaught TypeError: newSearch.toLowerCase is not a function'. I really just want to fire the Country function to display the country button that was pressed.
App.js
import React, { useState, useEffect } from 'react';
import './App.css';
import axios from 'axios';
const Country = ({country}) => {
return (
<>
<h2>{country.name}</h2>
<p>capital {country.capital}</p>
<p>population {country.population}</p>
<br/>
<h3>languages</h3>
{country.languages.map(language => <li key={language.name}>{language.name}</li>)}
<br/>
<img src={country.flag} alt="country flag" style={{ width: '250px'}}/>
</>
);
}
const Countries = ({countries, handleViewButton}) => {
const countriesLen = countries.length;
console.log(countriesLen)
if (countriesLen === 0) {
return (
<p>Please try another search...</p>
);
} else if (countriesLen === 1) {
return (
<ul>
{countries.map((country, i) => <Country key={i} countriesLen={countriesLen} country={country}/>)}
</ul>
);
} else if (countriesLen > 10) {
return (
<div>
<p>Too many matches, specify another filter</p>
</div>
);
} else {
return (
<ul>
{countries.map((country, i) => <li key={i}>{country.name}<Button handleViewButton={handleViewButton}/></li>)}
</ul>
)
};
};
const Button = ({handleViewButton}) => {
return (
<button onClick={handleViewButton}>Show</button>
);
};
const Input = ({newSearch, handleSearch}) => {
return (
<div>
find countries <input value={newSearch} onChange={handleSearch}/>
</div>
);
};
function App() {
const [countries, setCountries] = useState([]);
const [newSearch, setNewSearch] = useState('');
const handleSearch = (event) => {
const search = event.target.value;
setNewSearch(search);
};
const handleViewButton = (event) => {
const search = event.target.value;
setNewSearch(countries.filter(country => country === search));
};
const showCountrySearch = newSearch
? countries.filter(country => country.name.toLowerCase().includes(newSearch.toLowerCase()))
: countries;
useEffect(() => {
axios
.get('https://restcountries.eu/rest/v2/all')
.then(res => {
setCountries(res.data);
console.log('Countries array loaded');
})
.catch(error => {
console.log('Error: ', error);
})
}, []);
return (
<div>
<Input newSearch={newSearch} handleSearch={handleSearch}/>
<Countries countries={showCountrySearch} handleViewButton={handleViewButton}/>
</div>
);
};
export default App;
you can use a displayCountry to handle the country that should be displayed. Most often you would use an id, but I'm using here country.name since it should be unique.
Then you would use matchedCountry to find against your list of countries.
After that, a onHandleSelectCountry to select a given country. if it's already selected then you could set to null to unselect.
Finally, you would render conditionally your matchedCountry:
const Countries = ({countries}) => {
const [displayCountry, setDisplayCountry] = useState(null);
const countriesLen = countries.length;
const matchedCountry = countries.find(({ name }) => name === displayCountry);
const onHandleSelectCountry = (country) => {
setDisplayCountry(selected => {
return selected !== country.name ? country.name : null
})
}
if (countriesLen === 0) {
return (
<p>Please try another search...</p>
);
} else if (countriesLen === 1) {
return (
<ul>
{countries.map((country, i) => <Country key={i} countriesLen={countriesLen} country={country}/>)}
</ul>
);
} else if (countriesLen > 10) {
return (
<div>
<p>Too many matches, specify another filter</p>
</div>
);
} else {
return (
<>
<ul>
{countries.map((country, i) => <li key={i}>{country.name}<Button handleViewButton={() => onHandleSelectCountry(country)}/></li>)}
</ul>
{ matchedCountry && <Country countriesLen={countriesLen} country={matchedCountry}/> }
</>
)
};
};
I can only help to point out some guidelines.
First: The button does not have value attribute. Hence what you will get from event.target.value is always blank.
const Button = ({handleViewButton}) => {
return (
<button onClick={handleViewButton}>Show</button>
);};
First->Suggestion: Add value to the button, of course you need to pass the value in.
const Button = ({handleViewButton, value}) => {
return (
<button onClick={handleViewButton} value={value}>Show</button>
);};
Second: To your problem 'Uncaught TypeError: newSearch.toLowerCase is not a function'. Filter always returns an array, not a single value. if you do with console or some sandbox [1,2,3].filter(x=>x===2) you will get [2] not 2.
const handleViewButton = (event) => {
const search = event.target.value;
setNewSearch(countries.filter(country => country === search));
};
Second->Suggestion: To change it to get the first element in array, since country(logically) is unique.
const result = countries.filter(country => country === search)
setNewSearch(result.length>0?result[0]:"");
A better approach for array is find, which always return first result and as a value. E.g. [1,2,2,3].find(x=>x===2) you will get 2 not [2,2] or [2].
countries.find(country => country === search)
I have those 2 components
FormComponent
const FormComponent = (props) => {
//keep the current step in the component state
const [currentStep, setCurrentStep] = useState(props.step);
const [componentData, setComponentData] = useState(null);
const [componentQuestions, setQuestions] = useState(null);
const [errors, setErrors] = useState();
const updateUserResponse = props.updateUserResponse;
const renderStepsTracker = props.renderStepsTracker;
/**
* Get the component content after mount
*/
useEffect(() => {
if (componentData) {
setQustionsData(componentData);
}
//fire if componentData has changed or error object is changed
}, [errors,componentData]);
/**
* Get the component content after mount
*/
useEffect(() => {
if (currentStep && !componentData) {
let stepData = currentStep.stepdata;
stepData.update = getUpdateTime();
setComponentData(stepData);
}
});
/**
* Get the current time in milliseconds
*/
const getUpdateTime = () => {
var d = new Date();
var n = d.getTime();
return n
}
/**
* Extract the questions from the step object and
* set the state accordingly
*/
const setQustionsData = (stepData) => {
let questions = stepData.questions;
//assign a react component to each question
questions.map((question, key) => {
const component = getComponent(question);
const question_id = question.question_id;
questions[key].component = component;
questions[key].error = (null != errors && 'undefined' !== typeof errors[question_id]) ? errors[question_id] : '';
})
setQuestions(questions);
}
/**
* a callback function to validate the current form
* #param {object} step
*/
const validateForm = (step) => {
const formErrors = props.validateForm(step);
setErrors(formErrors)
}
/**
* Render the form
*/
const renderForm = () => {
return null !== componentQuestions ? (
<>
<div className="row full-height">
<div className="column">
<div className="form-wrapper">
<div className="questions">
{componentQuestions.map(question => {
return (
<>
{question.component}
</>
)
})}
</div>
<NextStepButton
step={currentStep}
nextStepCallback={props.jumpToStep}
validateForm={validateForm}
/>
</div>
</div>
</div>
</>
) : ''
}
/**
* Render the image in case it exists
*/
const renderImage = () => {
return (
<div className="full-height">
<img className="full-height-image" src={componentData.image} alt="" />
</div>
)
}
/**
* Get the question component
*/
const getComponent = (question) => {
switch (question.question_type) {
case "text":
return (
<div className="field-wrap">
<label>
<span className="question-label">{question.question_label}</span>
<span className="question-title">{question.question_title}</span>
<input type="text"
name={question.question_id}
onChange={updateUserResponse}
required={question.required}
/>
{question.error ?
<span className={"error-message " + question.error.reason}>{question.error_message}</span>
: ''}
</label>
</div>
)
}
}
return (
componentData ?
<>
<div className="row full-height expanded">
{componentData.image ?
<div className="column full-height col-collapse image-wrap">
{renderImage()}
</div>
: ''}
<div className="column full-height questions-container">
{renderStepsTracker()}
{renderForm()}
</div>
</div>
</>
: ''
)} enter code hereexport default FormComponent
**
NextStepButton
**
const NextStepButton = (props) => {
const step = props.step
const validateForm = props.validateForm
return props.step.nextButtonText ? (
<span className="next-step-button" onClick={() => {
validateForm( step )
}}>
{step.nextButtonText}
</span>
) : ''
}
export default NextStepButton
When the user clicks on the next step the form is validated and returns an array of errors,
The array of errors is set to the component state and then supposed to display the error
For some reason the error appears only after the user clicks 3 times on the next step button
Why is that ?
EDIT
this is the main component that contains the global form validation
const MainContainer = () => {
//set state variables
const [nextButtonText, setNextButtonText] = useState()
const [previousButtonText, setPreviousButtonText] = useState()
const [backButtonCls, setBackButtonCls] = useState()
const [currentStep, setCurrentStep] = useState()
const [stepsData, setStepsData] = useState()
const [currentStepNum, setCurrentStepNum] = useState(null)
const [classNames, setclassNames] = useState(null)
const [userData, setUserData] = useState({})
//set refrence for callbacks that use this component state
const ref = useRef({
stepsData: stepsData,
currentStep: currentStep
})
/**
* On Initial state
*/
useEffect(() => {
if (!stepsData) {
getStepsData().then((response) => {
initProcess(response)
})
}
})
/**
* Init the process after getting data from the server
*/
const initProcess = (response) => {
const steps = setStepsComponents(response.data.steps)
const initialStep = steps[0];
ref.current.stepsData = steps
setStepsData(steps)
setStep(initialStep, 1)
}
/**
* Form submission callback
* Loop over step questions and check for the appropriate userdata
*/
const validateForm = (submittedStep) => {
let errors = {};
if ('null' !== typeof submittedStep.stepdata.questions) {
const questions = submittedStep.stepdata.questions
questions.map((question, key) => {
const question_name = question.question_id
//if there is an error add it to the questions collection
if (question.required && !userData[question_name]) {
errors[question.question_id] = {
reason : 'required'
}
}
})
}
if (errors) {
return errors
}
return true;
}
/**
* Capture user settings on change
*/
const updateUserResponse = (event) => {
userData[event.target.name] = event.target.value;
setUserData(userData)
}
/**
* Set the appropriate step component
* #param {OBJECT} steps
*/
const setStepsComponents = (steps) => {
steps.map((step, key) => {
switch (steps[key]['component']) {
case "onboarding":
steps[key]['component'] = <Onboarding step={step} setUserData />
break;
case "questions":
steps[key]['component'] = <FormComponent
step={step}
renderStepsTracker={renderStepsTracker}
updateUserResponse={updateUserResponse}
validateForm={validateForm}
/>
break;
}
})
return steps
}
/**
* Callback function that creates the steps tracker on each component
*/
const renderStepsTracker = () => {
return ref.current.stepsData ? (
<>
<div className="stepsTracker">
<div className="row">
<div className="column">
<div className="stepsTrackerWrap">
{ref.current.stepsData.map((step, key) => {
const activeClass = ref.current.currentStep === key ? "active" : ""
return key > 0 ? (
<>
<div className={"stepTracker " + activeClass}>
<div className="stepTrackerImage">
<img src={step.icon} alt="" />
</div>
<label>{step.stepdata.title}</label>
</div>
<div className="stepSpacer"></div>
</>
) : ''
})}
</div>
</div>
</div>
</div>
</>
) : ''
}
/**
* Render the steps component if data is available
*/
const renderSteps = () => {
return stepsData ? (
<StepZilla
steps={stepsData}
onStepChange={onstepChange}
nextButtonText={nextButtonText}
showSteps={false}
backButtonText={previousButtonText}
backButtonCls={backButtonCls}
/>
) : ''
}
/**
* Set the data for the current step
* #param {object} step
*/
const setStep = (step, stepNum) => {
setNextButtonText(step.showNextButton ? step.nextButtonText : true)
setPreviousButtonText(step.previousButtonText)
setBackButtonCls(step.backButtonCls)
setCurrentStep(step)
setCurrentStepNum(stepNum)
ref.current.currentStep = stepNum
//add bottom padding in case the next button appears
if (step.showNextButton) {
setclassNames('padding')
} else {
setclassNames(' ')
}
}
/**
* Perform actions when step is changed
* #param {step} step
*/
const onstepChange = (stepNum) => {
if ('undefined' === typeof stepNum) {
stepNum = '0';
}
setCurrentStepNum(stepNum)
const nextStep = stepsData[stepNum];
setStep(nextStep, stepNum)
}
return (
<>
<div className={'step-progress ' + classNames}>
{renderSteps()}
</div>
</>
)
}
export default MainContainer
I am working on a grocery list project. With this project, when the user enters an item, I want to give the ability to edit said item. I am storing everything inside an array of objects in my state. The structure of my objects is:
{
product: 'toast',
category: 'bakery',
quantity: 3,
type: 'each
},
{
product: 'apple',
category: 'produce',
quantity: 2,
type: 'each'
},
{
product: 'hamburger',
category: 'meat',
quantity: 1,
type: 'Lb'
}
What I want to be able to do is have the user select one of those objects inside a card type function, then update it. Currently, I can add items to the list, but I can not update them.
I have tried setList(list[i].txt=v) and setList(list=>list[i].product=v) plus other variations trying to target the specific object. Any ideas would be greatly appreciated.
The following is my main app.js code. NOTE: const Change() is where I am trying to update the object. The variables that I am passing in come from my item.js code
import React ,{useState,useEffect} from 'react';
import List from './components/list';
import Header from './components/header';
function App() {
const [list, setList] = useState([]);
const Add = (p, c, q, t, crt) => {
console.log({product: p, category: c, quantity: q, type: t, cart: crt})
setList(list=>[...list,{product:p, category:c, quantity:q, type:t, cart: crt}])
}
const Change = (i, txt, v) => {
//setList(list[i].txt=v)
console.log('id: ' + i + ' topic: ' + txt + ' value: ' +v)
setList(list=>list[i].product=v)
}
const Delete = () => {
}
return (
<div>
{console.log(list)}
<h1>Grocery List App</h1>
<Header add={Add}/>
<List set={setList} lst={list} chg={Change} del={Delete} />
</div>
);
}
export default App;
This next code is my list.js file. I am iterating over my list state and creating the individual 'cards' for each item.
import React from 'react';
import Card from './item';
const List = (props) => {
const productChange = (txt, v) => {
console.log(props.lst[v].product)
}
const quantityChange = () => {
}
const cartChange = () => {
}
return(
<div>
<p>To Find:</p>
<ul>
{ props.lst.map((item, index) =>
item.cart === false ?
<Card
key={item.index}
index={index}
value={index}
cart={item.cart}
item={item.product}
units={item.quantity}
unitType={item.type}
cartChange={cartChange}
itemChange={productChange}
quantityChange={quantityChange}
change={props.chg}
delete={props.del}/>
: null)
}
</ul>
<p>Found</p>
<ul>
{ props.lst.map((item, index) =>
item.cart === true ?
<Card
key={item.index}
index={index}
value={index}
cart={item.cart}
item={item.product}
units={item.quantity}
unitType={item.unit}
cartChange={cartChange}
itemChange={productChange}
quantityChange={quantityChange}
change={props.chg}
delete={props.del}/>
: null)
}
</ul>
</div>
)
}
export default List;
This is the code for item.js. This is the final spot where I display the information from the list. NOTE: change() from the first file is getting called here when I change the text of an input.
import React from 'react';
const Card=(props)=>{
return (
<li key={props.value}>
<div>
<input
type="checkbox"
checked={props.cart}
onChange={(e)=> {props.cartChange(props.value)}} />
</div>
<div>
<input
id={'product '+ props.value}
className='update'
type='text'
value={props.item}
onChange={(e) =>
props.change(props.value,'product', e.target.value)
}
/>
<br/>
<input
id='quantityValue'
className='update'
type='number'
value={props.units}
// onChange={(e)=>
props.quantityChange(e.target.value, props.value)}
/>
<span id='quantityType' className='update'>{props.unitType}
</span>
</div>
<div>
<button
id='save-button'
type='button'
onClick={(e) => { props.change(
props.item,
props.units,
props.unitType,
props.value)
}
}>✓ save</button>
<button
id='delete-button'
type='button'
onClick={(e) => {props.delete(props.value)}}>✗ delete</button>
</div>
</li>
)
}
export default Card;
you want to call setList with a list where you just amend that one object.
You can use list.map() for this. Ideally you want to add ID field to your objects.
But even without ID you can use index:
setList(list.map((product, index)=>{index == i ? v : product}))
or more verbose:
const Change = (i, txt, v) =>{
const newList = list.map((product, index) => {
return index == i ? v : product
});
setList(newList);
}
You want to implement change function. Is this right?
Try this one.
const Change = (i,txt,v) =>{
setList(list.map((e, ei) => {
if (ei === i) {
e.product = v;
return e;
}
return e;
}));
}
I'm using a manually crafted form generator and using React Final Form to manage the state and data. The problem now is, I need from a component outside the form to read the data before is even submitted to show the user the actual status of how the input is looking.
import React, { Component } from 'react'
import PropTypes from 'prop-types';
import { I18n } from 'react-i18nify';
import * as INPUTTYPES from '../../constants/inputTypes';
import { CONCAT_ID_BASES } from '../../constants/config';
import 'babel-polyfill';
import { Form, Field } from 'react-final-form'
const weblog = require('webpack-log');
const log = weblog({ name: 'wds' }) // webpack-dev-server
class FormGenerator extends Component {
static propTypes = {
fields: PropTypes.any,
prefix: PropTypes.any,
children: PropTypes.any
}
state = {
data: {}
}
sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
onSubmit = async values => {
await this.sleep(300)
window.alert(JSON.stringify(values, 0, 2))
}
static simpleMemoize = fn => {
let lastArg
let lastResult
return arg => {
if (arg !== lastArg) {
lastArg = arg
lastResult = fn(arg)
}
return lastResult
}
}
static textValidate = FormGenerator.simpleMemoize(async value => {
if (!value) {
return I18n.t('error-no-text-written');
}
//await sleep(400)
if (value.trim().length() > 0) {
return I18n.t('error-no-text-found');
}
})
static createInput = (newKey, value, validate) => {
let data = {
type: value.type,
//disabled: typeof value.editable !== "undefined" ? !value.editable : false,
className: "form-control",
id: `${newKey}`,
value: value.value
}
return <Field key={newKey} name={data.id} validate={validate}>
{({ input, meta }) => (
<div className="form-group col-md-6">
<label htmlFor={`${newKey}`}>{I18n.t(`${newKey}`)}</label>
<input {...data} {...input} />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</Field>
}
static createSelectInput = (newKey, value) => {
let data = {
type: value.type,
disabled: typeof value.editable !== "undefined" ? !value.editable : false,
className: "form-control",
id: `${newKey}`,
value: value.value
}
return <React.Fragment key={newKey}>
<div className="form-group col-md-6">
<label htmlFor={`${newKey}`}>{I18n.t(`${newKey}`)}</label>
<input {...data} />
</div>
</React.Fragment>
}
initialValues = function () {
let { prefix, fields } = this.props;
prefix = prefix ? prefix + CONCAT_ID_BASES : '';
fields ? fields.map((field) => {
const newKey = `${prefix}${field.key}`
this.setState((prevState) => {
let newData = { ...prevState.data };
newData[newKey] = field.value.value;
return { data: newData };
})
}) : null;
}
componentDidMount() {
this.initialValues();
}
componentDidUpdate() {
//console.log(this.state)
//this.props.suscribeCallback(values)
}
inputGenerator(field, prefix) {
const { key, value } = field;
const { type } = value;
const textValidate = FormGenerator.textValidate;
const newKey = `${prefix}${key}`
let element = null;
const createInput = FormGenerator.createInput;
switch (true) {
case new RegExp(INPUTTYPES.TEXT.join("|"), "i").test(type):
value.type = "text";
element = createInput(newKey, value, textValidate)
break;
case new RegExp(INPUTTYPES.NUMBER.join("|"), "i").test(type):
value.type = "number";
element = createInput(newKey, value, textValidate)
break;
case new RegExp(INPUTTYPES.SELECT.join("|"), "i").test(type):
break;
default:
log.error("DATA NOT ITENDIFIED TYPE:" + type, key, value);
break;
}
return element;
}
render() {
let fields = this.props.fields;
let { prefix } = this.props;
prefix = prefix ? prefix + CONCAT_ID_BASES : ''
const { inputGenerator, onSubmit } = this;
return (
<Form
onSubmit={onSubmit}
initialValues={this.state.data}
render={({ values }) => {
return <div className="form-row">
{fields.map((field) => {
return inputGenerator(field, prefix);
})}
<pre>{JSON.stringify(values, 0, 2)}</pre>
</div>
}} />
)
}
}
export default FormGenerator;
And calling it like this:
{
detalles ? (() => {
return <FormGenerator
suscribeCallback={this.formDataChange}
prefix={this.props.prefix}
fields={detalles} />
})() : null
}
But now the issue is, I need to read values outside the <Form/> so I can read it in it's parent.
If I include a call back and toss it into the render method this.props.suscribeCallback(values) it will try to call it so much it will crash the site. Of course this is not a valid solution but I don't know how to solve it.
I'm kind of new to Reactjs so appologize if this is a beginner's mistake
If you need form data outside of the form, I'd suggest using something like the patterns explored in the Redux Example, where a FormStateToRedux component is listening to changes in form state and sending it...somewhere. It needn't be to Redux.