Skip to content

Instantly share code, notes, and snippets.

@shirakaba
Created January 1, 2022 17:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shirakaba/e6c1eed5bb261252b55a4928bf3f751e to your computer and use it in GitHub Desktop.
Save shirakaba/e6c1eed5bb261252b55a4928bf3f751e to your computer and use it in GitHub Desktop.
The many UI renderers of NativeScript

The many UI Renderers of NativeScript

Introduction

In this article, we'll learn how NativeScript was adapted into so many different flavours (Angular, Vue, React, Svelte, and more). We'll cover what makes it such a good target for adapting, but also lift the curtain on some of the technical challenges involved in the process. And through the article, you should gain a sense of what's involved in creating a new flavour for NativeScript (or indeed any other host!).

Background

NativeScript's cross-platform UI modules (broadly everything under the ui directory in the @nativescript/core npm package) can be described as a "UI runtime". In other words, a library that allows you to manipulate the user interface for your app while it is running.

The APIs from this library are written in an imperative style, just like the DOM APIs in a browser environment (e.g. document.createElement("div")). While imperative APIs provide a great foundation for building libraries upon, it's cumbersome to build a full app just by using them directly. As NativeScript is a Web-inspired framework, it has followed the trend in web development to prefer a declarative style for writing apps, popularised by libraries such as React, which can be described as a "UI renderer".

What is a UI renderer, exactly?

A UI renderer is something that interprets app state (e.g. a HTML file that describes the UI hierarchy, styling, and logic) and outputs a corresponding UI (e.g. a visual web page), updating it continuously as the state changes.

Which UI renderers can be used with NativeScript?

NativeScript can now be rendered by all the mainstream UI renderers used in web development. Angular support was implemented officially by the NativeScript Core team; and Vue, Svelte, and React flavours were implemented by community members (official support has since been extended to Vue as well).

There are also other renderers under construction, for Preact, Glimmer, Crank.js, and maybe more. Do get in touch if you're working on one!

Why are there so many UI renderers for NativeScript?

NativeScript is simply a nice target for adapting web UI renderers to. It's a JS framework to begin with; it implements certain web idioms such as CSS, Webpack bundling, Chrome debugger support, and a fair amount of the browser APIs; and a lot of logic can be re-used from efforts to adapt it to other renderers.

How to adapt a UI renderer for NativeScript

There are essentially two possible approaches, determined by the available APIs of the UI renderer.

Custom renderer API

A "custom renderer API" is when the renderer defines which APIs it expects to be implemented. You can expect it to involve all the basic UI operations such as appending child nodes, removing child nodes, mutating attributes, etc. Once you've implemented all of those for your host (e.g. NativeScript), you're done!

Let's take stock of which major web UI renderers support a custom renderer API.

React has long exposed a custom renderers API (probably since separating from React DOM), and so React NativeScript makes use of it. A great tutorial on how to use the API was written by Atul R through reverse-engineering efforts, before Sophie Alpert from the React team came along and gave this excellent presentation on how to use it; API docs finally followed afterwards.

Angular has also long exposed a custom renderer API, with Renderer2 appearing from v4 onwards, which NativeScript Angular is based on today (up until 2017 it was based on the older Renderer API). A blog post by Victor Savkin exploring the API can be found here.

Vue.js only exposes a custom renderer API from v3 onwards (see @vue/runtime-core). The current stable version of NativeScript Vue (v2) is based on Vue.js v2, so it doesn't benefit from this (we'll see in the next section how it surmounts this) – but do look out for the upcoming NativeScript Vue v3, which, having moved to Vue.js v3 has adopted the custom renderer API! The API is explored in some blog posts by Lachlan Miller and by DigitalOcean.

Trick the existing renderer into rendering your host

When no custom renderer API is available, you can instead create an interface layer for your host (e.g. NativeScript) to present it as if it were the host expected by the renderer (e.g. DOM). Essentially, this involves implementing the DOM standard for your host bit-by-bit until the renderer stops crashing and starts doing useful work when faced with each of your use-cases. As renderers don't use all of the DOM standard, implementing most of just Document, Node, and Element gets you most of the way, in fact!

Svelte is one such renderer that doesn't yet implement a custom renderers API. There are a couple of issues requesting one (#136 and #5496), but for now, the path of least resistance is to create a DOM interface for it.

This approach was taken by both Svelte Native and NativeScript Vue v2 (links point to their respective implementations of the DOM). In fact, Svelte Native adapts the very same DOM implementation created for NativeScript Vue.

Even when a custom renderer API is available, though, DOM is a very nice UI abstraction to begin with, and plugs in to such an API very nicely – so from v1 onwards, React NativeScript came to implement a DOM interface as well, based on that of NativeScript Vue v3.

In theory, all flavours of NativeScript could share a common DOM-compatible interface layer to prevent re-inventing the wheel, but there are some challenges in mapping NativeScript to DOM, which some renderers handle differently.

Challenges in mapping NativeScript to a DOM API

Although NativeScript has some good features for adapting UI renderers, there are still some challenges. Most of these revolve around frictions in mapping it to a DOM API. They are by no means unique to NativeScript, however – most of what is said here holds equally true for adapting frameworks like Qt, UIKit, or Android widgets, to a UI renderer based on DOM.

Appending and removing child nodes

In NativeScript, there is little consistency in the APIs for adding/removing child nodes. The most straightforward case is the LayoutBase elements, as they expose insertChild() and removeChild() APIs which match up well with the Node APIs in the DOM standard. However, there are also ContentView elements, which support only one child node (via setting the content property); various elements that accept no child nodes at all (e.g. TextBase); and some which accept only certain child nodes, and may handle certain child nodes in different ways – these elements have to be handled on a case-by-case basis, as they each have a bespoke API.

This last case is illustrated well by the RadSideDrawer UI component, which accepts two possible child nodes: the "main content" (set using the mainContent setter) and the "drawer content" (set using the drawerContent setter). This is quite unlike any parent-child relationship seen in the HTML standard, where all child nodes are treated equally. So if you did append a child to RadSideDrawer, how could the renderer know to assign it as the mainContent or the drawerContent?

We can solve some such cases by using instanceof checks to determine that a certain child View could only logically go in a certain place. For example, when appending a child View to ActionBar, if it's an instance of NavigationButton, then it's likely to be set via actionBar.navigationButton = child; while if it's an instance of ActionItem, then it's likely to be added via actionBar.actionItems.push(child). This doesn't handle all cases, though, so each renderer still needs a way to declare child roles explicitly. So one way or another, we need to pass a hint to the renderer to tell it how to handle the child node.

Property elements

In NativeScript Core, this was originally solved by specifying the property names within the element tags in the XML markup. Svelte Native implemented the same functionality, calling it the property element approach:

<!-- NativeScript Core -->
<nsDrawer:RadSideDrawer>
  <nsDrawer:RadSideDrawer.drawerContent>
    <StackLayout></StackLayout>
  </nsDrawer:RadSideDrawer.drawerContent>

  <nsDrawer:RadSideDrawer.mainContent>
    <StackLayout></StackLayout>
  </nsDrawer:RadSideDrawer.mainContent>
</nsDrawer:RadSideDrawer>

<!-- Svelte Native -->
<radSideDrawer>
    <radSideDrawer.drawerContent>
        <stackLayout />
    </radSideDrawer.drawerContent>
    <radSideDrawer.mainContent>
        <stackLayout />
    </radSideDrawer.mainContent>
</radSideDrawer>

Property directives

A 'property directive' approach is exposed by Svelte Native (in addition to the 'property element' approach), NativeScript Vue, NativeScript Angular, and React NativeScript:

<!-- Svelte Native -->
<radSideDrawer>
    <stackLayout prop:drawerContent />
    <stackLayout prop:mainContent/>
</radSideDrawer>

<!-- NativeScript Vue -->
<RadSideDrawer>
  <StackLayout ~drawerContent />
  <StackLayout ~mainContent />
</RadSideDrawer>

<!-- NativeScript Angular -->
<RadSideDrawer>
    <StackLayout tkDrawerContent></StackLayout>
    <StackLayout tkMainContent></StackLayout>
</RadSideDrawer>

<!-- React NativeScript -->
<radSideDrawer>
  <stackLayout nodeRole="mainContent"></stackLayout>
  <stackLayout nodeRole="drawerContent"></stackLayout>
</radSideDrawer>

Examples

A full example of how all the various node operations are handled in React NativeScript can be seen here.

NativeScript's UI is not structured as a single tree

In NativeScript (and indeed in the iOS and Android UI frameworks it abstracts), the UI can be composed of several trees. For example, the ListView element is composed of several cells, each one being its own independent UI tree with no parent views and therefore having no relation to the app root.

To manage multiple detached UI hierarchies (React calls these Portals) declaratively, each of the NativeScript renderers uses one hack or another to either force an immediate render create a sort of empty containing element that can forward a ref to the underlying element.

Svelte Native

This is used in NativeScript Vue

Key examples of this are in the app's root view, in navigation hierarchies such as Frame > Page, and in ListView. In each of these cases, NativeScript needs to set a root view

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment