Nativescript & Formily: A match made in heaven.
Tags: nativescript, ios, android, javascript
Published: 2022-10-21 12:51:19

---
title: Nativescript & Formily: A match made in heaven.
published: true
description: A deep dive into bringing together Nativescript  for mobile development and Formily for form state management.
tags: #nativescript #ios #android #javascript
cover_image: https://i.imgur.com/1hB8byf.png
# Use a ratio of 100:42 for best results.
# published_at: 2022-10-19 20:58 +0000
---


For any one who want to try the demo out immediately before reading the rest of the post. 
1. Open [Stackblitz Nativescript preview](https://stackblitz.com/edit/nativescript-stackblitz-templates-ntkzik)
2. Download the Nativescript preview app: [android](https://preview.nativescript.org/android) [ios](https://preview.nativescript.org/ios)
3. Login, scan the code and enjoy

## Context

Forms can always be a pain, especially if they large in size. Think of a KYC process in a highly regulated industry where there are plenty of steps and several complexities. As developers we code over and over the different elements and take care of all the dependencies. Once the development is done, two months pass, the requirements change, and the poor developers needs to visit back the code base and (hopefully) refactor to adapt to the changes. The bigger the form , the more complex the dependencies become. The more complex the dependencies become, the more refactoring involved. 

Further to that the mobile team needs to keep in sync with the changes as well and this may result as well in further divergences. 

Let's not forget also, the team developing the API layer. They also need to be able to maintain the fields with validations, etc.

What if there was a way in which a `schema` can be shared across all teams? 🤔 A schema which reflects the data, includes validations, can take care of dependencies, can go cross platform, can be shared with the backend devs.

Enter [Formily](https://formilyjs.org/), by Alibaba which does exactly the above. With a JSON schema, forms can be generated whilst keeping control of the data model. 

## Formily: a very brief description

I came across formily a couple of months ago. It takes care of the form state management. 

It does not provide its own library of components but instead it provides **bridges**, with which you can connect either a component library or your own set of components. 

As extra cherries on the cake formily also provides:
1. a drag and drop designer from where a schema can be copied
2. Web devtools extension to be able to debug. 
3. Wrappers for several component libraries (elements UI, ant design, Vant, semi etc)

Since it is component agnostic, this makes a perfect candidate for using it with Nativescript.

## Getting Started

Using the amazing Preview environment that the [Nativescript](https://nativescript.org/) team together with Stackblitz have done, it was time to start hacking at it. (More information can be found here at https://preview.nativescript.org/)

For those coming from web development, Nativescript utilises the V8 engine to interact directly with the Native platforms. This means that based on your mobile development expertise you can:
 
1. Write Javascript as if you don't know the underlying platform. Here you can choose your flavour be it React, Vue Svelte, Vanilla . My choice: definitely Vue 
2. Write Javascript using the Nativescript SDK to access native elements
3. Write Javascript which reflect the Platform SDK and is marshalled to the native language (Java/Kotlin/ObjC/Swift)
For a better explainer to this, have a look at [this Youtube video by Nathan Walker](https://www.youtube.com/watch?v=Mzy1jWxrSiw)

### Setting up
The first hiccup I came across was that Nativescript officially supports Vue2 (there is a way to run Vue3 with {N} using [DOMiNATIVE](https://github.com/SudoMaker/dominative), but thats a different topic)
Formily, already caters for this, however it utilises a package called `vue-demi`.
 
This took a while to sort out and for Stackblitz to work I had to use `npm vue-demi-switch 2 nativescript-vue` which according to the documentation is used to:
a) specify the version 
b) specify the alias for Vue.

The composition API was also installed using
```javascript
import Vue from 'nativescript-vue';
import VueCompositionAPI from '@vue/composition-api';
Vue.use(VueCompositionAPI);
```

Next in the page I followed the setup for formily
```javascript
import { createForm } from "@formily/core";
import { createSchemaField } from "@formily/vue";
```

```javascript
const { SchemaField } = createSchemaField({
  components: {}
})
```

From my understanding, `createForm` takes care of the data aspect and the reactivitiy, whilst `createSchemaField` creates a Vue component which supports a set of **bridged** components. It takes a JSON schema from which the child components would be generated.

A basic JSON schema was copied , the template was given 
```xml
<Form :form="form">
    <SchemaField :schema="schema" />
</Form>
```

....And the app crashed.

Since Formily is a framework still oriented mainly towards the web, it was obvious that at some point there was going to be either a `<div/>` or a `<form/>` . For now this was solved using two polyfills

```javascript
Vue.registerElement('div', () => StackLayout);
Vue.registerElement('form', () => StackLayout);
```

Once that was done, the crashes stopped 🎉.

However, so far there are no visible components.

### Creating Bridges.

So far `SchemaField` has no registered components. So now it is time to build some bridges. 

Formily provides a Vue library exactly for this.
These bridges consist of 3 parts.

1. Your component. In my case Nativescript Vue components.
2. Using the `connect` function from `@formily/vue` to be able to bridge between the format Formily uses and the properties, attributes, events and children your component has. Usually you would also use the `mapProps` function to be able to map between the two sides. One example would be Formily uses `value` but the Nativescript TextField component takes in a prop called `text`, hence we map the props
3. Did I say 3?

Here is a simple example of this:

```javascript
import { connect, mapProps, h } from '@formily/vue';
import { defineComponent } from 'vue-demi';

let input = defineComponent({
  name: 'FormilyTextField',
  props: {},
  setup(customProps: any, { attrs, slots, listeners }) {
    return () => {
      return h(
        'TextField',
        {
          attrs,
          on: listeners,
        },
        slots
      );
    };
  },
});

const Input = connect(input);
```
As can be seen in the above example vue-demi is used to define the component, and the Nativescript `TextField` component is being bridged for Formily. 

Currently I have built the below list of components. (No where near as exhaustive as Formily's wrappers).
Using the Nativescript native components
- TextField - which can be mapped to `Input` `Textarea` `Password` `Number` just by adjusting some props
- Switch
- DatePicker
- TimePicker
- ListPicker - which can be mapped to a `Select` 

To test out JSON schema is created, the components are registered with SchemaField and lo and behold with magic we have a JSON schema form generated! 🎉

### Creating the decorator 🎄
In Formily there is a clear split between what is a component for input and what is decoration. The base component that Formily indicates is the `FormItem` which takes care of:

1. The label
2. Any descriptions
3. Any feedbacks (error messages etc)
4. Any tooltips

Since this does not exist natively in Nativescript an initial one is created once again. 
This time round, the component to be bridged needs to be created.
```vue
<template>
  <StackLayout
    :style="wrapperStyle"
    :class="wrapperClass"
    :orientation="layout"
    verticalAlignment="top"
  >
    <GridLayout columns="*,40" rows="auto" class="w-full items-center">
      <Label
        :text="`${label}${asterisk && required ? '*' : ''}`"
        :style="labelStyle"
        verticalAlignment="center"
        class="text-lg font-semibold"
        :class="labelClass"
        :textWrap="labelWrap"
        v-if="label"
      />
      <Label
        v-if="tooltip"
        @tap="showTooltip"
        col="2"
        class="bg-gray-100 rounded-full w-7 h-7 text-center text-xl"
        text="ℹ"
        horizontalAlignment="right"
      />
    </GridLayout>
    <GridLayout rows="auto">
      <slot></slot>
      <!-- slot outlet -->
    </GridLayout>
    <Label v-if="feedbackText" :text="feedbackText" :class="feedbackClass" />
  </StackLayout>
</template>

<script lang="ts">
import { defineComponent } from "vue-demi";
import Vue from "nativescript-vue";
import BottomSheetView from "~/component/BottomSheet/BottomSheetView.vue";
import { OpenRootLayout } from "~/component/OpenRootLayout";
export default defineComponent({
  name: "FormItem",
  props: {
    required: {
      type: Boolean,
    },
    label: {
      type: String,
    },
    labelStyle: {},
    labelClass: {},
    labelWrap: {
      type: Boolean,
      default: false,
    },
    layout: {
      type: String,
      default: "vertical",
    },
    tooltip: {},
    wrapperStyle: {},
    wrapperClass: {},
    feedbackText: {},
    feedbackStatus: {
      type: String,
      enum: ["error", "success", "warning"],
    }, // error/success/warning
    asterisk: {
      type: Boolean,
    },
    gridSpan: {},
  },
  computed: {
    feedbackClass(): string {
      switch (this.feedbackStatus) {
        case "error":
          return "text-red-400";
        case "success":
          return "text-green-400";
        case "warning":
          return "text-yellow-400";
        default:
          return "text-gray-100";
      }
    },
  },
  methods: {
    showTooltip() {
      let tooltipText = this.tooltip;
      const view = new Vue({
        render: (h) =>
          h(BottomSheetView, { props: { label: "Information" } }, [
            h("Label", {
              attrs: { text: tooltipText, textWrap: true, row: 2 },
              class: "w-full text-lg mb-8 leading-tight",
            }),
          ]),
      }).$mount().nativeView;

      OpenRootLayout(view);
    },
  },
});
</script>
```

Nativescript here provides all the normal Vue functionality that a web developer is used to. With one subtle difference: there are no HTML attributes. However one can easily transfer the knowledge from HTML to Nativescript in this aspect.

1. `StackLayout` - Lets you stack children vertically or horizontally
2. `GridLayout` - is a layout container that lets you arrange its child elements in a table-like manner. The grid consists of rows, columns, and cells. A cell can span one or more rows and one or more columns. It can contain multiple child elements which can span over multiple rows and columns, and even overlap each other. By default, <GridLayout> has one column and one row.
3. `Label` - holds some text 

Style wise as one can easily see Tailwind utility classes are being used.
The props so far expose the base necessary functionality.

The component for now has one single method and that is to generate a tooltip. And on this aspect, we can leverage what another layout container that Nativescript supplies: the Root Layout

### The RootLayout

`<RootLayout> is a layout container designed to be used as the primary root layout container for your app with a built in api to easily control dynamic view layers. It extends a GridLayout so has all the features of a grid but enhanced with additional apis.` Is the definition in that the documentation gives.
In more humble terms, think of this layout in which it can open any component over everything else. This is exceptionally great for Bottom sheets, Modal like components, Side Menu's . Let your imagination go loose here.

To get it working with Nativescript Vue.

1. I created a Vue component
2. I mounted the view component
3. I called a helper function which summons 🧙🏽‍♂️ the rootLayout and tells it to open this component. 
This helper function does nothing more than 
```javascript
getRootLayout().open(myComponent, ...some overlay settings like color and opacity, ...some animation setting)
```

Long story short: This same component is utilised for the DatePicker 
![Tooltip using RootLayout](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ie6dg75e07753l3crdxg.jpeg)
![DatePicker using rootLayout](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g4fkfw7q9ocynjm2vaaz.jpeg)



### Completing the bridge

At this stage we have a function Form generated by a JSON schema. However the data is not yet reflected back properly.

Why?

The reason is simple, Formily expects to receive that information back from the components over a `change` event. 
Digging deep into their Elements UI wrapper, they use vue-demi to transform any component such that the web input functions are mapped to this change event.

One problem.

Vue does not support an `input` event or a `change` event. So, a listener is introduced to the bridges based on the components event (example: textField has `textChanged`). This component specific event, in turn, emits a consolidated event `input` with the value from the component. 
And this immediately gives back full reactivity back to Formily.

### Demo time

Before proceeding to a quick animated gif demo. Here is the demo JSON definition used:

```json
{
        type: "object",
        properties: {
          firstName: {
            type: "string",
            title: "Test2",
            required: true,
            "x-component": "Input",
            "x-component-props": {
              hint: "First Name",
            },
          },
          lastName: {
            type: "string",
            "x-component-props": {
              hint: "Last Name",
            },
            required: true,
            "x-component": "Input",
          },
          username: {
            type: "string",
            title: "Username",
            required: true,
            "x-decorator": "FormItem",
            "x-component": "Input",
            "x-component-props": {
              hint: "@ChuckNorris...",
              disabled: true,
            },
            "x-decorator-props": {
              tooltip: "Lorem ipsum test a tooltip sheet with some text here.",
            },
            "x-reactions": {
              dependencies: ["firstName", "lastName"],
              fulfill: {
                state: {
                  value:
                    "{{$deps[0] ? `@${$deps[0]}${($deps[1] ? '.' + $deps[1] : '')}` : undefined}}",
                },
              },
            },
          },
          password: {
            type: "string",
            title: "Password",
            required: true,
            "x-decorator": "FormItem",
            "x-component": "Password",
            pattern: '/^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{6,16}$/'
          },

          testSwitch: {
            type: "string",
            title: "Rememeber me?",
            required: true,

            "x-decorator": "FormItem",
            "x-component": "Switch",
          },
          from: {
            type: "string",
            title: "Appointment Date",
            required: true,
            "x-decorator": "FormItem",
            "x-component": "DatePicker",
          },
          time: {
            type: "string",
            title: "Appointment Time",
            required: true,
            "x-decorator": "FormItem",
            "x-component": "TimePicker",
          },
          country: {
            type: "string",
            title: "Country",
            required: true,
            "x-decorator": "FormItem",
            "x-component": "Select",
            enum: [{
              label: "🇨🇦 Canada",
              value: "CA"
            },{
              label: "🇬🇧 United Kingdom",
              value: "UK"
            },{
              label: "🇺🇸 United States",
              value: "Us"
            }]
          },
        },
      },
    }
```

Here are some pointers on this schema:

1. The key for each nested object like `firstName`, `lastName` etc is what final data object will have.
2. `x-component` indicates which component to use
3. `x-decorator` indicates as described above the decoration around the input component
4. Some base validations such as `required`, `pattern` live as top level. These include `minimum` , `maximum` but can also include custom validators including async validations
5. Any of the keys for a field can be written as JSX with `{{}}`. This gives the possibility to include some logic in the schema.
6. `x-reactions` found under the username snippet takes care of listening to two dependencies: `firstName` and `lastName` and fulfils the reaction by adjusting the value based on the dependencies. Formily supports two types of reactions
 - Active: Meaning the current active component, changes something in another component
 - Reactive: A component listens to another components changes.
7. Components and decorators can receive additional props using `x-component-props` and `x-decorator-props` respectively. 

And here is a quick screen grab of the app:


![Formily with NS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i4507j34epmeqwkrjcfe.gif)


### Wrapping it up

The full code can be found at the following repository and can be easily tested out using Stackblitz Preview and the Nativescript Preview application

https://github.com/azriel46d/nativescript-formily-example