react props create component app javascript reactjs

javascript - props - Forma recomendada de hacer que React component/div sea arrastrable



render in react (6)

Quiero hacer que un componente Reaccionar sea arrastrable (es decir, reposicionable por el mouse), lo que parece implicar necesariamente controladores de eventos de estado global y dispersos. Puedo hacerlo de forma sucia, con una variable global en mi archivo JS, y probablemente incluso podría envolverlo en una bonita interfaz de cierre, pero quiero saber si hay alguna manera de que se mezcle mejor con React.

Además, como nunca he hecho esto en JavaScript sin procesar, me gustaría ver cómo lo hace un experto, para asegurarme de que tengo todos los casos de esquina manejados, especialmente cuando se relacionan con React.

Gracias.


He actualizado la solución polkovnikov.ph a React 16 / ES6 con mejoras como el manejo táctil y el ajuste a una grilla, que es lo que necesito para un juego. Ajustar a una cuadrícula alivia los problemas de rendimiento.

import React from ''react''; import ReactDOM from ''react-dom''; import PropTypes from ''prop-types''; class Draggable extends React.Component { constructor(props) { super(props); this.state = { relX: 0, relY: 0, x: props.x, y: props.y }; this.gridX = props.gridX || 1; this.gridY = props.gridY || 1; this.onMouseDown = this.onMouseDown.bind(this); this.onMouseMove = this.onMouseMove.bind(this); this.onMouseUp = this.onMouseUp.bind(this); this.onTouchStart = this.onTouchStart.bind(this); this.onTouchMove = this.onTouchMove.bind(this); this.onTouchEnd = this.onTouchEnd.bind(this); } static propTypes = { onMove: PropTypes.func, onStop: PropTypes.func, x: PropTypes.number.isRequired, y: PropTypes.number.isRequired, gridX: PropTypes.number, gridY: PropTypes.number }; onStart(e) { const ref = ReactDOM.findDOMNode(this.handle); const body = document.body; const box = ref.getBoundingClientRect(); this.setState({ relX: e.pageX - (box.left + body.scrollLeft - body.clientLeft), relY: e.pageY - (box.top + body.scrollTop - body.clientTop) }); } onMove(e) { const x = Math.trunc((e.pageX - this.state.relX) / this.gridX) * this.gridX; const y = Math.trunc((e.pageY - this.state.relY) / this.gridY) * this.gridY; if (x !== this.state.x || y !== this.state.y) { this.setState({ x, y }); this.props.onMove && this.props.onMove(this.state.x, this.state.y); } } onMouseDown(e) { if (e.button !== 0) return; this.onStart(e); document.addEventListener(''mousemove'', this.onMouseMove); document.addEventListener(''mouseup'', this.onMouseUp); e.preventDefault(); } onMouseUp(e) { document.removeEventListener(''mousemove'', this.onMouseMove); document.removeEventListener(''mouseup'', this.onMouseUp); this.props.onStop && this.props.onStop(this.state.x, this.state.y); e.preventDefault(); } onMouseMove(e) { this.onMove(e); e.preventDefault(); } onTouchStart(e) { this.onStart(e.touches[0]); document.addEventListener(''touchmove'', this.onTouchMove, {passive: false}); document.addEventListener(''touchend'', this.onTouchEnd, {passive: false}); e.preventDefault(); } onTouchMove(e) { this.onMove(e.touches[0]); e.preventDefault(); } onTouchEnd(e) { document.removeEventListener(''touchmove'', this.onTouchMove); document.removeEventListener(''touchend'', this.onTouchEnd); this.props.onStop && this.props.onStop(this.state.x, this.state.y); e.preventDefault(); } render() { return <div onMouseDown={this.onMouseDown} onTouchStart={this.onTouchStart} style={{ position: ''absolute'', left: this.state.x, top: this.state.y, touchAction: ''none'' }} ref={(div) => { this.handle = div; }} > {this.props.children} </div>; } } export default Draggable;


Implementé react-dnd , una react-dnd flexible de arrastrar y soltar de HTML5 para Reaccionar con control DOM completo.

Las bibliotecas existentes de arrastrar y soltar no se ajustaban a mi caso de uso, así que escribí las mías. Es similar al código que hemos estado ejecutando durante aproximadamente un año en Stampsy.com, pero reescrito para aprovechar React y Flux.

Requisitos clave que tenía:

  • Emite cero DOM o CSS propios, dejándolo a los componentes consumidores;
  • Imponer la menor estructura posible a los componentes consumidores;
  • Utilice HTML5 arrastrar y soltar como back-end primario, pero haga posible agregar diferentes backends en el futuro;
  • Al igual que la API HTML5 original, enfatice el arrastre de datos y no solo las "vistas arrastrables";
  • Ocultar peculiaridades de API HTML5 del código de consumo;
  • Los diferentes componentes pueden ser "fuentes de arrastre" u "destinos de caída" para diferentes tipos de datos;
  • Permitir que un componente contenga varias fuentes de arrastre y soltar objetivos cuando sea necesario;
  • Facilite que los objetivos de colocación cambien su apariencia si se arrastran o cierran datos compatibles;
  • Facilite el uso de imágenes para arrastrar miniaturas en lugar de capturas de pantalla de elementos, eludiendo las peculiaridades del navegador.

Si esto te suena familiar, sigue leyendo.

Uso

Fuente de arrastre simple

Primero, declare los tipos de datos que se pueden arrastrar.

Estos se usan para verificar la "compatibilidad" de las fuentes de arrastre y soltar los objetivos:

// ItemTypes.js module.exports = { BLOCK: ''block'', IMAGE: ''image'' };

(Si no tiene múltiples tipos de datos, esta biblioteca puede no ser para usted).

Entonces, hagamos un componente muy simple que, al ser arrastrado, represente IMAGE :

var { DragDropMixin } = require(''react-dnd''), ItemTypes = require(''./ItemTypes''); var Image = React.createClass({ mixins: [DragDropMixin], configureDragDrop(registerType) { // Specify all supported types by calling registerType(type, { dragSource?, dropTarget? }) registerType(ItemTypes.IMAGE, { // dragSource, when specified, is { beginDrag(), canDrag()?, endDrag(didDrop)? } dragSource: { // beginDrag should return { item, dragOrigin?, dragPreview?, dragEffect? } beginDrag() { return { item: this.props.image }; } } }); }, render() { // {...this.dragSourceFor(ItemTypes.IMAGE)} will expand into // { draggable: true, onDragStart: (handled by mixin), onDragEnd: (handled by mixin) }. return ( <img src={this.props.image.url} {...this.dragSourceFor(ItemTypes.IMAGE)} /> ); } );

Al especificar configureDragDrop , le DragDropMixin a DragDropMixin el comportamiento de arrastrar y soltar de este componente. Tanto los componentes arrastrables como los que se pueden soltar usan el mismo mixin.

Dentro de configureDragDrop , necesitamos llamar a registerType para cada uno de nuestros ItemTypes personalizados que admite el componente. Por ejemplo, podría haber varias representaciones de imágenes en su aplicación, y cada una proporcionaría un dragSource para ItemTypes.IMAGE .

Un dragSource es solo un objeto que especifica cómo funciona la fuente de arrastre. Debe implementar beginDrag para devolver el elemento que representa los datos que está arrastrando y, opcionalmente, algunas opciones que ajustan la IU de arrastre. Opcionalmente puede implementar canDrag para prohibir el arrastre, o endDrag(didDrop) para ejecutar algo de lógica cuando el endDrag(didDrop) ha ocurrido (o no). Y puede compartir esta lógica entre componentes permitiendo que una dragSource compartida genere dragSource para ellos.

Finalmente, debe usar {...this.dragSourceFor(itemType)} en algunos (uno o más) elementos en el render para adjuntar controladores de arrastre. Esto significa que puede tener varios "controles de arrastre" en un elemento, e incluso pueden corresponder a diferentes tipos de elementos. (Si no está familiarizado con la sintaxis de JSX Spread Attributes , compruébelo).

Simple Drop Target

Digamos que queremos que ImageBlock sea ​​un objetivo de caída para IMAGE s. Es prácticamente lo mismo, excepto que necesitamos dar a registerType una implementación dropTarget :

var { DragDropMixin } = require(''react-dnd''), ItemTypes = require(''./ItemTypes''); var ImageBlock = React.createClass({ mixins: [DragDropMixin], configureDragDrop(registerType) { registerType(ItemTypes.IMAGE, { // dropTarget, when specified, is { acceptDrop(item)?, enter(item)?, over(item)?, leave(item)? } dropTarget: { acceptDrop(image) { // Do something with image! for example, DocumentActionCreators.setImage(this.props.blockId, image); } } }); }, render() { // {...this.dropTargetFor(ItemTypes.IMAGE)} will expand into // { onDragEnter: (handled by mixin), onDragOver: (handled by mixin), onDragLeave: (handled by mixin), onDrop: (handled by mixin) }. return ( <div {...this.dropTargetFor(ItemTypes.IMAGE)}> {this.props.image && <img src={this.props.image.url} /> } </div> ); } );

Arrastrar origen + soltar objetivo en un componente

Supongamos que ahora queremos que el usuario pueda arrastrar una imagen desde ImageBlock . Solo tenemos que agregarle el dragSource apropiado y algunos manejadores:

var { DragDropMixin } = require(''react-dnd''), ItemTypes = require(''./ItemTypes''); var ImageBlock = React.createClass({ mixins: [DragDropMixin], configureDragDrop(registerType) { registerType(ItemTypes.IMAGE, { // Add a drag source that only works when ImageBlock has an image: dragSource: { canDrag() { return !!this.props.image; }, beginDrag() { return { item: this.props.image }; } } dropTarget: { acceptDrop(image) { DocumentActionCreators.setImage(this.props.blockId, image); } } }); }, render() { return ( <div {...this.dropTargetFor(ItemTypes.IMAGE)}> {/* Add {...this.dragSourceFor} handlers to a nested node */} {this.props.image && <img src={this.props.image.url} {...this.dragSourceFor(ItemTypes.IMAGE)} /> } </div> ); } );

¿Qué más es posible?

No he cubierto todo, pero es posible usar esta API de varias maneras más:

  • Use getDragState(type) y getDropState(type) para saber si el arrastre está activo y utilícelo para alternar clases o atributos de CSS;
  • Especifique dragPreview para que sea Image para usar imágenes como marcadores de posición de arrastre (use ImagePreloaderMixin para cargarlas);
  • Digamos, queremos hacer que ImageBlocks reordenable. Solo los necesitamos para implementar dropTarget y dragSource para ItemTypes.BLOCK .
  • Supongamos que agregamos otros tipos de bloques. Podemos reutilizar su lógica de reordenamiento colocándola en una mezcla.
  • dropTargetFor(...types) permite especificar varios tipos a la vez, por lo que una zona de dropTargetFor(...types) puede atrapar muchos tipos diferentes.
  • Cuando necesita un control más preciso, la mayoría de los métodos se pasan al evento de arrastre que los causó como el último parámetro.

Para obtener la documentación actualizada y las instrucciones de instalación, diríjase a react-dnd .


La respuesta de Jared Forsyth es terriblemente incorrecta y obsoleta. Sigue un conjunto completo de antipatrones, como el uso de stopPropagation , el estado de inicialización de apoyos , el uso de jQuery, objetos anidados en estado y tiene algún campo de estado de dragging impar. Si se reescribe, la solución será la siguiente, pero aún obliga a la reconciliación DOM virtual en cada movimiento del mouse y no es muy eficiente.

const Draggable = React.createClass({ getInitialState() { return { relX: 0, relY: 0 }; }, onMouseDown(e) { if (e.button !== 0) return; const ref = ReactDOM.findDOMNode(this.refs.handle); const body = document.body; const box = ref.getBoundingClientRect(); this.setState({ relX: e.pageX - (box.left + body.scrollLeft - body.clientLeft), relY: e.pageY - (box.top + body.scrollTop - body.clientTop) }); document.addEventListener(''mousemove'', this.onMouseMove); document.addEventListener(''mouseup'', this.onMouseUp); e.preventDefault(); }, onMouseUp(e) { document.removeEventListener(''mousemove'', this.onMouseMove); document.removeEventListener(''mouseup'', this.onMouseUp); e.preventDefault(); }, onMouseMove(e) { this.props.onMove({ x: e.pageX - this.state.relX, y: e.pageY - this.state.relY }); e.preventDefault(); }, render() { return <div onMouseDown={this.onMouseDown} style={{ position: ''absolute'', left: this.props.x, top: this.props.y }} ref="handle" >{this.props.children}</div>; } }) const Test = React.createClass({ getInitialState() { return {x: 100, y: 200}; }, render() { const {x, y} = this.state; return <Draggable x={x} y={y} onMove={this.move}> Drag me </Draggable>; }, move(e) { this.setState(e); } }); ReactDOM.render(<Test />, document.getElementById(''container''));

Ejemplo en JSFiddle.


Me gustaría agregar un tercer escenario

La posición de movimiento no se guarda de ninguna manera. Piense en ello como un movimiento del mouse: su cursor no es un componente reactivo, ¿verdad?

Todo lo que debes hacer es agregar un elemento como "arrastrable" a tu componente y una secuencia de los eventos de arrastre que manipularán el dom.

setXandY: function(event) { // DOM Manipulation of x and y on your node }, componentDidMount: function() { if(this.props.draggable) { var node = this.getDOMNode(); dragStream(node).onValue(this.setXandY); //baconjs stream }; },

En este caso, una manipulación DOM es algo elegante (nunca pensé que diría esto)


Probablemente debería convertir esto en una publicación de blog, pero aquí hay un ejemplo bastante sólido.

Los comentarios deberían explicar bastante bien las cosas, pero avíseme si tiene alguna pregunta.

Y aquí está el violín para jugar: http://jsfiddle.net/Af9Jt/2/

var Draggable = React.createClass({ getDefaultProps: function () { return { // allow the initial position to be passed in as a prop initialPos: {x: 0, y: 0} } }, getInitialState: function () { return { pos: this.props.initialPos, dragging: false, rel: null // position relative to the cursor } }, // we could get away with not having this (and just having the listeners on // our div), but then the experience would be possibly be janky. If there''s // anything w/ a higher z-index that gets in the way, then you''re toast, // etc. componentDidUpdate: function (props, state) { if (this.state.dragging && !state.dragging) { document.addEventListener(''mousemove'', this.onMouseMove) document.addEventListener(''mouseup'', this.onMouseUp) } else if (!this.state.dragging && state.dragging) { document.removeEventListener(''mousemove'', this.onMouseMove) document.removeEventListener(''mouseup'', this.onMouseUp) } }, // calculate relative position to the mouse and set dragging=true onMouseDown: function (e) { // only left mouse button if (e.button !== 0) return var pos = $(this.getDOMNode()).offset() this.setState({ dragging: true, rel: { x: e.pageX - pos.left, y: e.pageY - pos.top } }) e.stopPropagation() e.preventDefault() }, onMouseUp: function (e) { this.setState({dragging: false}) e.stopPropagation() e.preventDefault() }, onMouseMove: function (e) { if (!this.state.dragging) return this.setState({ pos: { x: e.pageX - this.state.rel.x, y: e.pageY - this.state.rel.y } }) e.stopPropagation() e.preventDefault() }, render: function () { // transferPropsTo will merge style & other props passed into our // component to also be on the child DIV. return this.transferPropsTo(React.DOM.div({ onMouseDown: this.onMouseDown, style: { left: this.state.pos.x + ''px'', top: this.state.pos.y + ''px'' } }, this.props.children)) } })

Pensamientos sobre la propiedad estatal, etc.

"Quién debería tener qué estado" es una pregunta importante para responder, desde el principio. En el caso de un componente "arrastrable", pude ver algunos escenarios diferentes.

escenario 1

el padre debe poseer la posición actual del arrastrable. En este caso, el arrastrable aún sería propietario del estado "¿estoy arrastrando?", Pero llamaría a this.props.onChange(x, y) siempre que ocurra un evento mousemove.

Escenario 2

el padre solo necesita tener la "posición no móvil", por lo que el arrastrable tendría su propia "posición de arrastre", pero en el modo de funcionamiento llamaría this.props.onChange(x, y) y diferiría la decisión final del padre. Si al padre no le gusta dónde terminó el arrastrable, simplemente no actualizaría su estado, y el que se puede arrastrar "retrocederá" a su posición inicial antes de arrastrarlo.

Mixin o componente?

@ssorallen señaló que, como "arrastrable" es más un atributo que una cosa en sí misma, podría servir mejor como mixin. Mi experiencia con mixins es limitada, así que no he visto cómo podrían ayudar o interferir en situaciones complicadas. Esta bien podría ser la mejor opción.


react-draggable también es fácil de usar. Github:

https://github.com/mzabriskie/react-draggable

import React, {Component} from ''react''; import ReactDOM from ''react-dom''; import Draggable from ''react-draggable''; var App = React.createClass({ render() { return ( <div> <h1>Testing Draggable Windows!</h1> <Draggable handle="strong"> <div className="box no-cursor"> <strong className="cursor">Drag Here</strong> <div>You must click my handle to drag me</div> </div> </Draggable> </div> ); } }); ReactDOM.render( <App />, document.getElementById(''content'') );

Y mi index.html:

<html> <head> <title>Testing Draggable Windows</title> <link rel="stylesheet" type="text/css" href="style.css" /> </head> <body> <div id="content"></div> <script type="text/javascript" src="bundle.js" charset="utf-8"></script> <script src="http://localhost:8080/webpack-dev-server.js"></script> </body> </html>

Necesitas sus estilos, que es corto, o no obtienes el comportamiento esperado. Me gusta más el comportamiento que algunas de las otras opciones posibles, pero también hay algo que se llama react-reizable-and-movible. Estoy tratando de cambiar el tamaño de trabajo con draggable, pero no hay alegría hasta el momento.