I am trying to use react reference to render data into iframe but I am getting
reference.current value null.
I also check the value in componentDidMount() still it give me null value. As I set refrence value in constructor. Reference is set to "PreviewEmailModal" component insted of "iframe" component.
Thats why I am getting current value as null. So some can please help me to set refrence to iframe component.
React version- 16.4.2
React DOM version - 16.4.2
import React from "react";
import PropTypes from 'prop-types'
import { Button, Modal } from "react-bootstrap";
class PreviewEmailModal extends React.Component {
constructor() {
super();
this.textInput = React.createRef();
this.state = {
desktopPreview: true
};
}
render() {
this.props;
debugger
return (
<Modal
show={true} onHide={() => {
this.props.closeModal();
}}
className="preview-email-modal"
bsSize="lg">
<Modal.Header closeButton>
<Modal.Title>Preview</Modal.Title>
</Modal.Header>
<Modal.Body >
<div className="preview-icons">
<span className={this.state.desktopPreview ? "active-preview-icon margin-5" : ""}>
<i className="glyphicon glyphicon-blackboard" onClick={() => {
this.togglePreview();
}} />
</span>
<span className={this.state.desktopPreview ? "" : "active-preview-icon margin-5"}>
<i className="glyphicon glyphicon-phone" onClick={() => {
this.togglePreview();
}} />
</span>
</div>
<div>
<div className="text-center">
<iframe
title={"previewFrame"}
style={this.state.desktopPreview ?
{ width: "600px", height: "450px" } :
{ width: "320px", height: "450px" }}
id="previewIframe"
ref={(input) => {
this.textInput = input;
}}
/>
</div>
</div>
</Modal.Body>
</Modal>
);
}
componentDidUpdate() {
debugger
}
componentDidMount() {
const { current } = this.textInput;
//Here I get current value as null every time
const { data } = this.props;
if (current !== null) {
const doc = current.contentDocument;
doc.open();
doc.write(data);
doc.close();
}
}
focusTextInput() {
// Explicitly focus the text input using the raw DOM API
this.textInput.focus();
}
togglePreview() {
this.setState({ desktopPreview: !this.state.desktopPreview });
}
}
PreviewEmailModal.propTypes = {
closeModal: PropTypes.func,
data: PropTypes.string
};
export default PreviewEmailModal;
You are not using Ref correctly. Instead of passing callback to ref field pass instance.
OR pass
ref={this.textInput}
instead of
ref={(input) => {this.textInput = input;}}
You're likely trying to reference this.textInput when it's not actually on the DOM.
I solved this by adding componentDidUpdate like this:
componentDidUpdate() {
const { visible, emailHtml } = this.props;
if (visible) {
// this.iframe is my iframe ref
this.iframe.contentWindow.document.write(emailHtml);
}
}
On another note I don't think you need this line for the ref to work:
this.textInput = React.createRef();
I had the exact same issue (literally my component is called PreviewEmailModal too). I even have a very similar Modal component I'm using within PreviewEmailModal as well. Small world!
As I don't know all of your code I would recommend to move your code from componentDidMount to the ref callback. Please check the answer of Dan Abramov here (componentDidMount called BEFORE ref callback)
Related
My code generates an input field that allows a user to enter a value to search for. Then when they click the Submit button, it causes displayMap to be true, so that when the MapDisplay component renders, it will trigger an API search via the Map component and return values that are then displayed on the map.
The problem is that this process only works once. When I click the button again, it does do something, I confirmed that it is getting the new value in the input box, but I can't seem to figure out how to get the map to be rendered again.
I've tried setting other variables in the this.setState to try to get it to know that it needs to render the component again, but I guess I'm missing something, because nothing works.
I'm fairly new to React, so any help you can offer would be greatly appreciated.
This is the MainSearchBar.js, where most of the work as described above is happening:
import Map from './newMap.js';
function MapDisplay(props) {
if (props.displayMap) {
return <Map toSearch = {props.searchTerm}></Map>;
} else {
return "";
}
}
class MainSearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
displayMap: false,
value: '',
searchTerm: '',
isOpened: false
};
//this.handleClick = this.handleClick.bind(this);
this.handleChange = this.handleChange.bind(this);
}
handleClick = () => {
this.setState({
displayMap: true,
isOpened: !this.state.isOpened,
searchTerm: this.state.value
});
console.log(this.state.value);
}
handleChange(event) {
this.setState({value: event.target.value});
}
render() {
const displayMap = this.state.displayMap;
return (
<div class="homepage-search-bar">
<input
type="text" name="search" value={this.state.value} onChange={this.handleChange} className="main-search-bar" placeholder="Search hashtags">
</input>
<button onClick={this.handleClick}>Search</button>
<MapDisplay displayMap={displayMap} searchTerm={this.state.value} />
</div>
)
}
}
export default MainSearchBar;
This is where MainSearchBar is being called from
import Top20Box from '../components/getTop20Comp2.js';
import Header from '../components/Header.js';
import MainIntro from '../components/MainIntro.js';
import MainSearchBar from '../components/MainSearchBar.js';
import MainCTA from '../components/MainCTA.js';
import Footer from '../components/Footer.js';
export default class Home extends Component {
state = {
}
render () {
return (
<React.Fragment>
<Header>
</Header>
<MainIntro />
<MainSearchBar />
<div className="top20-text">
Top 20 trending hashtags
</div>
<Top20Box />
<MainCTA />
<Footer />
</React.Fragment>
)
}
}
And this is the Map component itself, in case you need it:
import React from 'react';
import ReactMapGL, {Marker, Popup} from 'react-map-gl';
import axios from 'axios';
//for the loading animation function
import FadeIn from "react-fade-in";
import Lottie from "react-lottie";
import * as loadingData from "../assets/loading.json";
var locationCoordinates = [];
var locationToSearch = "";
var returnedKeywordSearch = [];
var newArray = [];
const defaultOptions = {
loop: true,
autoplay: true,
animationData: loadingData.default,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice"
}
};
export default class Map extends React.Component {
//sets components for the map, how big the box is and where the map is centered when it opens
state = {
viewport: {
width: "75vw",
height: "50vh",
latitude: 40.4168,
longitude: 3.7038,
zoom: .5
},
tweetSpots: null, //data from the API
selectedSpot: null,
done: undefined, //for loading function
};
async componentDidMount() {
//searches the api for the hashtag that the user entered
await axios.get(`https://laffy.herokuapp.com/search/${this.props.toSearch}`).then(function(response) {
returnedKeywordSearch = response.data;
}) //if the api call returns an error, ignore it
.catch(function(err) {
return null;
});
//goes through the list of locations sent from the api above and finds the latitude/longitude for each
var count = 0;
while (count < returnedKeywordSearch.length) {
locationToSearch = returnedKeywordSearch[count].location;
if (locationToSearch !== undefined) {
var locationList = await axios.get(`https://api.mapbox.com/geocoding/v5/mapbox.places/${locationToSearch}.json?access_token=pk.eyJ1IjoibGF1bmRyeXNuYWlsIiwiYSI6ImNrODlhem95aDAzNGkzZmw5Z2lhcjIxY2UifQ.Aw4J8uxMSY2h4K9qVJp4lg`)
.catch(function(err) {
return null;
});
if (locationList !== null) {
if (Array.isArray(locationList.data.features) && locationList.data.features.length)
{
locationCoordinates.push(locationList.data.features[0].center);
if (returnedKeywordSearch[count].location!== null && returnedKeywordSearch[count].location!==""
&& locationList.data.features[0].center !== undefined)
{newArray.push({
id: returnedKeywordSearch[count].id,
createdAt: returnedKeywordSearch[count].createdAt,
text: returnedKeywordSearch[count].text,
name: returnedKeywordSearch[count].name,
location: returnedKeywordSearch[count].location,
coordinates: locationList.data.features[0].center
});
}
}
}
}
count++;
}
this.setState({tweetSpots: newArray});
this.setState({ done: true}); //sets done to true so that loading animation goes away and map displays
}
//is triggered when a marker on the map is hovered over
setSelectedSpot = object => {
this.setState({
selectedSpot: object
});
};
//creates markers that display on the map, using location latitude and longitude
loadMarkers = () => {
return this.state.tweetSpots.map((item,index) => {
return (
<Marker
key={index}
latitude={item.coordinates[1]}
longitude={item.coordinates[0]}
>
<img class="mapMarker"
onMouseOver={() => {
this.setSelectedSpot(item);
}}
src="/images/yellow7_dot.png" alt="" />
</Marker>
);
});
};
//closes popup when close is clicked
closePopup = () => {
this.setState({
selectedSpot: null
});
};
//renders map component and loading animation
render() {
return (
<div className="App">
<div className="map">
{!this.state.done ? (
<FadeIn>
<div class="d-flex justify-content-center align-items-center">
<Lottie options={defaultOptions} width={400} />
</div>
</FadeIn>
) : (
<ReactMapGL {...this.state.viewport} mapStyle="mapbox://styles/mapbox/outdoors-v11"
onViewportChange={(viewport => this.setState({viewport}))}
mapboxApiAccessToken="pk.eyJ1IjoibGF1bmRyeXNuYWlsIiwiYSI6ImNrODlhem95aDAzNGkzZmw5Z2lhcjIxY2UifQ.Aw4J8uxMSY2h4K9qVJp4lg">
{this.loadMarkers()}
{this.state.selectedSpot !== null ? (
<Popup
key={this.state.selectedSpot.id}
tipSize={5}
latitude={this.state.selectedSpot.coordinates[1]}
longitude={this.state.selectedSpot.coordinates[0]}
closeButton={true}
closeOnClick={false}
onClose={this.closePopup}
>
<div className="mapPopup">
<div className="header"> Tweet </div>
<div className="content">
{" "}
<p>
<b>Name:</b> {this.state.selectedSpot.name}
</p>
<p>
<b>Tweet:</b> {this.state.selectedSpot.text}</p>
<p>View Tweet in Twitter
</p>
</div>
</div>
</Popup>
) : null}
</ReactMapGL>
)}
</div>
</div>
);
}
}
Update: 4/28, per the answer I received, I update the render of the MainSearchBar.js to look like this:
render() {
const displayMap = this.state.displayMap;
return (
<div class="homepage-search-bar">
<input
type="text" name="search" value={this.state.value} onChange={this.handleChange} className="main-search-bar" placeholder="Search hashtags">
</input>
<button onClick={this.handleClick}>Search</button>
{this.state.displayMap && <Map toSearch = {this.searchTerm}></Map>}
</div>
)
}
When you click the button again, the state of MainSearchBar.js updates but the functional component MapDisplay does not and thus the Map does not update as well.
There are many ways to resolve this. Looking at the code, it looks like MapDisplay doesn't do much so you can consider replacing it with conditional rendering.
MainSearchBar.js
render() {
const displayMap = this.state.displayMap;
return (
<div class="homepage-search-bar">
<input
type="text" name="search" value={this.state.value} onChange={this.handleChange} className="main-search-bar" placeholder="Search hashtags">
</input>
<button onClick={this.handleClick}>Search</button>
{this.state.displayMap && <Map toSearch = {props.searchTerm}></Map>}
</div>
)
}
Then in your Map component, add a componentDidUpdate lifecycle method to detect updates to the prop which does the same thing as componentDidMount when the props are updated.
async componentDidMount(prevProps) {
if (props.toSearch != prevProps.toSearch) {
await axios.get(`https://laffy.herokuapp.com/search/${this.props.toSearch}`).then(function(response) {
returnedKeywordSearch = response.data;
}) //if the api call returns an error, ignore it
.catch(function(err) {
return null;
});
//goes through the list of locations sent from the api above and finds the latitude/longitude for each
var count = 0;
while (count < returnedKeywordSearch.length) {
locationToSearch = returnedKeywordSearch[count].location;
if (locationToSearch !== undefined) {
var locationList = await axios.get(`https://api.mapbox.com/geocoding/v5/mapbox.places/${locationToSearch}.json?access_token=pk.eyJ1IjoibGF1bmRyeXNuYWlsIiwiYSI6ImNrODlhem95aDAzNGkzZmw5Z2lhcjIxY2UifQ.Aw4J8uxMSY2h4K9qVJp4lg`)
.catch(function(err) {
return null;
});
if (locationList !== null) {
if (Array.isArray(locationList.data.features) && locationList.data.features.length)
{
locationCoordinates.push(locationList.data.features[0].center);
if (returnedKeywordSearch[count].location!== null && returnedKeywordSearch[count].location!==""
&& locationList.data.features[0].center !== undefined)
{newArray.push({
id: returnedKeywordSearch[count].id,
createdAt: returnedKeywordSearch[count].createdAt,
text: returnedKeywordSearch[count].text,
name: returnedKeywordSearch[count].name,
location: returnedKeywordSearch[count].location,
coordinates: locationList.data.features[0].center
});
}
}
}
}
count++;
}
this.setState({tweetSpots: newArray});
this.setState({ done: true}); //sets done to true so that loading animation goes away and map displays
}
}
#wxker Thanks for all your help! You certainly got me pointed in the right direction.
I changed render in MainSearchBar.js back to what it was originally.
And I added a ComponentDidUpdate to the Map component, as follows below:
async componentDidUpdate(prevProps) {
//searches the api for the hashtag that the user entered
if (this.props.toSearch !== prevProps.toSearch) {
and then the rest was the same as the original componentDidMount.
I have the following class that works fine (I know that DOMSubtreeModified is depreacted, I will update this too). It's a very basic WYIWYG I'm trying to refactor to a hook:
export class TextEditorClass extends React.Component {
constructor(props) {
super(props)
this.state ={
// this props contains the html content of the contentEditable
content: this.props.content,
}
}
componentDidMount() {
// here I add a listener to the contentEditable div that calls updateContent
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
document.getElementById("editor").innerHTML = this.props.content;
rangy.init();
}
setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
updateContent() {
this.props.setContent('content', document.getElementById('editor').innerHTML);
this.setState({
content: document.getElementById('editor').innerHTML,
})
}
render() {
return (
<div className='editor-content dashed'>
<input id="myInput" type="file" ref={(ref) => this.upload = ref} style={{ display: 'none' }} />
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
</div>
</div>
)
}
}
This is what I wrote so far:
export function TextEditorHook() {
const value = React.useContext(ManagerContext);
React.useEffect(() => {
document.getElementById("editor").innerHTML = value.state.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
rangy.init();
});
function setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
function updateContent() {
value.dispatch({type: 'content', value: document.getElementById('editor').innerHTML});
}
return (
<div className='editor-content dashed'>
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
</div>
</div>
)
}
But it doesn't work:
Warning: A component is changing a controlled input of type text to be
uncontrolled. Input elements should not switch from controlled to
uncontrolled (or vice versa). Decide between using a controlled or
uncontrolled input element for the lifetime of the component.
Second warning:
Warning: Maximum update depth exceeded. This can happen when a
component calls setState inside useEffect, but useEffect either
doesn't have a dependency array, or one of the dependencies changes on
every render.
The reason behind this I suppose is the fact that the typing triggers a loop of rerenders. Why it doesn't happen in the class?
EDIT: I believe the key to fix the issue is in useEffect(); I suppose it behaves differently from componentDidMount, in fact in the hook version I had to swap these two lines to prevent another error from occurring from this:
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
document.getElementById("editor").innerHTML = this.props.content;
to this:
document.getElementById("editor").innerHTML = this.props.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
EDIT 2:
Adding [] as a parameter for useEffect() I got rid of the first error:
React.useEffect(() => {
document.getElementById("editor").innerHTML = value.state.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
rangy.init();
}, []);
More info about it here: https://dev.to/trentyang/replace-lifecycle-with-hooks-in-react-3d4n
Your issue does not happen in React class component because componentDidMount is run once and you put the code of componentDidMount in useEffect without any conditions - which means it will run on each render.
Problems with your useEffect:
1) You are adding an event listender but you are not doing a clean-up afterwards.
2)You are mutating the DOM directly which is an anti pattern
3)You are applying any depedencies on your useEffect, which execut the useEffect on each render and will cause a memory leak.
Here is how your code should look like: (will be writing code shortly)
...
const [editorValue, setEditorValue] = React.useState('')
// If you want your hook to run once
React.useEffect( () => {
setEditorValue(value.state.content)
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
// The returning funnction will be executed on unmounting
() => document.getElementById("editor").removeEventListener("DOMSubtreeModified", () => updateContent(), false);
// note the `[]`] as second argument. That means to run only once.
}, [])
And the content of your #editor is then the controllable state editorValue.
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
{editorValue}
</div>
Matthew's answer pointed me to the right direction, and after a bit of tweaking I came up with this code that works fine:
import React from 'react'
import { Icon, IconButton, ButtonGroup, ButtonToolbar } from 'rsuite'
import rangy from 'rangy';
import 'rangy/lib/rangy-classapplier';
import dompurify from 'dompurify';
import './TextEditor.css';
import { ManagerContext } from './LessonManager.js';
export function TextEditorHook() {
const value = React.useContext(ManagerContext);
// here I initialize the editor content with the content I receive from the provider
const [editorValue, setEditorValue] = React.useState(value.state.content);
React.useEffect( () => {
rangy.init();
const editor = document.getElementById('editor');
// here I replaced the deprecated DOMSubtreeModified with MutationObserver
// basically when you change the dom of the contentEditable I call dispatch
// and update the content
let mutationObserver = new MutationObserver(function(mutations) {
value.dispatch({type: 'content', value: editor.innerHTML});
});
mutationObserver.observe(editor, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
// the cleanup can be done with .disconnect();
return () => mutationObserver.disconnect();
});
function setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
// the createMarkup() function is used to generate the markup that I will put inside
// my contentEditable, I installed an additional npm package (dompurify) to clean up
// the markup to prevent XSS attacks
// https://dev.to/jam3/how-to-prevent-xss-attacks-when-using-dangerouslysetinnerhtml-in-react-1464)
function createMarkup() {
const sanitizer = dompurify.sanitize;
return {__html: sanitizer(editorValue)}
};
return (
<div className='editor-content dashed'>
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
// here I insert the html with the purified html
dangerouslySetInnerHTML={createMarkup()}
>
</div>
</div>
)
}
To give a complete answer I will add also the parent components. This is the LessonContents component:
import React, { useEffect } from 'react';
import { Input } from 'rsuite';
// TextEditorHook is the WYSIWYG component
import { TextEditorHook } from './TextEditor.js';
// this context contain the value of the lesson manager
// we will use the context for the lesson's title, desc, content and location
import { ManagerContext } from './LessonManager.js';
// truncate is a function to shorten text in order to make it fit
// maxLenght is the default max text length
import { truncate, maxLength } from '../Common.js';
export default function Menu() {
const value = React.useContext(ManagerContext);
useEffect(() => {
// if not editing inject the html in the display div on load
if (!value.editing) document.querySelector('.lesson-content').innerHTML = value.state.content;
console.log(value);
});
const editorContent = () => {
return (
<React.Fragment>
<Input
className='rsuite-input dashed'
placeholder='Lesson title'
value={value.state.title}
onChange={(v) => value.dispatch({type: 'title', value: v})}
/>
<Input
className='rsuite-input dashed'
componentClass="textarea"
rows={1}
style={{ width: '100%' }}
placeholder='Lesson description'
value={value.state.desc}
onChange={(v) => value.dispatch({type: 'desc', value: v})}
/>
<TextEditorHook/>
</React.Fragment>
)
}
const viewerContent = () => {
return (
<React.Fragment>
<div className='content'>
<span>{truncate(value.state.title, maxLength, false)}</span>
</div>
<div className='content'>
<span>{value.state.desc}</span>
</div>
<div className='content'>
<span>{truncate(value.state.location, maxLength, false)}</span>
</div>
<div className='content justify-left lesson-content'>
</div>
</React.Fragment>
)
}
return (
<React.Fragment>
{value.editing ? editorContent() : viewerContent()}
</React.Fragment>
)
}
And finally the LessonManager component:
import React from 'react';
import LessonMenu from './LessonMenu.js';
// this context will be used from child components to access the lesson status
import LessonContents from './LessonContents.js';
export const ManagerContext = React.createContext(null);
const initialState = {
title: 'Lesson title',
desc: 'Lesson description',
content: 'Lesson content',
location: 'home / english'
};
function reducer(state, action) {
console.log(action.type, action.value, state)
switch (action.type) {
case 'title':
return {...state, title: action.value};
case 'desc':
return {...state, desc: action.value};
case 'content':
return {...state, content: action.value};
default:
throw new Error();
}
}
export default function LessonManager() {
const [editing, toggleEditor] = React.useState(true);
const [state, dispatch] = React.useReducer(reducer, initialState);
const value = React.useMemo(() => {
return {
state,
dispatch,
editing,
toggleEditor,
}
}, [state, editing]);
return (
<ManagerContext.Provider value={value}>
<div className='box-default expand'>
<div className='handle' style={{display: 'flex', justifyContent: 'center', width: '100%', cursor: 'grab'}}>
<LessonMenu />
</div>
<LessonContents />
</div>
</ManagerContext.Provider>
)
}
I am not completely sure this is the best approach to solve this problem, but it works fine.
What I want to do is to be able to toggle an active class on my elements that are dynamically created, as to be able to change the css for the selected checkbox, giving the impression that a certain filter is selected. I have looked at so many solutions and guides to make this work for my app, but I can't seem to implement it correctly. Any help would be appreciated.
Checkboxes component
import React from 'react';
const Checkbox = (props) => {
const { label, subKey } = props;
const sub1 = `${subKey}1`;
return (
<label htmlFor={sub1} className="check_label">
{label}
<input
type="checkbox"
id={sub1}
checked={props.isChecked}
onChange={props.handleCheck}
onClick={() => console.log(label)}
value={`${label.toLowerCase()}/?search=`}
/>
</label>
);
};
export default Checkbox;
and the Search component that implements checkboxes
import React, { Component } from 'react';
import Checkbox from './Checkbox';
const APIQuery = 'https://swapi.co/api/';
const searchLabels = ['Planets', 'Starships', 'People', 'Species', 'Films', 'Vehicles'];
export default class Searchbutton extends Component {
constructor(props) {
super(props);
this.state = {
endpointValue: '',
searchValue: '',
};
}
/* Funcionality to handle form and state of form */
/* Changes state of value whenever the form is changed, in realtime. */
handleChange(event) {
this.setState({ searchValue: event.target.value });
}
/* Prevents default formsubmit */
handleSubmit(event) {
event.preventDefault();
}
/* Handles state of checkboxes and sets state as to prepend necessary filter for request */
handleCheck(event) {
this.setState({ endpointValue: event.target.value });
if (this.state.endpointValue === event.target.value) {
this.setState({ endpointValue: '' });
}
}
/* Creates the checkboxes dynamically from the list of labels. */
createBoxes() {
const checkboxArray = [];
searchLabels.map(item => checkboxArray.push(
<Checkbox
key={item}
className="madeBoxes"
subKey={item}
endpointValue={this.state.endpointValue}
handleChange={e => this.handleChange(e)}
handleCheck={e => this.handleCheck(e)}
label={item}
/>,
));
return checkboxArray;
}
render() {
return (
<div className="search_content">
<div className="search_wrapper">
<form onSubmit={this.handleSubmit} method="#">
<label htmlFor="searchBar">
<input type="text" id="searchbar" className="search_bar" value={this.state.searchValue} onChange={e => this.handleChange(e)} />
</label>
<div>
<input type="submit" className="search_button" value="May the Force be with you." onClick={() => this.props.searchWithApi(APIQuery + this.state.endpointValue + this.state.searchValue)} />
</div>
</form>
</div>
<div className="checkboxes">
{this.createBoxes(this.labels)}
</div>
<div className="sort_filters">
{' '}
{/* These are options that the user can make in order to sort and filter the results.
The idea is to make it so that changing the value auto-perform a new request */}
{/* For sorting the returned objects based on user choice */}
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid, until href added */}
Choose sort method
<ul className="sorting">
<li className="sort_optn" href="#" value="lexicographical">Alphabetically</li>
<li className="sort_optn" href="#" value="by_added_date">By added date</li>
<li className="sort_optn" href="#" value="by_added_date_rev">By added date reversed</li>
</ul>
</div>
</div>
);
}
}
You don't really have to do it with react. You can reformat your code a little bit and solve it with CSS :checked pseudo-class.
In particular, don't wrap your checkbox within a label, but instead put the label after the input. Check this fiddle for example: https://jsfiddle.net/8c7a0fx5/
You can use the styled-component package. check the example below on how to use it:
import { Component } from 'react'
import { render } from 'react-dom'
import styled from 'styled-components'
const StyledCheckbox = styled.div`
label {
background: ${props => props.active ? 'red': 'white'}
}
`
class MyAwesomeComponent extends Component {
constructor(){
super()
this.state = {
isChecked: false
}
this.handleOnChange = this.handleOnChange.bind(this)
}
handleOnChange = ()=>{
this.setState({
isChecked: !this.state.isChecked,
})
}
render(){
const { isChecked } = this.state
return(
<StyledCheckbox active={isChecked}>
<label>Names</label>
<input type="checkbox" onChange={this.handleOnChange} />
</StyledCheckbox>
)
}
}
render(<MyAwesomeComponent/>, document.getElementById('root'))
Working code on codepen.io
I have a parent jsx component that has 2 different jsx components within it.
The one component is a button and the other is a div that opens and closes itself when you click on it (it has a click handler and a state of open or closed).
I now want to add the ability for the button to open and close the div as well.
Is the only way to accomplish this is to pass a handler function down to the button from the parent, moving the div’s open and closed state to the parent component, and pass the state down to the div as props? The reason I ask is that this particular div component is used in a number of different components and removing the open and closed state would affect a lot of different parent components.
Here's a code example of allowing external state manipulation where you can mix the usage of the button or the div to toggle the state. You extend your Collapsible component to use passed props to update the state.
class Collapsible extends React.Component {
constructor(props){
super(props);
this.state = { isOpen: this.props.isOpen !== false };
this.toggleOpen = this.toggleOpen.bind(this);
}
componentWillReceiveProps({ isOpen }) {
this.setState({ isOpen });
}
toggleOpen(){
this.setState((prevState) => ({ isOpen: !prevState.isOpen }))
}
render() {
let display = this.state.isOpen ? null : "none";
return (
<div
className="collapsible"
onClick={this.toggleOpen}
>
<header> header </header>
<div style={{ display }}>{this.props.children}</div>
</div>
);
}
}
class Parent extends React.Component {
constructor(props){
super(props);
this.state = { isOpen: true };
this.toggleOpen = this.toggleOpen.bind(this);
}
toggleOpen(){
this.setState((prevState) => ({ isOpen: !prevState.isOpen }))
}
render() {
return (
<div className="parent">
<Collapsible isOpen={this.state.isOpen}>content</Collapsible>
<button onClick={this.toggleOpen}>
toggle
</button>
</div>
);
}
}
Here is another code example, hope it helps:
I use the local state of the Container and pass this down to the child components. In a bigger app I'd advice you to use something like Redux to manage your state.
The central idea is that the parent component passes a function which can "change it's state" to the button child. It also passed the current isOpen state to the panel. Clicking the button will change the state of the parent, thus triggering a reflow, thus updating the collapsable.
For future reference:
import React from "react";
import { render } from "react-dom";
const Collapsable = ({ isOpen }) =>
isOpen ? (
<div
style={{
border: "1px solid orange",
padding: "1rem",
background: "maroon",
color: "white"
}}
>
{" "}
Hey, I'm open{" "}
</div>
) : (
<div>Oh no...closed :(</div>
);
const Button = ({ openPanel }) => (
<button onClick={() => openPanel()}>Open Panel</button>
);
class Container extends React.PureComponent {
state = { open: false };
openPanel = () => {
this.setState({
...this.state,
open: this.state.open ? false : true
});
};
render() {
return (
<div>
<Button openPanel={this.openPanel} />
<Collapsable isOpen={this.state.open} />
</div>
);
}
}
const App = () => (
<div>
<Container />
</div>
);
render(<App />, document.getElementById("root"));
I am looking to create a stateless component who's input element can be validated by the parent component.
In my example below, I am running into a problem where the input ref is never being assigned to the parent's private _emailAddress property.
When handleSubmit is called, this._emailAddress is undefined. Is there something I'm missing, or is there a better way to do this?
interface FormTestState {
errors: string;
}
class FormTest extends React.Component<void, FormTestState> {
componentWillMount() {
this.setState({ errors: '' });
}
render(): JSX.Element {
return (
<main role='main' className='about_us'>
<form onSubmit={this._handleSubmit.bind(this)}>
<TextInput
label='email'
inputName='txtInput'
ariaLabel='email'
validation={this.state.errors}
ref={r => this._emailAddress = r}
/>
<button type='submit'>submit</button>
</form>
</main>
);
}
private _emailAddress: HTMLInputElement;
private _handleSubmit(event: Event): void {
event.preventDefault();
// this._emailAddress is undefined
if (!Validators.isEmail(this._emailAddress.value)) {
this.setState({ errors: 'Please enter an email address.' });
} else {
this.setState({ errors: 'All Good.' });
}
}
}
const TextInput = ({ label, inputName, ariaLabel, validation, ref }: { label: string; inputName: string; ariaLabel: string; validation?: string; ref: (ref: HTMLInputElement) => void }) => (
<div>
<label htmlFor='txt_register_first_name'>
{ label }
</label>
<input type='text' id={inputName} name={inputName} className='input ' aria-label={ariaLabel} ref={ref} />
<div className='input_validation'>
<span>{validation}</span>
</div>
</div>
);
You can useuseRef hook which is available since v16.7.0-alpha.
EDIT: You're encouraged to use Hooks in production as of 16.8.0 release!
Hooks enable you to maintain state and handle side effects in functional components.
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
Read more in Hooks API documentation
EDIT: You now can with React Hooks. See the answer by Ante Gulin.
You can't access React like methods (like componentDidMount, componentWillReceiveProps, etc) on stateless components, including refs. Checkout this discussion on GH for the full convo.
The idea of stateless is that there isn't an instance created for it (state). As such, you can't attach a ref, since there's no state to attach the ref to.
Your best bet would be to pass in a callback for when the component changes and then assign that text to the parent's state.
Or, you can forego the stateless component altogether and use an normal class component.
From the docs...
You may not use the ref attribute on functional components because they don't have instances. You can, however, use the ref attribute inside the render function of a functional component.
function CustomTextInput(props) {
// textInput must be declared here so the ref callback can refer to it
let textInput = null;
function handleClick() {
textInput.focus();
}
return (
<div>
<input
type="text"
ref={(input) => { textInput = input; }} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
This is late but I found this solution much better.
Pay attention to how it uses useRef & how properties are available under current property.
function CustomTextInput(props) {
// textInput must be declared here so the ref can refer to it
const textInput = useRef(null);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input
type="text"
ref={textInput} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
For more reference check react docs
The value of your TextInput is nothing more than a state of your component. So instead of fetching the current value with a reference (bad idea in general, as far as I know) you could fetch the current state.
In a reduced version (without typing):
class Form extends React.Component {
constructor() {
this.state = { _emailAddress: '' };
this.updateEmailAddress = this.updateEmailAddress.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
updateEmailAddress(e) {
this.setState({ _emailAddress: e.target.value });
}
handleSubmit() {
console.log(this.state._emailAddress);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
value={this.state._emailAddress}
onChange={this.updateEmailAddress}
/>
</form>
);
}
}
You can also get refs into functional components with a little plumbing
import React, { useEffect, useRef } from 'react';
// Main functional, complex component
const Canvas = (props) => {
const canvasRef = useRef(null);
// Canvas State
const [canvasState, setCanvasState] = useState({
stage: null,
layer: null,
context: null,
canvas: null,
image: null
});
useEffect(() => {
canvasRef.current = canvasState;
props.getRef(canvasRef);
}, [canvasState]);
// Initialize canvas
useEffect(() => {
setupCanvas();
}, []);
// ... I'm using this for a Konva canvas with external controls ...
return (<div>...</div>);
}
// Toolbar which can do things to the canvas
const Toolbar = (props) => {
console.log("Toolbar", props.canvasRef)
// ...
}
// Parent which collects the ref from Canvas and passes to Toolbar
const CanvasView = (props) => {
const canvasRef = useRef(null);
return (
<Toolbar canvasRef={canvasRef} />
<Canvas getRef={ ref => canvasRef.current = ref.current } />
}