How to change a x3dom shape with onClick, when using React?

866 Views Asked by At

I am trying to use React together with X3DOM. I want to be able to click on the red x3dom <box> so that it changes its color to blue, when pressed. I have tried using a onClick method in the <shape> tag. I was only able to do this by pressing a html <button> instead. I have the button also in the code.

This is my index.js file.

import React from "react";
import ReactDOM from "react-dom";

class Toggle extends React.Component {
    constructor(props) {
        super(props);
        this.state = { isToggleOn: true };

        this.handleClick = this.handleClick.bind(this);
    }

    handleClick() {
        this.setState(prevState => ({
            isToggleOn: !prevState.isToggleOn
        }));
    }

    render() {
        return (
        <div>
            <x3d width='500px' height='300px' >
                <scene>
                    <shape onClick={this.handleClick}>
                        <appearance>
                            <material id='color' diffusecolor={this.state.isToggleOn ? '1 0 0' : '0 0 1'}> </material>
                        </appearance>
                        <box></box>
                    </shape>
                </scene>
            </x3d>

            <button onClick={this.handleClick}>
                {this.state.isToggleOn ? 'BLUE' : 'RED'}
            </button>

        </div>
        );
    }

}

ReactDOM.render(<Toggle />, document.getElementById('toggle'));

Can someone give me a solution, or an explanation why this doesn’t work. I would greatly appreciate any answer.

4

There are 4 best solutions below

0
On

The problem is that X3DOM calls eventHandlers directly. Quote from the docu:

The call of the onclick function is handled by x3dom by directly calling the callback function, since the addEventListener method needed to be overwritten. No page-wide onclick events are thrown, so attaching a listener to this object is only possible after.

I have not tested this but adding the onclick once in componentDidMount might be the way to go.

2
On

As @Johannes already pointed out, you need to add your event listeners after X3DOM has been initialized. If you look closely at https://doc.x3dom.org/tutorials/animationInteraction/picking/index.html you will find that they explicitly state:

Caveat: The call of the onclick function is handled by x3dom by directly calling the callback function, since the addEventListener method needed to be overwritten. No page-wide onclick events are thrown, so attaching a listener to this object is only possible after. In this case, do not use $("#myBox").on("click", doSomething(event)) but $("#myBox").attr("onclick", "doSomething(event)") instead. Alternatively, wait until the page is fully loaded and the document.onload event was fired.

A working example might look like this:

var glob = {};
class Toggle {
    ...    
    componentDidMount() {
      glob.handleClick = this.handleClick;
      var k = document.getElementsByTagName('shape')[0];
      k.setAttribute('onclick', 'glob.handleClick()');
    }

Please note that I had to introduce a global object in order to get it working, which is probably just a lack of my knowledge of React. You may find a better way.

You can also see it at: https://codepen.io/pgab/pen/gBpEWo

0
On

Not sure if this is the best solution, but I was able to make it work by using componentDidUpdate(). I added an eventListner by giving <shape> some id. I also had to remove the eventListner to avoid a infinity loop. The problem with using componentDidUpdate() is that the component has to update somehow, before the method can be used. I did this by adding an extra state 'start' which i update from false to true after the first second when the application runs.

import React from "react";
import ReactDOM from "react-dom";

class Toggle extends React.Component {
 constructor() {
    super();
    this.state = { isToggleOn: true, start: false};

    this.handleClick = this.handleClick.bind(this); 
 }

 handleClick() {
    this.setState(prevState => ({
        isToggleOn: !prevState.isToggleOn
    }));
    console.log("Pressed");
 }

 componentDidUpdate() {
    console.log('Component update');

    //Have to do this to avoid potential infinty loop
    document.getElementById('someId').removeEventListener("click", this.handleClick);

    document.getElementById('someId').addEventListener("click", this.handleClick);
 }

 render() {

    //Need to update component once, to run componentDidUpdate
    if(!(this.state.start)){
        setTimeout(() => {
            this.setState({start: true });
        }, 1000)
    }
    //

    return (
        <div>
        <x3d width='500px' height='300px'>
            <scene>
                <shape id="someId">
                    <appearance>
                        <material id='color' diffusecolor={this.state.isToggleOn ? '1 0 0' : '0 0 1'}> </material>
                    </appearance>
                    <box></box>
                </shape>
            </scene>
        </x3d>
    </div>
    );  
  }
 }

ReactDOM.render(<Toggle />, document.getElementById('toggle'));
0
On

I had the same issue for weeks, Thanks to the answers to your question, and to this non-related post : Why does an onclick property set with setAttribute fail to work in IE?, i was able to make it work with onClick and mouseover events.

As said in one of the above answer, X3Dom comes with its own set of functions attached to the onClick event. And as mentioned in the X3DOM documentation, you can only attach your event after the document has loaded. Also @mistapink idea was correct except the setAttributemethod does not work for some reasons. So I did that :

const Scene = () => {
   const [toggle, setToggle] = useState(false);
   const handleMouseOver = () => {/* do some mouse over stuff here */};
const handleClick = () => setToggle(prevToggle => !prevToggle};
   useEffect(() => {
      const 3dObject = document.getElementById('my-3d-object'); 
/* I guess you can also try to use a reference, don't know if it'll work */
      document.onload = () => {
         3dObject.addEventListener("mouseover", handleMouseOver);
/* magic happens here  */
         3dObject.onclick = () => handleClick(
/* you can pass some extra arguments here */
);
      }
      return () => sphere.removeEvenetListener("mouseover", handleMouseOver);
    });

  return (...)
};