React Router, change path alternative of link element - hyperlink

Is it possible to change route in react by react-router by other way than <Link></Link> ?
(e.g change route when onClick, or onKeydown event runs some function)

There is another way. You can use history.push in your code:
import { useHistory } from 'react-router-dom';
const YourComponent = () => {
const history = useHistory();
return <button onClick={() => history.push('/profile')}>Profile</button>;
};

Related

How to write unittest for table row selection using Antd Library in React

I have a react application and I'm using #testing-library/jest-dom for unit testing and antd 3.x library for UI design. In one of my UI screen there is a table and a button where the button only enables when one of the row in table is checked. So I wanted to do a unittest for this. So below is my src code,
import {Modal, Button, Form, Input, DatePicker, Table, Card, Tooltip} from 'antd';
...
...
return (
<>
<Form>
...
<Card>
<Table
data-testid={'lift-hold-grid'}
className="holdListResultsTable"
rowSelection={rowSelection}
columns={columns}
dataSource={dataSourcee}
components={{
body: {
row: showRestorationTooltip
}
}}
/>
</Card>
...
<div style = {...}>
<Button data-testid={'lift-hold-button'} disabled={...} onClick={...}>
Lift Hold
</Button>
</div>
<Form>
</>
)
and below is the unittest
import { render } from "#testing-library/react";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
function renderWithReduxAndThunk(ui, initialState) {
const createdStore = createStore(rootReducer, initialState, applyMiddleware(thunk));
return {
...render(<Provider store={createdStore}>{ui}</Provider>),
createdStore,
}
}
const changedState = {
...
}
it('should enable the lift hold button after checkbox selection and note input', () => {
const { container, getByTestId } = renderWithReduxAndThunk(<LegalHoldLiftScreen />, changedState);
const checkBox = container.querySelector('.ant-checkbox-input');
fireEvent.click(checkBox);
const downloadButton = getByTestId('lift-hold-button');
expect(downloadButton).toBeTruthy();
expect(downloadButton).not.toBeDisabled();
});
but this fails with below message
● should enable the lift hold button after checkbox selection and note input
Unable to fire a "click" event - please provide a DOM element.
what am I missing here??

Old component retains state on back button

I have a link in a modal that goes to a new page, and I can't seem to reset the state of the component with the modal when directing to that page.
The component with the modal seems to be keeping its state after directing to the new page, because when I hit the back button, it automatically opens the modal.
The modal is either opened or closed based on the state of modalIsOpen.
So I have my simplified Listings component:
import React from 'react'
import ListingModalContent from '../ListingModalContent'
import Modal from '../Modal'
export default class Listings extends React.Component {
constructor(props) {
super(props)
this.state = {
modalIsOpen: false,
modalContent: null
}
}
modalClick = (e, listing) => {
e.preventDefault()
this.setState({
modalContent: <ListingModalContent listing={listing}/>
}, () => {
this.setState({modalIsOpen: true})
})
}
modalClose = () => {
this.setState({modalIsOpen: false})
}
componentWillMount() {
this.setState({modalIsOpen: false})
console.log('mounting...')
console.log(this.state.modalIsOpen)
}
componentWillUnmount() {
console.log('unmounting...')
this.setState({
modalIsOpen: false
}, () => {
console.log('got here...')
console.log(this.state.modalIsOpen)
})
console.log(this.state.modalIsOpen)
}
render() {
const listings = this.props.listings.map(listing => (<div className="listing">
<a href="#" onClick={e => this.modalClick(e, listing)}>More Details</a>
</div>))
return (<div id="listings">
<section className="listings">
{listings}
<Modal visible={this.state.modalIsOpen} onClose={this.modalClose}>
{this.state.modalContent}
</Modal>
</section>
</div>)
}
}
And my ListingsModalContent component:
import React from 'react'
export default class ListingModalContent extends React.Component {
constructor(props) {
super(props)
}
render() {
const {listing} = this.props
return (<div className="listing-modal">
<div className="details">
<h2 className="address">{listing.address}</h2>
<p className="description">{listing.description}</p>
</div>
<div className="btn-container">
<a href={`/listings/${listing.slug}`} onClick={this.props.modalClose}>View Full Listing</a>
</div>
</div>)
}
}
The console output is...
// after initially mounting:
mounting...
false
// after clicking the listing link:
unmounting...
true
// after hitting the back button:
mounting...
false
I'm pretty sure I need to fix this by using componentWillUnmount to set the state of modalIsOpen to false before the component unmounts, but it never seems to finish setting the state before unmounting.
I'm using react on rails, which seems to use some hybrid routing rails/react routing system, but I'm not too familiar with it, and don't want to go down that rabbit hole at the moment if I don't have to.
So my question is, if this is expected behavior of the react component lifecycle, is there a way I can ensure the state of modalIsOpen is reset before unmounting? Or is there a way I can make sure my state is reset to its initial state when going back? Or is this more likely a consequence of the routing system I'm using?
This is strange, unexpected bahaviour in react and for sure is not caused by react (as #azium stated) but some 'things around', probably react_on_rails issue (or 'feature'). Report a bug/create an issue on github.
As you see in log state has proper value on mounting and there is no reason to render modal. 'Normal' react would work as expected.
There is no sense to set state on unmount - instance of component will be destroyed, its state, too.
HINTS
You shouldn't store modal content in state. It's possible, it works for simple cases, it can be used a kind of cache for parts of content, but you can have issues when conditional rerendering needed (using prop/state changes).
After setting state this.setState({modalIsOpen: true, modalContent:listing}) in click handler you can use conditional rendering (in render):
{this.state.modalIsOpen && <ListingModalContent listing={this.state.modalContent}/>}
To be true even this.setState({modalIsOpen: true}) can be removed (by save only listing idx in state, '-1' for closing) but then code can be less readable (storing additional pointer is cheap).

You have included the Google Maps JavaScript API multiple times on this page

how can I avoid “You have included the Google Maps JavaScript API multiple times on this page. This may cause unexpected errors.” if I am using google-map-react to display the map and react-places-autocomplete in another component to get the address and coordinates ?
//LocationMapPage component that displays the map box and pass the props to it
class LocationMapPage extends Component {
render() {
let {latLng,name,address} = this.props.location;
return (
<MapBox lat={latLng.lat} lng={latLng.lng} name={name} address={address}/>
)
}
}
//MapBox component
import React from "react";
import GoogleMapReact from 'google-map-react';
import apiKey from "../../configureMap";
const Marker = () => <i className="fa fa-map-marker fa-2x text-danger" />
const MapBox = ({lat,lng, name, address}) => {
const center = [lat,lng];
const zoom = 14;
return (
<div style={{ height: '300px', width: '100%' }}>
<GoogleMapReact
bootstrapURLKeys={{ key: apiKey }}
defaultCenter={center}
defaultZoom={zoom}
>
<Marker
lat={lat}
lng={lng}
text={`${name}, ${address}`}
/>
</GoogleMapReact>
</div>
);
}
export default MapBox;
Map is blank:
The Error in the console:You have included the Google Maps JavaScript API multiple times on this page. This may cause unexpected errors.
How to solve?
I am using google-map-react, react-places-autocomplete in the project.
AS temporary solution to my specific use case where I use the google map API's in two different components I have just added the script in the index.html:
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places"></script>
I did it in order to avoid that particular error as per of the documentation on the react-places-autocomplete GitHub page.
Unfortunately the link in the head of the index.html caused the same error. I found another workaround. Not the best solution, but works for now:
import React, { useEffect, useState } from 'react';
import GoogleMapReact from 'google-map-react';
export default () => {
const [mapActive, setMapActive] = useState(false);
useEffect(() => {
const t = setTimeout(() => {
setMapActive(true)
}, 100);
return () => {
window.clearTimeout(t);
};
}, [])
return (
<>
{ mapActive && <GoogleMapReact
bootstrapURLKeys={ {
key: ...,
language: ...
} }
defaultCenter={ ... }
defaultZoom={ ... }
>
</GoogleMapReact> }
</>
);
};
You could set a global variable and load the Google JavaScript only if the global variable is not set:
<script type="text/javascript">
if(document.isLoadingGoogleMapsApi===undefined) {
document.isLoadingGoogleMapsApi=true;
var script = document.createElement('script');
script.src='https://maps.googleapis.com/maps/api/js?key=[your-key]&callback=[yourInitMethodName]&v=weekly';
script.type='text/javascript';
script.defer=true;
document.getElementsByTagName('head')[0].appendChild(script);
}else{
[yourInitMethodName]();
}
</script>
In my case there is an arbitrary number of maps in a web application (starting at 0) and the user can add additional maps at runtime.
Most of the users do not use any map so loading it by default would cost unnecessarily loading time.

React Bootstrap OverlayTrigger with trigger="focus" bug work around

In iOS safari, OverlayTrigger with trigger="focus" isn't able to dismiss when tapping outside. Here is my code:
<OverlayTrigger
trigger="focus"
placement="right"
overlay={ <Popover id="popoverID" title="Popover Title">
What a popover...
</Popover> } >
<a bsStyle="default" className="btn btn-default btn-circle" role="Button" tabIndex={18}>
<div className="btn-circle-text">?</div>
</a>
</OverlayTrigger>
I know that this is a known bug for Bootstrap cuz this doesn't even work on their own website in iOS, but does anyone know any method to go around it? It would be the best if it is something that doesn't require jQuery, but jQuery solution is welcome. Thanks.
OK, since no one else gives me a work around, I worked on this problem with my co-worker together for 3 days, and we came up with this heavy solution:
THE PROBLEM:
With trigger="focus", Bootstrap Popover/Tooltip can be dismissed when CLICKING outside the Popover/Tooltip, but not TOUCHING. Android browsers apparently changes touches to clicks automatically, so things are fine on Android. But iOS safari and browsers that is based on iOS safari (iOS chrome, iOS firefox, etc...) don't do that.
THE FIX:
We found out that in React Bootstrap, the Overlay component actually lets you customize when to show the Popover/Tooltip, so we built this component InfoOverlay based on Overlay. And to handle clicking outside the component, we need to add event listeners for both the Popover/Tooltip and window to handle both 'mousedown' and 'touchstart'. Also, this method would make the Popover have its smallest width all the time because of the padding-right of the component is initially 0px, and we make based on the width of some parent component so that it is responsive based on the parent component. And the code looks like this:
import React, { Component, PropTypes as PT } from 'react';
import {Popover, Overlay} from 'react-bootstrap';
export default class InfoOverlay extends Component {
static propTypes = {
PopoverId: PT.string,
PopoverTitle: PT.string,
PopoverContent: PT.node,
// You need to add this prop and pass it some numbers
// if you need to customize the arrowOffsetTop, it's sketchy...
arrowOffsetTop: PT.number,
// This is to be able to select the parent component
componentId: PT.string
}
constructor(props) {
super(props);
this.state = {
showPopover: false,
popoverClicked: false
};
}
componentDidMount() {
// Here are the event listeners and an algorithm
// so that clicking popover would not dismiss itself
const popover = document.getElementById('popoverTrigger');
if (popover) {
popover.addEventListener('mousedown', () => {
this.setState({
popoverClicked: true
});
});
popover.addEventListener('touchstart', () => {
this.setState({
popoverClicked: true
});
});
}
window.addEventListener('mousedown', () => {
if (!this.state.popoverClicked) {
this.setState({
showPopover: false
});
} else {
this.setState({
popoverClicked: false
});
}
});
window.addEventListener('touchstart', () => {
if (!this.state.popoverClicked) {
this.setState({
showPopover: false
});
} else {
this.setState({
popoverClicked: false
});
}
});
// this is to resize padding-right when window resizes
window.onresize = ()=>{
this.setState({});
};
}
// This function sets the style and more importantly, padding-right
getStyle() {
if (document.getElementById(this.props.componentId) && document.getElementById('popoverTrigger')) {
const offsetRight = document.getElementById(this.props.componentId).offsetWidth - document.getElementById('popoverTrigger').offsetLeft - 15;
return (
{display: 'inline-block', position: 'absolute', 'paddingRight': offsetRight + 'px'}
);
}
return (
{display: 'inline-block', position: 'absolute'}
);
}
overlayOnClick() {
this.setState({
showPopover: !(this.state.showPopover)
});
}
render() {
const customPopover = (props) => {
return (
{/* The reason why Popover is wrapped by another
invisible Popover is so that we can customize
the arrowOffsetTop, it's sketchy... */}
<div id="customPopover">
<Popover style={{'visibility': 'hidden', 'width': '100%'}}>
<Popover {...props} arrowOffsetTop={props.arrowOffsetTop + 30} id={this.props.PopoverId} title={this.props.PopoverTitle} style={{'marginLeft': '25px', 'marginTop': '-25px', 'visibility': 'visible'}}>
{this.props.PopoverContent}
</Popover>
</Popover>
</div>
);
};
return (
<div id="popoverTrigger" style={this.getStyle()}>
<a bsStyle="default" className="btn btn-default btn-circle" onClick={this.overlayOnClick.bind(this)} role="Button" tabIndex={13}>
<div id="info-button" className="btn-circle-text">?</div>
</a>
<Overlay
show={this.state.showPopover}
placement="right"
onHide={()=>{this.setState({showPopover: false});}}
container={this}>
{customPopover(this.props)}
</Overlay>
</div>
);
}
}
In the end, this is a heavy work around because it is a big amount of code for a fix, and you can probably feel your site is slowed down by a tiny bit because of the 4 event listeners. And the best solution is just tell Bootstrap to fix this problem...

Ckeditor update textarea

I am trying to get the ckeditor working. Obviously it doesn't make use of the textarea so on submit the form doesn't submit the text in the editor. Beceause I make use of polymorphic associations etc. I can't make a onsubmit function to get the value of the textarea (when the form is submitted) .
So I found this question: Using jQuery to grab the content from CKEditor's iframe
with some very good answers. The answers posted there keep the textarea up to date. That is very nice and just what I need! Unfortunately I can't get it to work.
Does somebody know why (for example) this doesn't work?
I have a textarea (rails but it just translates to a normal textarea):
<%= f.text_area :body, :id => 'ckeditor', :rows => 3 %>
And the following js:
if(CKEDITOR.instances.ckeditor ) {
CKEDITOR.remove(CKEDITOR.instances.ckeditor);
}
CKEDITOR.replace( 'ckeditor',
{
skin : 'kama',
toolbar :[['Styles', 'Format', '-', 'Bold', 'Italic', '-', 'NumberedList', 'BulletedList', 'Link']]});
CKEDITOR.instances["ckeditor"].on("instanceReady", function()
{
//set keyup event
this.document.on("keyup", CK_jQ);
//and paste event
this.document.on("paste", CK_jQ);
}
function CK_jQ()
{
CKEDITOR.instances.ckeditor.updateElement();
}
I get the following "error" in my firebug.
missing ) after argument list
[Break on this error] function CK_jQ()\n
Before submit do:
for(var instanceName in CKEDITOR.instances)
CKEDITOR.instances[instanceName].updateElement();
have you figured it out?
I'm using CKEditor version 3.6.1 with jQuery form submit handler. On submit the textarea is empty, which to me is not correct. However there is an easy workaround which you can use, presuming all your CKEditor textareas have the css class ckeditor.
$('textarea.ckeditor').each(function () {
var $textarea = $(this);
$textarea.val(CKEDITOR.instances[$textarea.attr('name')].getData());
});
Execute the above before you do your submit handling ie. form validation.
Thanks #JohnDel for the info, and i use onchange to make it update every change.
CKEDITOR.on('instanceReady', function(){
$.each( CKEDITOR.instances, function(instance) {
CKEDITOR.instances[instance].on("change", function(e) {
for ( instance in CKEDITOR.instances )
CKEDITOR.instances[instance].updateElement();
});
});
});
Combination of all of the above answers into one.
Create a new custom.js file and add this:
CKEDITOR.on('instanceReady', function(){
$.each( CKEDITOR.instances, function(instance) {
CKEDITOR.instances[instance].on("instanceReady", function() {
this.document.on("keyup", CK_jQ);
this.document.on("paste", CK_jQ);
this.document.on("keypress", CK_jQ);
this.document.on("blur", CK_jQ);
this.document.on("change", CK_jQ);
});
});
});
function CK_jQ() {
for ( var instance in CKEDITOR.instances ) { CKEDITOR.instances[instance].updateElement(); }
}
You don't have to worry about the name of the textarea, just add a class ckeditor in the textarea, the above and you are done.
ADD Function JavaScript for Update
function CKupdate() {
for (instance in CKEDITOR.instances)
CKEDITOR.instances[instance].updateElement();
}
It's work. Cool
Just Add
CKEDITOR.instances.textAreaClientId.on('blur', function(){CKEDITOR.instances. textAreaClientId.updateElement();});
where textAreaClientId is your instance name
Regards
CKEDITOR.instances["ckeditor"].on("instanceReady", function()
{
//set keyup event
this.document.on("keyup", CK_jQ);
//and paste event
this.document.on("paste", CK_jQ);
})
I just increase that to the response of T.J. and worked for me:
$("form").on("submit", function(e){
$('textarea.ckeditor').each(function () {
var $textarea = $(this);
$textarea.val(CKEDITOR.instances[$textarea.attr('name')].getData());
});
});
On load:
$(function () {
setTimeout(function () {
function CK_jQ(instance) {
return function () {
CKEDITOR.instances[instance].updateElement();
};
}
$.each(CKEDITOR.instances, function (instance) {
CKEDITOR.instances[instance].on("keyup", CK_jQ(instance));
CKEDITOR.instances[instance].on("paste", CK_jQ(instance));
CKEDITOR.instances[instance].on("keypress", CK_jQ(instance));
CKEDITOR.instances[instance].on("blur", CK_jQ(instance));
CKEDITOR.instances[instance].on("change", CK_jQ(instance));
});
}, 0 /* 0 => To run after all */);
});
There have been some API changes with the latest versions of CKEditor, so here's an answer for CKEditor 5:
let ckeditor;
// Create a CKEditor, and store its handle someplace that you may
// access later. In this example, we'll use the `ckeditor` variable:
ClassicEditor
.create(document.querySelector("textarea"), {})
.then(editor => { ckeditor = editor; });
// When your form submits, use the `updateSourceElement` method
// on the editor's handle:
document.querySelector("form").addEventListener("submit", function() {
ckeditor.updateSourceElement();
});
To my knowledge, CKEditor does this automatically when you submit a form, so this particular example shouldn't actually do anything. But it is useful when you need the content of the textarea to udpate without submitting the form that contains it.
All above answer are focusing on how to fix this error but I want to take the answer on what cause me this error
I had a
<textarea class="ckeditor" rows="6" name="Cms[description]"></textarea>
changed to
<textarea class="ckedit" rows="6" name="Cms[description]"></textarea>
I changed class attribute value to anything other than ckeditor and boom error gone.
Hope that help

Resources