Как использовать React Component с Tether.js/drop.js
Я хочу создать и показать модал обратной связи рядом с различными элементами DOM в зависимости от действий пользователя на странице. Я могу позиционировать модальный режим, но всякий раз, когда я пытаюсь добавить информацию, он начинает выдавать эти ошибки - Нарушение инварианта: findComponentRoot". Мой вопрос: является ли это правильным способом использования библиотеки и как я могу исправить эти ошибки. Вот Плункер для того же http://plnkr.co/edit/alF7JyQAhBwcANyrQQiw
var Feedback = React.createClass({
clickHandler: function(){
console.log("form is submitted");
},
componentDidMount: function(){
var el = this.getDOMNode();
var drop = new Drop({
target: document.querySelector('#test'),
classes: 'drop-theme-arrows-bounce drop-hero',
content: el,
openOn: "click",
tetherOptions: {
attachment: 'bottom right',
targetOffset: "0 10px"
}
});
},
render: function(){
return (
<div className="drop-theme-hubspot-popovers">
<form>
<div className="form-group">
<label>Feedback</label>
<input type="text" className="form-control"
placeholder="Enter email"
onChange={this.changeHandler}/>
<a href="#" className="btn btn-default" onClick={this.clickHandler}>Submit</a>
</div>
</form>
</div>
);
}
});
var Demo = React.createClass({
getInitialState: function(){
return {feedback: null};
},
componentDidMount: function(){
var FeedbackElement = React.createFactory(Feedback);
var feedback = <FeedbackElement/>;
//React.render(feedback, document.querySelector('#targetName'));
this.setState({feedback:feedback});
},
render: function(){
return (
<div className="container">
<div className="page-header">
<h1>Hello</h1>
</div>
<div className="row">
<div className="col-sm-12">
<div className="col-lg-5">
<a name="test" id="test" className="btn btn-default" onClick={this.clickHandler}> Click</a>
</div>
</div>
</div>
{this.state.feedback}
</div>
);
}
});
React.render(Demo(), document.getElementById('app'));
2 ответа
У меня были похожие проблемы, и решение состояло в том, чтобы создать элементы, которые должны быть присоединены вне дерева, управляемого React.
Я также написал несколько помощников для интеграции Tether с React, вы можете увидеть их здесь.
Для информации мы используем Tether Tooltip. Это просто очень простая оболочка для DropJS (которая просто добавляет некоторые классы по умолчанию и CSS-классы), так что, надеюсь, вы сможете использовать такой же код с DropJS.
Мы создали компонент-оболочку WithTooltip. Вы можете просто использовать это так:
render: function () {
return (
<WithTooltip content={this.renderTooltipContent()} position="bottom left">
{this.renderContent()}
</WithTooltip>
);
}
Обратите внимание, что содержимое всплывающей подсказки (или удаления) может быть как простым текстом, так и компонентом React. Поведение очень похоже на "портал"
Вы также можете использовать контекст React внутри содержимого всплывающей подсказки, но начиная с 0.14 это потребует использования нового метода renderSubtreeIntoContainer
Вот необработанный полный код WithTooltip, который мы сейчас используем.
'use strict';
var React = require("react");
var _ = require("lodash");
var $ = require("jquery");
var TetherTooltip = require("tether-tooltip");
var WithLongHoverBehavior = require("common/withLongHoverBehavior");
var AppMediaqueries = require("appMediaqueries");
// See https://github.com/facebook/react/issues/4081
// See https://github.com/facebook/react/pull/4184
// See https://github.com/facebook/react/issues/4301
//var renderSubtreeIntoContainer = require("react-dom").unstable_renderSubtreeIntoContainer;
var ValidTooltipPositions = [
'top left',
'left top',
'left middle',
'left bottom',
'bottom left',
'bottom center',
'bottom right',
'right bottom',
'right middle',
'right top',
'top right',
'top center'
];
var TooltipConstraints = [
{
to: 'window',
attachment: 'together',
// Can be important because tether can switch from top to bottom, or left to right,
// but it does not handle correctly bottom-left to bottom-right for exemple
// Using pin will at least make the tooltip stay on the screen without overflow
// (but there's a CSS bug that makes the tooltip arrow hidden by the content I think)
pin: true
}
];
/**
* A wrapper to set around components that must have a tooltip
* The tooltip knows how to reposition itself according to constraints on scroll/resize...
* See http://github.hubspot.com/tooltip/
*/
var WithTooltip = React.createClass({
propTypes: {
// The children on which the tooltip must be displayed on hover
children: React.PropTypes.node.isRequired,
// The prefered position (by default it will try to constrain the tooltip into window boundaries
position: React.PropTypes.oneOf(ValidTooltipPositions),
// The tooltip content (can be an inlined HTML string or simple text)
// If not defined, the tooltip will be disabled
content: React.PropTypes.node,
// Permits to disable the tooltip
disabled: React.PropTypes.bool,
// Wether this tooltip can be hovered or not (useful if the tooltip contains buttons)
hoverable: React.PropTypes.bool
},
isDisabled: function() {
if ( this.props.disabled ) {
return true;
}
else if ( !this.props.content ) {
return true;
}
else {
return false;
}
},
// TODO can probably be optimized?
resetTooltipForCurrentProps: function() {
// The timeout is required because otherwise TetherTooltip messes up with animations entering (ReactCSSTransitionGroup)
// TODO find why! is there a better solution?
setTimeout(function() {
if (this.isMounted()) {
this.destroyTooltip();
// Disable tooltips for mobile, as there's no mouse it does not make sense
// In addition we have encountered weird behaviors in iPhone/iOS that triggers "mouseover" events on touch,
// even after calling preventDefault on the touchstart/end events :(
if ( AppMediaqueries.isMobile() ) {
this.destroyTooltip();
return;
}
if ( !this.isDisabled() ) {
var target = React.findDOMNode(this);
if ( $(target).width() === 0 && $(target).height() === 0 ) {
console.warn("WithTooltip: you are setting a tooltip on an element with 0 width/height. This is probably unwanted behavior",target);
}
this.tetherTooltip = new TetherTooltip({
target: target,
position: this.props.position || 'bottom left',
content: " ", // Disable as we manage the content ourselves
// See https://github.com/HubSpot/tooltip/issues/5#issuecomment-33735589
tetherOptions: {
constraints: TooltipConstraints
}
});
if ( this.props.hoverable ) {
$(this.getTetherTooltipNode()).addClass("tooltip-hoverable");
}
// We mount the tooltip content ourselves because we want to be able to mount React content as tooltip
var tooltipContentNode = $(this.getTetherTooltipNode()).find(".tooltip-content")[0];
if ( React.isValidElement(this.props.content) ) {
//renderSubtreeIntoContainer(this, this.props.content, tooltipContentNode);
React.render(this.props.content, tooltipContentNode);
}
else {
tooltipContentNode.innerHTML = this.props.content;
}
}
}
}.bind(this),0);
},
componentDidMount: function() {
this.resetTooltipForCurrentProps();
},
componentDidUpdate: function(previousProps) {
var positionHasChanged = (this.props.position !== previousProps.position);
var contentHasChanged = (this.props.content !== previousProps.content);
var disabledHasChanged = (this.props.disabled !== previousProps.disabled);
var childrenHasChanged = (this.props.children !== previousProps.children);
var hasChanged = positionHasChanged || disabledHasChanged || contentHasChanged || childrenHasChanged;
if ( hasChanged ) {
this.resetTooltipForCurrentProps();
}
},
componentWillUnmount: function() {
this.destroyTooltip();
},
destroyTooltip: function() {
if ( this.tetherTooltip ) {
this.tetherTooltip.destroy();
delete this.tetherTooltip;
}
},
getTooltipTarget: function() {
if (typeof this.props.children === 'string') {
return <span>{this.props.children}</span>;
} else {
return React.Children.only(this.props.children);
}
},
// It may return nothing if the tooltip is already removed from DOM
getTetherTooltipNode: function() {
return this.tetherTooltip && this.tetherTooltip.drop && this.tetherTooltip.drop.drop;
},
onLongHover: function() {
$(this.getTetherTooltipNode()).addClass("long-hover");
},
onHoverEnd: function() {
$(this.getTetherTooltipNode()).removeClass("long-hover");
},
render: function() {
return (
<WithLongHoverBehavior longHoverDelay={2500} onLongHover={this.onLongHover} onHoverEnd={this.onHoverEnd}>
{this.getTooltipTarget()}
</WithLongHoverBehavior>
);
}
});
module.exports = WithTooltip;