DOMiNATIVE

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
})
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
})
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.
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'
registerElement('RadSideDrawer', RadSideDrawer)
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
captures: boolean
cancelable: boolean
}
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,
__UI_USE_XML_PARSER__: false,
});
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) => {
const transformedName = PascalCaseName.toLowerCase()
return transformedName
}
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')
TextFieldElement.mapProp('value', 'text')
ButtonElement.mapEvent('click', 'tap')
const input = document.createElement('TextField')
input.addEventListener('input', (event) => {
console.log(event.target.value)
})
Then the following code could work:
<TextField v-model="userInput"/>
<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