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.
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.
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 ā¦
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>
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.