Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/colelawrence/jsx-view

Minimal reactive JSX UI builder designed to be highly ergonomic with MVVM
https://github.com/colelawrence/jsx-view

dom jsx jsx-renderer mvvm observables rxjs typescript

Last synced: 5 days ago
JSON representation

Minimal reactive JSX UI builder designed to be highly ergonomic with MVVM

Awesome Lists containing this project

README

        


jsx-view


A simple HTML DOM JSX renderer with RxJS



MIT License


Twitter









Write your web ui with battle-tested RxJS for granular updates.




> This is one of my favorite libraries, and I use it for several projects I maintain, including some work from [storyai](https://www.colelawrence.com/storyai) and a new product I'm actively working on.
> If you like what you see here, please reach out to me at cole @ [github user name] .com and I'd be happy to answer questions.

**Great for:**

- Business Logic Components [BLoC]
- Model-View-ViewModel [MVVM]

**Features**

- No DOM diffing and no "lifecycle loop". Only Observables which get subscribed to and [directly update the DOM elements](./tests/observable-elements.spec.tsx).
- Minimal JSX wiring up with full type definitions for all common `HTMLElement` attributes.
- Any attribute accepts an Observable of its value, and this is type checked.
- An Observable of any [`JSX.Child`](./jsx-view/src/lib/jsxSpec.ts) (`string`, `null`, `JSX.Element`, etc), can be used as a `JSX.Child`.
- Adds [special props](./jsx-view/src/lib/declare/declare-special-props.ts): `is`, `$style`, `$class`, `ref`, and `tags`.
- exports declaration maps (go-to-def goes to TypeScript source code)

## Creating your first component

```tsx
function MyComponent(props: { title: JSX.Child, children: JSX.Children }) {
return


{props.title}


{props.children}

}

content

Hello JSX-View}
children={

content

}
/>

// `JSX.Child` includes `string`
const $inputValue$ = new BehaviorSubject("Hello, JSX View!")
const usage3 = (or any Observable)
// in between any tags
title={Hello {$inputValue$}}
// You can also just use Observable as a JSX.Child value
// title={$inputValue$}
children={[
Title,
// Binding

$inputValue$.next((evt.target as HTMLInputElement).value)
}
/>,
]}
/>
```

## Todo App example

> This was adapted from [a similar demo I put together with React + RxJS](https://refactorordie.com/storybook/?path=/story/writing-observable-state-presentations--react-nyc-oct-2019-todo-app), so if tehre's something missing or misspelled, please accept my apologies.

```tsx
// TodoView.tsx
import { useContext, createContext, renderSpec } from "jsx-view"
import type { Subscription } from "rxjs"
import { map } from "rxjs/operators"
import createTodoState, { Todo } from "./TodoState"

const todos: Todo[] = [
createTodo("Build UI for TodoApp", true),
createTodo("Toggling a Todo"),
createTodo("Deleting a Todo"),
createTodo("Performant lists", true),
createTodo("Adding a Todo"),
]

export default function mountApp(parentSub: Subscription, container: HTMLElement) {
const element = renderSpec(parentSub, )
container.appendChild(element)
parentSub.add(() => container.removeChild(element))
}

const TodoState = createContext(createTodoState(todos))

function TodoApp() {
const state = useContext(TodoState)

return (



Todos APP


{/* Create an observable of a single element and drop it right in. */}
{state.todos$.pipe(
map((todosArr) => (

    {todosArr.map((todo) => (

    ))}

)),
)}



New Todo Title


Add



)
}

/** Todo Item appears within {@link TodoApp} */
function TodoItem({ todo }: { todo: Todo }) {
const state = useContext(TodoState)

return (

  • {
    state.toggleTodo(todo.id)
    })}
    >
    {todo.title}
    {
    state.deleteTodo(todo.id)
    })}
    >
    🗑


  • )
    }
    /**
    * Helper for creating `onchange` listeners
    * @example
    *
    */
    export function changeValue(handler: (value: string) => any) {
    return function (this: HTMLFormElement | HTMLInputElement, _evt: ChangeEvent) {
    handler(this.value)
    }
    }

    /**
    * Helper for canceling default behaviors in functions
    * @example
    * console.log('prevented default submit'))}
    * >
    * ...
    * Submit
    *
    */
    export function preventDefaultThen(handler: () => void) {
    return (evt: { preventDefault: Function }) => {
    evt.preventDefault()
    handler()
    }
    }

    /**
    * Helper for responding to enter key and click events.
    * This produces a set of properties that you must spread.
    *
    * Props:
    * * `tabindex` for making the element tabbable
    * * `onclick`
    * * `onkeydown` for detecting enter key pressed on the element
    *
    * Example:
    * ```jsx
    *

  • console.log('activated Item 1'))}>Item 1

  • * ```
    */
    export function onEnterOrClick(handler: () => void): JSX.HtmlProps {
    return {
    tabindex: "0",
    onclick: (evt) => {
    evt.stopPropagation()
    handler()
    },
    onkeydown: (evt) => {
    if (evt.key === "Enter") {
    evt.stopPropagation()
    if (!(evt.currentTarget instanceof HTMLButtonElement || evt.currentTarget instanceof HTMLAnchorElement)) {
    // onClick will handle this one
    handler()
    }
    }
    },
    }
    }

    function createTodo(title: string, done = false): Todo {
    return {
    id: Math.random(),
    title,
    done,
    }
    }
    ```

    ```ts
    // TodoState.ts
    import { BehaviorSubject } from "rxjs"

    export type Todo = {
    id: number
    done: boolean
    title: string
    }

    export default function createTodoState(initialTodos: Todo[] = []) {
    const $todos$ = new BehaviorSubject(initialTodos)
    const $todoInput$ = new BehaviorSubject("")

    return {
    todos$: $todos$.asObservable(),
    todoInput$: $todoInput$.asObservable(),
    updateNewTodoInput(value: string) {
    debug("updateNewTodoInput", value)
    $todoInput$.next(value)
    },
    toggleTodo(id: number) {
    debug("toggleTodo", id)
    $todos$.next(
    $todos$.value.map((todo) =>
    todo.id === id
    ? // toggle
    { ...todo, done: !todo.done }
    : // don't update
    todo,
    ),
    )
    },
    deleteTodo(id: number) {
    debug("deleteTodo", id)
    $todos$.next($todos$.value.filter((todo) => todo.id !== id))
    },
    addTodo() {
    if ($todoInput$.value) {
    debug("addTodo", $todoInput$.value)
    $todos$.next([
    ...$todos$.value,
    {
    id: Math.random(),
    done: false,
    title: $todoInput$.value,
    },
    ])
    $todoInput$.next("")
    }
    },
    }
    }

    const debug = console.log.bind(console, "%cTodoState", "color: dodgerblue")
    ```

    #### Setting up your `tsconfig.json` or `jsconfig.json`

    ```json
    {
    "compilerOptions": {
    "lib": ["DOM"],

    "jsx": "react-jsx",
    // Alternatively, use `addJSXDev(fn)` handler with source locations with
    // "jsx": "react-jsxdev",

    "jsxImportSource": "jsx-view",
    }
    }
    ```

    #### Setting up with babel

    ```json
    {
    "plugins": [
    [
    "@babel/plugin-transform-react-jsx",
    {
    "runtime": "automatic", // defaults to classic
    "importSource": "jsx-view" // defaults to react
    }
    ]
    ]
    }
    ```

    #### Setting up with vite

    ```js
    // vite.config.js or vite.config.ts
    import * as path from "path";
    import { defineConfig } from "vite";

    export default defineConfig({
    // ...

    esbuild: {
    jsx: "automatic",
    jsxImportSource: "jsx-view",
    // use in conjunction with providing your own `addJSXDev(fn)` handler
    // jsxDev: true,
    },
    });
    ```

    ### Contributing

    Clone the repository with
    ```sh
    git clone https://github.com/colelawrence/jsx-view.git
    ```

    Open the repository in terminal, and install dependencies using [pnpm](https://pnpm.io/).
    ```sh
    cd jsx-view
    pnpm install
    ```

    Now, you have this locally, you may try things out by opening the
    dev server with
    ```sh
    pnpm playground
    ```