This issue is a spinoff of issue #11. @dylans wrote there: "I will say that many of the virtual DOM APIs feel like they're going to turn code into long chains of imperative code make me think there must be a better way, as that style of coding discourages reuse." On "long chains of imperative code", some examples would be nice so I'm sure I know what Dylan mean. The rest of this assumes he means the imperative nature of the HyperScript-ish API. That may not be what he meant, but as I like talking about the benefits of the HyperScript API, it gives me a good excuse to talk about that. :-)
As I said there, based on an enjoyable experience with Mithril, I feel it is almost certain that an imperative HyperScript-like API is both a good choice for a modular interface to whatever vdom implementation is underneath that HyperScript layer and a good choice to support whatever Dojo2/Dijit2 abstractions are built above that HyperScript layer.
Advocating for a HyperScript API layer specifically is moving beyond the vdom pro/con issue towards a Dojo2/Dijit2 widget API, and so I created a new issue for it. Still, as HyperScript relates to the feasibility of using a vdom, it does have some bearing on deciding whether to proceed with a vdom approach. The rest of this tries to make a case that Dojo2 widgets should be written on top of a HyperScript-like API such as used by Mithril and Mercury to define their vdoms.
To clarify what this issue is not intended to be about:
- Whether Dojo2 should have its own HyperScript-ish layer or instead use an existing one is mostly a separate issue. Probably I think Dojo2 may end up with its own implementation, but I am not sure.
- The choice of a HyperScript-ish API is also a separate issue from deciding whether to use an existing vdom or writing a new one, since the same HyperScript-ish API could in theory target more than one vdom. A HyperScript layer could also be written for a preferred vdom that does not have one already, so using a HyperScript layer is not limiting in that sense.
- Whether users should expect to muck around directly with the vdom representation which a HyperScript layer produces (as with my "subtractive" kludge mentioned later) is again a different issue from whether there is a HyperScript-ish API easily available to Dojo2 developers.
- Whether Dojo2 should support templates of some sort is a different issue. I touched on that in issue #11, explaining why I feel ad-hoc HTML-ish templates are not a great idea for sophisticated webapps, and I know that may be still a controversial position, but since you can translate templates into either HyperScript API calls or directly to vdom structures, supporting a HyperScript API is not limiting in that sense of precluding templates.
== A motivating example
As an example of such a (hypothetical) HyperScript-like API in use for Dojo2, here is some hypothetical code to display a greeting and a timestamp, and to support updating the timestamp either via a button click or via some hypothetical TimeStampEditor widget (perhaps just a button that puts up a prompt or perhaps something fancier with a date picker and a clock face). Assumed below is that when the onclick function is set, it is automatically wrapped by another function that queues a redraw (similar to what Mithril does). Also, while this example uses mostly lower level HTML definition, I'd expect most Dojo2 applications are going to be composing their GUIs mostly with Dijit2 widgets of various sorts (including with i18n support and a11y support and so on).
import h = require("dijit2-vdom");
import TimeStampEditor = require("TimeStampEditor");
// Define a model as a plain JavaScript object (with no support for dependencies)
var model = {
lastTimestamp: null
};
function updateTimestamp() {
model.lastTimeStamp = new Date().toISOString();
}
// Create an editor component we will use later
var editor = new TimeStampEditor(model, "lastTimestamp");
// Return a vdom structure reflecting the current model and able to change that model
function render() {
return h("div#greeting", [
h("span.salutation","Hello!"),
h("hr"),
"Time: ",
model.lastTimestamp,
h("button", {onclick: updateTimeStamp}, "Update time"),
h("br"),
"Or edit it here:",
h("br"),
h.component(editor)
]);
}
updateTimestamp();
// Render a vdom into the DOM and get ready to rerender as needed on redraw requests
h.mount("someExistingDivID", render);
I wanted to use "d" for Dojo as in my example in the previous issue. However, "h" may be more practical given existing tools that can convert HTML to HyperScript using "h".
This example differs from Mithril in that the component construction process is more explicit that the one Mithril uses -- which otherwise involves a behind-the-scenes construction process. The Mithril approach to components has pros and cons. One benefit of the Mithril approach is with dynamic GUIs where components might come and go a lot and need to otherwise be tracked somehow by the developer. In general, choosing a good way to construct and track components is a challenge here, so the best way to do components is an open question to think through-- and the Mithril approach may still be best in practice, or might be built on in some other direction.
== The case for building Dojo2 widgets on a HyperScript-like API
Obviously, there are multiple programming paradigms (including Imperative, Procedural, Declarative, Functional, Object-oriented, Event-driven, and Automata-based). On typical current hardware, all running code is ultimately imperative code defined by machine language stored in sequential bytes in memory. So, the issue is what abstractions we choose put on top of that imperative base for what purposes (including subjective aesthetics).
One advantage of a low-level imperative base of using HyperScript to define vdoms via function calls is that you can then build whatever abstractions you want on top of that in a reasonably efficient way. Otherwise, you may end up trying to map whatever abstraction you want to use today onto, say, someone else's OO or functional model chosen yesterday which has its own set of assumptions, with a result that may be slow and hard to debug due to extra unneeded layers.
Working with a HyperScript-ish API feels very close to just specifying all the HTML DOM nodes yourself. Any webapp programmer is going to have to get comfortable with the DOM sooner or later, so why not sooner? Then, such an informed programmer will probably eventually want to turn towards abstractions over that layer to save time or avoid repetition to hopefully reduce maintenance costs.
In the simple example I provided of using a "h" function to compose a GUI in an imperative way, that's just the base. A single-page webapp I wrote with about forty virtual pages (first using Dijit and then converted to Mithril's vdom approach) uses a declarative approach to define most pages using JSON-ish structures that are then converted via a "builder pattern" to more imperative vdom construction functions (Mithril's API), where Mithril in turn then does more work on rendering to initialize and assemble components and translate them to DOM nodes. However, I could (in theory) have somehow used, say, a constraint resolving engine like Cassowary to go from specifications to Mithril m function calls. Or a trained neural network or whatever. Also, the application-specific widgets themselves were functions that usually created objects which then used the imperative Mithril functions to compose parts of UIs (including sometimes using other widgets instead of simple DOM objects).
So, in practice, I don't feel an imperative-ish vdom API base is limiting. It is even freeing in that you can build whatever you want on top of it. Granted, it may be sometimes useful or even more computationally efficient in some sense to work below that API, an example of which is below. But the imperative API by itself does not prevent you from creating good abstractions for using it towards a goal of reusable code.
Obviously, a good framework is going to provide opinionated tools for using that imperative base effectively and quickly to build data-rich web applications. That's where Dojo2/Dijit2 could shine best in my current opinion -- in innovative leveraging of a HyperScript-ish API above a vdom to support reuse at that higher level of abstraction (while also not preventing lower-level work at the HyperScript level or below when needed for some reason).
For example, "standardWidgets.ts" is some code I wrote for that webapp which creates some standard widgets for that builder like checkboxes, textareas, radio buttons, and so on using Mithril in a way where all those "widgets" pass W3C validation for basic accessibility (with labels and "for" attributes). That code could be clearer and more modular, so I'm not holding it up as something to emulate in that way. The point is that it shows how you can take the low level imperative approach of using a HyperScript-ish API and use that low-level API however you want within JavaScript/TypeScript -- in this case, driven by more abstract GUI specifications. If I someday had time to add ARIA support to improve accessibility beyond standard accessible HTML, then I could make some changes to that code (and elsewhere) to add the right ARIA attributes. Or I could replace most of that code with calls to a library that used a HyperScript-ish API to define ARIA-compliant labelled widgets.
In the case of Mithril, the vdom representation is essentially a relatively straighforward JSON-like object (though including functions). So, you can even bypass those imperative HyperScript-like function calls to some extent if you really want to, because they are just returning nested JavaScript data structures made mostly of basic JavaScript objects. Although then you are linking your code to a specific underlying vdom representation if you do that.
For example, as a kludge I did just that in the standardWigets.ts file mentioned above where it creates sets of checkboxes and radio buttons. I call delete questionLabel[0].attrs["for"];
to remove an unneeded "for" attribute generated into the vdom representation elsewhere by the panel builder system as a default which is appropriate most of the time given otherwise there is always one label to go with each input widget. That kludge unfortunately binds that code tightly to the Mithril vdom representation. It works, but "subtraction" is problematical, especially when it violates some abstraction boundary as it does in this case. So, ideally, I should refactor that entire build process so the "for" attribute is never added in those cases -- maybe someday. My main point is that you can get in there and bypass a HyperScript API and muck about with underlying representations the API constructs if you really want to -- once you have committed to a specific vdom. Or, at least, committed to a set of vdoms that use a common internal representation if you want your code to be usable for more than one vdom. Still, doing so even for just one vdom may be problematical if the vdom representation were to change -- although that is probably unlikely for any specific mature vdom library as a likely breaking change for many users who would have done this sort of thing.
Ultimately, building a complex webapp for a browser requires using JavaScript to create and configure trees of DOM nodes. There has to be some imperative layer in a webapp or supporting libraries that does that work (even if just ad-hoc internal APIs calling DOM functions). Typically, with a vdom approach, this DOM manipulation is isolated to some rendering function that does a diff from some new vdom structure you assembled somehow relative to the last one you supplied to decide how to change the DOM. The new vdom structure can be assembled via HyperScript-ish function calls. Or it can be assembled from interpreting some template or specification either indirectly into HyperScript calls or directly into assembling the vdom representation. The only question might be, do you try to completely hide that vdom construction layer for some reason in Dojo2? I'd advocate that a HyperScript-ish API layer should not be hidden, and instead such an API should be celebrated as an opinionated choice to support adaptation and extension of Dojo2/Dijit2 in unplanned ways -- similar to the approach Mithril, Mercury, and some other vdom systems take regarding that.
Now, it may be tempting to say, Dojo2 could construct vdom structures for Dijit2 widgets somehow better than via an imperative HyperScript API. Maybe it will someday. Dylan is right to question that imperative style and ask if there could be something better. But you generally also have to walk before you can run. Right now, Dojo2 does not have any released vdom Dijit2 widgets. A HyperScript API still provides a solid and proven-successful place to start in making Dojo2 widgets, even if down the road better approaches might be possible or even required for special cases including optimization for vdom construction or diffing.
But having better vdoms or using vdoms in better ways is orthogonal to having good vdom-based applications right now. The HyperScript API (coupled with some Mithril-like support code) provides a way to sidestep all that vdom experimentation, to start building applications now which can use what is out there. Such code can likely benefit from further vdom improvements later with hopefully relatively minor changes to most code if it works at the HyperScript API level.
Reuse is obviously desirable as Dylan mentioned in his comment in the other issue. However, reuse is also difficult given you need multiple examples to figure out how to design reusable stuff. You almost always also need to make assumptions that limit reuse in some direction. You also typically start wrestling with tradeoffs of adding complexity to be general (for related humor, see "Why I Hate Frameworks") versus writing simpler code to be faster and more understandable. You need to make such a tradeoff unless you get lucky with some new inventive idea to avoid a tradeoff, which is rare -- but I feel Leo Horie is onto that sort of invention with Mithril. Compared to what I've seen and heard about many other frameworks, Mithril just feels like an elegant and effective way to make webapps (although not without some warts like related to component initialization complexities).
That process of making such design decisions is of course a deep and perhaps endless discussion (perhaps as a design equivalent of Gรถdel's incompleteness theorems). There may well be other great ideas out there for vdom-based webapps which are much better than a HyperScript API used in a Mithril-ish way, and which I do not know of (and I welcome hearing about them). But what I do know is that by adopting the proven base of a HyperScript-like API for composing UIs in JavaScript/TypeScript using a vdom approach similar to Mithril, we empower Dojo2 developers to start having those sorts of deep discussions about reuse in the context of working Dijit2-based code, which may prompt further insights into better abstractions from practical experience.