As shown below, this TextInput component does a simple job: when input's value is empty, hides the title because placeholder shows the same words.
But the code doesn't work as expected. InputEvent does run, but reassign activeStyle has no effect.
import React, {useState} from 'react';
import './TextInput.css';
import * as CSS from 'csstype';
type TextInputProps = {
title: string
}
const TextInput: React.FC<TextInputProps> = ({title, children}) => {
const hiddenStyle: CSS.Properties = {
opacity: 0
};
const visibleStyle: CSS.Properties = {
opacity: 1
};
let activeStyle = hiddenStyle
const [rawTextInput, setRawTextInput] = useState("")
const InputEvent = (e: React.FormEvent<HTMLInputElement>) => {
const inputValue = e.currentTarget.value;
setRawTextInput(inputValue)
if(inputValue == ""){
activeStyle = hiddenStyle
} else {
activeStyle = visibleStyle
}
}
return (
<div className="TextInput">
<p
className="TextInputTitle"
style={activeStyle}
>
{title}
</p>
<input
className="TextInputField"
type="text"
placeholder={title}
value={rawTextInput}
onChange={InputEvent}
/>
{/*<p className="TextInputHint"></p>*/}
</div>
);
}
export default TextInput
import React, {useState} from 'react';
import './TextInput.css';
import * as CSS from 'csstype';
type TextInputProps = {
title: string
}
const TextInput: React.FC<TextInputProps> = ({title, children}) => {
const hiddenStyle: CSS.Properties = {
opacity: 0
};
const visibleStyle: CSS.Properties = {
opacity: 1
};
let activeStyle = hiddenStyle
const [rawTextInput, setRawTextInput] = useState("")
const InputEvent = (e: React.FormEvent<HTMLInputElement>) => {
const inputValue = e.currentTarget.value;
setRawTextInput(inputValue)
if(inputValue == ""){
activeStyle = hiddenStyle
} else {
activeStyle = visibleStyle
}
}
return (
<div className="TextInput">
<p
className="TextInputTitle"
style={activeStyle}
>
{title}
</p>
<input
className="TextInputField"
type="text"
placeholder={title}
value={rawTextInput}
onChange={InputEvent}
/>
{/*<p className="TextInputHint"></p>*/}
</div>
);
}
export default TextInput
Local variable doesn't affect re-render.
let activeStyle = hiddenStyle //local variable
You need to keep this in state and change using setter method.
const [activeStyle, setActiveStyle] = useState(hiddenStyle)
const InputEvent = (e: React.FormEvent<HTMLInputElement>) => {
const inputValue = e.currentTarget.value;
setRawTextInput(inputValue)
if(inputValue == ""){
setActiveStyle(hiddenStyle)
} else {
setActiveStyle(visibleStyle)
}
}
I've tried a simpler approach
import React, { useState } from "react";
type TextInputProps = {
title: string;
};
const TextInput: React.FC<TextInputProps> = ({ title, children }) => {
const [rawTextInput, setRawTextInput] = useState("");
const InputEvent = (e: React.FormEvent<HTMLInputElement>) => {
setRawTextInput(e.target.value)
};
return (
<div className="TextInput">
<p className="TextInputTitle" style={{opacity : rawTextInput == "" ? 0 : 1}}>
{title}
</p>
<input
className="TextInputField"
type="text"
placeholder={title}
value={rawTextInput}
onChange={InputEvent}
/>
{/*<p className="TextInputHint"></p>*/}
</div>
);
};
export default TextInput;
I hope it'll be useful
Changing the value of a local variable does not cause a re-render. And even if something else causes a render, that new render won't have access to local variables from the previous render. You need to use useState for the active style, not a local variable.
just use useState for activeStyle instead of declaring it on the scope of the component,this could cause unsync effect which could be a problem and indeterministic
import React, {useState} from 'react';
import './TextInput.css';
import * as CSS from 'csstype';
type TextInputProps = {
title: string
}
const TextInput: React.FC<TextInputProps> = ({title, children}) => {
const hiddenStyle: CSS.Properties = {
opacity: 0
};
const visibleStyle: CSS.Properties = {
opacity: 1
};
const [rawTextInput, setRawTextInput] = useState("")
//------Put something you want to change dynamically inside useState()
const [titleStyle, setTitleStyle] = useState(hiddenStyle)
//------
const InputEvent = (e: React.FormEvent<HTMLInputElement>) => {
const inputValue = e.currentTarget.value;
setRawTextInput(inputValue)
if(inputValue == ""){
setTitleStyle(hiddenStyle)
} else {
setTitleStyle(visibleStyle)
}
}
return (
<div className="TextInput">
<p
className="TextInputTitle"
style={titleStyle}
>
{title}
</p>
<input
className="TextInputField"
type="text"
placeholder={title}
value={rawTextInput}
onChange={InputEvent}
/>
{/*<p className="TextInputHint"></p>*/}
</div>
);
}
export default TextInput
Related
I'm using this example to set the height of the textarea.
but the size of the textarea is not set automatically if the textarea has a value.
https://codesandbox.io/s/autosize-textarea-forked-wdowvr?file=/src/App.tsx
import { useRef, useState } from "react";
import "./styles.css";
export default function App() {
const [value, setValue] = useState("");
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const handleChange = (evt) => {
const val = evt.target?.value;
setValue(val);
};
return (
<div className="App">
<label htmlFor="review-text">Review:</label>
<textarea
id="review-text"
onChange={handleChange}
onInput={(e: React.FormEvent<HTMLTextAreaElement>) => {
e.currentTarget.style.height = e.currentTarget.scrollHeight + "px";
}}
ref={textAreaRef}
rows={1}
value={value}
/>
</div>
);
}
for example
how can i fix this issue?
You can use the following snippets to adjust the height on loading the component.
Just for demo purposes:
const [value, setValue] = useState(
"already has a value with\nmore text\nand even more"
);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
You can access the ref once it is set. useEffect will do the trick here.
useEffect(() => {
if (textAreaRef && textAreaRef.current) {
textAreaRef.current.style.height = textAreaRef.current.scrollHeight + 'px';
}
}, []);
https://codesandbox.io/s/autosize-textarea-forked-symr7p?file=/src/App.tsx
You also need to set the height once the DOM is mounted. This way it can respond to whatever value is in there before any user input.
I did this with the useEffect hook. (Don't forget to import it from react).
I also tweaked the method of changing the height a bit, because it wasn't working properly.
import { useRef, useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [value, setValue] = useState("");
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const handleChange = (evt) => {
const val = evt.target?.value;
setValue(val);
};
const resizeTextarea = (t: HTMLTextAreaElement) => {
t.style.height = "auto";
const p = parseFloat(window.getComputedStyle(t, null).getPropertyValue("padding-top"))
+ parseFloat(window.getComputedStyle(t, null).getPropertyValue("padding-bottom"));
t.style.height = t.scrollHeight - p + "px";
}
const handleInput = (e: React.FormEvent<HTMLTextAreaElement>) => {
resizeTextarea(e.currentTarget);
}
useEffect(() => {
const t = textAreaRef.current;
if (t != null) {
t.style.overflowY = 'hidden';
resizeTextarea(t);
}
},[]);
return (
<div className="App">
<label htmlFor="review-text">Review:</label>
<textarea
id="review-text"
onChange={handleChange}
onInput={handleInput}
ref={textAreaRef}
rows={1}
value={value}
/>
</div>
);
}
https://codesandbox.io/s/autosize-textarea-forked-s39ur2?file=/src/App.tsx:0-1172
I am receiving an error of "HomePage.jsx:16 Uncaught TypeError: elements.map is not a function" when trying to change the boxtitle for the array component. I have tried moving around functions and such but nothing seems to work.
I basically only want to change the title for the certain array object with the same boxid. Below is my code.
HomePage.jsx:
import react from 'react';
import { useEffect, useContext } from 'react';
import '../App.css';
import Todobox from './Todobox';
import { ElementContext } from '../ElementContext';
export default function HomePage(){
const { elements, setElements, newElement, elementId } = useContext(ElementContext);
return(
<div className='page-container'>
<div className='header'>
<a className='header-title'>Trello Clone!</a>
<a className='header-button' onClick={newElement}>Create a list</a>
</div>
<div className='element-field'>
{elements.map((element) => <Todobox key={element.boxid} boxid={element.boxid} boxtitle={element.boxtitle}/>)}
</div>
</div>
)
}
Todobox.jsx:
import React from 'react';
import Item from './Item';
import { useState, useContext } from 'react';
import '../App.css';
import { ElementContext } from '../ElementContext';
export default function Todobox({ boxtitle, boxid }){
const { elements, setElements } = useContext(ElementContext);
const [boxheader, setBoxHeader] = useState();
const handleSubmit = (e) => {
const object = elements.find(obj => {
if (obj.boxid === boxid){
setBoxHeader(e.target.value)
return obj
}})
setElements({...object, boxtitle: boxheader})
}
const handleKeydown = (e) => {
if(e.keyCode == 13 && e.shiftKey == false){
e.preventDefault();
handleSubmit(e)
}
}
return(
<div className='element-box'>
<textarea className='element-title-input' placeholder={boxtitle} onKeyDown={handleKeydown}/>
<Item />
<textarea
className='element-input'
type='text'
placeholder={`Add item... ${boxid}`}
onClick={() => {console.log(boxid)}}
/>
</div>
)
}
ElementContext.js:
import React, { createContext, useState } from 'react';
import Todobox from './components/Todobox';
export const ElementContext = createContext();
export const ElementContextProvider = ({children}) => {
const [elements, setElements] = useState([]);
const [elementId, setElementId] = useState(1);
const [title, setTitle] = useState('Add title...');
const [refDict, setRefDict] = useState({});
const newElementId = (elements) =>{
setElementId(elementId + 1);
console.log(elementId)
}
const newElement = () => {
newElementId();
if (!refDict[elementId]) {
setElements(prev => [...prev, { boxtitle: title, boxid: elementId }]);
setRefDict((prev) => ({...prev, [elementId]: true}));
}
console.log(elements);
};
const value = {
elements,
setElements,
newElement,
elementId,
};
return(
<ElementContext.Provider value={value}>
{children}
</ElementContext.Provider>
)
};
Code Sandbox
Any help is appreciated since I am new and still learning! :)
Few points to handle
const newElement = () => {
newElementId(); // this wont update as react batches the state updates
// try using userRef for elementId
// create new element id here, and then set it
// const newId = elementId + 1
// setElementId(newElementId)
if (!refDict[elementId]) {
setElements(prev => [...prev, { boxtitle: title, boxid: elementId }]);
setRefDict((prev) => ({...prev, [elementId]: true}));
}
console.log(elements);
};
on submit
const object = elements.find(obj => {
if (obj.boxid === boxid){
setBoxHeader(e.target.value) // wont update the as React will likely batch the state updates
return obj
}})
setElements({...object, boxtitle: boxheader}) // is this an array ?
instead try
const object = elements?.find(obj => obj.boxid === boxid)
if (object) {
setBoxHeader(e.target.value)
setElements([ object, { boxtitle: e.target.value, boxId: elementId } ]) // what is the structure of array elements
}
you will find the new React documentation about updating arrays useful
I use Next.js for my react app, and i met this problem while using 'createPortal'. I spent almost a day for this problem, but still not solved... I check this code from only react app, but i can't find any problems. and it drive me crazy.(actually not really same code but almost correct!!)
plz help me guys :(
NavBar.tsx
// NavBar.tsx (to controll Modal.tsx)
const [ modalClick, setModalClick ] = useState(false);
const modalOpen = () => { setModalClick(true); }
const modalClose = () => { setModalClick(false); }
<p id = "link" onClick = { modalOpen }>Register</p>
<div>
{modalClick && <Modal component = {<LogIn/>}
closePortal = { modalClose }
selector = "#portal"/>}
</div>
_documents.tsx
// _documents.tsx
<body>
<Main/>
<NextScript/>
<div id = "portal"/>
</body>
Modal.tsx
// Modal.tsx
import React, { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
const Modal = ({ closePortal, component, selector }: any) => {
const [modalOpen, setModalOpen] = useState(false);
const ref = useRef<any>();
useEffect(() => {
setModalOpen(true);
document.body.style.overflow = "hidden";
if(document){
console.log("plz help me");
ref.current = document.querySelector(selector);
}
return () => {
setModalOpen(false);
document.body.style.overflow = "unset"
};
}, [selector]);
const exit = (e: any) => {
if(e.target === e.currentTarget){
closePortal()
}
}
if(modalOpen){
return createPortal(
<>
////////////
</>,
ref.current
)
}else return null;
};
export default Modal
I am not able to load default value style in React-draft-wysiwyg .
Codesandbox Link: Editor
what I tried ?
I am using react-draft-wysiwyg library for editor and draft-js for initializing and converting , and I have passed default value with style. if i remove style tag it works fine. but after adding style it doesn't work. how to fix style issue in default value
import { Editor } from "react-draft-wysiwyg";
import { EditorState, ContentState, convertFromHTML } from "draft-js";
import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css";
export default function App() {
const defaultValueRender = !true;
const defaultValue = "**<p style="color:red**">This is a paragraph.</p>";
const initialState = () => EditorState.createEmpty();
const [editorState, setEditorState] = useState(initialState);
useEffect(() => {
if (defaultValue !== "") {
setEditorState(
EditorState.createWithContent(
ContentState.createFromBlockArray(convertFromHTML(defaultValue))
)
);
}
}, []);
const onChange = async (value) => {
await setEditorState(value);
};
return (
<div className="App">
<Editor
editorState={editorState}
toolbarClassName="toolbarClassName"
wrapperClassName="wrapperClassName"
editorClassName="editorClassName"
onEditorStateChange={(value) => onChange(value)}
stripPastedStyles
ariaLabel="draftEditor"
/>
</div>
);
}
thanks to gokhan answer, i finally solved to get initial values to react-draft-wysiwyg + react-hook-form
import React, { useState, useEffect, useCallback } from 'react'
import { ContentState, EditorState, convertToRaw } from 'draft-js'
import { Editor } from 'react-draft-wysiwyg'
import htmlToDraft from 'html-to-draftjs'
import draftToHtml from 'draftjs-to-html'
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'
import { styContainer, styWrapper, styToolbar, styEditor } from './style'
const getInitialState = (defaultValue) => {
if (defaultValue) {
const blocksFromHtml = htmlToDraft(defaultValue)
const { contentBlocks, entityMap } = blocksFromHtml
const contentState = ContentState.createFromBlockArray(
contentBlocks,
entityMap
)
return EditorState.createWithContent(contentState)
} else {
return EditorState.createEmpty()
}
}
const RichEditor = ({ defaultValue, onChange }) => {
const [editorState, setEditorState] = useState()
const [defaultValueState, setdefaultValueState] = useState()
useEffect(() => {
if (defaultValue) {
const initialState = getInitialState(defaultValue)
onEditorDefaultStateChange(initialState)
}
}, [onEditorDefaultStateChange, defaultValue])
const onEditorDefaultStateChange = useCallback(
(editorState) => {
setdefaultValueState(editorState)
return onChange(
draftToHtml(convertToRaw(editorState.getCurrentContent()))
)
},
[onChange]
)
const onEditorStateChange = useCallback(
(editorState) => {
setEditorState(editorState)
return onChange(
draftToHtml(convertToRaw(editorState.getCurrentContent()))
)
},
[onChange]
)
return (
<div className={styContainer}>
<Editor
editorState={editorState ? editorState : defaultValueState}
onEditorStateChange={onEditorStateChange}
wrapperClassName={styWrapper}
toolbarClassName={styToolbar}
editorClassName={styEditor}
/>
</div>
)
}
RichEditor.propTypes = {}
RichEditor.defaultProps = {}
export default RichEditor
You could use html-to-draftjs for converting html strings with inline styles.
import React, { useState } from 'react';
import PropTypes from 'prop-types';
// DRAFT
import { EditorState, ContentState, convertToRaw } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
// PURIFY
import DOMPurify from 'dompurify';
// INITIAL STATE
// EditorState.createWithContent(ContentState.createFromBlockArray(convertFromHTML(defaultValue)))
const getInitialState = (defaultValue) => {
if (defaultValue) {
const blocksFromHtml = htmlToDraft(defaultValue);
const { contentBlocks, entityMap } = blocksFromHtml;
const contentState = ContentState.createFromBlockArray(contentBlocks, entityMap);
return EditorState.createWithContent(contentState);
} else {
return EditorState.createEmpty();
}
};
const DraftEditor = ({ defaultValue, onChange }) => {
const [editorState, setEditorState] = useState(getInitialState(defaultValue));
const onEditorChange = (val) => {
setEditorState(val);
const rawContentState = convertToRaw(val.getCurrentContent());
const htmlOutput = draftToHtml(rawContentState);
const cleanHtml = DOMPurify.sanitize(htmlOutput);
onChange && onChange(cleanHtml);
};
return (
<Editor
editorState={editorState}
onEditorStateChange={onEditorChange} />
);
};
DraftEditor.propTypes = {
defaultValue: PropTypes.string,
onChange: PropTypes.func.isRequired,
};
export default DraftEditor;
I am trying to find out if a div has overflown text and show show more link if it does. I found this stackoverflow answer to check if a div is overflowing. According to this answer, I need to implement a function which can access styles of the element in question and do some checks to see if it is overflowing. How can I access the styles of an element. I tried 2 ways
1. Using ref
import React from "react";
import "./styles.css";
export default function App(props) {
const [showMore, setShowMore] = React.useState(false);
const onClick = () => {
setShowMore(!showMore);
};
const checkOverflow = () => {
const el = ref.current;
const curOverflow = el.style.overflow;
if ( !curOverflow || curOverflow === "visible" )
el.style.overflow = "hidden";
const isOverflowing = el.clientWidth < el.scrollWidth
|| el.clientHeight < el.scrollHeight;
el.style.overflow = curOverflow;
return isOverflowing;
};
const ref = React.createRef();
return (
<>
<div ref={ref} className={showMore ? "container-nowrap" : "container"}>
{props.text}
</div>
{(checkOverflow()) && <span className="link" onClick={onClick}>
{showMore ? "show less" : "show more"}
</span>}
</>
)
}
2. Using forward ref
Child component
export const App = React.forwardRef((props, ref) => {
const [showMore, setShowMore] = React.useState(false);
const onClick = () => {
setShowMore(!showMore);
};
const checkOverflow = () => {
const el = ref.current;
const curOverflow = el.style.overflow;
if (!curOverflow || curOverflow === "visible") el.style.overflow = "hidden";
const isOverflowing =
el.clientWidth < el.scrollWidth || el.clientHeight < el.scrollHeight;
el.style.overflow = curOverflow;
return isOverflowing;
};
return (
<>
<div ref={ref} className={showMore ? "container-nowrap" : "container"}>
{props.text}
</div>
{checkOverflow() && (
<span className="link" onClick={onClick}>
{showMore ? "show less" : "show more"}
</span>
)}
</>
);
});
Parent component
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
const rootElement = document.getElementById("root");
const ref = React.createRef();
ReactDOM.render(
<React.StrictMode>
<App
ref={ref}
text="Start editing to see some magic happen! Click show more to expand and show less to collapse the text"
/>
</React.StrictMode>,
rootElement
);
But I got the following error in both approaches - Cannot read property 'style' of null.
What am I doing wrong? How can I achieve what I want?
As Jamie Dixon suggested in the comment, I used useLayoutEffect hook to set showLink true. Here is the code
Component
import React from "react";
import "./styles.css";
export default function App(props) {
const ref = React.createRef();
const [showMore, setShowMore] = React.useState(false);
const [showLink, setShowLink] = React.useState(false);
React.useLayoutEffect(() => {
if (ref.current.clientWidth < ref.current.scrollWidth) {
setShowLink(true);
}
}, [ref]);
const onClickMore = () => {
setShowMore(!showMore);
};
return (
<div>
<div ref={ref} className={showMore ? "" : "container"}>
{props.text}
</div>
{showLink && (
<span className="link more" onClick={onClickMore}>
{showMore ? "show less" : "show more"}
</span>
)}
</div>
);
}
CSS
.container {
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 200px;
}
.link {
text-decoration: underline;
cursor: pointer;
color: #0d6aa8;
}
We could create a custom hooks to know if we have overflow.
import * as React from 'react';
const useIsOverflow = (ref, isVerticalOverflow, callback) => {
const [isOverflow, setIsOverflow] = React.useState(undefined);
React.useLayoutEffect(() => {
const { current } = ref;
const { clientWidth, scrollWidth, clientHeight, scrollHeight } = current;
const trigger = () => {
const hasOverflow = isVerticalOverflow ? scrollHeight > clientHeight : scrollWidth > clientWidth;
setIsOverflow(hasOverflow);
if (callback) callback(hasOverflow);
};
if (current) {
trigger();
}
}, [callback, ref, isVerticalOverflow]);
return isOverflow;
};
export default useIsOverflow;
and just check in your component
import * as React from 'react';
import { useIsOverflow } from './useIsOverflow';
const App = () => {
const ref = React.useRef();
const isOverflow = useIsOverflow(ref);
console.log(isOverflow);
// true
return (
<div style={{ overflow: 'auto', height: '100px' }} ref={ref}>
<div style={{ height: '200px' }}>Hello React</div>
</div>
);
};
Thanks to Robin Wieruch for his awesome articles
https://www.robinwieruch.de/react-custom-hook-check-if-overflow/
Solution using TS and Hooks
Create your custom hook:
import React from 'react'
interface OverflowY {
ref: React.RefObject<HTMLDivElement>
isOverflowY: boolean
}
export const useOverflowY = (
callback?: (hasOverflow: boolean) => void
): OverflowY => {
const [isOverflowY, setIsOverflowY] = React.useState(false)
const ref = React.useRef<HTMLDivElement>(null)
React.useLayoutEffect(() => {
const { current } = ref
if (current) {
const hasOverflowY = current.scrollHeight > window.innerHeight
// RHS of assignment could be current.scrollHeight > current.clientWidth
setIsOverflowY(hasOverflowY)
callback?.(hasOverflowY)
}
}, [callback, ref])
return { ref, isOverflowY }
}
use your hook:
const { ref, isOverflowY } = useOverflowY()
//...
<Box ref={ref}>
...code
Import your files as need be and update code to your needs.