Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using NexusUI with React - Solution #65

Closed
hepiyellow opened this issue Jan 17, 2017 · 30 comments
Closed

Using NexusUI with React - Solution #65

hepiyellow opened this issue Jan 17, 2017 · 30 comments

Comments

@hepiyellow
Copy link

Hi,

Previously I mentioned that in order to use NexusUI with react the property name "nx" must be changed to "data-nx" (See issue #59).

I did that localy, and it works in a very simple case, but here is the next blocker issue !

It is common in React to have a React Component render different React DOM Elements along it's lifecycle.
For example in this render method:
render() { if (!this.state.connected) { return <div>not connected</div>; } return <canvas data-nx="dial" />; }
it renders a <div/> when not connected , otherwise, renders a NexusUI dial canvas.
In this case the problem is that the dial is not visible, not painted. I can see the 'canvas' element on the DOM (chrome debug tools) but that's it. (same code without the condition rendering of the

works fine).

I think this happens, because NexusUI scans for canvas element on the onLoad event only. And React doesn't reload the document, it manipulates the dom. So thought I would use the manager.add function but it creates a canvas element. I don't want NexusUI to create the canvas, I guess I need React to create it.

So I guess I need to add an ability to add/remove existing canvas elements to the manager.
How would I add that, roughly?

@brucelane
Copy link

I tryed to use React too, I'm interested in this!

@taylorbf
Copy link
Contributor

Hey there @hepiyellow , I'll do my best to answer this, albeit with very little knowledge of react.

It sounds like you might be able to use nx.transform() instead of nx.add()

nx.add() creates a canvas element and turns it into a nexusUI interface

nx.transform() transforms an existing canvas element into a nexusUI interface

Let me know if this is a viable solution and I'll do my best to figure out alternative solutions if it's not!

@hepiyellow
Copy link
Author

Well,
I didn't find a solution for the scenario I proposed. But I did solve the problem using nx.transform().
In my scenario a Main React component is using the conditional rendering shown above. So any children of my Main component will not be transformed be NexusUI.

The solution is to call nx.transform() when a canvas is mounted.
Need to add the special React ref property to the canvas as such :
<canvas data-nx='whatever' ref={(canvas)=>{window.nx.transform(canvas)} /> The callback passed to ref will be called when the element is mounted, and canvas` will be the DOM element.

I am just not sure yet if and how I "un-transform" a element which has been unmounted. (unmounted=removed from the DOM without a document re-load)
Is there a NexusUI function for that?

@taylorbf
Copy link
Contributor

I think the best way to do this, currently, is to use widget.destroy()

So, if your slider looks like this....

<canvas data-nx="slider" id="masterfader"></canvas>

Then you could call this when you want to remove it:

nx.widgets.masterfader.destroy()

@brucelane
Copy link

would you share your code, please?

@hepiyellow
Copy link
Author

I am still investigating this, so can't share code yet. There are more issues ahead.
I am using nx.transform() and nx.widgets[id].destroy() to update nx whenever a relevant DOM change is done.
I am using React's ref callback to do so. It is supposed to be called when the DOM element is mounted and unmounted.
But React has a reconciliation process where it can decide to only update a DOM element's attributes, instead of unmounting and mounting a new Element. And it seems that the ref callback is called for an unmount (with arg null) also when only an DOM update is done.

I will share some code or explain the whole solution once I have it working. (might take a few days)

@brucelane
Copy link

thank you, I'm not in a hurry so take your time.
I made a website with React/nexusui last month you can see on http://videodromm.com/
It sends websocket messages to a nodejs websocket server.
It's working though it's not clean...

@hepiyellow
Copy link
Author

hepiyellow commented Jan 18, 2017

Ok, main issue solved. And turns out there no need for any change in NexusUI. Even no need to change 'nx' to 'data-nx'.

Here is my minimal React component wrapper for a NexusUI canvas (using typescript):

type NxType = string; //'dial' | 'slider' ,etc...

export interface INexusUICanvasProps {
    type: NxType
    attrs?: {
        [index: string]: any
    }
}

export default class NexusUICanvas extends Component<INexusUICanvasProps, {}> {
    mountedCanvas: any;

    componentDidMount() {
        this.mountedCanvas.setAttribute('nx', this.props.type);
        if (this.props.attrs) {
            Object.keys(this.props.attrs).forEach((attrName) => {
                this.mountedCanvas.setAttribute(attrName, this.props.attrs[attrName]);
            });
        }
        (window as any).nx.transform(this.mountedCanvas);
    }

    componentWillUnmount() {
        (window as any).nx.widgets[this.mountedCanvas.id].destroy();
    }

    render() {
        return (
            <canvas ref={this.refCallback.bind(this)} />
        )
    }

    refCallback(domElement: any) {
        this.mountedCanvas = domElement;
    }
}

The solution to the issue I mentioned before is not to do anything in the ref callback except update the components this.mountedCanvas property. And do everything in componentDidMount and componentWillUnmount.

So actually the nx property and any other property you want to pass to the canvas is assigned in the componentDidMount before calling nx.transform.

It seems that the whole lifecycle of Nexus canvas is working.

@hepiyellow
Copy link
Author

My issue now is that I can't do any styling.
My dials look like this by default:
screen shot 2017-01-18 at 2 27 14 pm

Instead of like this:
screen shot 2017-01-18 at 2 35 59 pm

Seems that no styling is shown.
'label' property is ignored also.

Ben? Any idea?

@taylorbf
Copy link
Contributor

I'm not sure. I have a few guesses.

In preliminary tests (in vanilla js), I did not experience this issue. The following code produced a styled dial with a label:

function addComponent() {
          var c = document.createElement("canvas")
          c.setAttribute("nx","dial")
          c.setAttribute("label","volume")
          document.body.appendChild(c)
          nx.transform(c)
}

The dial you are seeing is also an older style. I wonder if you are using an older version of NexusUI?

Your react strategy -- using nx.transform, widget.destroy, etc -- will work in the newest version as well, I believe.

Lastly, just a note that there is an optional second argument for nx.transform that denotes the widget's type. That might be more elegant than manually giving your canvas an nx attribute.

nx.transform( canvasElement, "dial" )

Good luck and also thank you for your work getting nexusUI working with React! I am terribly sorry that I was not more help earlier in the process. I graduated from the university where this was created as a research project, and I'm not sure they have found anyone to replace me on authorship yet. We will add a note to the website indicating as much. I hope to do a refactor later this year, in which case I would love to discuss ways to increase compatibility with React.

@hepiyellow
Copy link
Author

Yep. I was using an old version of NexusUI. Thanks (-:

About using nx.transform( canvasElement, "dial" ) with the type as the second argument. I saw blockMove() method which seems to be checking for a the nx attribute on a DOM element (I guess a canvas), and I didn't see that calling transform adds the nx attribute to the given canvas.
So, either this is a bug, of the blockMove() method is not relevant. I just wanted to be on the safe side.

@hepiyellow
Copy link
Author

hepiyellow commented Jan 18, 2017

One last issue, I'll check tomorrow,
is that is see all my nexusUI interfaces animate (less than 0.5 sec) from position 0,0 to their final position (along with the whole other DOM elements) every time they are created (and transformed).

Any pointers on that?

BTW: How do I change the label's font color?

@taylorbf
Copy link
Contributor

taylorbf commented Jan 18, 2017 via email

@hepiyellow
Copy link
Author

Putting the init issue aside.
I found a new issue with using widget.destroy(). It seems that it also removes the canvas from the DOM:

var elemToKill = document.getElementById(this.canvasID)
if (elemToKill) {
    elemToKill.parentNode.removeChild(elemToKill);
}

This might lead to a React error:

 Uncaught TypeError: Cannot read property 'replaceChild' of null
    at Function.replaceChildWithTree (DOMLazyTree.js:70)

I encounter this case, and removing those line solves it.
I think nexusUI (when working with React) should not intervene with DOM structure.

I also see in the widget.destroy() function, these lines:

var id = this.canvasID
  delete nx.widgets[id];
  delete window[id];

Are you adding the canvas IDs as properties on the global window object? it that safe?

@taylorbf
Copy link
Contributor

taylorbf commented Jan 19, 2017 via email

@hepiyellow
Copy link
Author

hepiyellow commented Jan 27, 2017

There was another issue with resizing.
When the NexuxUICanvas component gets updated and the style.height / style.width changes, it didn't update the widget. So I added a call to widget,resize(). While at it also added a call to widget.checkPercentage() in case the style was changed to be percentage based.

It would be best if the widget had an reInitFromCanvas() method which re-initializes all internal state which is derived from the canvas.

Here is my current version of NexusUICanvas (with typescript):

import * as React from 'react';
import { Component } from 'react';

import * as Debug from "debug";
var debug = Debug('LM2.NexusUICanvas');

export type NxType = string; //'dial' | 'matrix' ,etc...

export interface NxWidget {
    canvas: any;
    height: number;
    width: number;
    preMove: any;
    preRelease: any;
    canvasID: any;
    init(): void;
    draw(): void;
    resize(w?: number, h?: number): void;
    checkPercentage(): void;
    transmit(data: any): void;
    set(value: any): void;
    getName(): string;
    [index: string]: any;
}

interface Attributes {
    [index: string]: any
}

export interface INexusUICanvasPropsBase {
    /**
     * A style object to be set on the created canvas element.
     */
    style?: {
        height: number,
        width: number,
        [index: string]: any
    }

    /**
     * An object containing properties that are to be set on the created 
     * canvas element before it is transformed into an NX interface.
     */
    canvasAttrs?: Attributes
    /**
     * An object containing properties that are to be set on the created
     * widget.
     */
    widgetAttrs?: Attributes
}

export interface INexusUICanvasProps extends INexusUICanvasPropsBase {
    type: NxType

}

// Need to make sure nx.globalWidgets = false to avoid global variable to the window object,
// which is 
(window as any).nx.globalWidgets = false;



export default class NexusUICanvas extends Component<INexusUICanvasProps, {}> {
    mountedCanvas: any;
    widget: NxWidget;

    componentDidMount() {
        debug('componentDidMount() canvas.id: ', this.mountedCanvas.id, 'elm=', this.mountedCanvas);
        // We set the canvas attribute 'nx' = type instead of calling transform(type),
        // because manager.blockMove() is looking for the canvas.nx attribute.
        this.mountedCanvas.setAttribute('nx', this.props.type);
        this.updateCanvasFromProps();
        
        // widget has internal state which is initialized (by the canvas) only when the widget is created.
        // If the canvas is changed, we need to create a new widget.
        // For example widget.height is initialized from canvas style when creating a new Widget. But it 
        // doesn't update when canvas style is changed.
        this.widget = (window as any).nx.transform(this.mountedCanvas);

        this.updateWidgetFromProps();
        this.widget.draw();
    }
   
    componentWillUnmount() {
        debug('componentWillUnmount() destroy canvas.id= ', this.mountedCanvas.id);

        // We don't call the original widget.destroy() because it 
        // removes the canvas from the DOM, and we should leave that to React.
        NexusUICanvas.destroyWidget(this.widget);
    }

    componentWillUpdate() {
        debug('componentWillUpdate()');
    }

    shouldComponentUpdate(nextProps: INexusUICanvasProps, nextState: any) {
        //TODO: optimize. 
        return true;
    }

    componentDidUpdate() {
        debug('componentDidUpdate() widget=', this.widget);
        this.updateCanvasFromProps();
        this.updateWidgetFromProps();
    }

    render() {
        debug('render(): props=', this.props);
        return (
            <canvas
                style={this.props.style}
                ref={this.refCallback.bind(this)}>
            </canvas>
        )
    }

    refCallback(domElement: any) {
        debug('refCallback() domElm=', domElement);
        this.mountedCanvas = domElement;
    }

    updateCanvasFromProps() {
        const attrs = this.props.canvasAttrs;
        if (attrs) {
            Object.keys(attrs).forEach((attrName) => {
                this.mountedCanvas.setAttribute(attrName, attrs[attrName]);
            });
        }
    }

    updateWidgetFromProps() {
        const attrs = this.props.widgetAttrs;
        if (attrs) {
            Object.keys(attrs).forEach((attrName) => {
                this.widget[attrName] = attrs[attrName];
            });
        }

        // This is partial solution if we don't re-create a new widget when component is updated
        if (this.props.style) {
            this.widget.resize(this.props.style.width, this.props.style.height)
            this.widget.checkPercentage();
        }

        this.widget.init();
    }

    /**  @method destroy
    Remove the widget object, canvas, and all related event listeners from the document.
    */
    static destroyWidget(widget: NxWidget) {
        const nx = (window as any).nx;
        var type = nx.elemTypeArr.indexOf(widget.getName())
        nx.elemTypeArr.splice(type, 1)

        widget.canvas.ontouchmove = null;
        widget.canvas.ontouchend = null;
        widget.canvas.onclick = null;
        widget.canvas.onmousemove = null;
        widget.canvas.onmouseoff = null;
        document.removeEventListener("mousemove", widget.preMove, false);
        document.removeEventListener("mouseup", widget.preRelease, false);

        // Commented-out original code which is inappropriate for React (EM)
        // var elemToKill = document.getElementById(this.canvasID)
        // if (elemToKill) {
        //   elemToKill.parentNode.removeChild(elemToKill);
        // }

        var id = widget.canvasID
        delete nx.widgets[id];
        // Added a check for nx.globalWidgets (EM)
        if ((window as any).nx.globalWidgets === true) {
            delete window[id];
        }
    }
}

@hepiyellow hepiyellow changed the title Blocker Issue using NexusUI with ReactJS Using NexusUI with React - Solution Jan 27, 2017
@taylorbf
Copy link
Contributor

Hi @hepiyellow,

Yes, widget.resize( w, h ) is the best way to resize an interface element.

There is an .init() method for each widget, which is called at the end of .resize(). .init() is mostly for the purpose of re-calculating size-specific variables. Can you explain further what you mean by re-initializing internal states which are derived from the canvas?

By the way, I noticed a bug with .resize and the latest dial... if given a non-square canvas, the dial does not center correctly.

I am working on a refactor a.t.m., taking into account some of your concerns with react as well as other concerns from the past year. I imagine this revision will take some time, but I wanted to let you know it is in the works.

Ben

@brucelane
Copy link

hi @hepiyellow , could you make a repo with a sample project?
I don't know about typescript, I guess it needs a transpiler task with gulp or webpack?
thank you very much
Bruce

@hepiyellow
Copy link
Author

hepiyellow commented Jan 31, 2017 via email

@hepiyellow
Copy link
Author

Here is my example for using NexusUI with React (and typescript)
It is still under construction. Yet some things to solve...

I think NexusUI is great to use with Mobx, I will add examples later on how to do it.

https://github.com/hepiyellow/nexusui-mobx-react-typescript-sample

@brucelane
Copy link

oh yes, works fine!

@turbo5
Copy link

turbo5 commented Mar 21, 2017

In the npm registry (npm install) the version is still 1.0.8, sith the old widgets, and no labels, no preset values. Can you update it please?

@taylorbf
Copy link
Contributor

Thanks @hepiyellow for your work sharing this tutorial.

@turbo5 that's a different issue but thanks for catching it -- i've updated the npm registry to 1.2 with #76 .

@turbo5
Copy link

turbo5 commented Mar 27, 2017

@taylorbf Thanx, it looks updated now. I have asked this here #52 as well, sorry for posting here at the wrong issue :)

@JamesTheHacker
Copy link

JamesTheHacker commented May 19, 2018

I know this is an old issue but it's the first result on Google for "nexus ui react". I am just posting my solution here incase anyone else is wondering how to do this.

You will notice I am generating a random ID. This is because I do not rely on the ID for anything other than generating the dial and is purely personal choice. Everything else will be controlled by props and/or state. This is a rough solution to demonstrate because I felt the solution by @hepiyellow was overly complex.

Here's a Dial Component:

import React from 'react'
import Nexus from 'nexusui'
import randomID from 'random-id'
import PropTypes from 'prop-types'

class Dial extends React.Component {
    static propTypes = {
        size: PropTypes.arrayOf(PropTypes.number),
        interaction: PropTypes.string,
        mode: PropTypes.string,
        min: PropTypes.number,
        max: PropTypes.number,
        step: PropTypes.number,
        value: PropTypes.number,
    }

    static defaultProps = {
        size: [75, 75],
        interaction: 'radial',
        mode: 'relative',
        min: 0,
        max: 1,
        step: 0,
        value: 0,
    }

    state = {
        id: null
    }

    componentWillMount = () => {
        this.setState({ id: randomID(10) })
    }

    onChange = value => {
        console.log(`Dial value changed: ${value}`)
        // You could pass in a callback via props to pass value to parent ...
        //  this.props.onChange(value)
    } 

    componentDidMount = () => {
        const dial = new Nexus.Dial(`#${this.state.id}`, {
            size: this.props.size,
            interaction: this.props.interaction, // "radial", "vertical", or "horizontal"
            mode: this.props.mode, // "absolute" or "relative"
            min: this.props.min,
            max: this.props.max,
            step: this.props.step,
            value: this.props.value
        })
        dial.on('change', this.onChange)
    }

    render() {
        return <div id={this.state.id}></div>
    }
}

export default Dial

Usage like so ...

<Dial size={[ 100, 100 ]}/>

And here is a Select Component

import React from 'react'
import Nexus from 'nexusui'
import randomID from 'random-id'
import PropTypes from 'prop-types'

class Select extends React.Component {
    static propTypes = {
        size: PropTypes.arrayOf(PropTypes.number),
        options: PropTypes.arrayOf(PropTypes.string),
        value: PropTypes.number,
        onChange: PropTypes.func,
    }

    static defaultProps = {
        size: [ 100, 30 ],
        value: null,
        options: [],
        onChange: () => {},
    }

    state = {
        id: null,
        selectedIndex: 0,
        selectedValue: null
    }

    componentWillMount = () => {
        this.setState({ id: randomID(10) })
    }

    onChange = ({ value, index }) => {
        this.setState({
            selectedIndex: index,
            selectedValue: value
        })
        this.props.onChange(value, index)
    } 

    componentDidMount = () => {
        const select = new Nexus.Select(`#${this.state.id}`, {
            size: this.props.size,
            options: this.props.options
        })
        select.on('change', this.onChange)
    }

    render() {
        return <div id={this.state.id}></div>
    }
}

export default Select

Usage:

<Select 
    options={[ 'Major', 'Minor', 'Diminished' ]}
    size={[ 100, 30 ]}
    onChange={(value, index) => console.log(`Value ${value} is at index ${index}`)}
/>

If you guys don't mind I would happily spend some weeks creating a react library for Nexus? I'd love to take on that task if nobody else is doing it as I plan to use Nexus in my many Web Audio projects with React.

@taylorbf
Copy link
Contributor

Thank you so much! This is great. Yes a react/Nexus library would be welcome.

Accomplishing this with version 2 should be a lot more straightforward than it was in version 1. Please post issues if you find them.

@JamesTheHacker
Copy link

Working on it @taylorbf :)

@ghost
Copy link

ghost commented May 23, 2018

I am trying to figure out how to use the Nexus.Piano, would it be similar to how this dial is set-up?

@cyrusvahidi
Copy link

I know this is an old issue but it's the first result on Google for "nexus ui react". I am just posting my solution here incase anyone else is wondering how to do this.

You will notice I am generating a random ID. This is because I do not rely on the ID for anything other than generating the dial and is purely personal choice. Everything else will be controlled by props and/or state. This is a rough solution to demonstrate because I felt the solution by @hepiyellow was overly complex.

Here's a Dial Component:

import React from 'react'
import Nexus from 'nexusui'
import randomID from 'random-id'
import PropTypes from 'prop-types'

class Dial extends React.Component {
    static propTypes = {
        size: PropTypes.arrayOf(PropTypes.number),
        interaction: PropTypes.string,
        mode: PropTypes.string,
        min: PropTypes.number,
        max: PropTypes.number,
        step: PropTypes.number,
        value: PropTypes.number,
    }

    static defaultProps = {
        size: [75, 75],
        interaction: 'radial',
        mode: 'relative',
        min: 0,
        max: 1,
        step: 0,
        value: 0,
    }

    state = {
        id: null
    }

    componentWillMount = () => {
        this.setState({ id: randomID(10) })
    }

    onChange = value => {
        console.log(`Dial value changed: ${value}`)
        // You could pass in a callback via props to pass value to parent ...
        //  this.props.onChange(value)
    } 

    componentDidMount = () => {
        const dial = new Nexus.Dial(`#${this.state.id}`, {
            size: this.props.size,
            interaction: this.props.interaction, // "radial", "vertical", or "horizontal"
            mode: this.props.mode, // "absolute" or "relative"
            min: this.props.min,
            max: this.props.max,
            step: this.props.step,
            value: this.props.value
        })
        dial.on('change', this.onChange)
    }

    render() {
        return <div id={this.state.id}></div>
    }
}

export default Dial

Usage like so ...

<Dial size={[ 100, 100 ]}/>

And here is a Select Component

import React from 'react'
import Nexus from 'nexusui'
import randomID from 'random-id'
import PropTypes from 'prop-types'

class Select extends React.Component {
    static propTypes = {
        size: PropTypes.arrayOf(PropTypes.number),
        options: PropTypes.arrayOf(PropTypes.string),
        value: PropTypes.number,
        onChange: PropTypes.func,
    }

    static defaultProps = {
        size: [ 100, 30 ],
        value: null,
        options: [],
        onChange: () => {},
    }

    state = {
        id: null,
        selectedIndex: 0,
        selectedValue: null
    }

    componentWillMount = () => {
        this.setState({ id: randomID(10) })
    }

    onChange = ({ value, index }) => {
        this.setState({
            selectedIndex: index,
            selectedValue: value
        })
        this.props.onChange(value, index)
    } 

    componentDidMount = () => {
        const select = new Nexus.Select(`#${this.state.id}`, {
            size: this.props.size,
            options: this.props.options
        })
        select.on('change', this.onChange)
    }

    render() {
        return <div id={this.state.id}></div>
    }
}

export default Select

Usage:

<Select 
    options={[ 'Major', 'Minor', 'Diminished' ]}
    size={[ 100, 30 ]}
    onChange={(value, index) => console.log(`Value ${value} is at index ${index}`)}
/>

If you guys don't mind I would happily spend some weeks creating a react library for Nexus? I'd love to take on that task if nobody else is doing it as I plan to use Nexus in my many Web Audio projects with React.

I've had a problem using this approach. When passing a callback to a Nexus components on method, the called method will refer to any this references in the Nexus context and not in the context of the Component.

E.g when onChange is called by Nexus it will called on changes to the dial this.props.onChange(value, index) fail because this is the Nexus class.

@rakannimer
Copy link

rakannimer commented Apr 9, 2019

In case anyone here is interested,

I needed to use NexusUI with React, so I wrote this library : react-nexusui

Cheers !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants