Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# NativeScript SolidJS

### **Custom render and patches for SolidJS to work with [DOMiNATIVE](https://github.com/SudoMaker/DOMiNATIVE) on [NativeScript](https://nativescript.org/)**

[Playground](https://stackblitz.com/edit/nativescript-dominative-solid?file=app/app.jsx)
[Try on StackBlitz](https://stackblitz.com/edit/nativescript-dominative-solid?file=app/app.jsx)

---

Expand All @@ -11,10 +9,33 @@
Via npm:

```shell
npm install @nativescript-community/solid-js dominative undom-ng solid-js @babel/preset-typescript babel-preset-solid
npm install @nativescript-community/solid-js dominative undom-ng solid-js @solidjs/universal @babel/preset-typescript babel-preset-solid
```

**Note:** `dominative`, `undom-ng`, `solid-js`, `@solidjs/universal` are peer dependencies, you have to install them manually. As the benefit for using peer dependencies, you'll be able to upgrade these dependencies directly from upstream, no need to wait for an update with `@nativescript-community/solid-js`

| `@nativescript-community/solid-js` | `solid-js` |
| ---------------------------------- | ----------- |
| `2.x` | `2.x` |
| `0.1.x` | `1.8`–`1.9` |

---

## Build setup

The JSX compiler has to emit calls against this renderer rather than the DOM one, so whichever bundler you use needs these compiler options:

```js
{
generate: 'universal',
hydratable: false,
moduleName: '@nativescript-community/solid-js',
}
```

**Note:** `dominative`, `undom-ng`, `solid-js` are peer dependencies, you have to install them manually. As the benefit for using peer dependencies, you'll be able to upgrade these dependencies directly from upstream, no need to wait for an update with `@nativescript-community/solid-js`
With webpack, `nativescript.webpack.js` in this package wires that up (plus the `@NativeClass` transform) — register it from your project's `webpack.config.js`. With Vite, pass them to `@solidjs/vite-plugin` as its `solid` option.

Either way, pin `solid-js`, `@solidjs/signals`, and `@solidjs/universal` to their browser bundles. Solid 2's exports map resolves the `node` condition to the SSR build, which renders once and never updates.

---

Expand Down Expand Up @@ -52,6 +73,20 @@ Application.run({ create })

---

## Upgrading from Solid 1.x

Solid 2 is a rewrite; [its cheatsheet](https://github.com/solidjs/solid/blob/main/packages/solid/CHEATSHEET.md) and [migration guide](https://github.com/solidjs/solid/blob/main/documentation/solid-2.0/MIGRATION.md) cover the reactive changes (two-argument `createEffect`, draft-first store setters, deferred reads until flush). What changes for a NativeScript app specifically:

- `solid-js/store` merged into `solid-js`, and `solid-js/universal` moved to `@solidjs/universal` — install it alongside `solid-js`.
- Control flow re-exported from this package is now `For`, `Repeat`, `Show`, `Switch`, `Match`, `Loading`, `Reveal`, `Errored`. `Suspense` → `Loading`, `SuspenseList` → `Reveal`, `ErrorBoundary` → `Errored`, and `<Index>` → `<For keyed={false}>`.
- `onMount` is gone; use `onSettled`, which takes the teardown as its return value.
- `use:` directives are gone (this package no longer exports `use`). A directive is now a ref: `ref={myDirective(options)}`, and refs compose as arrays.
- `<For>` callback arguments follow the keying mode. The default is keyed by identity, giving `(item, indexAccessor)`; `keyed={false}` gives `(itemAccessor, index)`.
- Event handlers keep the `on:` / `oncapture:` prefixes described below. Solid 2's DOM renderer dropped them, but NativeScript event names are case sensitive and don't bubble, so they stay the binding for this renderer.
- `solid-refresh` has no Solid 2 release that matches this range, so the webpack config no longer registers it. NativeScript's own HMR still replaces modules.

---

## Caveats

### Event handling
Expand Down
35 changes: 24 additions & 11 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@ export type AnyNode = any;
/** Mount a Solid tree into a NativeScript host element. Returns a disposer. */
export function render(code: () => any, element: AnyNode): () => void;

/** Create a reactive effect. */
export function effect<T>(fn: (prev?: T) => T, init?: T): void;
/** Create a reactive effect. The compute phase tracks; the apply phase does not. */
export function effect<T>(
fn: (prev?: T) => T,
effect: (value: T, prev?: T) => void
): void;

/** Create a memoized computation. */
export function memo<T>(fn: () => T, equal?: boolean): () => T;

/** Create a component instance. */
export function createComponent<P>(Comp: (props: P) => any, props: P): any;

/** Create an element node by tag name. */
export function createElement(tagName: string): AnyNode;
/** Create an element node by tag name, applying any compile-time static props. */
export function createElement(
tagName: string,
staticProps?: Record<string, any>
): AnyNode;

/** Create a text node. */
export function createTextNode(value: string): AnyNode;
Expand All @@ -33,20 +39,27 @@ export function insert(
export function spread(
node: AnyNode,
props: Record<string, any>,
isSVG?: boolean,
skipChildren?: boolean
): AnyNode;

/** Set a single prop on a node. */
export function setProp(node: AnyNode, name: string, value: any, prev?: any): void;

/** Merge props objects similarly to Solid's mergeProps. */
/** Merge props objects similarly to Solid's merge. */
export function mergeProps<T extends object, U extends object>(
...sources: Array<Partial<T> | Partial<U> | undefined>
): T & U;

/** Optional helper to call a function if provided, returning its result. */
export function use<TArgs, TReturn>(
fn: ((args: TArgs) => TReturn) | undefined,
args: TArgs
): TReturn | undefined;
/** Apply a ref (or array of refs) to a node immediately. */
export function applyRef(
r: ((element: AnyNode) => void) | ((element: AnyNode) => void)[],
element: AnyNode
): void;

/** Resolve a ref accessor untracked and apply it to a node. */
export function ref(
fn: () => ((element: AnyNode) => void) | ((element: AnyNode) => void)[],
element: AnyNode
): void;

export { For, Repeat, Show, Switch, Match, Loading, Reveal, Errored } from 'solid-js';
57 changes: 32 additions & 25 deletions nativescript.webpack.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,37 @@ const {
getPlatformName,
} = require('@nativescript/webpack/dist/helpers/platform');

// Solid 2 publishes an exports map whose `node` condition points at the SSR
// build. Resolving through it leaves the app with a renderer that never applies
// updates, so every entry point is pinned to an explicit browser bundle. `$`
// keeps each alias exact so subpaths still resolve normally.
const solidAliases = (production) => {
const solidDist = path.join(
path.dirname(require.resolve('solid-js/package.json')),
'dist'
);
const signalsDist = path.join(
path.dirname(require.resolve('@solidjs/signals/package.json')),
'dist'
);
const universalDist = path.dirname(require.resolve('@solidjs/universal'));

return {
'solid-js$': path.join(solidDist, production ? 'solid.js' : 'dev.js'),
'@solidjs/signals$': path.join(
signalsDist,
production ? 'prod/index.js' : 'dev.js'
),
'@solidjs/universal$': path.join(
universalDist,
production ? 'universal.js' : 'dev.js'
),
};
};

const solid = (config, env) => {
const platform = getPlatformName();

const solidPath = path.resolve(require.resolve("solid-js"), "../..");

config.resolve.extensions
.prepend('.js')
.prepend('.ts')
Expand All @@ -18,23 +44,10 @@ const solid = (config, env) => {
.prepend(`.${platform}.jsx`)
.prepend(`.${platform}.tsx`);

config.resolve.alias
.set(
'solid-js/universal',
path.resolve(solidPath, `universal/dist/${env.production ? 'universal' : 'dev'}.js`)
)
.set(
'solid-js/store',
path.resolve(solidPath, `store/dist/${env.production ? 'store' : 'dev'}.js`)
)
.set(
'solid-js',
path.resolve(solidPath, `dist/${env.production ? 'solid' : 'dev'}.js`)
)
.set(
'solid-js/web',
path.resolve(solidPath, `dist/${env.production ? 'web' : 'dev'}.js`)
);
const aliases = solidAliases(env.production);
for (const [key, value] of Object.entries(aliases)) {
config.resolve.alias.set(key, value);
}

config.module
.rule('bundle-source')
Expand Down Expand Up @@ -62,11 +75,6 @@ const solid = (config, env) => {
['@babel/plugin-proposal-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties', { loose: true }]
],
env: {
development: {
plugins: [['solid-refresh/babel', { bundler: 'webpack5' }]],
},
},
});

if (!env.production) {
Expand All @@ -77,4 +85,3 @@ const solid = (config, env) => {
};

module.exports = webpack => webpack.chainWebpack(solid);

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nativescript-community/solid-js",
"version": "0.1.2",
"version": "2.0.0-rc.0",
"description": "SolidJS to work with NativeScript",
"main": "src/index.js",
"types": "index.d.ts",
Expand All @@ -25,9 +25,10 @@
},
"peerDependencies": {
"@babel/preset-typescript": "^7.23.3",
"babel-preset-solid": "^1.8.9",
"@solidjs/universal": "^2.0.0-rc.0",
"babel-preset-solid": "^2.0.0-rc.0",
"dominative": "^0.1.2",
"solid-js": "^1.8.11"
"solid-js": "^2.0.0-rc.0"
},
"publishConfig": {
"access": "public"
Expand Down
113 changes: 64 additions & 49 deletions src/renderer.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,45 @@
import './dom.js'
import { createRenderer } from 'solid-js/universal'
import { createRenderer } from '@solidjs/universal'

// eslint-disable-next-line max-params
const setProperty = (node, name, value, prev) => {
if (name === 'style') return Object.assign(node.style, value)
if (value === prev) return

export const {
render,
effect,
memo,
createComponent,
createElement,
createTextNode,
insertNode,
insert,
spread,
setProp,
mergeProps
} = createRenderer({
createElement(string) {
return document.createElement(string)
if (name.startsWith('on:')) {
const eventName = name.slice(3)
if (prev) node.removeEventListener(eventName, prev)
if (value) node.addEventListener(eventName, value)
} else if (name.startsWith('oncapture:')) {
const eventName = name.slice(10)
if (prev) node.removeEventListener(eventName, prev, true)
if (value) node.addEventListener(eventName, value, true)
} else {
if (process.env.NODE_ENV !== 'production' && name.startsWith('on')) {
console.warn(`[DOMiSOLID] Can not register '${name}' as an event handler.
For event handlers, pleas use 'on:raw-eventName' or 'oncapture:rawEvent-name'.
Event delegation isn't supported, also event names are case sensitive on NativeScript.
Refer to https://github.com/SudoMaker/dominative#tweaking to learn how to enable event bubbling and capturing.`)
}
node.setAttribute(name, value)
}
}

const renderer = createRenderer({
createElement(tagName, staticProps) {
const node = document.createElement(tagName)
if (staticProps) {
for (const name in staticProps) setProperty(node, name, staticProps[name])
}
return node
},
createTextNode(value) {
return document.createTextNode(value)
},
replaceText(textNode, value) {
textNode.nodeValue = value
},
// eslint-disable-next-line max-params
setProperty(node, name, value, prev) {
if (name === 'style') return Object.assign(node.style, value)
if (value === prev) return

if (name === 'ref') return value(node)

if (name.startsWith('on:')) {
const eventName = name.slice(3)
if (prev) node.removeEventListener(eventName, prev)
if (value) node.addEventListener(eventName, value)
} else if (name.startsWith('oncapture:')) {
const eventName = name.slice(10)
if (prev) node.removeEventListener(eventName, prev, true)
if (value) node.addEventListener(eventName, value, true)
} else {
if (process.env.NODE_ENV !== 'production' && name.startsWith('on')) {
console.warn(`[DOMiSOLID] Can not register '${name}' as an event handler.
For event handlers, pleas use 'on:raw-eventName' or 'oncapture:rawEvent-name'.
Event delegation isn't supported, also event names are case sensitive on NativeScript.
Refer to https://www.solidjs.com/docs/latest/api#on___oncapture___ for details about 'on:___' and 'oncapture:___'.
Refer to https://github.com/SudoMaker/dominative#tweaking to learn how to enable event bubbling and capturing.`)
}
node.setAttribute(name, value)
}
},
setProperty,
insertNode(parent, node, anchor) {
parent.insertBefore(node, anchor)
},
Expand All @@ -70,18 +60,43 @@ export const {
}
});

export function use(fn, args) {
return fn?.(args);
export const {
render,
effect,
memo,
createComponent,
createElement,
createTextNode,
insertNode,
spread,
setProp,
mergeProps,
ref,
applyRef
} = renderer

// Give every compiled insertion point a private anchor. Sibling blocks compiled
// against the same following element otherwise collapse onto it once one of them
// renders empty — the runtime drops the sentinel it made for that block and the
// remaining ones then append in flush order rather than source order.
// An empty text node is what the runtime itself uses to hold an empty slot, and
// DOMiNATIVE logs every comment node it sees.
// eslint-disable-next-line max-params
export const insert = (parent, accessor, marker, initial) => {
if (marker === undefined) return renderer.insert(parent, accessor, marker, initial)
const anchor = document.createTextNode('')
parent.insertBefore(anchor, marker)
return renderer.insert(parent, accessor, anchor, initial)
}

// Forward Solid control flow
export {
For,
Repeat,
Show,
Suspense,
SuspenseList,
Switch,
Match,
Index,
ErrorBoundary
} from "solid-js"
Loading,
Reveal,
Errored
} from "solid-js"