What I'm trying to achieve is a textarea that starts out as a single line but will grow up to 4 lines and at that point start to scroll if the user continues to type. I have a partial solution kinda working, it grows and then stops when it hits the max, but if you delete text it doesn't shrink like I want it to.
This is what I have so far.
export class foo extends React.Component {
constructor(props) {
super(props);
this.state = {
textareaHeight: 38
};
}
handleKeyUp(evt) {
// Max: 75px Min: 38px
let newHeight = Math.max(Math.min(evt.target.scrollHeight + 2, 75), 38);
if (newHeight !== this.state.textareaHeight) {
this.setState({
textareaHeight: newHeight
});
}
}
render() {
let textareaStyle = { height: this.state.textareaHeight };
return (
<div>
<textarea onKeyUp={this.handleKeyUp.bind(this)} style={textareaStyle}/>
</div>
);
}
}
Obviously the problem is scrollHeight doesn't shrink back down when height is set to something larger. Any suggestion for how I might be able to fix this so it will also shrink back down if text is deleted?
ANOTHER SIMPLE APPROACH (without an additional package)
export class foo extends React.Component {
handleKeyDown(e) {
e.target.style.height = 'inherit';
e.target.style.height = `${e.target.scrollHeight}px`;
// In case you have a limitation
// e.target.style.height = `${Math.min(e.target.scrollHeight, limit)}px`;
}
render() {
return <textarea onKeyDown={this.handleKeyDown} />;
}
}
The problem when you delete the text and textarea doesn't shrink back is because you forget to set this line
e.target.style.height = 'inherit';
Consider using onKeyDown because it works for all keys while others may not (w3schools)
In case you have padding or border of top or bottom. (reference)
handleKeyDown(e) {
// Reset field height
e.target.style.height = 'inherit';
// Get the computed styles for the element
const computed = window.getComputedStyle(e.target);
// Calculate the height
const height = parseInt(computed.getPropertyValue('border-top-width'), 10)
+ parseInt(computed.getPropertyValue('padding-top'), 10)
+ e.target.scrollHeight
+ parseInt(computed.getPropertyValue('padding-bottom'), 10)
+ parseInt(computed.getPropertyValue('border-bottom-width'), 10);
e.target.style.height = `${height}px`;
}
I hope this may help.
you can use autosize for that
LIVE DEMO
import React, { Component } from 'react';
import autosize from 'autosize';
class App extends Component {
componentDidMount(){
this.textarea.focus();
autosize(this.textarea);
}
render(){
const style = {
maxHeight:'75px',
minHeight:'38px',
resize:'none',
padding:'9px',
boxSizing:'border-box',
fontSize:'15px'};
return (
<div>Textarea autosize <br/><br/>
<textarea
style={style}
ref={c=>this.textarea=c}
placeholder="type some text"
rows={1} defaultValue=""/>
</div>
);
}
}
or if you prefer react modules https://github.com/andreypopp/react-textarea-autosize
Just use useEffect hook which will pick up the height during the renderer:
import React, { useEffect, useRef, useState} from "react";
const defaultStyle = {
display: "block",
overflow: "hidden",
resize: "none",
width: "100%",
backgroundColor: "mediumSpringGreen"
};
const AutoHeightTextarea = ({ style = defaultStyle, ...etc }) => {
const textareaRef = useRef(null);
const [currentValue, setCurrentValue ] = useState("");// you can manage data with it
useEffect(() => {
textareaRef.current.style.height = "0px";
const scrollHeight = textareaRef.current.scrollHeight;
textareaRef.current.style.height = scrollHeight + "px";
}, [currentValue]);
return (
<textarea
ref={textareaRef}
style={style}
{...etc}
value={currentValue}
onChange={e=>{
setCurrentValue(e.target.value);
//to do something with value, maybe callback?
}}
/>
);
};
export default AutoHeightTextarea;
Really simple if you use hooks "useRef()".
css:
.text-area {
resize: none;
overflow: hidden;
min-height: 30px;
}
react componet:
export default () => {
const textRef = useRef<any>();
const onChangeHandler = function(e: SyntheticEvent) {
const target = e.target as HTMLTextAreaElement;
textRef.current.style.height = "30px";
textRef.current.style.height = `${target.scrollHeight}px`;
};
return (
<div>
<textarea
ref={textRef}
onChange={onChangeHandler}
className="text-area"
/>
</div>
);
};
you can even do it with react refs. as setting ref to element
<textarea ref={this.textAreaRef}></textarea> // after react 16.3
<textarea ref={textAreaRef=>this.textAreaRef = textAreaRef}></textarea> // before react 16.3
and update the height on componentDidMount or componentDidUpdate as your need. with,
if (this.textAreaRef) this.textAreaRef.style.height = this.textAreaRef.scrollHeight + "px";
actually you can get out of this with useState and useEffect
function CustomTextarea({minRows}) {
const [rows, setRows] = React.useState(minRows);
const [value, setValue] = React.useState("");
React.useEffect(() => {
const rowlen = value.split("\n");
if (rowlen.length > minRows) {
setRows(rowlen.length);
}
}, [value]);
return (
<textarea rows={rows} onChange={(text) => setValue(text.target.value)} />
);
}
Uses
<CustomTextarea minRows={10} />
I like using this.yourRef.current.offsetHeight. Since this is a textarea, it wont respond to height:min-content like a <div style={{height:"min-content"}}>{this.state.message}</div> would. Therefore I don't use
uponResize = () => {
clearTimeout(this.timeout);
this.timeout = setTimeout(
this.getHeightOfText.current &&
this.setState({
heightOfText: this.getHeightOfText.current.offsetHeight
}),
20
);
};
componentDidMount = () => {
window.addEventListener('resize', this.uponResize, /*true*/)
}
componentWillUnmount = () => {
window.removeEventListener('resize', this.uponResize)
}
but instead use
componentDidUpdate = () => {
if(this.state.lastMessage!==this.state.message){
this.setState({
lastMessage:this.state.message,
height:this.yourRef.current.offsetHeight
})
}
}
on a hidden div
<div
ref={this.yourRef}
style={{
height:this.state.height,
width:"100%",
opacity:0,
zIndex:-1,
whiteSpace: "pre-line"
})
>
{this.state.message}
</div>
Using hooks + typescript :
import { useEffect, useRef } from 'react';
import type { DetailedHTMLProps, TextareaHTMLAttributes } from 'react';
// inspired from : https://stackoverflow.com/a/5346855/14223224
export const AutogrowTextarea = (props: DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>) => {
const ref = useRef<HTMLTextAreaElement>(null);
let topPadding = 0;
let bottomPadding = 0;
const resize = () => {
ref.current.style.height = 'auto';
ref.current.style.height = ref.current.scrollHeight - topPadding - bottomPadding + 'px';
};
const delayedResize = () => {
window.setTimeout(resize, 0);
};
const getPropertyValue = (it: string) => {
return Number.parseFloat(window.getComputedStyle(ref.current).getPropertyValue(it));
};
useEffect(() => {
[topPadding, bottomPadding] = ['padding-top', 'padding-bottom'].map(getPropertyValue);
ref.current.focus();
ref.current.select();
resize();
}, []);
return <textarea ref={ref} onChange={resize} onCut={delayedResize} onPaste={delayedResize} onDrop={delayedResize} onKeyDown={delayedResize} rows={1} {...props} />;
};
import { useRef, useState } from "react"
const TextAreaComponent = () => {
const [inputVal, setInputVal] =useState("")
const inputRef = useRef(null)
const handleInputHeight = () => {
const scrollHeight = inputRef.current.scrollHeight;
inputRef.current.style.height = scrollHeight + "px";
};
const handleInputChange = () => {
setInputVal(inputRef.current.value)
handleInputHeight()
}
return (
<textarea
ref={inputRef}
value={inputVal}
onChange={handleInputChange}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSubmit(e);
inputRef.current.style.height = "40px";
}
}}
/>
)}
Extremely simple solution:
function allowTextareasToDynamicallyResize() {
let textareas = document.getElementsByTagName('textarea');
for (let i = 0; i < textareas.length; i++) {
textareas[i].style.height = textareas[i].scrollHeight + 'px';
textareas[i].addEventListener('input', (e) => {
e.target.style.height = textareas[i].scrollHeight + 'px';
});
}
}
// Call this function in the componentDidMount() method of App,
// or whatever class you want that contains all the textareas
// you want to dynamically resize.
This works by setting an event listener to all textareas. For any given textarea, new input will trigger a function that resizes it. This function looks at any scrollHeight, i.e. the height that is overflowing out of your existing container. It then increments the textarea height by that exact height. Simple!
As mentioned in the comment, you have to call this function in some method, but the important part is that you call it AFTER everything is mounted in React / populated in JS. So the componentDidMount() in App is a good place for this.
Related
I have a componenet that wraps its children and slides them in and out based on the stage prop, which represents the active child's index.
As this uses a .map() to wrap each child in a div for styling, I need to give each child a key prop. I want to assign a random key as the children could be anything.
I thought I could just do this
key={`pageSlide-${uuid()}`}
but it causes an infinite loop/React to freeze and I can't figure out why
I have tried
Mapping the children before render and adding a uuid key there, calling it via key={child.uuid}
Creating an array of uuids and assigning them via key={uuids[i]}
Using a custom hook to store the children in a state and assign a uuid prop there
All result in the same issue
Currently I'm just using the child's index as a key key={pageSlide-${i}} which works but is not best practice and I want to learn why this is happening.
I can also assign the key directly to the child in the parent component and then use child.key but this kinda defeats the point of generating the key
(uuid is a function from react-uuid, but the same issue happens with any function including Math.random())
Here is the full component:
import {
Children,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import PropTypes from "prop-types";
import uuid from "react-uuid";
import ProgressBarWithTicks from "./ProgressBarWithTicks";
import { childrenPropType } from "../../../propTypes/childrenPropTypes";
const calculateTranslateX = (i = 0, stage = 0) => {
let translateX = stage === i ? 0 : 100;
if (i < stage) {
translateX = -100;
}
return translateX;
};
const ComponentSlider = ({ stage, children, stageCounter }) => {
const childComponents = Children.toArray(children);
const containerRef = useRef(null);
const [lastResize, setLastResize] = useState(null);
const [currentMaxHeight, setCurrentMaxHeight] = useState(
containerRef.current?.childNodes?.[stage]?.clientHeight
);
const updateMaxHeight = useCallback(
(scrollToTop = true) => {
if (scrollToTop) {
window.scrollTo(0, 0);
}
setCurrentMaxHeight(
Math.max(
containerRef.current?.childNodes?.[stage]?.clientHeight,
window.innerHeight -
(containerRef?.current?.offsetTop || 0) -
48
)
);
},
[stage]
);
useEffect(updateMaxHeight, [stage, updateMaxHeight]);
useEffect(() => updateMaxHeight(false), [lastResize, updateMaxHeight]);
const resizeListener = useMemo(
() => new MutationObserver(() => setLastResize(Date.now())),
[]
);
useEffect(() => {
if (containerRef.current) {
resizeListener.observe(containerRef.current, {
childList: true,
subtree: true,
});
}
}, [resizeListener]);
return (
<div className="w-100">
{stageCounter && (
<ProgressBarWithTicks
currentStage={stage}
stages={childComponents.length}
/>
)}
<div
className="position-relative divSlider align-items-start"
ref={containerRef}
style={{
maxHeight: currentMaxHeight || null,
}}>
{Children.map(childComponents, (child, i) => (
<div
key={`pageSlide-${uuid()}`}
className={`w-100 ${
stage === i ? "opacity-100" : "opacity-0"
} justify-content-center d-flex`}
style={{
zIndex: childComponents.length - i,
transform: `translateX(${calculateTranslateX(
i,
stage
)}%)`,
pointerEvents: stage === i ? null : "none",
cursor: stage === i ? null : "none",
}}>
{child}
</div>
))}
</div>
</div>
);
};
ComponentSlider.propTypes = {
children: childrenPropType.isRequired,
stage: PropTypes.number,
stageCounter: PropTypes.bool,
};
ComponentSlider.defaultProps = {
stage: 0,
stageCounter: false,
};
export default ComponentSlider;
It is only called in this component (twice, happens in both instances)
import { useEffect, useReducer, useState } from "react";
import { useParams } from "react-router-dom";
import {
FaCalendarCheck,
FaCalendarPlus,
FaHandHoldingHeart,
} from "react-icons/fa";
import { IoIosCart } from "react-icons/io";
import { mockMatches } from "../../../templates/mockData";
import { initialSwapFormState } from "../../../templates/initalStates";
import swapReducer from "../../../reducers/swapReducer";
import useFetch from "../../../hooks/useFetch";
import useValidateFields from "../../../hooks/useValidateFields";
import IconWrap from "../../common/IconWrap";
import ComponentSlider from "../../common/transitions/ComponentSlider";
import ConfirmNewSwap from "./ConfirmSwap";
import SwapFormWrapper from "./SwapFormWrapper";
import MatchSwap from "../Matches/MatchSwap";
import SwapOffers from "./SwapOffers";
import CreateNewSwap from "./CreateNewSwap";
import smallNumberToWord from "../../../functions/utils/numberToWord";
import ComponentFader from "../../common/transitions/ComponentFader";
const formStageHeaders = [
"What shift do you want to swap?",
"What shifts can you do instead?",
"Pick a matching shift",
"Good to go!",
];
const NewSwap = () => {
const { swapIdParam } = useParams();
const [formStage, setFormStage] = useState(0);
const [swapId, setSwapId] = useState(swapIdParam || null);
const [newSwap, dispatchNewSwap] = useReducer(swapReducer, {
...initialSwapFormState,
});
const [matches, setMatches] = useState(mockMatches);
const [selectedMatch, setSelectedMatch] = useState(null);
const [validateHook, newSwapValidationErrors] = useValidateFields(newSwap);
const fetchHook = useFetch();
const setStage = (stageIndex) => {
if (!swapId && stageIndex > 1) {
setSwapId(Math.round(Math.random() * 100));
}
if (stageIndex === "reset") {
setSwapId(null);
dispatchNewSwap({ type: "reset" });
}
setFormStage(stageIndex === "reset" ? 0 : stageIndex);
};
const saveMatch = async () => {
const matchResponse = await fetchHook({
type: "addSwap",
options: { body: newSwap },
});
if (matchResponse.success) {
setStage(3);
} else {
setMatches([]);
dispatchNewSwap({ type: "setSwapMatch" });
setStage(1);
}
};
useEffect(() => {
// set matchId of new selected swap
dispatchNewSwap({ type: "setSwapMatch", payload: selectedMatch });
}, [selectedMatch]);
return (
<div>
<div className="my-3">
<div className="d-flex justify-content-center w-100 my-3">
<ComponentSlider stage={formStage}>
<IconWrap colour="primary">
<FaCalendarPlus />
</IconWrap>
<IconWrap colour="danger">
<FaHandHoldingHeart />
</IconWrap>
<IconWrap colour="warning">
<IoIosCart />
</IconWrap>
<IconWrap colour="success">
<FaCalendarCheck />
</IconWrap>
</ComponentSlider>
</div>
<ComponentFader stage={formStage}>
{formStageHeaders.map((x) => (
<h3
key={`stageHeading-${x.id}`}
className="text-center my-3">
{x}
</h3>
))}
</ComponentFader>
</div>
<div className="mx-auto" style={{ maxWidth: "400px" }}>
<ComponentSlider stage={formStage} stageCounter>
<SwapFormWrapper heading="Shift details">
<CreateNewSwap
setSwapId={setSwapId}
newSwap={newSwap}
newSwapValidationErrors={newSwapValidationErrors}
dispatchNewSwap={dispatchNewSwap}
validateFunction={validateHook}
setStage={setStage}
/>
</SwapFormWrapper>
<SwapFormWrapper heading="Swap in return offers">
<p>
You can add up to{" "}
{smallNumberToWord(5).toLowerCase()} offers, and
must have at least one
</p>
<SwapOffers
swapId={swapId}
setStage={setStage}
newSwap={newSwap}
dispatchNewSwap={dispatchNewSwap}
setMatches={setMatches}
/>
</SwapFormWrapper>
<SwapFormWrapper>
<MatchSwap
swapId={swapId}
setStage={setStage}
matches={matches}
selectedMatch={selectedMatch}
setSelectedMatch={setSelectedMatch}
dispatchNewSwap={dispatchNewSwap}
saveMatch={saveMatch}
/>
</SwapFormWrapper>
<SwapFormWrapper>
<ConfirmNewSwap
swapId={swapId}
setStage={setStage}
selectedSwap={selectedMatch}
newSwap={newSwap}
/>
</SwapFormWrapper>
</ComponentSlider>
</div>
</div>
);
};
NewSwap.propTypes = {};
export default NewSwap;
One solution
#Nick Parsons has pointed out I don't even need a key if using React.Children.map(), so this is a non issue
I'd still really like to understand what was causing this problem, aas far as I can tell updateMaxHeight is involved, but I can't quite see the chain that leads to an constant re-rendering
Interstingly if I use useMemo for an array of uuids it works
const uuids = useMemo(
() => Array.from({ length: childComponents.length }).map(() => uuid()),
[childComponents.length]
);
/*...*/
key={uuids[i]}
I make a list of div tags for listing filename.
and After selecting a div, then I can change focus up and down using Arrow keys
Because I have a long file list, I add overflow: scroll to the container
but Scroll does not move along with my focus(so active div disappear from the viewport),
How can I make scroll behavior move down along with active div?
I create an example in codesandbox
import "./styles.css";
import { useEffect, useState } from "react";
export default function App() {
const [selectedItem, setSelectedItem] = useState(0);
useEffect(() => {
const keyPress = (e) => {
if (e.key === "ArrowLeft") {
setSelectedItem((prev) => Number(prev) - 1);
}
if (e.key === "ArrowRight") {
setSelectedItem((prev) => Number(prev) + 1);
}
};
window.addEventListener("keydown", keyPress);
return () => {
window.removeEventListener("keydown", keyPress);
};
}, [selectedItem]);
const onClickDiv = (e) => {
setSelectedItem(e.target.id);
};
const renderList = () => {
let items = [];
console.log(selectedItem);
for (let i = 0; i < 60; i++) {
items.push(
<div
key={i}
className={`item ${Number(selectedItem) === i ? "active" : ""}`}
id={i}
onClick={onClickDiv}
>
Item{i}.png
</div>
);
}
return items;
};
return (
<div className="App">
<div className="list-container">{renderList()}</div>
</div>
);
}
.list-container {
height: 300px;
overflow: scroll;
}
.active {
background-color: orangered;
}
------------------ EDIT -----------------------
I finally complete this example, I sincerely thank you guys for answering my question.
Here is code sandbox final code
Here's my take on it.
I am using refs as well along with scrollIntoView.This way we don't have to scroll by a fixed amount and also we only are scrolling when we are at the end of the viewport.
Here's the demo
I am storing refs of each element.
ref={(ref) => {
elementRefs.current = { ...elementRefs.current, [i]: ref };
}}
And then we will use scrollIntoView when focus changes.
const prevItem = elementRefs.current[selectedItem - 1];
prevItem && prevItem.scrollIntoView({ block: "end" });
Notice the {block:"end"} argument here. It makes sure we only scroll if the element is not in the viewport.
You can learn more about scrollIntoView here.
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import { useEffect, useState, useRef } from "react";
export default function App() {
const [selectedItem, setSelectedItem] = useState(0);
const elementRefs = useRef({});
useEffect(() => {
const keyPress = (e) => {
if (e.key === "ArrowLeft") {
setSelectedItem((prev) => Number(prev) - 1);
const prevItem = elementRefs.current[selectedItem - 1];
prevItem && prevItem.scrollIntoView({ block: "end" });
}
if (e.key === "ArrowRight") {
console.log(elementRefs.current[selectedItem]);
// if (selectedItem < elementRefs.current.length)
const nextItem = elementRefs.current[selectedItem + 1];
nextItem && nextItem.scrollIntoView({ block: "end" });
setSelectedItem((prev) => Number(prev) + 1);
}
};
window.addEventListener("keydown", keyPress);
return () => {
window.removeEventListener("keydown", keyPress);
};
}, [selectedItem]);
const onClickDiv = (e) => {
setSelectedItem(e.target.id);
};
const renderList = () => {
let items = [];
console.log(selectedItem);
for (let i = 0; i < 60; i++) {
items.push(
<div
key={i}
className={`item ${Number(selectedItem) === i ? "active" : ""}`}
id={i}
onClick={onClickDiv}
ref={(ref) => {
elementRefs.current = { ...elementRefs.current, [i]: ref };
}}
>
Item{i}.png
</div>
);
}
return items;
};
return (
<div className="App">
<div className="list-container">{renderList()}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("container"));
One way to approach this problem is to use ref in React.
First define a ref using
const scrollRef = useRef(null);
Then, assign it to your scrolling element like this,
<div className="list-container" ref={scrollRef}>
What this does is, gives you a reference to the html element inside your code.
ref.current is the HTML Div element now.
Now, you can use scrollBy method on HTML element to scroll up or down.
Like this,
useEffect(() => {
const keyPress = (e) => {
if (e.key === "ArrowLeft") {
setSelectedItem((prev) => Number(prev) - 1);
scrollRef.current.scrollBy(0, -18); // <-- Scrolls the div 18px to the top
}
if (e.key === "ArrowRight") {
setSelectedItem((prev) => Number(prev) + 1);
scrollRef.current.scrollBy(0, 18); // <-- Scrolls the div 18px to the bottom
}
};
window.addEventListener("keydown", keyPress);
return () => {
window.removeEventListener("keydown", keyPress);
};
}, [selectedItem]);
I have given 18 because I know the height of my list item.
I have updated your Sandbox. Check it out.
I'm trying to prevent a modal element to re-render when it's supposed to be invisible.
The course I'm following does this by converting the component to a class based component and using shouldComponentUpdate() but I wanted to check if using React.memo() did the same thing.
I tried, and it doesn't, but I'm not sure why.
The component that should not render is this:
import React , {useEffect} from 'react'
import Aux from '../../../hoc/Aux';
import Backdrop from '../Backdrop/Backdrop';
import classes from './Modal.module.css';
const Modal = (props) => {
useEffect(() => {
console.log('[Modal.js] useEffect')
});
return (
<Aux>
<Backdrop show={props.show} clicked={props.modalClosed} />
<div
className={classes.Modal}
style={{
transform: props.show ? 'translateY(0)' : 'translateY(-100vh)',
opacity: props.show ? '1': '0'
}}>
{props.children}
</div>
</Aux>)
};
export default React.memo(Modal);
Which is managed by
import React, {Component} from 'react'
import Aux from '../../hoc/Aux';
import Burger from '../../components/Burger/Burger'
import BuildControls from '../../components/Burger/BuildControls/BuildControls'
import Modal from '../../components/UI/Modal/Modal'
import OrderSummary from '../../components/Burger/OrderSummary/OrderSummary'
const INGREDIENT_PRICES = {
salad: 0.5,
cheese: 0.4,
meat: 1.3,
bacon: 0.7
}
class BurgerBuilder extends Component {
state = {
ingredients: {
salad: 0,
bacon: 0,
cheese: 0,
meat: 0
},
totalPrice: 4,
purchasable: false,
purchasing: false
}
purchaseHandler = () => {
this.setState({purchasing: true});
};
purchaseCancelHandler = () => {
this.setState({purchasing:false});
};
purchaseContinueHandler = () => {
alert('You Continue!')
};
updatePurchaseState = (ingredients) => {
let purchasable = false;
for (let ingredient in ingredients){
if (ingredients[ingredient]>0){
purchasable = true;
break;
}
}
this.setState({purchasable:purchasable})
}
addIngredientHandler = (type) => {
const oldCount = this.state.ingredients[type];
const updatedCount = oldCount +1;
const updatedIngredients = {
...this.state.ingredients
};
updatedIngredients[type] = updatedCount;
const priceAddition = INGREDIENT_PRICES[type];
const oldPrice = this.state.totalPrice;
const newPrice = oldPrice + priceAddition;
this.setState({totalPrice: newPrice, ingredients: updatedIngredients});
this.updatePurchaseState(updatedIngredients);
};
removeIngredientHandler = (type) => {
const oldCount = this.state.ingredients[type];
if (oldCount <= 0)
return;
const updatedCount =oldCount -1;
const updatedIngredients = {
...this.state.ingredients
};
updatedIngredients[type] = updatedCount;
const priceAddition =INGREDIENT_PRICES[type];
const oldPrice = this.state.totalPrice;
const newPrice = oldPrice - priceAddition;
this.setState({totalPrice: newPrice, ingredients: updatedIngredients});
this.updatePurchaseState(updatedIngredients);
};
render () {
const disabledInfo = {
...this.state.ingredients
};
for (let key in disabledInfo){
disabledInfo[key] = disabledInfo[key] <= 0;
}
return (
<Aux>
<Modal show={this.state.purchasing} modalClosed={this.purchaseCancelHandler}>
<OrderSummary
ingredients={this.state.ingredients}
purchaseCancelled={this.purchaseCancelHandler}
purchaseContinued={this.purchaseContinueHandler}
price={this.state.totalPrice} />
</Modal>
<Burger ingredients={this.state.ingredients}/>
<BuildControls
ingredientAdded={this.addIngredientHandler}
ingredientRemoved={this.removeIngredientHandler}
disabled={disabledInfo}
price={this.state.totalPrice}
purchasable={this.state.purchasable}
ordered={this.purchaseHandler}/>
</Aux>
);
}
}
export default BurgerBuilder;
With BuildControls I change the Ingredients state; but not the props I pass to modal, purchasing and purchaseHandler
Does changing the ingredients prop I pass to it's child always prompt a re-render even when Modal itself is under React.memo() ?
You are changing one of the props you pass to Modal - the children prop. It is passed implicitly by adding children to a react element. And since you are changing the child element it will cause a re-render.
I'm adding an image carousel to my website. The div that is linked to carousel.js is totally blank. It seems that the images are not rendering. My images are local and stored in an array in carousel.js. I've gone through similar threads but haven't been able to get the images to render. For example, I've made the image size smaller, tried importing the images at the top of carousel.js, moved the images folder to the public folder generated by node module, and about ten other things. This is my first react project so any help would be appreciated.
The full project can be viewed on cloud 9 here
Here's my carousel.js:
import React from 'react';
import ReactDOM from 'react-dom';
const imgUrls = [
"./images/croissant.jpg",
"./images/herbal-tea.jpg",
"./images/matcha-latte.jpg",
"./images/mochaLatte.jpg",
"./images/waffle.jpg"
];
class Carousel extends React.Component {
constructor (props) {
super(props);
this.state = {
currentImageIndex: 0
};
this.nextSlide = this.nextSlide.bind(this);
this.previousSlide = this.previousSlide.bind(this);
}
previousSlide () {
const lastIndex = imgUrls.length - 1;
const { currentImageIndex } = this.state;
const shouldResetIndex = currentImageIndex === 0;
const index = shouldResetIndex ? lastIndex : currentImageIndex - 1;
this.setState({
currentImageIndex: index
});
}
nextSlide () {
const lastIndex = imgUrls.length - 1;
const { currentImageIndex } = this.state;
const shouldResetIndex = currentImageIndex === lastIndex;
const index = shouldResetIndex ? 0 : currentImageIndex + 1;
this.setState({
currentImageIndex: index
});
}
render () {
return (
<div className="carousel">
<Arrow direction="left" clickFunction={ this.previousSlide } glyph="◀" />
<ImageSlide url={ imgUrls[this.state.currentImageIndex] } />
<Arrow direction="right" clickFunction={ this.nextSlide } glyph="▶" />
</div>
);
}
}
const Arrow = ({ direction, clickFunction, glyph }) => (
<div
className={ `slide-arrow ${direction}` }
onClick={ clickFunction }>
{ glyph }
</div>
);
const ImageSlide = ({ url }) => {
const styles = {
backgroundImage: `url(${url})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
};
return (
<div className="image-slide" style={styles}></div>
);
}
ReactDOM.render(
<Carousel />,
document.getElementById('container')
);
If you are using webpack you need to use require('./imagedirectory')
So it is like this...
const imgUrls = [ require("./images/croissant.jpg") , require("./images/herbal-tea.jpg") , require("./images/matcha-latte.jpg") , require("./images/mochaLatte.jpg") , require("./images/waffle.jpg") ];
What about this :
const ImageSlide = ({ url }) => {
const img = require(`${url}`);
const styles = {
backgroundImage: `url(${img})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
};
return (
<div className="image-slide" style={styles}></div>
);
}
This is my Buttons component code:
import React from 'react';
import RaisedButton from 'material-ui/RaisedButton';
const style = {
button: {
margin: 2,
padding: 0,
minWidth: 1,
},
};
const Buttons = props => {
const arrayFromInput = props.array;
const buttonsArray = [];
for (let i = 1; i <= arrayFromInput; i++) {
buttonsArray.push(i);
}
const handleButtonSelectZero = props.handleButtonSelectOne;
const allButtons = buttonsArray.map(el => (
<RaisedButton
key={el}
label={el}
style={style.button}
onClick={() => handleButtonSelectZero(el)}
/>
));
if (arrayFromInput > 0) {
return <div>{allButtons}</div>;
}
return <div />;
};
export default Buttons;
In material-ui docs is info that to achieve Raised Button disabled state we should add disabled={true} to it.
My coding problem/question:
What should I add to this component code to have particular Rasied Button get disabled after that particular button is clicked?
EDIT:
SOLUTION:
import React from 'react';
import RaisedButton from 'material-ui/RaisedButton';
const style = {
button: {
margin: 2,
padding: 0,
minWidth: 1,
},
};
const Buttons = props => {
const arrayFromInput = props.array;
const buttonsArray = [];
for (let i = 1; i <= arrayFromInput; i++) {
buttonsArray.push(i);
}
const handleButtonSelectZero = props.handleButtonSelectOne;
const allButtons = buttonsArray.map(el => (
<MyButton key={el} onClick={handleButtonSelectZero} el={el} />
));
if (arrayFromInput > 0) {
return <div>{allButtons}</div>;
}
return <div />;
};
class MyButton extends React.Component {
constructor() {
super();
this.state = { disabled: false };
}
handleClick = () => {
this.setState({ disabled: !this.state.disabled });
this.props.onClick(this.props.el);
};
render() {
return (
<RaisedButton
disabled={this.state.disabled}
key={this.props.el}
label={this.props.el}
style={style.button}
onClick={() => this.handleClick()}
/>
);
}
}
export default Buttons;
You need to use state somehow, i think i would do something like this:
import React from 'react';
import RaisedButton from 'material-ui/RaisedButton';
const style = {
button: {
margin: 2,
padding: 0,
minWidth: 1,
},
};
const Buttons = props => {
const arrayFromInput = props.array;
const buttonsArray = [];
for (let i = 1; i <= arrayFromInput; i++) {
buttonsArray.push(i);
}
const handleButtonSelectZero = props.handleButtonSelectOne;
const allButtons = buttonsArray.map(el => (
<MyButton onClick={handleButtonSelectZero} el={el} />
));
if (arrayFromInput > 0) {
return <div>{allButtons}</div>;
}
return <div />;
};
class MyButton extends React.Component {
constructor() {
super()
this.state = { disabled: false }
}
handleClick = () => {
this.setState({ disabled: !this.state.disabled })
this.props.onClick(this.props.el)
}
render() {
return (
<RaisedButton
disabled={this.state.disabled}
key={this.props.el}
label={this.props.el}
style={style.button}
onClick={() => handleButtonSelectZero(el)}
/>
)
}
}
export default Buttons;
I have not tested it, but hopefully it can guide you in the correct direction.
If you just need to disable it you can expand the
onClick={() => handleButtonSelectZero(el)}
like this onClick={() => {handleButtonSelectZero(el);this.disabled=true}};
EDIT: fixed missing {}