I have an image that when I drag I want to implement a rotation too. The solution I had in mind was to use React DnD to get the xy coordinates of the dragged image position and calculate the difference between the original image location. The value from this difference would form the basis to make the rotation.
I've looked at examples on the ReactDnD library and see that the DragSource Specification can access the Monitor variable. This monitor variable has access to methods like getInitialClientOffset(). When I implement a console.log() of this value it shows me the last last coordinate set when I release the mouse.
Using this library, is there an easy way to get the current position of the dragged element as I move the mouse?
import React from 'react'
import { DragSource } from 'react-dnd'
// Drag sources and drop targets only interact
// if they have the same string type.
// You want to keep types in a separate file with
// the rest of your app's constants.
const Types = {
CARD: 'card',
}
/**
* Specifies the drag source contract.
* Only `beginDrag` function is required.
*/
const cardSource = {
beginDrag(props,monitor,component) {
// Return the data describing the dragged item
const clientOffset = monitor.getSourceClientOffset();
const item = { id: props.id }
console.log(clientOffset);
return item
},
isDragging(props, monitor){
console.log(monitor.getClientOffset())
},
endDrag(props, monitor, component) {
if (!monitor.didDrop()) {
return
}
// When dropped on a compatible target, do something
const item = monitor.getItem()
const dropResult = monitor.getDropResult()
console.log(item,dropResult)
//CardActions.moveCardToList(item.id, dropResult.listId)
},
}
/**
* Specifies which props to inject into your component.
*/
function collect(connect, monitor) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource(),
// You can ask the monitor about the current drag state:
isDragging: monitor.isDragging(),
}
}
function Card(props) {
// Your component receives its own props as usual
const { id } = props
// These two props are injected by React DnD,
// as defined by your `collect` function above:
const { isDragging, connectDragSource } = props
return connectDragSource(
<div>
I am a draggable card number {id}
{isDragging && ' (and I am being dragged now)'}
</div>,
)
}
// Export the wrapped version
export default DragSource(Types.CARD, cardSource, collect)(Card)
From what I remember, custom drag layer had serious performance issues. It's better to directly subscribe to the underlying API (the official documentation is severely lacking, but you can get this information from reading drag layer source):
const dragDropManager = useDragDropManager();
const monitor = dragDropManager.getMonitor();
React.useEffect(() => monitor.subscribeToOffsetChange(() => {
const offset = monitor.getClientOffset();
// do stuff like setState, though consider directly updating style through refs for performance
}), [monitor]);
You can also use some flag based on drag state to only subscribe when needed (simple isDragging && monitor.subscribe... plus dependencies array does the trick).
I'm posting this way after the fact, but in case anyone is looking for this answer, the react-dnd way of doing this is by using what they call a Drag Layer - it gives you a way to use a custom component to be displayed when dragging.
They have a full example here:
https://codesandbox.io/s/github/react-dnd/react-dnd/tree/gh-pages/examples_hooks_js/02-drag-around/custom-drag-layer?from-embed=&file=/src/CustomDragLayer.jsx
Also in the docs you want to look at useDragLayer and DragLayerMonitor
The useDrop hook has a hover(item, monitor) method which does what you are looking for. It triggers repeatedly while you are hovering over the drop target.
You can use
ReactDOM.findDOMNode(component).getBoundingClientRect();
Ref: https://reactjs.org/docs/react-dom.html#finddomnode
Or just make a ref to your component instance and get getBoundingClientRect() on the instance within a onmousemove event.
As far as I understand all you need offset of currently dragging element, via HTML5Backend you can't get, but if you use MouseBackend you can easily get the coordinates
https://github.com/zyzo/react-dnd-mouse-backend
see example of with preview
Related
Angular v12.2,
NgRx v12.4,
RxJs v6.6.0
I am developing an Angular web app and one part of it displays a data bound grid of approximately 300 squares. A little like a chess board. As the user moves the mouse over the grid I display some data relating to the individual square the mouse is hovering over. However, no matter what I try the displayed data lags behind the mouse and only updates when the mouse slows or stops. Curiously I am following a method which I successfully adopted for another project which is really performant but was using Angular v10, NgRx v10.
The grid of components is a simple *ngFor="let location of locations; let i = index; trackBy:locationTrackByFunction".
I have chosen to create a mouse-tracker component which sits above my grid and its only responsibility is to get the mouse offsetX and offsetY and emit events outside of the Angular Zone for the mouse-tracker-container parent component to receive. I did this to minimise the change detection being triggered for every change in the mousemove event. My mouse-tracker template is as follows:
<div #eventDiv id="mouse-tracker"></div>
The code is as follows:
#ViewChild('eventDiv', { static: true }) eventDiv!: ElementRef;
#Output() mouseMove = new EventEmitter();
private move$: Observable<MouseEvent> | undefined;
private leave$: Observable<MouseEvent> | undefined;
private moveSubscription: Subscription = new Subscription;
constructor(private ngZone: NgZone) {}
ngOnInit() {}
ngOnDestroy(): void {
this.moveSubscription.unsubscribe();
}
ngAfterViewInit() {
const eventElement = this.eventDiv.nativeElement;
this.move$ = fromEvent(eventElement, 'mousemove');
this.leave$ = fromEvent(eventElement, 'mouseleave');
/*
* We are going to detect mouse move events outside of
* Angular's Zone to prevent Change Detection every time
* a mouse move event is fired.
*/
this.ngZone.runOutsideAngular(() => {
// Check we have a move$ and leave$ objects.
if (this.move$ && this.leave$) {
// Configure moveSubscription.
this.moveSubscription = this.move$.pipe(
takeUntil(this.leave$),
repeat(),
).subscribe((e: MouseEvent) => {
//e.stopPropagation();
e.preventDefault();
this.mouseMove.emit(e);
});
};
});
};
The mouseMove event is received by the hosting grid component and it simply emits its own event passing the data back to its own grid-container parent as follows:
public onMouseMove(e: MouseEvent) {
this.mouseMove.emit(e);
};
The grid-container component, on receiving the mouseMove event simply updates an RxJs Subject as follows:
public onMouseMove(e: MouseEvent) {
this.mouseOffsetsSubject$.next({ x: e.offsetX, y: e.offsetY });
};
The mouseoverLocationSubject$ along with two other Observables are brought together in an RxJs CombineLatest and ultimately after performing some calculations Dispatch an NgRx Action every time the mouse is over a different location (using distinctUntilChanged()) in the Angular Zone (to implement change detection) as follows:
this.ngZone.run(() => {
if (location !== null) {
// Dispatch an NgRx Action to save the Location in the Store.
this.store.dispatch(fromCoreActions.LocationActions.setMouseoverLocationId(
{ mouseoverLocationId: location.id }
));
}
else {
// Dispatch an NgRx Action to save the Location in the Store.
this.store.dispatch(fromCoreActions.LocationActions.setMouseoverLocationId(
{ mouseoverLocationId: null }
));
};
});
If I console log out the code in the reducer, or use the Redux dev tools in Chrome, I can see the NgRx Store being rapidly updated. No problem.
A location-display component is responsible for displaying the name of the mouse-over location. Its parent location-display-container component has an Observable being updated by the changing NgRx Selector as follows:
public mouseoverLocation$: Observable<Location | null>;
...
this.mouseoverLocation$ = this.store.pipe(
select(fromCoreSelectors.locationSelectors.selectMouseoverLocation),
tap(location => {
//console.log(`mouseoverLocation$: ${location?.name}`);
})
);
If I un-comment the console.log() the changing stream of data being received is rapid and closely matches the moving mouse. No delays up to here.
A simple async pipe passes the mouseoverLocation object to the child component [mouseoverLocation]="mouseoverLocation$ | async" which receives the object #Input() mouseoverLocation: Location | null = null; and actually displays the data in the template using interpolation as follows:
<div fxFlex="{{lineHeight}}px">
<div fxLayout="row" fxLayoutGap="8px">
<div fxFlex="200px;" class="mouse-over-sub-hdg">Location: </div>
<div fxFlex>{{mouseoverLocation?.name}}</div>
</div>
</div>
When run, both in developer and production builds, the displayed value lags badly behind the mouse and does not at all match the stream of data being received in the Observable. In fact if I circle the mouse over the grid so it never stops moving the template does not update at all. Only when I slow the mouse or stop does it jump to the value of the square the mouse is over.
I have tried refactoring this in many different ways including forcing change detection as each new value is received by the Observable cdRef.markForCheck() and cdRef.detectChanges(). I tried creating a #ViewChild of the location-display component and directly updating the mouseoverLocation value. I tried using the ngrxPush pipe. Every time the same result. All components implement OnPush.
I removed all the Zone related code thinking that this may be the root of the problem, but it lagged just the same, though I could see the considerable increase in change detection for every mousemove event.
Perhaps Chrome's DevTools Performance tab may provide a clue but honestly I can't get my head around how to use it.
Any advise, thoughts or suggestions very welcome. Thank you.
first, code sandbox demonstrating the issue.
we have 2 components - DraggableBox which is a wrapper around react-draggable and SimpleArrow which is a much simplified version of react-xarrows(I'm the author).
we can see a visual bug and that's because both DraggableBox and SimpleArrow update their state based on the same DOM frame(and the same react render phase - useEffect), while SimpleArrow should be updated based on the position of DraggableBox.
we could solve it if we would force 2 renders for each render on 'SimpleArrow', and then on the 2'th render 'SimpleArrow' will read the updated 'DraggableBox' position. we can force 2 renders by writing a custom hook:
export const useMultipleRenders = (renders = 2, effect = useEffect) => {
const [, setRender] = useState({});
const reRender = () => setRender({});
const count = useRef(0);
effect(() => {
if (count.current != renders) {
reRender();
count.current += 1;
} else {
count.current = 0;
}
});
};
now we will consume useMultipleRenders() on SimpleArrow and the visual glitch would be fixed.
here's a code sandbox with the fix. you can see this ugly workaround works.
this actually happens all the time in React when accessing the DOM.
you access the dom during a render using a ref (useRef) value, and during this render, you can only have access to what is currently in the dom, which is the result of the previous render, but you actually need the result of the current render!
for example, in SimpleArrow I'm using getBoundingClientRect on the inner of the svg to determine the svg hight and width:
const SimpleArrow = (props) => {
const svgInnersRef = useRef<SVGGElement>(null);
const {
width: gWidth,
height: gHeight
} = svgInnersRef.current?.getBoundingClientRect() ?? { width: 0, height: 0 };
return (
<svg
width={gWidth}
height={gHeight}
// ...
>
<g ref={svgInnersRef}>
{/* ... */}
</g>
</svg>
);
};
but in order to make sure the height and width are updated I have to double render so I get the right dimensions on the last render.
another thing is that the implementation of useMultipleRenders is not safe as it changing ref value during a render. React core members claims that setting ref value during a render is not safe.
what can I do? what are the alternatives?
TLDR;
can I get the most updated position of a DOM element without manually rerender?
how can I manually re-render without changing a ref value during a render(as it is not safe - on React strict mode it will be called twice and normally only once)?
This is an interesting problem and I am not sure I understand fully, but here's a shot.
You could try adding a drag event listener inside of SimpleArrow.js.
When there is a drag, you would check if it is one of your draggable boxes, and then if it is, you update their positions with a setState, which should trigger a rerender of your svg component and with the new positions of the boxes.
I am currently developing a package, which gives my React-widget responsiveness. The problem is, that the responsiveness does not depends on the viewport-width but on on the width of the widget-container-element.
Currently I am wrapping my <App> with a <ResponsiveProvider>. This provider subscribes to the windows.resize event and stores the format into the context's value.
All children elements get re-rendered if the format changes. That's fine.
Now, for show/hide components based on the current widget format, I just could implement a component, which accesses this context with contextType.
But I need a function, which I can use in any place of my application like: ResponsiveUtil.isSmall() or ResponsiveUtil.getCurrentFormat().
What would be the best approach to make the information (format) accessable via a function?
Thanks
I'm not sure if this would be the best approach, but it will work. You can set up a global event listener that will be dispatched each time the format changes in your component. I found a package here for the global events, but it wouldn't be hard to write your own either. Using react-native-event-listeners, it would look something like:
ResponsiveUtil.js
import { EventRegister } from 'react-native-event-listeners';
let format = {};
EventRegister.addEventListener('responsive-format-changed', data => {
format = data;
});
const ResponsiveUtils = {
getCurrentFormat() {
return Object.assign({}, format);
},
isSmall() {
//isSmall logic
}
};
export default ResponsiveUtils;
Then, in your <ResponsiveProvider>, during the resize event, dispatch the new format when you update the context
ResponsiveProvider.js
import { EventRegister } from 'react-native-event-listeners';
//...Other component code
window.resize = () => {
let format = calculateNewFormat();
//update context...
//dispatch new format
EventRegister.emit('responsive-format-changed', format);
};
I was wondering why couldn't I get some of my components to work using ReactDnD and mapDispatchToProps.
I'm trying to drag and drop Services to Clients but I can't find my dispatch functions in props at my serviceSpec on the endDrag method.
Considering my mapDispatchToProps on my Service component:
const mapDispatchToProps = (dispatch) => ({
dragService: (service) => { dispatch(dragService(service)) },
dropService: (service, clientTarget) => { dispatch(dropService(service, clientTarget)) }
});
High-order functions to bond together DragSource + Service + State + Dispatch:
var reduxConnectedService = connect(mapStateToProps, mapDispatchToProps)(Service);
export default DragSource('service', serviceSpec, collect)(reduxConnectedService);
render() method:
render (){
const { isDragging, connectDragSource, service } = this.props;
return connectDragSource(
<a className="panel-block is-active">
<CategoryIcon category={service.category}/>
{service.name} | ${service.price}
</a>
)
}
The spec object used to implement the dragSource specification (here is the problem):
const serviceSpec = {
beginDrag(props) {
return props.service;
},
endDrag(props, monitor, component){
console.log(props);
}
}
The console.log at endDrag function just show my Service Object because is being returned on the beginDrag function:
{service: {…}}
But my plan was to dispatch the action dropService here on endDrag, but I couldn't. The documentation says that (http://react-dnd.github.io/react-dnd/docs/api/drag-source):
beginDrag(props, monitor, component): Required. When the dragging
starts, beginDrag is called. You must return a plain JavaScript object
describing the data being dragged. What you return is the only
information available to the drop targets about the drag source so
it's important to pick the minimal data they need to know. You may be
tempted to put a reference to the component into it, but you should
try very hard to avoid doing this because it couples the drag sources
and drop targets. It's a good idea to return something like { id:
props.id } from this method.
I don't believe that I should return the dropService(dispatch) function on the beginDrag definition. So after hours trying to make it work, I started to pass the dropService function as a prop directly through the parent component (ServiceList):
{this.props.filteredServices.map((service, index) => (
<Service service={service} key={service.name} dropService={this.props.dropService}/>
))}
Making this way I could dispatch the dropService action on the endDrag method like I wanted, the console.log can proves that:
{service: {…}, dropService: ƒ}
I could make it work but I can't understand why I couldn't get this to work using mapDispatchToProps. Is there any limitation while using React-DnD or am I making something wrong?
Any help will be appreciated, I cannot die with this doubt. Thank you in advance.
Your problem is with these two lines:
var reduxConnectedService = connect(mapStateToProps, mapDispatchToProps)(Service);
export default DragSource('service', serviceSpec, collect)(reduxConnectedService);
Note the order: you wrap Service into a Redux container. Then you wrap the Redux Container with the DragSource container. Thus, in the component tree, the drag container is the parent of the Redux container, which means it doesn't receive the Redux props from it.
To fix that, make the drag container the child of the Redux container. You can do so by simply swapping the DragSource() and connect() calls:
var dragService = DragSource('service', serviceSpec, collect)(Service);
var reduxConnectedService = connect(mapStateToProps, mapDispatchToProps)(dragService);
Having more or less completed my first React+Redux application, I have come to the point where I would like to apply animations to various parts of the application. Looking at existing solutions, I find that there is nothing even close to what I would like. The whole ReactCSSTransitionGroup seems to be both an intrusive and naive way to handle animations. Animation concerns bleed out of the component you want to animate and you have no way to know anything about what happens in the application. From my initial analysis I have come up with the following requirements for what I would consider a good animation API:
The parent component should be ignorant of how the child component fades in/out (maybe with the exception of staggered animations).
The animations should integrate with React such that components are allowed to fade out (or otherwise complete their animations) before they are removed.
It should be possible to apply an animation to a component without modifying the component. It is ok that the component is styled such as to be compatible with the animation, but there should be no props, state, contexts or components related to the animation, nor should the animation dictate how the component is created.
It should be possible to perform an animation based on an action and the application state - in other words, when I have the full semantic context of what happened. For example, I might fade in a component when a thing has been created, but not when the page loads with the item in it. Alternatively, I might select the proper fade out animation, based on the user's settings.
It should be possible to either enqueue or combine an animation for a component.
It should be possible to enqueue an animation with a parent component. For example, if a component has two sub components and opening one would first trigger the other to close before opening itself.
The specifics of enqueuing animations can be handled by an existing animation library, but it should be possible to tie it in with the react and redux system.
One approach I have tried out is to create a decorator function like this (it is TypeScript, but I don't think that matters too much with regards to the problem):
export function slideDown<T>(Component: T) {
return class FadesUp extends React.Component<any, any> {
private options = { duration: 0.3 };
public componentWillEnter (callback) {
const el = findDOMNode(this).childNodes[0] as Element;
if (!el.classList.contains("animated-element")) {
el.classList.add("animated-element");
}
TweenLite.set(el, { y: -el.clientHeight });
TweenLite.to(el, this.options.duration, {y: 0, ease: Cubic.easeIn, onComplete: callback });
}
public componentWillLeave (callback) {
const el = findDOMNode(this).childNodes[0] as Element;
if (!el.classList.contains("animated-element")) {
el.classList.add("animated-element");
}
TweenLite.to(el, this.options.duration, {y: -el.clientHeight, ease: Cubic.easeIn, onComplete: callback});
}
public render () {
const Comp = Component as any;
return <div style={{ overflow: "hidden", padding: 5, paddingTop: 0}}><Comp ref="child" {...this.props} /></div>;
}
} as any;
}
...which can be applied like this...
#popIn
export class Term extends React.PureComponent<ITermStateProps & ITermDispatchProps, void> {
public render(): JSX.Element {
const { term, isSelected, onSelectTerm } = this.props;
return <ListItem rightIcon={<PendingReviewIndicator termId={term.id} />} style={isSelected ? { backgroundColor: "#ddd" } : {}} onClick={onSelectTerm}>{term.canonicalName}</ListItem>;
}
}
Unfortunately it requires the component to be defined as a class, but it does make it possible to declaratively add an animation to a component without modifying it. I like this approach but hate that I have to wrap the component in a transition group - nor does it address any of the other requirements.
I don't know enough about the internal and extension points of React and Redux to have a good idea how to approach this. I figured thunk actions would be a good place to manage the animation flows but I don't want to send the action components into the actions. Rather, I would like to be able to retrieve the source component for an action or something like that. Another angle could be a specialized reducer which passes in both the action and the source component, allowing you to match them somehow and schedule animations.
So I guess what I'm after is one or more of the following:
Ways to hook into React and/or Redux, preferably without destroying performance or violating the basic assumptions of the libraries.
Whether there are any existing libraries that solves some or all of these issues and which would be easy to integrate into the application.
Techniques or approaches to achieve all or most of these goals by either working with the normal animation tools or integrating well into the normal building blocks.
I hope I understood everything right… Dealing with React + Redux means, that in best case your components are pure functional. So a component that should be animated should (IMHO) at least take one parameter: p, which represents the state of the animation. p should be in the interval [0,1] and zero stands for the start, 1 for the end and everything in between for the current progress.
const Accordion = ({p}) => {
return (
…list of items, each getting p
);
}
So the question is, how to dispatch actions over time (what is an asynchronous thing), after the animation started, until the animation is over, after a certain event triggered that process.
Middleware comes in handy here, since it can »process« dispatched actions, transform them into another, or into multiple
//middleware/animatror.js
const animator = store => next => action => {
if (action.type === 'animator/start') {
//retrieve animation settings
const { duration, start, … } = action.payload;
animationEngine.add({
dispatch,
action: {
progress: () => { … },
start: () => { … },
end: () => { … }
}
})
} else {
return next(action);
}
}
export default animator;
Whereby the animationEngine is an Instance of AnimatoreEngine, an Object that listens to the window.requestAnimationFrame event and dispatches appropriate Actions. The creation of the middleware can be used the instantiate the animationEngine.
const createAnimationMiddleware = () => {
const animatoreEngine = new AnimatorEngine;
return const animator = store => next => action => { … }
}
export default createAnimationMiddleware;
//store.js
const animatorMiddleware = createAnimationMiddleware();
…
const store = createStore(
…,
applyMiddleware(animatorMiddleware, …)
)
The basic Idea is, to »swallow« actions of type animator/start, or something else, and transform them into a bunch of »subactions«, which are configured in the action.payload.
Within the middleware, you can access dispatch and the action, so you can dispatch other actions from there, and those can be called with a progress parameter as well.
The code showed here is far from complete, but tried to figure out the idea. I have build »remote« middleware, which handles all my request like that. If the actions type is get:/some/path, it basically triggers a start action, which is defined in the payload and so on. In the action it looks like so:
const modulesOnSuccessAction => data {
return {
type: 'modules/listing-success',
payload: { data }
}
}
const modulesgetListing = id => dispatch({
type: `get:/listing/${id}`,
payload: {
actions: {
start: () => {},
…
success: data => modulesOnSuccessAction(data)
}
}
});
export { getListing }
So I hope I could transport the Idea, even if the code is not ready for Copy/Paste.