HtmlDialog and React.js

Hi all.

If you are interested in React.js (or any of the other new JS hotness) here is a small demo extension that shows how to use the default app template generated by create-react-app to build an extension based on React and the HtmlDialog new in SU2017.

GitHub repo: GitHub - tbleicher/sketchup-react-demo: An example for the use of React.js with SketchUp's UI::HtmlDialog

Code writeup: SketchUp React Demo App | sketchup-react-demo

Hope this helps with your projects.
Thomas

1 Like

Nice, and clean design. Iā€™ve previously played around with using EmberJS or Angular, but it quickly became bloated (too many files for a simple action).


In your diagram you have setState/handle_action and update_data separate, that means setState is actually not able to receive the response object directly, instead the other method is used for this. Wouldnā€™t it be nice to be able within a single function to call a ā€œremoteā€ method and get the response in-place?

Iā€™ve for long tried to tackle this problem, and since communication is not synchronous, Iā€™ve been favoring (asynchronous) JavaScript Promises. I see in your case, the returned data does not need to be processed individually, because React just fetches from the state object whatever is there and renders whatever changed. In another situation, it could look like this:

sketchupAction('load_materials').then(function (materials) {
    // Process the returned materials and update specific UI elements.
});

What do you think? Can this be useful? Iā€™m definitely going to try out React.

Yes indeed! I was very disappointed when I found out that the onCompleted callback option to the Ruby action callback only returns true or false. It would have been so easy to return the Ruby response object instead and deal with it in a standard JavaScript async callback. As it is, this option is next to useless.

On the plus side, the data flow in React provides a logical point for updates from the Ruby ā€˜backendā€™. In the example I had to mess with componentWillReceiveProps to keep it simple. In a larger app I would introduce Redux which then provides a central `store

In most cases it is desirable to have an intermediate state to provide immediate feedback to the user like an updated status line or a ā€œloading ā€¦ā€ spinner to show that something is happening in the background.

PS: handle_action() is now process_action() in the code. I renamed it after I made the diagram.

[sorry, butterfingers ā€¦]

In a larger app I would introduce Redux (redux.js.org) which then provides a central data store with a dispatch method to do centralised data updates. No mucking about with the component lifecycle functions. Unfortunately, Redux also introduces a number of abstractions that I didnā€™t want to deal with in this example.

In most cases it is desirable to have an intermediate state to provide immediate feedback to the user like an updated status line or a ā€œloading ā€¦ā€ spinner to show that something is happening in the background. So there are two setState calls involved in one user action: one from the UI action (onClick handler) and one from Ruby via update_data() and componentWillReceiveProps(). It would be nice to have a logical connection between action and result in the code but the Ruby API doesnā€™t support this.

You could extend the sketchupAction idea to a function that also maintains a list of active requests in the form of Promises. Then there would be a global sketchupResult function (to replace update_state) that resolves one of these Promises with the data returned from Ruby. Itā€™s doable but I wonder if it would be easier to understand than the data flow in the example.

Itā€™s definitely not in the scope of the example, but for a separate library. What you described is actually what I am doing in my library (ā€œBridgeā€, Bridge.get('callbackName', arg1, arg2ā€¦) ā†’ Promise). I am wondering how it can be integrated with latest frontend technology (of which I donā€™t have a lot of experience), as it seems to me they donā€™t invoke callback actions directly but rely on listening to changes of the data model.

I gave the Promises a thought and came up with the code below. It sets up a new global sketchupAction that handles both the action request from JavaScript and the response from Ruby. For a request it creates and returns a new promise and keeps a reference to the resolve and reject functions. When a response is received from Ruby the resolve function is called with the data in the response.

Requests are assigned an id that is returned with the response. To streamline the update the response data is now kept in a payload object. This payload can be directly used in the setState() function. The typical use with React looks like this:

global
  .sketchupAction({ type: 'LOAD_MATERIALS' })
  .then(response => this.setState(response.payload));

In React setState() is usually the only action you need to update the interface. As such promises seem to be a bit of an overkill. If you need to perform other actions with the received data, this could be a solution.

The promise handling is shown below. The complete code for the app is in the promises branch in the GitHub repo.

function makeSketchupAction() {
  const promises = {};

  return function(action) {
    if (!action.id) {
      // if action doesn't have an id it's a request from the app
      const id = uuidv4();
      console.log(`creating promise for action ${action.type}`);
      const promise = new Promise((resolve, reject) => {
        promises[id] = [resolve, reject];
      });
      action.id = id;
      // make action request to Ruby
      sketchupActionRequest(action);
      return promise;

    } else if (promises[action.id]) {
      // if id is present it's a response from Ruby
      console.log(`received response for action ${action.type}`);
      const resolve = promises[action.id][0];
      delete promises[action.id];
      resolve(action);

    } else {
      // id is not listed in our promises; we would be stuck with
      // this but luckily we have a global update_data function
      global.update_data({
        status: 'ERROR',
        error: 'no matching resolver found for action result'
      });
    }
  };
}
global.sketchupAction = makeSketchupAction();


function sketchupActionRequest(action) {
  console.log('action:', JSON.stringify(action, null, 2));
  try {
    sketchup.su_action(action);
  } catch (e) {
    // ignore 'sketchup is not defined' in development
    // but report other errors
    if (!e instanceof ReferenceError) {
      console.error(e);
    } else {
      const data = browser_action(action);
      setTimeout(() => global.sketchupAction(data), 50);
    }
  }
}

Interesting, I think I can learn a bit from how you would implement it (as I said, I have already a library though not yet published separately). My approach is slightly more complex (and instead of response/payload objects, I use a variable number of arguments).

Until know Iā€™ve tried to stay with pure JavaScript and backward-compatibility to WebDialog/IE. You develop with latest features (ES6) and ā€œbuildā€ the browser version of your code?

Yes, the configuration created by create-react-app includes Babel to translate ES6 syntax. Chromium supports quite a lot on its own, though. I tried to use plain React in a WebDialog window early this year (before I knew about HtmlDialog) but could only get it to work on a Mac. I didnā€™t have the patience to dig into the issues on Windows. Perhaps it was just a missing html5shim ā€¦

Oh nice. I need to look closer into this.

Iā€™m actually in the middle of creating some examples/tutorials on HtmlDialog. I was using Vue in my examples.

Hi!!! Can U share one of these examples? Iā€™ve a problem with loading .js files inside html.

I need to add jQuery lib to my dialog, but I noticed that the browser do not load the file. The only way I can use jQuery is pasting the .js content inside a script tag. I am missing someting (on ruby or html side)?

Bellow is the beginning of my html file:

< html>< head>
< meta charset="UTF-8">
< meta http-equiv="X-UA-Compatible" content="IE=edge"/>
< title>___< /title>
< script type="text/javascript" src="jquery-3.2.1.min.js"></script> < !-- ------------- THIS WAY IT WORKS -------------------->
< script type="text/javascript">
< !--
	/* ------------- THIS WAY IT WORKS ------------------*/
	/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */
	!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?...b),b&&a.jQuery===r&&(a.jQuery=Vb),r},b||(a.jQuery=a.$=r),r});...----------REST OF THE CODE DELETED!!!---------

	function addLayer(lName) {
		$('#tbllayers tbody').append($('<tr/>', {text: lName}));
	}
	
	function unload() {
		sketchup.sendCstAction('closetool');
	}
-->
< /script>
< /head>< body>

ā€¦

Hi

You can find the code for the whole extension on GitHub:

I expect that you need to set the HTML base tag in the header. This needs to be set dynamically because SketchUp creates a temporary directory for the content it loads into the dialog. In my code I have a placeholder string that getā€™s replaced by Ruby when the dialog is displayed.

1 Like

We just uploaded examples of UI::HtmlDialog and Vue.js which we presented at DevCamp 2017 in Leeds earlier this year.

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.