React TypeScript document.getElementById always selects the first instance - reactjs

I've got 2 of the same components being rendered
<div><Modal title='Join'/></div>
<div><Modal title='Login'/></div>
the modal components is like this
import React, {useState} from 'react';
import {Join} from './join';
import {Login} from './login';
interface propsInterface {
title: string;
}
const Modal: React.FC<propsInterface> = (props) => {
const [state, setState] = useState({showLogin: props.title === "login" ? false : true});
let modalState = false;
function toggleLogin(event: any) {
setState({...state, showLogin: !state.showLogin});
}
function toggleModal(event: any) {
if (event.target.id !== 'modal') return;
modalState = !modalState;
const modal = document.getElementById('modal'); <<==this always selects the first one
const card = document.getElementById('card'); <<==this always selects the first one
if (modal && card && modalState === true) {
modal.style.display = "flex";
modal.animate([{backgroundColor: 'rgba(0, 0, 0, 0)'}, {backgroundColor: 'rgba(0, 0, 0, 0.6)'}], {duration: 200, easing: 'ease-in-out', fill: 'forwards'});
card.animate([{opacity: 0}, {opacity: 1}], {duration: 200, easing: 'ease-in-out', fill: 'forwards'});
card.animate([{transform: 'translateY(-200px)'}, {transform: 'translateY(0)'}], {duration: 200, easing: 'ease-in-out', fill: 'forwards'});
}
if (modal && card && modalState === false) {
modal.animate([{backgroundColor: 'rgba(0, 0, 0, 0.6)'}, {backgroundColor: 'rgba(0, 0, 0, 0)'}], {duration: 200, easing: 'ease-in-out', fill: 'forwards'});
card.animate([{opacity: 1}, {opacity: 0}],{duration: 200, easing: 'ease-in-out', fill: 'forwards'});
card.animate([{transform: 'translateY(0)'}, {transform: 'translateY(-200px)'}], {duration: 200, easing: 'ease-in-out', fill: 'forwards'});
setTimeout(() => modal.style.display = "none", 200);
}
}
return (
<div>
<div className='modal' id='modal' onClick={toggleModal}>
<div className='card' id='card'>
{props.title}
{state.showLogin
? <Login toggleLogin={toggleLogin} toggleModal={toggleModal}/>
: <Join toggleLogin={toggleLogin} toggleModal={toggleModal}/>}
</div>
</div>
<div onClick={toggleModal} className='modal-title' id='modal'> {props.title}</div>
</div>
);
}
export {Modal};
Because there are 2 of this component now in the dom when I use
const modal = document.getElementById('modal');
the first instance of the component works as expected but the second instance is selecting the first instance not the second instance.
Is there a way to getElementById but only in this component?

You should only ever have one element with a given id in the same page, in your case you may want to use classes and use document.getElementsByClassName("classname") which is going to return an array of elements with the given class name.
Hope this helps

Instead of setting the Id as 'modal', you can pass id of your field as props and then set its id attribute.Then use getElementById for them.
Your can do like this:
<Modal title='Join' UniqId="modal1"/>
<Modal title='Login' UniqId="modal2"/>
Then in your component, you can do something like this:
<div className='modal' id={props.UniqId} onClick={toggleModal}>
After that, in your JS file, you can use this id:
const modal1 = document.getElementById('modal1');
const modal2 = document.getElementById('modal2');

The way that you are doing it looks like you are still using the mindset of using vanilla JavaScript to manipulate DOM elements after events.
Generally in the React world, you should be linking everything into your render() methods which are raised every time the state or the properties change within a react component.
So what I would do in your situation is remove all your code which is grabbing DOM elements, e.g. your document.getElementById code, and purely work from your state change. I am sorry that this won't be complete code, as I am not sure what your UI is supposed to look like.
const Modal: React.FC<propsInterface> = (props) => {
const [state, setState] = useState({ showLogin: props.title === "login" ? false : true, showModal: false, showCard: false });
function toggleLogin() {
setState({ showLogin: !state.showLogin });
}
function toggleModal() {
setState({ showModal: !state.showModal, showCard: !state.showCard });
}
const modalClassName = state.showModal ? 'modal show' : 'modal';
const cardClassName = state.showLogin ? 'card show' : 'card';
return (
<div>
<div className={modalClassName} onClick={toggleModal}>
<div className={cardClassName}>
{props.title}
{state.showLogin
? <Login toggleLogin={toggleLogin} toggleModal={toggleModal}/>
: <Join toggleLogin={toggleLogin} toggleModal={toggleModal}/>}
</div>
</div>
<div onClick={toggleModal} className='modal-title' id='modal'> {props.title}</div>
</div>
);
}
Effectively what happens is that when the toggleModal method is called, it just toggles the two state properties between true/false. This causes the component to re-render, and the class names change on the div elements.
From this, you will need to move all your animations into your css file and probably make use of animations in there (css animations are not my greatest skill). It's not a simple change to what you have, but I would always keep my styling and animations outside of a component and in a css file, and always work from rendering rather than trying to manipulate react nodes after render.

Related

How to apply animation in react-native?

I want to add animation to this search bar.
when the user touches the search bar it size decreases and again increases and gets to its default size(like animation in popups)
This is my code
<View style={{flexDirection:'row',alignSelf:'center'}}>
<TextInput
onChangeText={(text) => setSearch(text)}
onFocus={()=>{
setSize('92%');
setInterval(()=>{setSize('95%')},1000)
}}
placeholder="Search"
style={{...styles.searchbox,width:size}}
></TextInput>
</View>
I am currently trying to change width..
Firstly, I suggest you to take a look at RN animated documentation, maybe it will help you to understand better how the animations work.
Also, it depends on what you're having there: a class component or a function component.
If you're using a function component, you could do it like this:
Creating a custom hook, called, let's say useAnimation(), which would look something like this
export const useAnimation = ({ doAnimation, duration, initialValue, finalValue }) => {
const [animation, setAnimation] = useState(new Animated.Value(initialValue))
useEffect(() => {
Animated.spring(animation, {
toValue: doAnimation ? initialValue : finalValue,
duration,
bounciness: 8,
useNativeDriver: false
}).start();
}, [doAnimation]);
return animation
}
As it is said in the documentation, you could animate only Animated components, and for example if you want to have an animated View, the tag will be <Animated.View> {...} </Animated.View, but for the <TextInput> we have to create the animated component:
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
and combining the first 2 steps
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
const [collapsed, setCollapsed] = useState(true)
const animation = useAnimation({ doAnimation: collapsed, duration: 300, initialValue: 20, finalValue: 200 });
const onFocusText = () => setWidth(false)
return (
<AnimatedTextInput
onFocus={onFocusText}
placeholder={"Search something"}
style={{width: animation, height: 50, borderColor: 'gray', borderWidth: 1, borderRadius: 4, padding: 10}}
/>
)
Also, if you're having a class component, you could have a method which will start the animation (but don't forget about the step 2 which is essential)
private size: Animated.Value = new Animated.Value(COLLAPSED_VALUE)
get resizeInputWidth(): Animated.CompositeAnimation {
return Animated.timing(this.size, {
toValue: EXPANDED_VALUE,
duration: 500,
})
}
startAnimation = () => this.resizeInputWidth.start()
<AnimatedTextInput
onFocus={this.startAnimation}
style={{ width: this.size }}
/>

Testing component with lodash.debounce delay failing

I have a rich text editor input field that I wanted to wrap with a debounced component. Debounced input component looks like this:
import { useState, useCallback } from 'react';
import debounce from 'lodash.debounce';
const useDebounce = (callback, delay) => {
const debouncedFn = useCallback(
debounce((...args) => callback(...args), delay),
[delay] // will recreate if delay changes
);
return debouncedFn;
};
function DebouncedInput(props) {
const [value, setValue] = useState(props.value);
const debouncedSave = useDebounce((nextValue) => props.onChange(nextValue), props.delay);
const handleChange = (nextValue) => {
setValue(nextValue);
debouncedSave(nextValue);
};
return props.renderProps({ onChange: handleChange, value });
}
export default DebouncedInput;
I am using DebouncedInput as a wrapper component for MediumEditor:
<DebouncedInput
value={task.text}
onChange={(text) => onTextChange(text)}
delay={500}
renderProps={(props) => (
<MediumEditor
{...props}
id="task"
style={{ height: '100%' }}
placeholder="Task text…"
disabled={readOnly}
key={task.id}
/>
)}
/>;
MediumEditor component does some sanitation work that I would like to test, for example stripping html tags:
class MediumEditor extends React.Component {
static props = {
id: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
disabled: PropTypes.bool,
uniqueID: PropTypes.any,
placeholder: PropTypes.string,
style: PropTypes.object,
};
onChange(text) {
this.props.onChange(stripHtml(text) === '' ? '' : fixExcelPaste(text));
}
render() {
const {
id,
value,
onChange,
disabled,
placeholder,
style,
uniqueID,
...restProps
} = this.props;
return (
<div style={{ position: 'relative', height: '100%' }} {...restProps}>
{disabled && (
<div
style={{
position: 'absolute',
width: '100%',
height: '100%',
cursor: 'not-allowed',
zIndex: 1,
}}
/>
)}
<Editor
id={id}
data-testid="medium-editor"
options={{
toolbar: {
buttons: ['bold', 'italic', 'underline', 'subscript', 'superscript'],
},
spellcheck: false,
disableEditing: disabled,
placeholder: { text: placeholder || 'Skriv inn tekst...' },
}}
onChange={(text) => this.onChange(text)}
text={value}
style={{
...style,
background: disabled ? 'transparent' : 'white',
borderColor: disabled ? 'grey' : '#FF9600',
overflowY: 'auto',
color: '#444F55',
}}
/>
</div>
);
}
}
export default MediumEditor;
And this is how I am testing this:
it('not stripping html tags if there is text', async () => {
expect(editor.instance.state.text).toEqual('Lorem ipsum ...?');
const mediumEditor = editor.findByProps({ 'data-testid': 'medium-editor' });
const newText = '<p><b>New text, Flesk</b></p>';
mediumEditor.props.onChange(newText);
// jest.runAllTimers();
expect(editor.instance.state.text).toEqual(newText);
});
When I run this test I get:
Error: expect(received).toEqual(expected) // deep equality
Expected: "<p><b>New text, Flesk</b></p>"
Received: "Lorem ipsum ...?"
I have also tried running the test with jest.runAllTimers(); before checking the result, but then I get:
Error: Ran 100000 timers, and there are still more! Assuming we've hit an infinite recursion and bailing out...
I have also tried with:
jest.advanceTimersByTime(500);
But the test keeps failing, I get the old state of the text.
It seems like the state just doesn't change for some reason, which is weird since the component used to work and the test were green before I had them wrapped with DebounceInput component.
The parent component where I have MediumEditor has a method onTextChange that should be called from the DebounceInput component since that is the function that is being passed as the onChange prop to the DebounceInput, but in the test, I can see this method is never reached. In the browser, everything works fine, so I don't know why it is not working in the test?
onTextChange(text) {
console.log('text', text);
this.setState((state) => {
return {
task: { ...state.task, text },
isDirty: true,
};
});
}
On inspecting further I could see that the correct value is being passed in the test all the way to handleChange in DebouncedInput. So, I suspect, there are some problems with lodash.debounce in this test. I am not sure if I should mock this function or does mock come with jest?
const handleChange = (nextValue) => {
console.log(nextValue);
setValue(nextValue);
debouncedSave(nextValue);
};
This is where I suspect the problem is in the test:
const useDebounce = (callback, delay) => {
const debouncedFn = useCallback(
debounce((...args) => callback(...args), delay),
[delay] // will recreate if delay changes
);
return debouncedFn;
};
I have tried with mocking debounce like this:
import debounce from 'lodash.debounce'
jest.mock('lodash.debounce');
debounce.mockImplementation(() => jest.fn(fn => fn));
That gave me error:
TypeError: _lodash.default.mockImplementation is not a function
How should I fix this?
I'm guessing that you are using enzyme (from the props access).
In order to test some code that depends on timers in jest:
mark to jest to use fake timers with call to jest.useFakeTimers()
render your component
make your change (which will start the timers, in your case is the state change), pay attention that when you change the state from enzyme, you need to call componentWrapper.update()
advance the timers using jest.runOnlyPendingTimers()
This should work.
Few side notes regarding testing react components:
If you want to test the function of onChange, test the immediate component (in your case MediumEditor), there is no point of testing the entire wrapped component for testing the onChange functionality
Don't update the state from tests, it makes your tests highly couple to specific implementation, prove, rename the state variable name, the functionality of your component won't change, but your tests will fail, since they will try to update a state of none existing state variable.
Don't call onChange props (or any other props) from test. It makes your tests more implementation aware (=high couple with component implementation), and actually they doesn't check that your component works properly, think for example that for some reason you didn't pass the onChange prop to the input, your tests will pass (since your test is calling the onChange prop), but in real it won't work.
The best approach of component testing is to simulate actions on the component like your user will do, for example, in input component, simulate a change / input event on the component (this is what your user does in real app when he types).

Cannot crop image using react-image-crop

I trying to crop image using react-image-crop (https://www.npmjs.com/package/react-image-crop/v/6.0.7). Actually I cannot move cropped area or stretch it. I also use there React DragZone to upload image. Could you explain what I'm doing wrong and what could be the problem ? React-image-crop css is also imported. Can anybody help?
import React, {useCallback} from 'react'
import {useDropzone} from 'react-dropzone'
import ReactCrop from 'react-image-crop'
import ReactDOM from "react-dom";
import 'react-image-crop/dist/ReactCrop.css';
import "../Styles/ImageUpload.css"
function ImageUpload(files, addFile) {
const crop = {
disabled: false,
locked: false,
unit: 'px', // default, can be 'px' or '%'
x: 130,
y: 50,
width: 200,
height: 200
}
const onCrop = (image, pixelCrop, fileName) => {
}
const onImageLoaded = (image) => {
}
const onCropComplete = async crop => {
}
const onDrop = useCallback((acceptedFiles, rejectedFiles) => {
if (Object.keys(rejectedFiles).length !== 0) {
}
else {
files.addFile(acceptedFiles);
let popupBody = document.getElementsByClassName("popup-container")[0];
popupBody.innerHTML = "";
let cropElement = (<ReactCrop src = {URL.createObjectURL(acceptedFiles[0])} crop={crop}
onImageLoaded={onImageLoaded}
onComplete={onCropComplete}
onChange={onCrop} />);
ReactDOM.render(cropElement, popupBody);
}, [])
const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop, noKeyboard: true})
return (
<div className = "popup-container">
<p>Upload new photo</p>
{
(() => {
if (files.length == undefined) {
return (<div>
<input className = "testinput" {...getInputProps()} />
<div className = "popup-body" {...getRootProps()}>
<img src={require("../Resources/upload-image.png")} className = "image-upload-img"/>
<p style = {{'font-family':'Verdana'}}>Chouse a <b className = "file-manualy-
upload">file</b> or drag it here</p>
</div> </div>)
}
else {
}
})()}
</div>
)
}
I also face the same issue, Here you are trying to add disabled, locked in crop property but it is invalid because crop property takes following values:
{
unit: 'px', // default, can be 'px' or '%'
x: 130,
y: 50,
width: 200,
height: 200
}
To solve this add disabled, locked as a props in component instead of crop property.
I'm not sure sure why you would do a ReactDom.render from within the function. It seems to be counter to what React is and guessing that the library isn't binding events properly.
I would suggest, after the user drops the image, you load the image using a file reader and set it as state and change the display to show the <ReactCrop src=={this.state.img} />. The crop also needs to be in a state, so that when it changes, it updates the crop rectangle.
Here's a working example from the creator himself: https://codesandbox.io/s/72py4jlll6

How to re-animate react-spring animation using hooks on button click?

Following simple component from the official examples:
import {useSpring, animated} from 'react-spring'
function App() {
const props = useSpring({opacity: 1, from: {opacity: 0}})
return <animated.div style={props}>I will fade in</animated.div>
}
Question
How do I animate the fadeIn-effect (or any other animation) again for example when I click on a button or when a promise is resolved?
You can basically make two effect with useSpring and an event.
You can change the style for example the opacity depending on the state of an event.
You can restart an animation on state change. The easiest way to restart is to rerender it.
I created an example. I think you want the second case. In my example I rerender the second component with changing its key property.
const Text1 = ({ on }) => {
const props = useSpring({ opacity: on ? 1 : 0, from: { opacity: 0 } });
return <animated.div style={props}>I will fade on and off</animated.div>;
};
const Text2 = () => {
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
return <animated.div style={props}>I will restart animation</animated.div>;
};
function App() {
const [on, set] = React.useState(true);
return (
<div className="App">
<Text1 on={on} />
<Text2 key={on} />
<button onClick={() => set(!on)}>{on ? "On" : "Off"}</button>
</div>
);
}
Here is the working example: https://codesandbox.io/s/upbeat-kilby-ez7jy
I hope this is what you meant.

React track height of div in state?

I found the code snippet from this answer for tracking page size to be useful. I want to switch window.innerHeight with $("#list_container").height:
constructor(props) {
super(props);
this.state = { width: 0, height: 0 };
this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
}
componentDidMount() {
this.updateWindowDimensions();
window.addEventListener('resize', this.updateWindowDimensions);
}
componentWillUnmount() {
window.removeEventListener('resize', this.updateWindowDimensions);
}
updateWindowDimensions() {
// !!! This works:
this.setState({ width: window.innerWidth, height: window.innerHeight });
// !!! This doesn't work:
this.setState({ width: $("#list_container").width(), height: $("#list_container").height() });
}
Edit: Updated to .width() and .height(), had tried both but neither is working for me.
List container is defined in the same module as the outer div:
render() {
return (
<div id="list_container">
<ListGroup>
<VirtualList
width='100%'
height={this.state.height}
itemCount={this.state.networks.length}
itemSize={50}
renderItem={({index, style}) =>
<ListGroupItem
onClick={() => this.updateNetwork(this.state.networks[index].ssid, this.state.networks[index].security)}
action>
{this.state.networks[index].ssid}
</ListGroupItem>
}
/>
</ListGroup>
<Modal/>
// ...
Note:
If I do:
<p>{this.state.height}</p>
It isn't 0 just empty, with the example that doesn't work.
If it's jQuery you're using, width() and height() are functions, not properties. Try:
this.setState({ width: $("#list_container").width(), height: $("#list_container").height() });
you need to use refs since the js might be running before the component renders to the dom.
React documentation on refs
render() {
return (
<div id="list_container" ref={ el => this.componentName = el }>
// ...
to reference this dom node in this component you just have to call this.componentName
updateWindowDimensions = () => {
$(this.componentName).height() // returns height
}
To expand on Erics answer using refs, to pull it off without jQuery, you can use getBoundingClientRect:
const listRect = document.getElementById('list_container').getBoundingClientRect();
or if using the ref from Erics answer:
const listRect = this.componentName.getBoundingClientRect();
followed by
this.setState({
width: listRect.width,
height: listRect.height
});
I would suggest this page for some good examples of life without jQuery.
Also, I would strongly suggest debouncing the resize handler.

Resources