/dominative

Minimally viable DOM Document implementation for NativeScript.

Primary LanguageJavaScriptMIT LicenseMIT

DOMiNATIVE

NPM

Minimally viable DOM Document implementation for NativeScript


Installation

Via npm:

npm install dominative undom-ng

Note: undom-ng is a peer dependency, you have to install it manually.


Usage

Vanilla

app.js

import { Application } from '@nativescript/core'
import { document } from 'dominative'

const page = document.body
const actionBar = document.createElement('ActionBar')

actionBar.title = 'Hello World!'

page.appendChild(actionBar)

Application.run({
	create: () => document
})

with ef.js

Playground

App.eft

>ActionBar
	#title = Hello World!
	>ActionBarItem
		#text = Button
>StackLayout
	>Label
		.Welcome to the wonderland of ef.native!

app.js

import { Application } from '@nativescript/core'
import { domImpl, document } from 'dominative'
import { setDOMImpl } from 'ef-core'
import App from 'App.eft'

setDOMImpl(domImpl)

const app = new App()
app.$mount({target: document.body})

Application.run({
	create: () => document
})

with SingUI

Playground

app.js

import { Application } from '@nativescript/core'
import { document } from 'dominative'
import { browser, prop, setGlobalCtx, useTags, useElement, build } from 'singui'

setGlobalCtx(browser(document))

const tags = useTags(false)

const app = (target) =>
	build(({attach}) => {
		const { ActionBar, NavigationButton, ActionItem, StackLayout, Label, Button } = tags

		ActionBar(() => {
			prop.title = 'Hello World!'
		})

		StackLayout(() => {
			let count = 0

			const {ret: updateText} = Label(() => {
				return text().$textContent(
					() => `You have tapped ${count} time${count === 1 ? '' : 's'}`
				)
			})

			Button(() => {
				prop.text = 'Tap me!'
				on('tap', () => {
					count += 1
					updateText()
				})
			})

			updateText()
		})

		attach(target)
	})

app(document.body)

Application.run({
	create: () => {
		return document
	},
})

with React + react-dom

Playground - by Ammar Ahmed

Note: This demo might have some issues with Chrome. Use Firefox if necessary.

with Vue 3 + runtime-dom + DOMiNATIVE-Vue

Playground

app.js

import { Application } from '@nativescript/core'
import { createApp } from '@dominative/vue'
import App from './App.vue'

const app = createApp(App)

app.$run()

Playground

app.jsx

import { Application } from "@nativescript/core"
import { render } from "@dominative/solid"
import { createSignal } from "solid-js"

const App = () => {
	const [count, setCount] = createSignal(0)
	const increment = () => {
		setCount(c => c + 1)
	}
	return <>
	<actionbar title="Hello, SolidJS!"></actionbar>
	<stacklayout>
		<label>You have taapped {count()} time(s)</label>
		<button class="-primary" on:tap={increment}>Tap me!</button>
	</stacklayout>
	</>
}

render(App, document.body)

const create = () => document

Application.run({ create })

Prepare global environment

Automatically register document, window and related variables globally:

import { globalRegister } from 'dominative'

globalRegister(global)

Register Elements

import { RadSideDrawer } from 'nativescript-ui-sidedrawer'
import { RadListView } from 'nativescript-ui-listview'
import { registerElement, makeListView } from 'dominative'

// If you cannot determin what the component is based on, you can register it directly.
registerElement('RadSideDrawer', RadSideDrawer)
// Register with a specific type by using a pre-defined maker. Usually we check for inheritance, but with force we can make magic happen
registerElement('RadListView', makeListView(RadListView, {force: true}))

Virtual Elements

Virtual elements are not real elements, but they appear as DOM elements to help organizing composition.

Prop

Helper to put it's child/children to it's parent node's property by key

Attributes:

key: String: RW The prop name to set on parent.

type: <'array'|'key'>: RW Property type, could be an array prop or a single object prop. Once set, this prop couldn't be changed.

value: any: RW Value to be set to parent. Usually children of this current node. Don't touch unless you know what you're doing.

parent: Element: R Parent node of this node.

class: String: RW Helper to set key and type, could be key:type or multi.level.key:type

Events:

None.

KeyProp

Prop but type already set to key.

ArrayProp

Prop but type already set to array.

ItemTemplate

* Template was renamed to ItemTemplate to avoid conflict with HTML template tag.

An ItemTemplate element holds a template to be replicated later, or can create views programmatically.

Attributes:

Share mostly from Prop. Differences are listed below:

key: String: RW Same form Prop, also serves the key name of a KeyedTemplate. Default to itemTemplate.

type: 'single': R Should not be able to set type on a ItemTemplate.

value: Function<T extends ViewBase>: R Same as createView.

content: <T extends ViewBase>: RW The single child of this node. Don't touch unless you know what you're doing.

patch: Function<T extends ViewBase>(PatchOptions): R Method to patch an existing clone.

createView: Function<T extends ViewBase>: R Function to create view from this template.

Events:

itemLoading: Triggered when patching and template has no content. Set event.view to change the view of this item. Additional props on event: view, index, item, data. This event's callback argument doesn't extend from NativeScript's data object.

createView: Triggered when creating view from the template and template has no content. Set created view to event.view. If not set, view will be created by cloning the template. This event's callback argument doesn't extend from NativeScript's data object.

Note:

ItemTemplate element could only have one element child. If you'd like to have multiple children in a template, just use a different type of view or layout as the only child and insert your other contents inside.

KeyedTemplates

By simpling putting ItemTemplates inside an array Prop we could set up a KeyedTemplate.

Example:

<ListView itemTemplateSelector="$item % 2 ? 'odd' : 'even'">
	<Prop key="itemTemplates" type="array">
		<ItemTemplate key="odd">
			<Label text="odd"/>
		</ItemTemplate>
		<ItemTemplate key="even">
			<Label text="even"/>
		</ItemTemplate>
	</Prop>
</ListView>

Template Handling for Custom Components

There's a special maker caller makeTemplateReceiver, which you can use when a NativeScript component accepts templates.

Example:

import { RadListView } from 'nativescript-ui-listview'
import { registerElement, makeTemplateReceiver } from 'dominative'

registerElement('RadListView', makeTemplateReceiver(RadListView, {
	templateProps: ['itemTemplate'],
	loadingEvents: ['itemLoading']
}))

templateProps: Array<String>: Props that accepts a template. Do not write keyed template props.

loadingEvents: Array<String>: Events that will fire on the component when items loading.

itemEvents: Array<String>: Custom events that will fire on the component referencing to an item.


Tweaking

All elements added with registerElement is automatically extended with tweaking ability.

Tweakable.defineEventOption(eventName: string, option: EventOption)

Define how a event should be initialized. If an event is defined with bubbles: true or captures: true, they'll automatically be registered to native at element creation.

Event option:

{
	bubbles: boolean // should this event bubble, default false
	captures: boolean // should this event have capture phase, default false
	cancelable: boolean // should this event be cancelable, defalut true
}

Usage:

const ButtonElement = document.defaultView.Button
ButtonElement.defineEventOption('tap', {
	bubbles: true,
	captures: true
})

Tweakable.mapEvent(fromEvent: string, toEvent: string)

See below

Tweakable.mapProp(fromProp: string, toProp: string)

See below


Tree shaking

Tree shaking is off by default, but if you want a smaller bundle size, you can enable it manually by setting __UI_USE_EXTERNAL_RENDERER__ global variable to true in your project's webpack config. For example:

const { merge } = require('webpack-merge');

module.exports = (env) => {
	webpack.init(env);

	webpack.chainWebpack((config) => {
		config.plugin('DefinePlugin').tap((args) => {
			args[0] = merge(args[0], {
				__UI_USE_EXTERNAL_RENDERER__: true, // Set true to enable tree shaking
				__UI_USE_XML_PARSER__: false, // Usually XML parser isn't needed as well, so disabling it as well
			});

			return args;
		});
	});

	return webpack.resolveConfig();
};

But, PLEASE NOTICE, after tree shaking is enabled, you'll need to register {N} core components manually, otherwise they won't be available as elements. For example:

import { AbsoluteLayout, StackLayout, Label, Button, registerElement } from 'dominative'

registerElement('AbsoluteLayout', AbsoluteLayout)
registerElement('StackLayout', StackLayout)
registerElement('Label', Label)
registerElement('Button', Button)

or you can just register them all with registerAllElements, although it's pointless when tree shaking is enabled:

import { registerAllElements } from 'dominative'

registerAllElements()

Frame, Page and ContentView are registered by default.


Caveats

Hardcoding in Frameworks

Frameworks are great, but they're not great when it comes to hardcoded things. We have to invest methods to counter the harm done by those hardcodings.

Hardcoding is harmful, please do not hardcode.

Always lowercased tag names

Sometimes frameworks are just too thoughtful for you, they translate all your tag names to lowercase when compiling, which means your camelCase or PascalCase tag names won't work as intended.

We can alias our tag names to lowercase if you like:

import { aliasTagName } from 'dominative'

const tagNameConverter = (PascalCaseName) => {
	// ...whatever your transformation code here
	// This is useful when your framework/renderer doesn't support document.createElement with uppercase letters.
	const transformedName = PascalCaseName.toLowerCase()
	return transformedName
}

// Convert all built-in tag names
aliasTagName(tagNameConverter)

Hardcoded events and props

Some frameworks work like magic by providing lots of "just works" features that you don't even need to think about what's going on behind. They're actually done by heavily assuming you're on a specific platform - browser, and hardcoded these features to browser specific features.

We have to mimic the events and props they hardcoded in order to make these frameworks happy:

import { document } from 'dominative'

const TextFieldElement = document.defaultView.TextField
const ButtonElement = document.defaultView.Button

TextFieldElement.mapEvent('input', 'inputChange') // This is redirecting event handler registering for 'input' to 'inputChange'
TextFieldElement.mapProp('value', 'text') // This is redirecting access from 'value' prop to 'text' prop

ButtonElement.mapEvent('click', 'tap') // Redirect 'click' event to 'tap'

const input = document.createElement('TextField')
input.addEventListener('input', (event) => { // This is actually registered to `inputChange`
	console.log(event.target.value) // This is actually accessing `event.target.text`
})

Then the following code could work:

<TextField v-model="userInput"/>
<!-- 'v-model' hardcoded with `input` or `change` event and `value` or `checked` prop, so we have to provide it with a emulated `input` event and `value` prop -->
<button onClick="onTapHandler"></button> // 'onTapHandler' is actually registered to 'tap', since some frameworks hardcoded "possible" event names so they can know it's an event handler

License

MIT