React-diagrams custom node widget not updating on engine.repaintCanvas() - javascript

I have copied the react-diagrams demo project and done some changes to the TSCustomNodeWidget.tsx, and when I call engine.repaintCanvas() it does not update the values I have set (whereas the name of the DefaultNodeModel does).
TSCustomNodeModel.ts
import { NodeModel, DefaultPortModel, DefaultNodeModel } from '#projectstorm/react-diagrams';
import { BaseModelOptions } from '#projectstorm/react-canvas-core';
export interface TSCustomNodeModelOptions extends BaseModelOptions {
color?: string;
value?: number;
name?: string;
}
export class TSCustomNodeModel extends DefaultNodeModel {
color: string;
name: string;
value: number;
constructor(options: TSCustomNodeModelOptions = {}) {
super({
...options,
type: 'ts-custom-node'
});
this.color = options.color || 'red';
this.value = options.value || 0;
this.name = options.name || 'name';
// setup an in and out port
this.addPort(
new DefaultPortModel({
in: true,
name: 'IN'
})
);
this.addPort(
new DefaultPortModel({
in: false,
name: 'OUT'
})
);
}
serialize() {
return {
...super.serialize(),
color: this.color
};
}
deserialize(event:any): void {
super.deserialize(event);
this.color = event.data.color;
}
}
TSCustomNodeFactory.tsx
import * as React from 'react';
import { TSCustomNodeModel } from './TSCustomNodeModel';
import { TSCustomNodeWidget } from './TSCustomNodeWidget';
import { AbstractReactFactory } from '#projectstorm/react-canvas-core';
import { DiagramEngine } from '#projectstorm/react-diagrams-core';
export class TSCustomNodeFactory extends AbstractReactFactory<TSCustomNodeModel, DiagramEngine> {
constructor() {
super('ts-custom-node');
}
generateModel(initialConfig:any) {
return new TSCustomNodeModel();
}
generateReactWidget(event:any): JSX.Element {
return <TSCustomNodeWidget engine={this.engine as DiagramEngine} node={event.model} />;
}
}
TSCustomNodeWidget.tsx
import * as React from 'react';
import { DiagramEngine, PortWidget } from '#projectstorm/react-diagrams-core';
import { TSCustomNodeModel } from './TSCustomNodeModel';
export interface TSCustomNodeWidgetProps {
node: TSCustomNodeModel;
engine: DiagramEngine;
}
export interface TSCustomNodeWidgetState {}
export class TSCustomNodeWidget extends React.Component<TSCustomNodeWidgetProps, TSCustomNodeWidgetState> {
constructor(props: TSCustomNodeWidgetProps) {
super(props);
this.state = {};
}
render() {
return (
<div className="custom-node">
<PortWidget engine={this.props.engine} port={this.props.node.getPort('IN')}>
<div className="circle-port" />
</PortWidget>
<PortWidget engine={this.props.engine} port={this.props.node.getPort('OUT')}>
<div className="circle-port" />
</PortWidget>
<div className="custom-node-color"
style={{ backgroundColor: this.props.node.color }}>
{this.props.node.value}
</div>
</div>
);
}
}
App.tsx
Setup
// create an instance of the engine with all the defaults
const engine = createEngine();
const state = engine.getStateMachine().getCurrentState();
if (state instanceof DefaultDiagramState) {
state.dragNewLink.config.allowLooseLinks = false;
}
const model = new DiagramModel();
engine.getNodeFactories().registerFactory(new TSCustomNodeFactory());
model.setGridSize(5);
engine.setModel(model);
Adding the node with button (repaintCanvas works)
function addConstNode() {
const node = new TSCustomNodeModel({
name: "CONST",
value: 0
});
node.setPosition(50, 50);
model.addAll(node);
engine.repaintCanvas(); //This works
return node;
}
Updating the node (repaintCanvas does not work)
currentNode.value = value;
...
engine.repaintCanvas();

I had a similar issue, where changing nodes would not update canvas immediately, and I managed to get around it by forcing the react component to re-render by doing this:
function useForceUpdate(){
const [, setValue] = useState(0);
return () => setValue(value => ++value);
}
const MyComponent = () => {
const forceUpdate = useForceUpdate();
// do some update to diagram
...
forceUpdate();
};
Can't remember if I also had to call engine.repaintCanvas() also or not.
Bit late to the party but hope this helps.

Related

React - Slide out component on unmount

I want to slide out a React component when it umounts. I am using CSSTransition for the animation which works great for mounting, but not unmounting. Somehow I need to delay the unmount process. All the off-the-shelf solutions sadly do not work for me. I am removing an element by doing a post request and then actually removing it in the UI with a SignalR callback.
To make my sequence more clear, I created a sequence diagram:
This is my code right now:
Board.tsx
import React from 'react';
import { Config } from 'util/config';
import { container } from 'tsyringe';
import { AppState } from 'store';
import { connect } from 'react-redux';
import { BoardState } from 'store/board/types';
import { BoardHubService } from 'services/hubs/boardHub.service';
import { BoardElementViewModel } from 'models/BoardElementViewModel';
import { BoardViewModel } from 'models/BoardViewModel';
import { BoardElement } from './boardElement/boardElement';
import { HttpService } from 'services/http.service';
import { setActiveBoard } from 'store/board/actions';
import './board.scss'
import { mapToType } from 'helpers/helpers';
import { TransitionGroup } from 'react-transition-group';
interface BoardProps {
activeBoardState: BoardState;
setActiveBoard: typeof setActiveBoard;
}
interface LocalBoardState {
boardElements: Array<BoardElementViewModel>
}
class Board extends React.Component<BoardProps, LocalBoardState> {
private config: Config;
private httpService: HttpService;
private boardHubService: BoardHubService;
constructor(props: any) {
super(props);
this.config = container.resolve(Config);
this.boardHubService = container.resolve(BoardHubService);
this.httpService = container.resolve(HttpService);
this.state = {
boardElements: []
}
}
async componentDidMount() {
// If there was any active board on page load...
if (this.props.activeBoardState.boardId) {
await this.loadBoardElements();
}
this.boardHubService.getConnection().on('SwitchedBoard', (response: BoardViewModel | null) => {
console.log(response);
this.setState({
boardElements: (response) ? response.elements : []
});
this.updateSiteTitle(response);
});
this.boardHubService.getConnection().on('ReceiveElement', (response: BoardElementViewModel) => {
let elements = this.state.boardElements;
elements.unshift(response);
this.setState(() => ({
boardElements: elements
}))
});
this.boardHubService.getConnection().on('RemoveElement', (response: string) => {
let elements = this.state.boardElements;
let element = mapToType<BoardElementViewModel>(elements.find(x => x.id === response));
elements.splice(elements.indexOf(element), 1);
this.setState(() => ({
boardElements: elements
}))
});
}
/**
* Load the elements from the board that was already active on page load.
*/
private async loadBoardElements() {
await this.httpService.getWithAuthorization<Array<BoardElementViewModel>>(`/boards/${this.props.activeBoardState.boardId}/elements`)
.then((response: Array<BoardElementViewModel>) => {
this.setState({
boardElements: response
});
})
.catch((e) => console.warn(e));
}
private updateSiteTitle(board: BoardViewModel | null) {
if (board != null) {
document.title = `${board.name} | ${this.config.siteName}`;
}
else {
document.title = this.config.siteName;
}
}
render() {
return (
<>
{this.props.activeBoardState.boardId != null
?
<div className="board-elements">
{this.state.boardElements.map((element: BoardElementViewModel, index) => {
return (
<BoardElement
key={index}
id={element.id}
// TODO: Use number from server
number={element.elementNumber}
user={element.user}
direction={element.direction}
note={element.note}
imageId={element.imageId}
createdAt={element.createdAt}
/>
)
})}
</div>
:
<div className="select-board-instruction">
<h1>Please select or create a board.</h1>
</div>
}
</>
)
}
}
const mapStateToProps = (state: AppState) => ({
activeBoardState: state.activeBoard
});
export default connect(mapStateToProps, { setActiveBoard })(Board);
BoardElement.tsx
import React from 'react';
import { UserViewModel } from 'models/UserViewModel';
import { Direction } from 'models/Direction';
import './boardElement.scss';
import { dateToReadableString } from 'helpers/helpers';
import { Config } from 'util/config';
import { container } from 'tsyringe';
import { HttpService } from 'services/http.service';
import $ from 'jquery'
import { CSSTransition } from 'react-transition-group';
interface BoardElementProps {
id: string;
number: number;
user: UserViewModel;
// TODO: Use direction Enum
direction?: Direction;
note?: string;
imageId?: string;
createdAt: Date;
}
interface BoardElementState {
show: boolean;
}
export class BoardElement extends React.Component<BoardElementProps, BoardElementState> {
private config: Config;
private httpService: HttpService;
private ref: any;
constructor(props: BoardElementProps) {
super(props);
this.state = {
show: false,
}
this.config = container.resolve(Config);
this.httpService = container.resolve(HttpService);
}
getReadableDirection(direction: Direction) {
// TODO: Richtingen vertalen
switch (direction) {
case Direction.North: return 'Noord';
case Direction.NorthEast: return 'Noordoost';
case Direction.East: return 'Oost';
case Direction.SouthEast: return 'Zuidoost';
case Direction.South: return 'Zuid';
case Direction.SouthWest: return 'Zuidwest';
case Direction.West: return 'West';
case Direction.NorthWest: return 'Noordwest';
}
}
removeElement() {
this.httpService.deleteWithAuthorization(`/boards/elements/${this.props.id}`).then(() => {
}, (error) => {
console.warn(error);
});
}
componentDidMount() {
setTimeout(() => {
this.setState(() => ({
show: true
}));
}, 500);
}
componentWillUnmount() {
this.setState(() => ({
show: false
}));
}
render() {
return (
<CSSTransition in={this.state.show} timeout={200} classNames={{
enter: 'animation-height',
enterDone: 'animation-height',
exit: ''
}}>
<div className="animation-wrapper animation-height-0" >
<div className="board-element" >
<div className="board-element-header">
<span className="board-element-number">{this.props.number}</span>
<span className="board-element-creator">{this.props.user.username}</span>
<i className="fas fa-trash ml-auto delete-icon" onClick={() => this.removeElement()}></i>
</div>
<div className="board-element-body">
{this.props.imageId
? <img className="board-element-image" src={`${this.config.apiUrl}/content/${this.props.imageId}`} />
: <p className="board-element-message">{this.props.note}</p>
}
</div>
<div className="board-element-footer">
{this.props.direction &&
<div className="board-element-direction">
<i className="fas fa-location-arrow direction mr-2"></i>{this.getReadableDirection(this.props.direction)}
</div>
}
<time className="board-element-timestamp" dateTime={this.props.createdAt.toString()}>{dateToReadableString(this.props.createdAt)}</time>
</div>
</div>
</div>
</CSSTransition >
)
}
}
For better illustration take a look at this GIF:
https://gyazo.com/3c933851ecec39029f25d4df3a136c2a
That is using jQuery in another project of mine. That is what I want to achive in React.
You can delay unmounting the component. Write a hoc and use a setTimeout. Maintain a state say shouldRender.
hoc
function delayUnmounting(Component) {
return class extends React.Component {
state = {
shouldRender: this.props.isMounted
};
componentDidUpdate(prevProps) {
if (prevProps.isMounted && !this.props.isMounted) {
setTimeout(
() => this.setState({ shouldRender: false }),
this.props.delayTime
);
} else if (!prevProps.isMounted && this.props.isMounted) {
this.setState({ shouldRender: true });
}
}
render() {
return this.state.shouldRender ? <Component {...this.props} /> : null;
}
};
}
usage
function Box(props) {
return (
<BoxWrapper isMounted={props.isMounted} delay={props.delay}>
✨🎶✨🎶✨🎶✨🎶✨
</BoxWrapper>
);
}
const DelayedComponent = delayUnmounting(Box);
See complete code in the demo
Read this article on medium

Regarding the unit test case implementation in jest for redux containers

How to write the unit test case for the given container which is associated with the component given as below?
Here the PopupNotification.jsx is a component file which has PopupNotification.js as a container file.
I want a jest unit test case implementation for the container file.
PopupNotification.jsx --> component file
import React from "react";
import PropTypes from "prop-types";
import { notyIcons, notyTimeout } from "../helpers/constants";
class PopupNotification extends React.Component {
constructor(props) {
super(props);
this.state = {
showNoty: props.showNoty
};
this.notyTimer = null;
}
static getDerivedStateFromProps(nextProps, prevState) {
return {
showNoty: nextProps.showNoty
};
}
shouldComponentUpdate() {
return true;
}
startNotyTimer() {
let __self = this;
if (this.notyTimer) {
clearTimeout(this.notyTimer);
}
this.notyTimer = setTimeout(function() {
__self.closeNoty();
}, notyTimeout);
}
componentWillUnmount() {
if (this.notyTimer) {
clearTimeout(this.notyTimer);
}
this.setState({ showNoty: false });
}
closeNoty() {
let { id } = this.props;
this.props.clearNotification(id);
this.setState({
showNoty: false
});
}
getNotyIcon() {
return <i className={`notification-popup__icon pos-l-m ${notyIcons[this.props.type]}`} />;
}
getNotyLayout() {
let notyIcon = this.getNotyIcon();
let { message: notyMessage, type } = this.props;
return (
<div className={`pure-g notification-popup ${type}`}>
<div className="pure-u-1-8 pos">{notyIcon}</div>
<div className="pure-u-7-8">
<div className="u-margin-8--left">{notyMessage}</div>
</div>
</div>
);
}
render() {
var notyLayout = null;
if (this.state.showNoty) {
this.startNotyTimer();
notyLayout = this.getNotyLayout();
}
return notyLayout;
}
}
PopupNotification.propTypes = {
message: PropTypes.string,
type: PropTypes.string,
id: PropTypes.number,
showNoty: PropTypes.bool,
clearNotification: PropTypes.func
};
PopupNotification.defaultProps = {
message: "",
type: ""
};
export default PopupNotification;
PopupNotification.js --> container File
import { connect } from "react-redux";
import { actions } from "../ducks/common";
import PopupNotification from "../components/PopupNotification";
const mapStateToProps = state => {
let { common: { popupNotifications = [] } } = state;
let showNoty = false;
if (popupNotifications.length) {
showNoty = true;
}
return {
showNoty,
...popupNotifications[popupNotifications.length-1]
};
};
const mapDispatchToProps = dispatch => {
return {
clearNotification: notyId => {
dispatch(actions.clearPopupNotification({id: notyId}));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(PopupNotification);
I suggest using redux-mock-store to gain full control over your store. Then you can do things like
const actions = store.getActions()
expect(actions).toEqual([expectedPayload])
Copied from their docs
Since this is a unit test, you just need to test what this container is responsible for, and from what I see from your example that it is exposing the redux connected component, so you should test that your mapStateToProps & mapDispatchToProps are executing correctly.

How to handle two similar components in react

I am very new to React. I have two components: TimePickerComponent and the TimeDurationPickerComponent.
The TimePickerComponent gets passed a TimeString(string) via props(only if initial data exists) and displays it like "08:00". Code:
class TimePickerComponent extends React.Component {
_placeholder;
_defaultTimeString;
_timeString;
_errorStatus;
_classes;
constructor({ placeholder, defaultTimeString, timeString, errorStatus, classes }) {
super();
this._placeholder = placeholder;
this._defaultTimeString = defaultTimeString;
this._timeString = timeString;
this._errorStatus = errorStatus;
this._classes = classes;
}
get Placeholder() {
return this._placeholder;
}
get DefaultTimeString() {
return this._defaultTimeString ? this._defaultTimeString : CONTROLS_CONSTANTS.DEFAULT_TIME_STRING;
}
get TimeString() {
return this._timeString;
}
get ErrorStatus() {
return this._errorStatus;
}
get Classes() {
return this._classes;
}
render() {
return <FormControl>
<TextField error={this.ErrorStatus}
label={this.Placeholder}
defaultValue={this.TimeString ? this.TimeString : this.DefaultTimeString}
className={this.Classes.layout}
type="time"
InputLabelProps={{
shrink: true
}}
/>
</FormControl>
}
}
TimePickerComponent.propTypes = {
placeholder: PropTypes.string.isRequired,
defaultTimeString: PropTypes.string,
timeString: PropTypes.string,
errorStatus: PropTypes.bool
}
export default withStyles(styles)(TimePickerComponent);
The TimeDurationPickerComponent gets passed a TimeInMinutes(number) via props. But the display is the same as of the TimePickerComponent("08:00"). Code:
class TimeDurationPickerComponent extends React.Component {
_placeholder;
_timeInMinutes;
_classes;
constructor({ placeholder, timeInMinutes, classes }) {
super();
this._placeholder = placeholder;
this._timeInMinutes = timeInMinutes;
this._classes = classes;
}
get Placeholder() {
return this._placeholder;
}
get TimeInMinutes() {
return this._timeInMinutes;
}
get Classes() {
return this._classes;
}
get TimeString() {
let timeFormat = CONTROLS_CONSTANTS.TIME_FORMATS.HOURS_MINUTES_COLON_SEPARATED;
let duration = moment.duration({ minutes: this.TimeInMinutes });
//https://github.com/moment/moment/issues/463
return moment.utc(duration.asMilliseconds()).format(timeFormat);
}
render() {
return <TimePickerComponent
placeholder={this.Placeholder}
timeString={this.TimeString}
classes={this.Classes}
/>
}
}
TimeDurationPickerComponent.propTypes = {
placeholder: PropTypes.string.isRequired,
timeInMinutes: PropTypes.number
}
export default TimeDurationPickerComponent;
To avoid code redundancy I reused my TimePickerComponent in the TimeDurationPickerComponent and just convert the TimeInMinutes in a TimeString and pass it down to the TimePickerComponent via props.
My question now: Is this a good practice how I solved this or should I use a HigherOrderComponent for that? Or should I use an inheritance approach for that? Which solution would be the best and why?
Thank you in advance.
What you've done here is probably fine. It could be done with a higher order component as well but a composition based approach like what you have won't have any performance issues and to be honest it's probably more readable than using an HOC.
On another note you should be using this.props and this.state to represent your class properties. They are build into React components and are what will cause your component to automatically re-render upon change.
It also makes your code significantly more concise so for example you could reduce your second component down to something like this:
class TimeDurationPickerComponent extends React.Component {
constructor(props) {
super(props);
}
createTimeString() {
let timeFormat = CONTROLS_CONSTANTS.TIME_FORMATS.HOURS_MINUTES_COLON_SEPARATED;
let duration = moment.duration({ minutes: this.props.TimeInMinutes });
//https://github.com/moment/moment/issues/463
return moment.utc(duration.asMilliseconds()).format(timeFormat);
}
render() {
return <TimePickerComponent
placeholder={this.props.Placeholder}
timeString={this.createTimeString()}
classes={this.props.Classes}
/>
}
}
Example of a component that uses flow:
// #flow
import React from 'react';
import './css/ToggleButton.css';
type Props = {
handleClick: Function;
label: string;
};
type LocalState = {
active: bool,
};
class ToggleButton extends React.Component<Props, LocalState> {
clickHandler: () => void;
constructor(props: Props) {
super(props);
this.state = {
active: true,
};
this.clickHandler = this.clickHandler.bind(this);
}
clickHandler() {
this.setState({ active: !this.state.active });
this.props.handleClick();
}
render() {
const buttonStyle = this.state.active ? 'toggle-btn-active' : 'toggle-btn-inactive';
return (
<button
className={`toggle-btn ${buttonStyle}`}
onClick={this.clickHandler}
>{this.props.label}
</button>
);
}
}
export default ToggleButton;

How should I instantiate my state from props?

Reading around, I see that initializing state from props in the getInitialState()/constructor can be an anti-pattern.
What is the best way of initializing state from props and managing to be consistent?
As you can see below, I'm trying to initialize my "Card" component so that I may have a likeCount and isLikedByMe states initialized. I do this so that I may have a custom like counter displayed and the text of the Like button to change, by resetting the state.
At this point, I'm doing this in the constructor, but that is the wrong way to do it. How should I manage this?
import * as React from "react";
import { CardLikeButton } from "./buttons";
export enum CardType {
None = 0,
Text,
Image
}
export interface CardMedia {
text?: string;
imageUrl?: string;
}
export interface CardDetails {
isLikedByMe: boolean;
likeCount: number;
}
export interface CardParams extends React.Props<any> {
cardType: number;
cardId: string;
cardMedia: CardMedia;
cardDetails: CardDetails;
}
export class Card extends React.Component<CardParams, CardDetails> {
state: CardDetails;
constructor(props: CardParams) {
super(props);
console.log("in card constructor");
console.log("card type: " + props.cardType);
this.state = { // setting state from props in getInitialState is not good practice
isLikedByMe: props.cardDetails.isLikedByMe,
likeCount: props.cardDetails.likeCount
};
}
componentWillReceiveProps(nextProps: CardParams) {
this.setState({
isLikedByMe: nextProps.cardDetails.isLikedByMe,
likeCount: nextProps.cardDetails.likeCount
});
}
render() {
console.log("RENDERING CARD");
// console.dir(this.props.cardDetails);
// console.dir(this.props.cardMedia);
// console.dir(this.props.cardType);
if (this.props.cardType === CardType.Text) { // status card
return (
<div className="general-card">
<p>Text card.ID: {this.props.cardId}</p>
<p>{this.props.cardMedia.text}</p>
<CardLikeButton onButClick={this.likeButtonClicked} buttonText={this.state.isLikedByMe ? "Liked" : "Like"} isPressed={this.state.isLikedByMe}/>
<p>Like count: {this.state.likeCount}</p>
</div>
);
} else { //photo card
return (
<div className="general-card">
<p>Image card.ID: {this.props.cardId}</p>
<p> {this.props.cardMedia.text} </p>
<img src={this.props.cardMedia.imageUrl} />
<br/>
<CardLikeButton onButClick={this.likeButtonClicked} buttonText={this.state.isLikedByMe ? "Liked" : "Like"} isPressed={this.state.isLikedByMe}/>
<p>Like count: {this.state.likeCount}</p>
</div>
);
}
}
likeButtonClicked = () => {
console.log('in card => like button clicked!');
var _isLikedByMe = this.state.isLikedByMe;
var _likeCount = this.state.likeCount;
if (_isLikedByMe) {
_likeCount--;
} else {
_likeCount++;
}
_isLikedByMe = !_isLikedByMe;
this.setState({
isLikedByMe: _isLikedByMe,
likeCount: _likeCount
})
}
}
Here is the main list component:
/// <reference path="../../typings/index.d.ts" />
import * as React from "react";
import * as ReactDOM from "react-dom";
import {Card} from "./card";
import {CardParams, CardType, CardMedia, CardDetails} from "./card";
var card1: CardParams = {
cardType: CardType.Image,
cardId: "card1234",
cardDetails: {
isLikedByMe: false,
likeCount: 3
},
cardMedia: {
text: "some test text; badescuga",
imageUrl: "http://www9.gsp.ro/usr/thumbs/thumb_924_x_600/2016/06/19/738742-rkx1568-lucian-sinmartean.jpg"
}
};
var card2: CardParams = {
cardId: "card35335",
cardType: CardType.Text,
cardDetails: {
isLikedByMe: true,
likeCount: 1
},
cardMedia: {
text: "some test 2 text"
}
};
var cards = [card1, card2];
ReactDOM.render(
<div>
{
cards.map((item) => {
return (
<Card key={item.cardId} cardId={item.cardId} cardType={item.cardType} cardDetails={item.cardDetails} cardMedia={item.cardMedia}/>
);
})
}
</div>,
document.getElementById("mainContainer")
);
Without getting into working with Flux, or Redux, and focusing on your question.
IMHO, state and props need to be separated, where Card only gets props, and state is managed from above. Card component will get an event handler to raise once the like button has been clicked. You could either do the "like" logic inside the Card component, and just raise the event handler with the output of that logic, for example:
this.props.likeClicked(isLikedByMe, updatedLikeCount).
Or, do the whole logic in the parent component.
I would also wrap all cards in another component.
Example:
class Card extends React.Component {
constructor(props: CardParams) {
super(props);
}
render() {
return (
<div>
<button onClick={this.likeButtonClicked}>
{this.props.isLikedByMe ? 'Unlike' : 'Like'}
</button>
<p>Like count: {this.props.likeCount}</p>
</div>
)
}
likeButtonClicked = () => {
console.log('in card => like button clicked!');
var _isLikedByMe = this.props.isLikedByMe;
var _likeCount = this.props.likeCount;
if (_isLikedByMe) {
_likeCount--;
} else {
_likeCount++;
}
_isLikedByMe = !_isLikedByMe;
if (this.props.likeUpdated) {
this.props.likeUpdated({
cardId: this.props.cardId,
isLikedByMe: _isLikedByMe,
likeCount: _likeCount
})
}
}
}
class CardList extends React.Component {
constructor(props) {
super(props)
this.state = {
// Could use es6 map
cards: {123: {isLikedByMe: false, likeCount: 3},
124: {isLikedByMe: true, likeCount: 2}}
}
}
_onLikeUpdated({cardId, isLikedByMe, likeCount}) {
const cards = Object.assign({}, this.state.cards)
cards[cardId] = {isLikedByMe, likeCount}
this.setState({cards})
}
_getCards() {
return Object.keys(this.state.cards).map(cardId => {
return <Card key={cardId}
cardId={cardId}
likeUpdated={this._onLikeUpdated.bind(this)}
{...this.state.cards[cardId]} />
})
}
render() {
return <div>
{this._getCards()}
</div>
}
}
Fiddle: https://jsfiddle.net/omerts/do13ez79/

How to create multiple instances of popper?

In my app, I'm trying to use Popper to create a tooltip over every element in the app.
(Usually, I would only show a single tooltip, but for a presentation I want to show more than one).
I wrote this utility Component to attach tooltip directly to ref.
It works pretty well, but when I try to use it inside an [].map() like regular react components, I lose all my positioning:
https://bit.dev/bit/base/atoms/ref-tooltip?example=5e81d946443f4900195606b7
import React, { Component } from 'react';
import { RefTooltip } from '#bit/bit.base.atoms.ref-tooltip'
export default class ExampleUsage extends Component {
state = { ref: [] };
handleRef = (elem) => {
if (this.state.ref.some(x => x === elem)) return;
this.setState({ ref: [elem] });
}
render() {
return (
<div>
<span ref={this.handleRef}>target</span>
{ /*
* (!)
* This .map() breaks tooltip
*
*/ }
{this.state.ref.map((elem, idx) => (
<RefTooltip key={idx} targetElement={elem}>
"tooltip"
</RefTooltip>
))}
</div>
);
}
}
//ref-tooltip.tsx
import React, { Component } from 'react';
import classNames from 'classnames';
//#ts-ignore
import createRef from 'react-create-ref';
import { createPopper, Instance, Options } from '#popperjs/core';
import styles from './ref-tooltip.module.scss';
export type RefTooltipProps = {
targetElement?: HTMLElement;
popperOptions?: Partial<Options>;
} & React.HTMLAttributes<HTMLDivElement>;
export class RefTooltip extends Component<RefTooltipProps> {
private ref = createRef();
private popperInstance?: Instance;
componentWillUnmount() {
this.destroy();
}
componentDidUpdate(prevProps: RefTooltipProps) {
const nextProps = this.props;
if (prevProps.targetElement !== nextProps.targetElement) {
this.reposition(nextProps.targetElement);
}
}
private reposition = (targetElement?: HTMLElement) => {
const { popperOptions = popperDefaultOptions } = this.props;
const popperElement = this.ref.current;
if (!targetElement) {
this.destroy();
}
if (!targetElement || !popperElement) return;
this.popperInstance = createPopper(targetElement, popperElement, popperOptions);
};
private destroy() {
if (!this.popperInstance) return;
this.popperInstance.destroy();
this.popperInstance = undefined;
}
render() {
const { className, targetElement, ...rest } = this.props;
return (
<div
{...rest}
ref={this.ref}
className={classNames(styles.tooltipWrapper, className)}
data-ignore-component-highlight
/>
);
}
}
const popperDefaultOptions: Partial<Options> = {
placement: 'top',
modifiers: [
{
name: 'flip',
enabled: false,
},
],
};
Expected:
Actual:
I don't understand why the .map() breaks popper. At least for an array of 1, it should behave the same.
Any ideas why this isn't working?

Categories

Resources