Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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
- Host: GitHub
- URL: https://github.com/colelawrence/jsx-view
- Owner: colelawrence
- License: mit
- Created: 2022-06-04T13:02:55.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2023-03-03T15:57:18.000Z (over 1 year ago)
- Last Synced: 2024-09-15T05:48:29.773Z (about 2 months ago)
- Topics: dom, jsx, jsx-renderer, mvvm, observables, rxjs, typescript
- Language: TypeScript
- Homepage:
- Size: 265 KB
- Stars: 20
- Watchers: 2
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
jsx-view
A simple HTML DOM JSX renderer with RxJS
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
*
* ```
*/
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
```