Nativescript & Formily: A match made in heaven.

This page summarizes the projects mentioned and recommended in the original post on dev.to

Our great sponsors
  • SurveyJS - Open-Source JSON Form Builder to Create Dynamic Forms Right in Your App
  • WorkOS - The modern identity platform for B2B SaaS
  • InfluxDB - Power Real-Time Data Analytics at Scale
  • nativescript-dom-ng

    Minimally viable DOM Document implementation for NativeScript.

    The first hiccup I came across was that Nativescript officially supports Vue2 (there is a way to run Vue3 with {N} using DOMiNATIVE, but thats a different topic) Formily, already caters for this, however it utilises a package called vue-demi.

  • nativescript-formily-example

    Created with StackBlitz ⚡️

    or a . For now this was solved using two polyfills Vue.registerElement('div', () => StackLayout); Vue.registerElement('form', () => StackLayout); Enter fullscreen mode Exit fullscreen mode 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. Your component. In my case Nativescript Vue components. 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 Did I say 3? Here is a simple example of this: 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); Enter fullscreen mode Exit fullscreen mode 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: The label Any descriptions Any feedbacks (error messages etc) 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. :style="wrapperStyle" :class="wrapperClass" :orientation="layout" verticalAlignment="top" > columns="*,40" rows="auto" class="w-full items-center"> :text="`${label}${asterisk && required ? '*' : ''}`" :style="labelStyle" verticalAlignment="center" class="text-lg font-semibold" :class="labelClass" :textWrap="labelWrap" v-if="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" /> rows="auto"> v-if="feedbackText" :text="feedbackText" :class="feedbackClass" /> template> 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> Enter fullscreen mode Exit fullscreen mode 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. StackLayout - Lets you stack children vertically or horizontally 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, has one column and one row. 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 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. I created a Vue component I mounted the view component I called a helper function which summons 🧙🏽‍♂️ the rootLayout and tells it to open this component. This helper function does nothing more than getRootLayout().open(myComponent, ...some overlay settings like color and opacity, ...some animation setting) Enter fullscreen mode Exit fullscreen mode Long story short: This same component is utilised for the DatePicker 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: { 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" }] }, }, }, } Enter fullscreen mode Exit fullscreen mode Here are some pointers on this schema: The key for each nested object like firstName, lastName etc is what final data object will have. x-component indicates which component to use x-decorator indicates as described above the decoration around the input component Some base validations such as required, pattern live as top level. These include minimum , maximum but can also include custom validators including async validations Any of the keys for a field can be written as JSX with {{}}. This gives the possibility to include some logic in the schema. 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. 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: 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

  • SurveyJS

    Open-Source JSON Form Builder to Create Dynamic Forms Right in Your App. With SurveyJS form UI libraries, you can build and style forms in a fully-integrated drag & drop form builder, render them in your JS app, and store form submission data in any backend, inc. PHP, ASP.NET Core, and Node.js.

  • NativeScript

    ⚡ Empowering JavaScript with native platform APIs. ✨ Best of all worlds (TypeScript, Swift, Objective C, Kotlin, Java). Use what you love ❤️ Angular, Capacitor, Ionic, React, Solid, Svelte, Vue with: iOS (UIKit, SwiftUI), Android (View, Jetpack Compose), Dart (Flutter) and you name it compatible.

    Using the amazing Preview environment that the Nativescript 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/)

  • formily

    📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3

    Enter Formily, by Alibaba which does exactly the above. With a JSON schema, forms can be generated whilst keeping control of the data model.

NOTE: The number of mentions on this list indicates mentions on common posts plus user suggested alternatives. Hence, a higher number means a more popular project.

Suggest a related project

Related posts