How and When to use subscribe? - rxjs

Im very new to typescript.
I'm trying to understand the Observables but I'm kinda lost here.
The function below searches for videos on Youtube API v3.
Is it a good approach?
Is subscribing inside a function which will be called many times a good idea?
This function is called whenever user types something.
Should I have an unsubscribe somewhere?
searchVideos(searchbar: any): void{
const typedValue: string = searchbar.srcElement.value;
if(typedValue.length > 2){
this.videosList = this.youtubeProvider.searchVideos(typedValue);
this.videosList.subscribe(data => {
if( data.length == 0 ){
this.notFoundAnyVideo = true;
}else{
this.notFoundAnyVideo = false;
}
})
}
}

It's a good question!
They are some ways to answer your question:
1/ you can debounce the action which call your function
Imagine, your action is triggered by a keyup in input field:
HTML
<input type="text" (keyup)="onSearchKeyup(this.value, $event)">
Component
export class MyComponent implements OnInt {
onSearch$: Subject<string>
ngOnInt(): void {
this.onSearch$
.debounceTime(500) //-> put your time here
.subscribe(search => searchVideos(search)
}
onSearchKeyup(search: string, e: any) {
this.onSearch$.next(search)
e.preventDefault()
}
}
2/ you can cancel the observable with takeUntil
Component
export class MyComponent implements OnInt {
onStopSearch$: Subject<void> = new Subject<void>();
onSearchKeyup(search: string, e: any) {
this.onStopSearch$.next()
this.searchVideos(string)
e.preventDefault()
}
private searchVideos(search: string): void{
if(typedValue.length > 2){
this.videosList = this.youtubeProvider.searchVideos(typedValue);
this.videosList
.takeUntil(this.onSearchStop$)
.subscribe(data => {
if( data.length == 0 ){
this.notFoundAnyVideo = true;
}else{ this.notFoundAnyVideo = false; }
})
}
}
}
Of course you can combine 1 and 2
Why I use takeUntil to cancel my requests: https://medium.com/#benlesh/rxjs-dont-unsubscribe-6753ed4fda87

I suppose you could use RxJS all the way through, cause its reactive paradigm lends itself very well for search components. Take a look at the code below, I implemented variations of it in few applications.
import {Component, ViewChild, ElementRef} from "#angular/core";
#Component({
selector: 'search',
styleUrls: ['./search.component.scss'],
template: `
<form #searchBoxEl action="" class="search-form" [formGroup]="form">
<fieldset>
<input #searchBoxEl type="text" placeholder='Search for Youtube videos'
autocomplete="off" />
<nwum-list (itemSelected)="onItemSelect($event)"></nwum-list>
</fieldset>
</form>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SearchComponent implements OnInit {
#ViewChild('searchBoxEl') searchBoxEl: ElementRef;
componentDestroyed$: Subject<void> = new Subject<void>();
videosList: Video[];
constructor(public videoService: VideoService){}
ngOnInit(){
subscribeToSearchQueryChanges();
}
subscribeToSearchQueryChanges(){
const minNumOfChars = 2;
Observable.fromEvent(this.searchBoxEl.nativeElement, 'keyup')
.debounceTime(300)
.pluck('target', 'value')
.map(value => value.trim())
// .map(() => this.searchBoxEl.nativeElement.value.trim())
.filter(value => value.length >= minNumOfChars)
.takeUntil(this.componentDestroyed$)
.switchMap(value => this.videoService.fetchVideos(value))
.subscribe((videos: Video[]) => {
//show videos, etc
this.videosList = this.videoService.groupSuggestions(suggestions);
}, err => {
console.error('failed fetching videos', err);
this.removeAllSubscriptions();
this.subscribeToSearchQueryChanges();
});
this.addSubscription(sub);
}
ngOnDestroy() {
this.removeAllSubscriptions();
}
removeAllSubscriptions(){
this.componentDestroyed$.next();
this.componentDestroyed$.complete();
}
}

Related

State updates but Component doesn't re-render

I'm creating a simple react-redux chat application. I managed to display some dummy data from my redux state in my Message component. I succeed to push a new 'message' to the redux state from my Submit component. But the new item doesn´t render in the Message component.
So I tried to console log the previous state and the new state from the messageReducer and it seems to work. I get the state array with all the dummy data + the new pushed object.
Here is the Github repo if needed: https://github.com/MichalK98/Chat.V.2
// Message Component
import React, { Component } from 'react';
import { connect } from 'react-redux';
class Message extends Component {
render() {
return (
<ul id="chatroom">
{this.props.messages.map((msg) => (
<li className={(msg.username == 'You' ? "chat-me" : "")} key={msg.id}>
<p>{msg.message}</p>
<small>{msg.username}</small>
</li>
)).reverse()}
</ul>
)
}
}
const mapStateToProps = (state) => {
return {
messages: state.message.messages
}
}
export default connect(mapStateToProps)(Message);
// Submit Component
...
import { connect } from 'react-redux';
...
class Submit extends Component {
state = {
message: []
}
clear = async () => {
await this.setState({
message: ''
});
}
handleChange = async (e) => {
await this.setState({
message: e.target.value
});
}
handleSubmit = (e) => {
e.preventDefault();
this.props.writeMessage(this.state.message);
this.clear();
}
render() {
return (
<div className="chat-footer">
<form onSubmit={this.handleSubmit} autoComplete="off">
<input onChange={this.handleChange} value={this.state.message} type="text" placeholder="Skriv något..."/>
<button id="btnSend"><SendSvg/></button>
</form>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
messages: state.messages
}
}
const mapDispatchToProps = (dispatch) => {
return {
writeMessage: (message) => { dispatch({type: 'WRITE_MESSAGE', messages: {id: Math.random(), username: 'You', message: message}})}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Submit);
// messageReducer
const initState = {
messages: [
{id: 1, username: 'You', message: 'Hi, data from reducer!'},
{id: 2, username: 'Mattias', message: 'Wow..'},
{id: 3, username: 'Alien', message: 'Awesome!'}
]
}
const messageReducer = (state = initState, action) => {
if (action.type === 'WRITE_MESSAGE') {
state.messages.push(action.messages);
console.log('Action ',action.messages);
console.log('State ',state.messages);
}
return state;
}
export default messageReducer;
I expect that the new data will render in my Message component when I add a new object to the state array in messageReducer.
first of all in your Message Component you should Change mapStateToProps :
const mapStateToProps = (state) => {
return {
messages : state.messages
}
}
And then in your message reducer you should change reducer. this is better way for reducer. you shouldn't directly change state :
const messageReducer = (state = initState, action) => {
switch (action.type) {
case "WRITE_MESSAGE":
return { ...state, messages: [...state.messages, { ...action.messages }] }
default:
return state;
}
}
if you need any help i can help you to complete your project :-)

Is there a simple way of implementing a column picker for a List?

We are going to implement a columnpicker and currently the only idea I have is to implement a ColumnPickableList that wraps a List. This would also hold a list of checkboxes that will enable the user to hide a column.
But before I go ahead do that I just wondered if I'm reinveting the wheel and if there is a simpler approach to solving this?
No simpler way. You'll have to implement your own List component for that
I'm following up on this since I'm struggling to make this work. Maybe it is because I have chosen to create a wrapper that filters the children to be displayed. So techically this approach doesn't implement its own List.
I have made a naive draft which I was hoping would work, but it fails to re-render the children even though they are changed/filtered in the parent component.
The console.log(..) in ColumnPickableList render()-function does print the correct children/props, but still the children won't update/re-render. Any clues as to why? Is this approach too naive?
So here is the current draft:
ColumnPicker.js
import React, { PropTypes } from 'react';
import Checkbox from 'material-ui/Checkbox';
export default class ColumnPicker extends React.Component {
constructor(props) {
super(props);
this.onCheck = this.onCheck.bind(this);
}
onCheck(column, isChecked) {
return this.props.onCheckboxChanged(column, isChecked);
}
renderCheckbox(column, onCheck) {
const disabled = (column.source === 'id');
return (<Checkbox key={column.source} label={column.source.toUpperCase()} onCheck={(event, checked) => onCheck(column, checked)} defaultChecked disabled={disabled} />);
}
render() {
const columns = this.props.columns || [];
return (
<div className="column-picker">
{columns.map((column) => {
return this.renderCheckbox(column, this.onCheck);
})}
</div>
);
}
}
ColumnPicker.propTypes = {
columns: PropTypes.array,
onCheckboxChanged: PropTypes.func,
};
ColumnPicker.defaultProps = {
columns: [], // [{source: myField, checked: true} ...]
};
ColumnPickableList.js:
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { List, Datagrid } from 'admin-on-rest';
import ColumnPicker from './ColumnPicker';
import { toggleColumnPickerStatusAction, initializeColumnPickerAction } from './actions';
export class ColumnPickableList extends React.Component {
componentWillMount() {
let columnSourceNames = [];
if (this.props.children) {
columnSourceNames = React.Children.map(this.props.children, (child) => {
return ({ source: child.props.source, checked: true });
});
}
const columnsDisplayed = columnSourceNames.filter((column) => column.source);
this.props.initializeColumnPicker(this.props.resource, columnsDisplayed);
}
shouldComponentUpdate(nextProps) {
const diff = nextProps.columnsDisplayed.filter((currentColumn) => {
return !this.props.columnsDisplayed.some((prevColumn) => {
return currentColumn.source === prevColumn.source && currentColumn.checked === prevColumn.checked;
});
});
return diff.length > 0;
}
removeHiddenColumns(children) {
return React.Children.map(children, (child) => {
if (!child.props.source) {
return child;
}
const column = this.props.columnsDisplayed.find((columnDisplayed) => {
return columnDisplayed.source === child.props.source;
});
if (this.props.columnsDisplayed.length === 0 || (column && column.checked)) {
return React.cloneElement(child);
}
return null;
});
}
render() {
const { children, ...rest } = this.props;
const displayedChildren = this.removeHiddenColumns(children);
console.log('Does it render? Rendering children', displayedChildren.map((child) => child.props.source));
return (
<div className="columnpickable-list">
<ColumnPicker columns={this.props.columnsDisplayed} onCheckboxChanged={this.props.handleCheckboxChanged} />
<List {...rest}>
<Datagrid>
{displayedChildren}
</Datagrid>
</List>
</div>
);
}
}
ColumnPickableList.propTypes = {
resource: PropTypes.string,
columnsDisplayed: PropTypes.array,
children: PropTypes.node,
initializeColumnPicker: PropTypes.func,
handleCheckboxChanged: PropTypes.func,
};
ColumnPickableList.defaultProps = {
columnsDisplayed: [],
};
function mapStateToProps(state) {
return {
columnsDisplayed: state.columnsDisplayed || [],
};
}
actions.js:
export const actions = {
INIT_COLUMNPICKER: 'INIT_COLUMNPICKER',
TOGGLE_COLUMNPICKER_STATUS: 'UPDATE_COLUMNPICKER_STATUS',
UPDATE_COLUMNPICKER_STATUSES: 'UPDATE_COLUMNPICKER_STATUSES',
}
export function initializeColumnPickerAction(resource, columns) {
return {
type: actions.INIT_COLUMNPICKER,
columns,
meta: { resource },
};
}
export function toggleColumnPickerStatusAction(column) {
return {
type: actions.TOGGLE_COLUMNPICKER_STATUS,
column,
};
}
reducers.js:
import { actions } from './actions';
function columnPickerReducer(state = [], action) {
switch (action.type) {
case actions.INIT_COLUMNPICKER: {
console.log('Init columnopicker reducer');
return action.columns;
}
case actions.TOGGLE_COLUMNPICKER_STATUS: {
const columns = state.map((column) => {
if (column.source === action.column.source) {
return { ...column, checked: !column.checked };
}
return column;
});
return columns;
}
default:
return state;
}
}
export default columnPickerReducer;
Example snippet of parent component:
...
<ColumnPickableList title="SillyStuff" {...props}>
<TextField source="id" />
<TextField source="NAME" />
<TextField source="SILLY_NAME" />
<TextField source="CHANGED_BY" />
<DateField source="CHANGED_TS" showTime />
<EditButton />
<DeleteButton />
</ColumnPickableList>
...

ReactJS pass props to child via redux ajax

I have a reactjs component with redux which passes asynchronously props to child component.
In child component I try to catch the data in componentDidMount but somehow does not work either, however the child component is getting rendered.
This is my parent component
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as slidesActions from '../../actions/slidesActions';
import Slider from '../Partials/Slider'
import _ from 'underscore';
class HomePage extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.actions.getSlides()
}
componentWillMount() {
const {slides} = this.props;
}
render() {
const {slides} = this.props;
return (
<div className="homePage">
<Slider columns={1} slides={slides} />
</div>
);
}
}
function mapStateToProps(state) {
return {
slides: state.slides
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(slidesActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(HomePage);
here comes my child component where I try to get passed slides props but is empty
import React from 'react';
import _ from 'underscore';
import Hammer from 'hammerjs';
class Slider extends React.Component {
constructor(props) {
super(props)
this.updatePosition = this.updatePosition.bind(this);
this.next = this.next.bind(this);
this.prev = this.prev.bind(this);
this.state = {
images: [],
slidesLength: null,
currentPosition: 0,
slideTransform: 0,
interval: null
};
}
next() {
const currentPosition = this.updatePosition(this.state.currentPosition - 10);
this.setState({ currentPosition });
}
prev() {
//TODO: work on logic
if( this.state.currentPosition !== 0) {
const currentPosition = this.updatePosition(this.state.currentPosition + 10);
this.setState({currentPosition});
}
}
componentDidMount() {
//here I try set a state variable on slides
let {slides} = this.props
let slidesLength = slides.length
this.setState({slidesLength})
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next);
this.hammer.on('swiperight', this.prev);
}
componentWillUnmount() {
this.hammer.off('swipeleft', this.next)
this.hammer.off('swiperight', this.prev)
}
updatePosition(nextPosition) {
const { visibleItems, currentPosition } = this.state;
return nextPosition;
}
render() {
let {slides, columns} = this.props
let {currentPosition} = this.state
let sliderNavigation = null
//TODO: this should go to slides actions
let slider = _.map(slides, function (slide) {
let Background = slide.featured_image_url.full;
if(slide.status === 'publish')
return <div className="slide" id={slide.id} key={slide.id}><div className="Img" style={{ backgroundImage: `url(${Background})` }} data-src={slide.featured_image_url.full}></div></div>
});
if(slides.length > 1 ) {
sliderNavigation = <ul className="slider__navigation">
<li data-slide="prev" className="" onClick={this.prev}>previous</li>
<li data-slide="next" className="" onClick={this.next}>next</li>
</ul>
}
return <div ref={
(el) => this._slider = el
} className="slider-attached"
data-navigation="true"
data-columns={columns}
data-dimensions="auto"
data-slides={slides.length}>
<div className="slides" style={{ transform: `translate(${currentPosition}%, 0px)`, left : 0 }}> {slider} </div>
{sliderNavigation}
</div>
}
}
export default Slider;
and here I have my actions for slider
import * as types from './actionTypes';
import axios from 'axios';
import _ from 'underscore';
//TODO: this should be accessed from DataService
if (process.env.NODE_ENV === 'development') {
var slidesEndPoint = 'http://dev.server/wp-json/wp/v2/slides';
} else {
var slidesEndPoint = 'http://prod.server/wp-json/wp/v2/slides';
}
export function getSlides () {
return dispatch => {
dispatch(setLoadingState()); // Show a loading spinner
axios.get(slidesEndPoint)
.then(function (response) {
dispatch(setSlides(response.data))
dispatch(doneFetchingData(response.data))
})
/*.error((response) => {
dispatch(showError(response.data))
})*/
}
}
function setSlides(data) {
return {
type: types.SLIDES_SUCCESS,
slides: data
}
}
function setLoadingState() {
return {
type: types.SHOW_SPINNER,
loaded: false
}
}
function doneFetchingData(data) {
return {
type: types.HIDE_SPINNER,
loaded: true,
slides: data
}
}
function showError() {
return {
type: types.SHOW_ERROR,
loaded: false,
error: 'error'
}
}
Reason is, componentDidMount will get called only once, just after the initial rendering, since you are fetching the data asynchronously so before you get the data Slider component will get rendered.
So You need to use componentwillreceiveprops lifecycle method.
componentDidMount:
componentDidMount() is invoked immediately after a component is
mounted. Initialization that requires DOM nodes should go here. If you
need to load data from a remote endpoint, this is a good place to
instantiate the network request. Setting state in this method will
trigger a re-rendering.
componentWillReceiveProps:
componentWillReceiveProps() is invoked before a mounted component
receives new props. If you need to update the state in response to
prop changes (for example, to reset it), you may compare this.props
and nextProps and perform state transitions using this.setState() in
this method.
Write it like this:
componentWillReceiveProps(nextProps){
if(nextProps.slides){
let {slides} = nextProps.props
let slidesLength = slides.length;
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next);
this.hammer.on('swiperight', this.prev);
this.setState({slidesLength})
}
}
As far as I understand, you are doing an axios call to fetch the data and then set it in the reducer which you are returning later. Also initially reducer data is empty . Now since componentDidMount is called only once, and initially no data may have been there you are not seeing any values. Use a componentWillReceiveProps function
componentWillReceiveProps(nextProps) {
//here I try set a state variable on slides
let {slides} = nextProps
let slidesLength = slides.length
this.setState({slidesLength})
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next);
this.hammer.on('swiperight', this.prev);
}

Validation Error Message not getting displayed for custom validation in Angular 2

I have a register form where user need to provide username. When customer enters username, I want to show validation error message if that username already exists in db or not.
register.html
<-- code here-->
<div class="form-group">
<label for="username" class="col-sm-3 control-label">UserName</label>
<div class=" col-sm-6">
<input type="text" ngControl="userName" maxlength="45" class="form-control" [(ngModel)]="parent.userName" placeholder="UserName" #userName="ngForm" required data-is-unique/>
<validation-message control="userName"></validation-message>
</div>
</div>
<--code here-->
register.component.ts
import {Component} from 'angular2/core';
import {NgForm, FormBuilder, Validators, FORM_DIRECTIVES} from 'angular2/common';
import {ValidationService} from '../services/validation.service';
import {ValidationMessages} from './validation-messages.component';
#Component({
selector: 'register',
templateUrl: './views/register.html',
directives: [ROUTER_DIRECTIVES, ValidationMessages, FORM_DIRECTIVES],
providers: []
})
export class ParentSignUpComponent {
parentSignUpForm: any;
constructor(private _formBuilder: FormBuilder) {
this._stateService.isAuthenticatedEvent.subscribe(value => {
this.onAuthenticationEvent(value);
});
this.parent = new ParentSignUpModel();
this.parentSignUpForm = this._formBuilder.group({
'firstName': ['', Validators.compose([Validators.required, Validators.maxLength(45), ValidationService.nameValidator])],
'middleName': ['', Validators.compose([Validators.maxLength(45), ValidationService.nameValidator])],
'lastName': ['', Validators.compose([Validators.required, Validators.maxLength(45), ValidationService.nameValidator])],
'userName': ['', Validators.compose([Validators.required, ValidationService.checkUserName])]
});
}
}
validation-message.component
import {Component, Host} from 'angular2/core';
import {NgFormModel} from 'angular2/common';
import {ValidationService} from '../services/validation.service';
#Component({
selector: 'validation-message',
inputs: ['validationName: control'],
template: `<div *ngIf="errorMessage !== null" class="error-message"> {{errorMessage}}</div>`
})
export class ValidationMessages {
private validationName: string;
constructor (#Host() private _formDir: NgFormModel) {}
get errorMessage() {
let control = this._formDir.form.find(this.validationName);
for (let propertyName in control.errors) {
if (control.errors.hasOwnProperty(propertyName) && control.touched) {
return ValidationService.getValidatorErrorMessage(propertyName);
}
}
return null;
}
}
validation-service.ts
import {Injectable, Injector} from 'angular2/core';
import {Control} from 'angular2/common';
import {Observable} from 'rxjs/Observable';
import {Http, Response, HTTP_PROVIDERS} from 'angular2/http';
import 'rxjs/Rx';
interface ValidationResult {
[key:string]:boolean;
}
#Injectable()
export class ValidationService {
static getValidatorErrorMessage(code: string) {
let config = {
'required': 'This field is required!',
'maxLength': 'Field is too long!',
'invalidName': 'This field can contain only alphabets, space, dot, hyphen, and apostrophe.',
'userAlreadyInUse': 'UserName selected already in use! Please try another.'
};
return config[code];
}
static checkUserName(control: Control): Promise<ValidationResult> {
let injector = Injector.resolveAndCreate([HTTP_PROVIDERS]);
let http = injector.get(Http);
let alreadyExists: boolean;
if (control.value) {
return new Promise((resolve, reject) => {
setTimeout(() => {
http.get('/isUserNameUnique/' + control.value).map(response => response.json()).subscribe(result => {
if (result === false) {
resolve({'userAlreadyInUse': true});
} else {
resolve(null);
}
});
}, 1000);
});
}
}
}
Now, when i run, and give a username that already exists in db, the value of 'result' variable i am getting as false, which is expected and correct. But validation error message is not getting displayed. I am able to run and get validation error message for other custom validation functions. I am using Angular 2.0.0-beta.15. Can somebody help me to understand what could be the issue?
There are some known issues with async validation
https://github.com/angular/angular/issues/1068
https://github.com/angular/angular/issues/7538
https://github.com/angular/angular/issues/8118
https://github.com/angular/angular/issues/8923
https://github.com/angular/angular/issues/8022
This code can be simplified
return new Promise((resolve, reject) => {
setTimeout(() => {
http.get('/isUserNameUnique/' + control.value).map(response => response.json())
.subscribe(result => {
if (result === false) {
resolve({'userAlreadyInUse': true});
} else {
resolve(null);
}
});
}, 1000);
});
to
return http.get('/isUserNameUnique/' + control.value).map(response => response.json())
.timeout(200, new Error('Timeout has occurred.'));
.map(result => {
if (result === false) {
resolve({'userAlreadyInUse': true});
} else {
resolve(null);
}
}).toPromise();
Don't forget to import map, timeout, and toPromise.
If you use subscribe() instead of then() on the caller site, then you can event omit toPromise()
if you look into this -
'userName': ['', Validators.compose([Validators.required, ValidationService.checkUserName])] });
-- you can see I am using both synchronous and asynchronous validations together. When i changed method for checkUserName like 'Validators.composeAsync(ValidationService.checkUserName)' instead of Validators.compose method, error message got displayed.

Double click and click on ReactJS Component

I have a ReactJS component that I want to have different behavior on a single click and on a double click.
I read this question.
<Component
onClick={this.onSingleClick}
onDoubleClick={this.onDoubleClick} />
And I tried it myself and it appears as though you cannot register both single click and double click on a ReactJS component.
I'm not sure of a good solution to this problem. I don't want to use a timer because I'm going to have 8 of these single components on my page.
Would it be a good solution to have another inner component inside this one to deal with the double click situation?
Edit:
I tried this approach but it doesn't work in the render function.
render (
let props = {};
if (doubleClick) {
props.onDoubleClick = function
} else {
props.onClick = function
}
<Component
{...props} />
);
Here is the fastest and shortest answer:
CLASS-BASED COMPONENT
class DoubleClick extends React.Component {
timer = null
onClickHandler = event => {
clearTimeout(this.timer);
if (event.detail === 1) {
this.timer = setTimeout(this.props.onClick, 200)
} else if (event.detail === 2) {
this.props.onDoubleClick()
}
}
render() {
return (
<div onClick={this.onClickHandler}>
{this.props.children}
</div>
)
}
}
FUNCTIONAL COMPONENT
const DoubleClick = ({ onClick = () => { }, onDoubleClick = () => { }, children }) => {
const timer = useRef()
const onClickHandler = event => {
clearTimeout(timer.current);
if (event.detail === 1) {
timer.current = setTimeout(onClick, 200)
} else if (event.detail === 2) {
onDoubleClick()
}
}
return (
<div onClick={onClickHandler}>
{children}
</div>
)
}
DEMO
var timer;
function onClick(event) {
clearTimeout(timer);
if (event.detail === 1) {
timer = setTimeout(() => {
console.log("SINGLE CLICK");
}, 200)
} else if (event.detail === 2) {
console.log("DOUBLE CLICK");
}
}
document.querySelector(".demo").onclick = onClick;
.demo {
padding: 20px 40px;
background-color: #eee;
user-select: none;
}
<div class="demo">
Click OR Double Click Here
</div>
I know this is an old question and i only shoot into the dark (did not test the code but i am sure enough it should work) but maybe this is of help to someone.
render() {
let clicks = [];
let timeout;
function singleClick(event) {
alert("single click");
}
function doubleClick(event) {
alert("doubleClick");
}
function clickHandler(event) {
event.preventDefault();
clicks.push(new Date().getTime());
window.clearTimeout(timeout);
timeout = window.setTimeout(() => {
if (clicks.length > 1 && clicks[clicks.length - 1] - clicks[clicks.length - 2] < 250) {
doubleClick(event.target);
} else {
singleClick(event.target);
}
}, 250);
}
return (
<a onClick={clickHandler}>
click me
</a>
);
}
I am going to test this soon and in case update or delete this answer.
The downside is without a doubt, that we have a defined "double-click speed" of 250ms, which the user needs to accomplish, so it is not a pretty solution and may prevent some persons from being able to use the double click.
Of course the single click does only work with a delay of 250ms but its not possible to do it otherwise, you have to wait for the doubleClick somehow...
All of the answers here are overcomplicated, you just need to use e.detail:
<button onClick={e => {
if (e.detail === 1) handleClick();
if (e.detail === 2) handleDoubleClick();
}}>
Click me
</button>
A simple example that I have been doing.
File: withSupportDoubleClick.js
let timer
let latestTouchTap = { time: 0, target: null }
export default function withSupportDoubleClick({ onDoubleClick = () => {}, onSingleClick = () => {} }, maxDelay = 300) {
return (event) => {
clearTimeout(timer)
const touchTap = { time: new Date().getTime(), target: event.currentTarget }
const isDoubleClick =
touchTap.target === latestTouchTap.target && touchTap.time - latestTouchTap.time < maxDelay
latestTouchTap = touchTap
timer = setTimeout(() => {
if (isDoubleClick) onDoubleClick(event)
else onSingleClick(event)
}, maxDelay)
}
}
File: YourComponent.js
import React from 'react'
import withSupportDoubleClick from './withSupportDoubleClick'
export default const YourComponent = () => {
const handleClick = withSupportDoubleClick({
onDoubleClick: (e) => {
console.log('double click', e)
},
onSingleClick: (e) => {
console.log('single click', e)
},
})
return (
<div
className="cursor-pointer"
onClick={handleClick}
onTouchStart={handleClick}
tabIndex="0"
role="button"
aria-pressed="false"
>
Your content/button...
</div>
)
}
onTouchStart start is a touch event that fires when the user touches the element.
Why do you describe these events handler inside a render function? Try this approach:
const Component = extends React.Component {
constructor(props) {
super(props);
}
handleSingleClick = () => {
console.log('single click');
}
handleDoubleClick = () => {
console.log('double click');
}
render() {
return (
<div onClick={this.handleSingleClick} onDoubleClick={this.handleDoubleClick}>
</div>
);
}
};

Resources