Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use React Component with Tether.js/drop.js

I want to create and show a feedback modal next to different DOM elements depending upon the user actions on the page. I am able to position the modal but whenever I try to add info, it starts giving these errors - Invariant Violation: findComponentRoot". My question is, Is this the right way to use the library and how can I fix these errors. Here is plunker for the same 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'));
like image 674
sanjeev Avatar asked Sep 28 '22 14:09

sanjeev


2 Answers

I had similar problems, and the solution was to create the elements that are to be attached outside the React-controlled tree.

I also wrote a few helpers for integrating Tether with React, you can see them here.

like image 89
mik01aj Avatar answered Oct 19 '22 13:10

mik01aj


For information, we use Tether Tooltip. It is just a very simple wrapper around DropJS (that simply adds some default and CSS classes) so hopefully you'll be able to use the same kind of code with DropJS.

We have created a wrapper component WithTooltip. You can simply use it this way:

render: function () {
    return (
        <WithTooltip content={this.renderTooltipContent()} position="bottom left">
            {this.renderContent()}
        </WithTooltip>
    );
}

Notice that the tooltip (or drop) content can be both simple text, but also a React component. The behavior is quite similar to a "portal"

You can use React context inside the tooltip content too, but as of 0.14 it will require the use of the new method renderSubtreeIntoContainer

Here's the raw full WithTooltip code we currently use.

'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;
like image 23
Sebastien Lorber Avatar answered Oct 19 '22 12:10

Sebastien Lorber