diff --git a/.changeset/witty-tips-admire.md b/.changeset/witty-tips-admire.md
new file mode 100644
index 0000000..2e29ed9
--- /dev/null
+++ b/.changeset/witty-tips-admire.md
@@ -0,0 +1,7 @@
+---
+'@reactlit/vanilla': minor
+'@reactlit/radix': minor
+'@reactlit/core': minor
+---
+
+Make layout plugin into a view, wrapper functionality
diff --git a/apps/reactlit-docs/astro.config.mjs b/apps/reactlit-docs/astro.config.mjs
index 361c131..2b64793 100644
--- a/apps/reactlit-docs/astro.config.mjs
+++ b/apps/reactlit-docs/astro.config.mjs
@@ -31,7 +31,8 @@ export default defineConfig({
{ label: 'Installation', slug: 'guides/installation' },
{ label: 'Basics', slug: 'guides/basics' },
{ label: 'Data Fetching', slug: 'guides/data-fetching' },
- { label: 'Layout Plugin', slug: 'guides/layout' },
+ { label: 'Wrappers', slug: 'guides/wrappers' },
+ { label: 'Layout', slug: 'guides/layout' },
{ label: 'Managed State', slug: 'guides/managed-state' },
{ label: 'Defining Views', slug: 'guides/defining-views' },
],
diff --git a/apps/reactlit-docs/src/content/docs/guides/data-fetching.mdx b/apps/reactlit-docs/src/content/docs/guides/data-fetching.mdx
index 9ad5757..0178a53 100644
--- a/apps/reactlit-docs/src/content/docs/guides/data-fetching.mdx
+++ b/apps/reactlit-docs/src/content/docs/guides/data-fetching.mdx
@@ -85,8 +85,8 @@ import ContactListDataFetchApp from '/src/examples/contact-list-data-fetch.tsx';
{ range: '38-39', label: 'get data' },
{ range: '44-50' },
{ range: '51-52', label: 'refetch' },
- { range: '87-98', label: 'update' },
- { range: '102', label: 'disable' },
+ { range: '85-96', label: 'update' },
+ { range: '100', label: 'disable' },
]}
/>
diff --git a/apps/reactlit-docs/src/content/docs/guides/layout.mdx b/apps/reactlit-docs/src/content/docs/guides/layout.mdx
index 550b0e9..3f4f38e 100644
--- a/apps/reactlit-docs/src/content/docs/guides/layout.mdx
+++ b/apps/reactlit-docs/src/content/docs/guides/layout.mdx
@@ -1,24 +1,19 @@
---
-title: Layout Plugin
-description: How to use the layout plugin
+title: Layout
+description: How to use the layout view
---
-By default, Reactlit uses a simple layout system where each `display` call
+By default, Reactlit uses a simple flat layout system where each `display` call
concatenates a `ReactNode` to the DOM. This works great for simple applications,
where you are ok just showing your content in a single column,
but as your application grows, you may want to have more control over the layout.
-The plugin provides a `layout` helper function to let you define more custom layouts with
+The built-in layout view lets you define more custom layouts with
your Reactlit script.
import { Card, Aside } from '@astrojs/starlight/components';
-
- Currently this plugin is provided in the core package, but this may change in
- the future.
-
-
-The `layout` function works via the [tunnel-rat](https://github.com/pmndrs/tunnel-rat)
+The `layout` view works via a modified version of the [tunnel-rat](https://github.com/pmndrs/tunnel-rat)
library. It takes a number of slots to expose and a render function that defines how
those slots are layed out. Then, for each slot, it returns a mini-context with
`view` and `display` functions that render specifically within that slot.
diff --git a/apps/reactlit-docs/src/content/docs/guides/wrappers.mdx b/apps/reactlit-docs/src/content/docs/guides/wrappers.mdx
new file mode 100644
index 0000000..84ff9a5
--- /dev/null
+++ b/apps/reactlit-docs/src/content/docs/guides/wrappers.mdx
@@ -0,0 +1,64 @@
+---
+title: Wrappers
+---
+
+You might encounter use cases that make Reactlit feel limiting when it comes to the heirarchical nature
+of UI. The fact that the render is a flat list of elements can make it hard to build even moderately complex
+interfaces.
+
+To solve this, Reactlit provides a way to wrap elements in custom components. These wrappers can be used to
+apply styles and add structure to your UI.
+
+Any `display` or `view` call actually allows a varidic number of arguments. Penultmate arguments are considered wrappers
+and will automatically `wrap` their successors. The structure can look a bit unusual coming from standard React, but
+it does allow for a lot of flexibility in a very succinct format.
+
+Wrappers come in two flavors.
+
+1. They can be static React elements with their children omitted:
+
+```tsx
+const name = view(
+ 'name',
+
,
+ Inputs.Text({ placeholder: 'Enter your name' })
+);
+display(
+
,
+
+
Hello World
+
+);
+```
+
+2. They can react components of type `WrapperComponent`.
+
+define the component builder. This is a function that creates the wrapper component
+you want to use.
+
+```tsx
+export const Label = (label: string) => {
+ const LabelComponent: WrapperComponent = ({ children, stateKey }) => {
+ return (
+
+ {label}
+ {children}
+
+ );
+ };
+ return LabelComponent;
+};
+```
+
+use the component:
+
+```tsx
+const name = view(
+ 'name',
+ Label('Name'),
+ Inputs.Text({ placeholder: 'Enter your name' })
+);
+```
+
+This makes views and displays much more composable allowing you to build
+custom interfaces without a lot of boilerplate.
diff --git a/apps/reactlit-docs/src/examples/apps/contact-list-basic-async.tsx b/apps/reactlit-docs/src/examples/apps/contact-list-basic-async.tsx
index 87f1168..40c2af5 100644
--- a/apps/reactlit-docs/src/examples/apps/contact-list-basic-async.tsx
+++ b/apps/reactlit-docs/src/examples/apps/contact-list-basic-async.tsx
@@ -1,6 +1,6 @@
import { Button } from '@radix-ui/themes';
-import { FormInput, type ReactlitContext } from '@reactlit/core';
-import { Inputs } from '@reactlit/radix';
+import { type ReactlitContext } from '@reactlit/core';
+import { Inputs, Label } from '@reactlit/radix';
import { TopRightLoader } from '../components/loader';
import { ContactsMockService } from '../mocks/contacts';
@@ -32,15 +32,13 @@ export async function ContactListApp(app: ReactlitContext) {
if (!selectedContact) return;
app.display(Selected Contact Details );
if (app.changed('selectedContact')) {
- app.set('updates', selectedContact);
+ app.set('name', selectedContact.name);
+ app.set('email', selectedContact.email);
}
- const updates = app.view(
- 'updates',
- FormInput({
- name: Inputs.Text({ label: 'Name' }),
- email: Inputs.Text({ label: 'Email' }),
- })
- );
+ const updates = {
+ name: app.view('name', Label('Name'), Inputs.Text()),
+ email: app.view('email', Label('Email'), Inputs.Text()),
+ };
// if you wish, you can use an AsyncButton view for a button that
// has a loading state during async operations
app.view(
diff --git a/apps/reactlit-docs/src/examples/apps/contact-list.tsx b/apps/reactlit-docs/src/examples/apps/contact-list.tsx
index fe8d78b..3397c8d 100644
--- a/apps/reactlit-docs/src/examples/apps/contact-list.tsx
+++ b/apps/reactlit-docs/src/examples/apps/contact-list.tsx
@@ -1,7 +1,7 @@
-import { FormInput, type ReactlitContext } from '@reactlit/core';
-import { Inputs } from '@reactlit/radix';
-import { ContactMockApi as api } from '../mocks/contacts';
import { Button } from '@radix-ui/themes';
+import { type ReactlitContext } from '@reactlit/core';
+import { Inputs, Label } from '@reactlit/radix';
+import { ContactMockApi as api } from '../mocks/contacts';
export async function ContactListApp(app: ReactlitContext) {
const contacts = await api.getContacts();
@@ -25,16 +25,14 @@ export async function ContactListApp(app: ReactlitContext) {
if (!selectedContact) return;
app.display(Selected Contact Details );
if (app.changed('selectedContact')) {
- app.set('updates', selectedContact);
+ app.set('name', selectedContact.name);
+ app.set('email', selectedContact.email);
}
- // the built-in FormInput allows you to group inputs together
- const updates = app.view(
- 'updates',
- FormInput({
- name: Inputs.Text({ label: 'Name' }),
- email: Inputs.Text({ label: 'Email' }),
- })
- );
+ // the built-in FormView allows you to group inputs together
+ const updates = {
+ name: app.view('name', Label('Name'), Inputs.Text()),
+ email: app.view('email', Label('Email'), Inputs.Text()),
+ };
app.display(
{
diff --git a/apps/reactlit-docs/src/examples/contact-list-data-fetch.tsx b/apps/reactlit-docs/src/examples/contact-list-data-fetch.tsx
index aac70a5..41d9164 100644
--- a/apps/reactlit-docs/src/examples/contact-list-data-fetch.tsx
+++ b/apps/reactlit-docs/src/examples/contact-list-data-fetch.tsx
@@ -1,7 +1,7 @@
import { Button, Theme } from '@radix-ui/themes';
import '@radix-ui/themes/styles.css';
-import { DataFetchingPlugin, FormInput, useReactlit } from '@reactlit/core';
-import { Inputs } from '@reactlit/radix';
+import { DataFetchingPlugin, useReactlit } from '@reactlit/core';
+import { Inputs, Label } from '@reactlit/radix';
import { TopRightLoader } from './components/loader';
import { ContactsMockService } from './mocks/contacts';
@@ -68,15 +68,13 @@ const ContactListApp = () => {
Selected Contact Details
);
if (app.changed('selectedContact')) {
- app.set('updates', selectedContact);
+ app.set('name', selectedContact.name);
+ app.set('email', selectedContact.email);
}
- const updates = app.view(
- 'updates',
- FormInput({
- name: Inputs.Text({ label: 'Name' }),
- email: Inputs.Text({ label: 'Email' }),
- })
- );
+ const updates = {
+ name: app.view('name', Label('Name'), Inputs.Text()),
+ email: app.view('email', Label('Email'), Inputs.Text()),
+ };
app.display(
{
diff --git a/apps/reactlit-docs/src/examples/contact-list-react.tsx b/apps/reactlit-docs/src/examples/contact-list-react.tsx
index 1dd8392..4761119 100644
--- a/apps/reactlit-docs/src/examples/contact-list-react.tsx
+++ b/apps/reactlit-docs/src/examples/contact-list-react.tsx
@@ -89,6 +89,8 @@ function ContactListApp() {
value={selectedContactId}
setValue={(id) => setSelectedContactId(id)}
getRowId={(c) => c.id}
+ display={() => {}}
+ view={() => undefined as any}
/>
{selectedContact && (
<>
@@ -98,12 +100,16 @@ function ContactListApp() {
label="Name"
value={name}
setValue={setName}
+ display={() => {}}
+ view={() => undefined as any}
/>
{}}
+ view={() => undefined as any}
/>
- {async ({ layout }) => {
- const [col1, col2, col3] = layout(ThreeColumnLayout);
+ {async ({ view }) => {
+ const [col1, col2, col3] = view(
+ 'cols',
+
,
+ // the second argument here wraps each slot in a div so that they show
+ // up as a single grid column in the layout
+ LayoutView(3,
)
+ );
col1.display('First Name');
const first = col1.view('first', TextInput);
diff --git a/apps/reactlit-examples/src/components/debug-toggle.tsx b/apps/reactlit-examples/src/components/debug-toggle.tsx
new file mode 100644
index 0000000..a59017a
--- /dev/null
+++ b/apps/reactlit-examples/src/components/debug-toggle.tsx
@@ -0,0 +1,51 @@
+'use client';
+import { Button, Tooltip } from '@radix-ui/themes';
+import { Wrapper } from '@reactlit/core';
+import { Bug, BugOff } from 'lucide-react';
+import { createContext, useContext, useState } from 'react';
+
+type DebugContextType = {
+ debug: boolean;
+ setDebug: (debug: boolean) => void;
+};
+
+const DebugContext = createContext({
+ debug: false,
+ setDebug: () => {},
+});
+
+export const DebugToggle = () => {
+ const { debug, setDebug } = useContext(DebugContext);
+ return (
+
+ setDebug(!debug)}>
+ {debug ? : }
+
+
+ );
+};
+
+export const DebugProvider = ({ children }: { children: React.ReactNode }) => {
+ const [debug, setDebug] = useState(false);
+ return (
+
+ {children}
+
+ );
+};
+
+export function useDebug() {
+ const { debug } = useContext(DebugContext);
+ return debug;
+}
+
+export const Debug: Wrapper = ({ children, stateKey }) => {
+ const debug = useDebug();
+ if (!debug) return children;
+ return (
+
+
{stateKey}
+
{children}
+
+ );
+};
diff --git a/apps/reactlit-examples/src/components/main.tsx b/apps/reactlit-examples/src/components/main.tsx
index e40a416..bcd6cb0 100644
--- a/apps/reactlit-examples/src/components/main.tsx
+++ b/apps/reactlit-examples/src/components/main.tsx
@@ -1,6 +1,8 @@
import { Box, Container, Flex, Text } from '@radix-ui/themes';
import { Geist, Geist_Mono } from 'next/font/google';
import Head from 'next/head';
+import React from 'react';
+import { DebugToggle } from './debug-toggle';
import { Menu } from './menu';
import { ThemeToggle } from './theme-toggle';
@@ -35,6 +37,7 @@ export function Main({
width="100%"
p="4"
align="center"
+ gap="4"
style={{
background: 'var(--color-background)',
borderBottom: '1px solid var(--gray-4)',
@@ -44,6 +47,7 @@ export function Main({
{title}
+
diff --git a/apps/reactlit-examples/src/components/menu.tsx b/apps/reactlit-examples/src/components/menu.tsx
index ead74f7..682d58c 100644
--- a/apps/reactlit-examples/src/components/menu.tsx
+++ b/apps/reactlit-examples/src/components/menu.tsx
@@ -23,6 +23,7 @@ export function Menu() {
Radix Inputs
Todo List
Starter
+ Layout Test
);
}
diff --git a/apps/reactlit-examples/src/pages/_app.tsx b/apps/reactlit-examples/src/pages/_app.tsx
index e0d118d..7aa93eb 100644
--- a/apps/reactlit-examples/src/pages/_app.tsx
+++ b/apps/reactlit-examples/src/pages/_app.tsx
@@ -4,14 +4,17 @@ import { Theme } from '@radix-ui/themes';
import { ThemeProvider } from 'next-themes';
import type { AppProps } from 'next/app';
import { Main } from '@/components/main';
+import { DebugProvider } from '@/components/debug-toggle';
export default function App({ Component, pageProps }: AppProps) {
return (
-
-
-
+
+
+
+
+
);
diff --git a/apps/reactlit-examples/src/pages/hello-world-vanilla/index.tsx b/apps/reactlit-examples/src/pages/hello-world-vanilla/index.tsx
index 9f0d928..180bcfb 100644
--- a/apps/reactlit-examples/src/pages/hello-world-vanilla/index.tsx
+++ b/apps/reactlit-examples/src/pages/hello-world-vanilla/index.tsx
@@ -1,5 +1,7 @@
import { Reactlit, useReactlitState } from '@reactlit/core';
-import { Inputs } from '@reactlit/vanilla';
+import { Inputs, Label } from '@reactlit/vanilla';
+
+const LabelProps = { className: 'flex items-center gap-2 mb-2' };
export default function HelloWorldVanilla() {
const [appState, setAppState] = useReactlitState({
@@ -9,58 +11,59 @@ export default function HelloWorldVanilla() {
chosenNumber: '',
chosenColor: '',
});
+
return (
{async ({ display, view }) => {
- display(Hello World Vanilla
);
+ display(Hello World Vanilla
);
+
const name = view(
'name',
+ Label('Name', LabelProps),
Inputs.Text({
- id: 'name-input',
+ id: 'name',
className: 'border p-0.5',
- label: 'Name',
placeholder: 'Enter your name',
})
);
+
display(Hello {name}!
);
+
const picked = view(
'pickedNumbers',
+ Label('Pick any number', LabelProps),
Inputs.Check(['One', 'Two', 'Three'], {
className: {
- container: 'flex gap-2 items-center',
wrapper: 'flex gap-2',
item: {
input: 'border p-0.5 mr-1',
},
},
- label: 'Pick any number',
})
);
display(Picked: {picked.join(', ')}!
);
+
const pickedColors = view(
'pickedColors',
+ Label('Pick any color', LabelProps),
Inputs.Check(
[
{ label: 'Red', value: '#FF0000' },
{ label: 'Green', value: '#00FF00' },
{ label: 'Blue', value: '#0000FF' },
- { label: 'White', value: '#FFFFFF' },
],
{
className: {
- container: 'flex gap-2 items-center',
wrapper: 'flex gap-2',
item: {
input: 'border p-0.5 mr-1',
},
},
- label: 'Pick any color',
valueof: (item) => item.value,
format: (item) => (
Colors: {JSON.stringify(pickedColors)}!);
+
const chosenNumber = view(
'chosenNumber',
+ Label('Choose a number', LabelProps),
Inputs.Radio(['One', 'Two', 'Three'], {
className: {
- container: 'flex gap-2 items-center',
wrapper: 'flex gap-2',
item: {
input: 'border p-0.5 mr-1',
},
},
- label: 'Choose a number',
})
);
display(Chosen Number: {chosenNumber}!
);
+
const chosenColor = view(
'chosenColor',
+ Label('Choose a color', LabelProps),
Inputs.Radio(
[
{ label: 'Red', value: '#FF0000' },
{ label: 'Green', value: '#00FF00' },
{ label: 'Blue', value: '#0000FF' },
- { label: 'White', value: '#FFFFFF' },
],
{
className: {
- container: 'flex gap-2 items-center',
wrapper: 'flex gap-2',
item: {
input: 'border p-0.5 mr-1',
},
},
- label: 'Choose a color',
valueof: (item) => item.value,
format: (item) => (
+
+ {async (ctx) => {
+ const { view } = ctx;
+ const [col1, col2] = view(
+ 'layout1',
+ Debug,
+
,
+ LayoutView(2,
)
+ );
+ const v1 = col1.view(
+ 'leftInput',
+ Debug,
+ Label('Column Left'),
+ Inputs.Text()
+ );
+ col1.display(Debug, v1);
+ const v2 = col2.view(
+ 'rightInput',
+ Debug,
+ Label('Column Right'),
+ Inputs.Text()
+ );
+ col2.display(Debug, v2);
+ }}
+
+
+ );
+}
diff --git a/apps/reactlit-examples/src/pages/radix-inputs/index.tsx b/apps/reactlit-examples/src/pages/radix-inputs/index.tsx
index 32482dd..7a37cbc 100644
--- a/apps/reactlit-examples/src/pages/radix-inputs/index.tsx
+++ b/apps/reactlit-examples/src/pages/radix-inputs/index.tsx
@@ -1,6 +1,20 @@
-import { Badge, DataList } from '@radix-ui/themes';
-import { Reactlit, useReactlitState } from '@reactlit/core';
-import { DefaultRadixWrapper, Inputs } from '@reactlit/radix';
+import { useDebug } from '@/components/debug-toggle';
+import {
+ Badge,
+ ChevronDownIcon,
+ DataList,
+ ThickChevronRightIcon,
+} from '@radix-ui/themes';
+import {
+ defaultLayoutState,
+ LayoutView,
+ Reactlit,
+ useReactlitState,
+ Wrapper,
+ WrapperComponent,
+} from '@reactlit/core';
+import { DefaultRadixWrapper, Inputs, Label } from '@reactlit/radix';
+import { useState } from 'react';
interface Country {
name: string;
@@ -12,7 +26,7 @@ interface Country {
export async function fetchCountries(): Promise {
const results = await fetch(
- 'https://restcountries.com/v3.1/all?fields=name,region,subregion,population,cca3',
+ 'https://restcountries.com/v3.1/all?fields=name,region,subregion,population,cca2',
{
cache: 'force-cache',
next: {
@@ -28,132 +42,146 @@ export async function fetchCountries(): Promise {
region: r.region,
subregion: r.subregion,
population: r.population,
- code: r.cca3,
+ code: r.cca2,
}));
}
+const DisplayLabel = (label: string) => {
+ const DisplayLabelComponent: WrapperComponent = ({ children }) => (
+
+ {label}
+ {children}
+
+ );
+ return DisplayLabelComponent;
+};
+
+const ResultsWrapper: Wrapper = ({ children }) => {
+ const [open, setOpen] = useState(true);
+ return (
+ setOpen(!open)}
+ >
+
+ {open ? : }
+ Results
+
+ {children}
+
+ );
+};
+
export default function RadixInputs() {
const [appState, setAppState] = useReactlitState({
countrySearch: '',
country: undefined as Country | undefined,
- form: {
- name: '',
- bio: '',
- number: [],
- letter: undefined as string | undefined,
- color: 'red',
- slider: 0,
- rangeSlider: [20, 80],
- },
+ results: defaultLayoutState(1),
+ name: '',
+ bio: '',
+ number: [],
+ letter: undefined as string | undefined,
+ color: 'red',
+ slider: 0,
+ rangeSlider: [20, 80],
});
+ const debug = useDebug();
return (
-
- {async ({ display, view }) => {
- display(Inputs test
);
- const results = view(
- 'form',
- Inputs.Form({
- name: Inputs.Text({
- label: 'Name',
+
+
+ {async ({ display, view }) => {
+ display(Inputs test
);
+ const [results] = view('results', ResultsWrapper, LayoutView(1));
+ const name = view(
+ 'name',
+ Label('Name'),
+ Inputs.Text({
placeholder: 'Enter your name',
- }),
- bio: Inputs.TextArea({
- label: 'Bio',
+ })
+ );
+ results.display(DisplayLabel('Name'), name);
+ const bio = view(
+ 'bio',
+ Label('Bio'),
+ Inputs.TextArea({
placeholder: 'Enter your bio',
- }),
- number: Inputs.Check(
- { one: '1', two: '2', three: '3' },
- {
- label: 'Pick any numbers',
- }
- ),
- letter: Inputs.Radio(['A', 'B', 'C'], {
- label: 'Pick one Letter',
- }),
- color: Inputs.Select(['red', 'blue', 'green'] as const, {
- label: 'Pick a color',
- }),
- slider: Inputs.Slider({
- label: 'Slider',
+ })
+ );
+ results.display(DisplayLabel('Bio'), bio);
+ const number = view(
+ 'number',
+ Label('Pick any numbers'),
+ Inputs.Check({ one: '1', two: '2', three: '3' })
+ );
+ results.display(DisplayLabel('Numbers'), number);
+ const letter = view(
+ 'letter',
+ Label('Pick one Letter'),
+ Inputs.Radio(['A', 'B', 'C'])
+ );
+ results.display(DisplayLabel('Letter'), letter);
+ const color = view(
+ 'color',
+ Label('Pick a color'),
+
,
+ Inputs.Select(['red', 'blue', 'green'] as const)
+ );
+ results.display(
+ DisplayLabel('Color'),
+ {color}
+ );
+ const slider = view(
+ 'slider',
+ Label('Slider'),
+ Inputs.Slider({
min: 0,
max: 100,
- }),
- rangeSlider: Inputs.RangeSlider({
- label: 'Range Slider',
+ })
+ );
+ results.display(DisplayLabel('Slider'), slider);
+ const rangeSlider = view(
+ 'rangeSlider',
+ Label('Range Slider'),
+ Inputs.RangeSlider({
min: 0,
max: 100,
- }),
- })
- );
-
- const countries = await fetchCountries();
- display(Select a country
);
- const filteredCountries = view(
- 'countrySearch',
- Inputs.Search(countries, {
- label: 'Search',
- placeholder: 'Search countries...',
- })
- );
- const selectedCountry = view(
- 'country',
- Inputs.Table(filteredCountries, {
- getRowId: (country) => country.code,
- className: 'h-[300px]',
- label: 'Countries',
- })
- );
-
- const { name, bio, number, letter, color, slider, rangeSlider } =
- results;
- display(
- <>
- Results
-
-
- Name
- {name}
-
-
- Bio
-
- {bio}
-
-
-
- Numbers
- {number.join(', ')}
-
-
- Letter
- {letter}
-
-
- Color
-
- {color}
-
-
-
- Slider
- {slider}
-
-
- Range Slider
- {rangeSlider.join(' - ')}
-
-
- Country
- {selectedCountry?.name}
-
-
- >
- );
- }}
-
+ })
+ );
+ results.display(
+ DisplayLabel('Range Slider'),
+ rangeSlider.join(' - ')
+ );
+ const countries = await fetchCountries();
+ display(Select a country
);
+ const filteredCountries = view(
+ 'countrySearch',
+ Label('Search'),
+ Inputs.Search(countries, {
+ placeholder: 'Search countries...',
+ })
+ );
+ const selectedCountry = view(
+ 'country',
+ Label('Countries'),
+ Inputs.Table(filteredCountries, {
+ getRowId: (country) => country.code,
+ className: 'h-[300px]',
+ })
+ );
+ results.display(
+ DisplayLabel('Country'),
+ <>
+ {selectedCountry?.code ? (
+
+ ) : (
+ 'Select a country'
+ )}
+ >
+ );
+ }}
+
+
);
}
diff --git a/apps/reactlit-examples/src/pages/starter/index.tsx b/apps/reactlit-examples/src/pages/starter/index.tsx
index 7ca5d9b..67fcf6f 100644
--- a/apps/reactlit-examples/src/pages/starter/index.tsx
+++ b/apps/reactlit-examples/src/pages/starter/index.tsx
@@ -1,12 +1,12 @@
-import { Box, Text } from '@radix-ui/themes';
+import { Debug, useDebug } from '@/components/debug-toggle';
+import { Text } from '@radix-ui/themes';
import { textPropDefs } from '@radix-ui/themes/props';
import {
DataFetchingPlugin,
- LayoutPlugin,
useReactlit,
useReactlitState,
} from '@reactlit/core';
-import { Inputs, DefaultRadixWrapper } from '@reactlit/radix';
+import { DefaultRadixWrapper, Inputs, Label } from '@reactlit/radix';
export default function Starter() {
const [appState, setAppState] = useReactlitState({
@@ -14,46 +14,40 @@ export default function Starter() {
weight: 'regular',
size: 1,
});
- const Reactlit = useReactlit(
- LayoutPlugin,
- DataFetchingPlugin
- );
+ const Reactlit = useReactlit(DataFetchingPlugin);
+ const debug = useDebug();
return (
-
- {async ({ display, view }) => {
- const name = view(
- 'name',
- Inputs.Text({
- label: 'What is your name?',
- placeholder: 'Enter name',
- })
- );
- const weight = view(
- 'weight',
- Inputs.Radio(['light', 'regular', 'medium', 'bold'] as const, {
- label: 'Weight',
- })
- );
- const size = view(
- 'size',
- Inputs.Slider({
- label: 'Size',
- min: 1,
- max: 9,
- })
- );
+
+
+ {async (ctx) => {
+ const { display, view } = ctx;
+ const name = view(
+ 'name',
+ Debug,
+ Label('What is your name?'),
+ Inputs.Text({
+ placeholder: 'Enter name',
+ })
+ );
+ const weight = view(
+ 'weight',
+ Debug,
+ Label('Weight'),
+ Inputs.Radio(['light', 'regular', 'medium', 'bold'] as const)
+ );
+ const size = view(
+ 'size',
+ Debug,
+ Label('Size'),
+ Inputs.Slider({
+ min: 1,
+ max: 9,
+ })
+ );
- display(
-
-
-
- );
- display(
-
+ display( );
+ display(
+ Debug,
Enter Name } from
Reactlit!
-
- );
- }}
-
+ );
+ }}
+
+
);
}
diff --git a/apps/reactlit-examples/src/pages/todo-list/index.tsx b/apps/reactlit-examples/src/pages/todo-list/index.tsx
index 89c3d10..0744108 100644
--- a/apps/reactlit-examples/src/pages/todo-list/index.tsx
+++ b/apps/reactlit-examples/src/pages/todo-list/index.tsx
@@ -1,15 +1,6 @@
import { Button, Callout, Spinner } from '@radix-ui/themes';
-import {
- DataFetchingPlugin,
- defineLayout,
- LayoutPlugin,
- useReactlit,
-} from '@reactlit/core';
-import {
- BoxContainerWrapper,
- DefaultRadixWrapper,
- Inputs,
-} from '@reactlit/radix';
+import { DataFetchingPlugin, useReactlit } from '@reactlit/core';
+import { DefaultRadixWrapper, Inputs } from '@reactlit/radix';
import { InfoIcon } from 'lucide-react';
import { TodoService } from '../../mocks/todos';
@@ -23,108 +14,94 @@ export const Loader = ({ message }: { message: string }) => {
const api = new TodoService([], 1000);
-const TwoColumnLayout = defineLayout(2, ({ slots: [Slot1, Slot2] }) => {
- return (
-
-
-
-
-
-
-
-
-
-
- );
-});
-
export default function TodoList() {
- const Reactlit = useReactlit(LayoutPlugin, DataFetchingPlugin);
+ const Reactlit = useReactlit(DataFetchingPlugin);
return (
-
- {async ({ display, view, set, changed, fetcher, layout }) => {
- display(
-
-
-
-
-
- This app is purposely slow to show how Reactlit handles loading
- states.
-
-
- );
- const todosFetcher = fetcher(['todos'], () => api.getTodos());
- display(
-
- todosFetcher.refetch()}>Refetch
-
- );
- view(
- 'adding',
- Inputs.AsyncButton(
- async () => {
- const newTodo = await api.addTodo();
- await todosFetcher.refetch();
- set('selectedTodo', newTodo.id);
- },
- {
- disabled: todosFetcher.isFetching(),
- content: 'Add Todo',
- }
- )
- );
- const todos = todosFetcher.get() ?? [];
- const selectedTodo = view(
- 'selectedTodo',
- Inputs.Table(todos, {
- getRowId: (todo) => todo.id,
- columns: ['task', 'completed'],
- format: {
- completed: (completed) => (completed ? '☑️' : ''),
- },
- })
- );
- if (selectedTodo) {
- if (changed('selectedTodo')) {
- set('task', selectedTodo.task);
- set('completed', selectedTodo.completed);
- }
- const [c1, c2] = layout(TwoColumnLayout);
- const task = c1.view(
- 'task',
- Inputs.Text({
- label: 'Task',
- })
+
+
+ {async ({ display, view, set, changed, fetcher }) => {
+ display(
+
+
+
+
+
+ This app is purposely slow to show how Reactlit handles loading
+ states.
+
+
);
- const completed = c2.view(
- 'completed',
- Inputs.Switch({
- label: 'Completed',
- })
+ const todosFetcher = fetcher(['todos'], () => api.getTodos());
+ display(
+
+ todosFetcher.refetch()}>Refetch
+
);
view(
- 'updaing',
+ 'adding',
Inputs.AsyncButton(
async () => {
- // todosFetcher.update((todos) => {
- // return todos.map((todo) =>
- // todo.id === selectedTodo.id
- // ? { ...todo, task, completed }
- // : todo
- // );
- // });
- await api.updateTodo(selectedTodo.id, { task, completed });
+ const newTodo = await api.addTodo();
await todosFetcher.refetch();
+ set('selectedTodo', newTodo.id);
},
{
disabled: todosFetcher.isFetching(),
- content: 'Update',
+ content: 'Add Todo',
}
)
);
- }
- }}
-
+ const todos = todosFetcher.get() ?? [];
+ const selectedTodo = view(
+ 'selectedTodo',
+ Inputs.Table(todos, {
+ getRowId: (todo) => todo.id,
+ columns: ['task', 'completed'],
+ format: {
+ completed: (completed) => (completed ? '☑️' : ''),
+ },
+ })
+ );
+ if (selectedTodo) {
+ if (changed('selectedTodo')) {
+ set('task', selectedTodo.task);
+ set('completed', selectedTodo.completed);
+ }
+ const task = view(
+ 'task',
+ Inputs.Text({
+ label: 'Task',
+ })
+ );
+ const completed = view(
+ 'completed',
+ Inputs.Switch({
+ label: 'Completed',
+ })
+ );
+ view(
+ 'updaing',
+ Inputs.AsyncButton(
+ async () => {
+ // todosFetcher.update((todos) => {
+ // return todos.map((todo) =>
+ // todo.id === selectedTodo.id
+ // ? { ...todo, task, completed }
+ // : todo
+ // );
+ // });
+ await api.updateTodo(selectedTodo.id, { task, completed });
+ await todosFetcher.refetch();
+ },
+ {
+ disabled: todosFetcher.isFetching(),
+ content: 'Update',
+ }
+ )
+ );
+ }
+ }}
+
+
);
}
diff --git a/apps/reactlit-examples/src/styles/globals.css b/apps/reactlit-examples/src/styles/globals.css
index 8e4ea0d..ef253ea 100644
--- a/apps/reactlit-examples/src/styles/globals.css
+++ b/apps/reactlit-examples/src/styles/globals.css
@@ -23,3 +23,36 @@ body {
tr.rt-TableRow:nth-child(even) {
background-color: var(--gray-2);
}
+
+.overlay {
+ position: fixed;
+ cursor: pointer;
+ top: 4rem;
+ right: 2rem;
+ padding: 1rem;
+ background: var(--gray-2);
+ border: 1px solid #bbb;
+ border-radius: 0.5rem;
+ opacity: 0.8;
+ transition: all 0.2s;
+ z-index: 100;
+ width: 25%;
+ overflow-x: hidden;
+ overflow-y: hidden;
+ max-height: 3.5rem;
+}
+
+.overlay.open {
+ max-height: 45rem;
+}
+
+.overlay-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.overlay:hover {
+ opacity: 1;
+}
diff --git a/libs/core/package.json b/libs/core/package.json
index 06420f3..a84f30c 100644
--- a/libs/core/package.json
+++ b/libs/core/package.json
@@ -34,7 +34,7 @@
"@tanstack/react-query": "^5.62.3",
"fast-deep-equal": "^3.1.3",
"react-error-boundary": "^4.1.2",
- "tunnel-rat": "^0.1.2"
+ "zustand": "^5.0.3"
},
"devDependencies": {
"@mollycule/vigilante": "^1.0.2",
diff --git a/libs/core/src/builtins/changed.ts b/libs/core/src/builtins/changed.ts
new file mode 100644
index 0000000..ca1c324
--- /dev/null
+++ b/libs/core/src/builtins/changed.ts
@@ -0,0 +1,48 @@
+import { useCallback } from 'react';
+import { ReactlitContext, StateBase } from './types';
+import { InternalReactlitState } from './internal-state';
+import { deepEqual } from '../utils/deep-equal';
+
+function deltas(
+ state: T,
+ previousState: T | undefined
+): (keyof T)[] {
+ return Object.keys(state).filter(
+ (key: string) =>
+ !previousState || !deepEqual(state[key], previousState[key])
+ );
+}
+
+export function useReactlitChanged(
+ { state, previousState, setPreviousState }: InternalReactlitState,
+ debug: boolean
+) {
+ return useCallback['changed']>(
+ (...keys) => {
+ const changedKeys = deltas(state, previousState);
+ const selectedChangedKeys = keys.filter((k) =>
+ changedKeys.includes(k as string)
+ );
+ const isChanged = selectedChangedKeys.length > 0;
+ if (isChanged) {
+ if (debug) {
+ for (const k of selectedChangedKeys) {
+ // eslint-disable-next-line no-console
+ console.debug(
+ `changed ${String(k)}: ${previousState?.[k]} -> ${state[k]}`
+ );
+ }
+ }
+ setPreviousState((prev) => {
+ let newState = prev;
+ for (const k of selectedChangedKeys) {
+ newState = { ...newState, [k]: state[k] };
+ }
+ return newState;
+ });
+ }
+ return isChanged;
+ },
+ [state, previousState, setPreviousState, debug]
+ );
+}
diff --git a/libs/core/src/builtins/display.tsx b/libs/core/src/builtins/display.tsx
new file mode 100644
index 0000000..df7a660
--- /dev/null
+++ b/libs/core/src/builtins/display.tsx
@@ -0,0 +1,119 @@
+import { ReactNode, useCallback, useState } from 'react';
+import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
+import { tail } from '../utils/tail';
+import { ApplyWrappers, Wrapper } from '../wrappers';
+import { ReactlitContext, ReactlitProps, StateBase } from './types';
+
+interface DisplayState {
+ position: number;
+ elements: [string, React.ReactNode][];
+}
+
+type KeyedDisplayArgs = [string, ...Wrapper[], ReactNode];
+type UnkeyedDisplayArgs = [...Wrapper[], ReactNode];
+
+export type DisplayArgs = KeyedDisplayArgs | UnkeyedDisplayArgs;
+
+export function isKeyedDisplayArgs(
+ args: DisplayArgs
+): args is KeyedDisplayArgs {
+ return args.length > 1 && typeof args[0] === 'string';
+}
+
+export function normalizeDisplayArgs(args: DisplayArgs) {
+ const manualKey = isKeyedDisplayArgs(args) ? args[0] : undefined;
+ const restArgs = isKeyedDisplayArgs(args)
+ ? (args.slice(1) as UnkeyedDisplayArgs)
+ : args;
+ const [wrappers, node] = tail(restArgs);
+ return {
+ manualKey,
+ wrappers,
+ node,
+ };
+}
+
+export function useReactlitDisplay({
+ renderError,
+}: Pick, 'renderError'>) {
+ const [renderState, setRenderState] = useState({
+ position: 0,
+ elements: [],
+ });
+
+ const display = useCallback['display']>(
+ (...args: DisplayArgs) => {
+ const { manualKey, wrappers, node } = normalizeDisplayArgs(args);
+
+ setRenderState(({ position, elements }) => {
+ const key = manualKey ?? `${position}`;
+ const keyIndex = elements
+ .slice(0, position)
+ .findIndex(([k]) => manualKey && k === manualKey);
+
+ const element = (
+
+
+ {node}
+
+
+ );
+ const newEntry = [key, element] as [string, React.ReactNode];
+
+ if (keyIndex !== -1) {
+ return {
+ position,
+ elements: [
+ ...elements.slice(0, keyIndex),
+ newEntry,
+ ...elements.slice(keyIndex + 1),
+ ],
+ };
+ } else if (position < elements.length) {
+ return {
+ position: position + 1,
+ elements: [
+ ...elements.slice(0, position),
+ newEntry,
+ // for manual keys that haven't been found by the above case
+ // we don't want to overwrite the index element because
+ // it's likely a different element
+ ...elements
+ .slice(position + (manualKey ? 0 : 1))
+ .filter((e) => !manualKey || e[0] !== manualKey),
+ ],
+ };
+ } else {
+ return {
+ position: elements.length + 1,
+ elements: [...elements, newEntry],
+ };
+ }
+ });
+ },
+ [setRenderState, renderError]
+ );
+
+ const resetRenderPosition = useCallback(() => {
+ setRenderState(({ elements }) => ({ elements, position: 0 }));
+ }, [setRenderState]);
+
+ // truncates stranded elements after the last position
+ const finalizeRender = useCallback(() => {
+ setRenderState(({ elements, position }) => ({
+ position,
+ elements: elements.slice(0, position),
+ }));
+ }, [setRenderState]);
+
+ return {
+ renderState,
+ display,
+ resetRenderPosition,
+ finalizeRender,
+ };
+}
diff --git a/libs/core/src/builtins/internal-state.ts b/libs/core/src/builtins/internal-state.ts
new file mode 100644
index 0000000..6465651
--- /dev/null
+++ b/libs/core/src/builtins/internal-state.ts
@@ -0,0 +1,25 @@
+import { useState } from 'react';
+import { useDeepMemo } from '../hooks/use-deep-memo';
+import { ReactlitStateSetter, StateBase } from './types';
+
+export interface InternalReactlitState {
+ state: T;
+ setState: ReactlitStateSetter;
+ previousState: T;
+ setPreviousState: React.Dispatch>;
+}
+
+export function useInternalReactlitState(
+ rawState: T,
+ setState: ReactlitStateSetter
+): InternalReactlitState {
+ const state = useDeepMemo(() => rawState, [rawState]);
+ const [previousState, setPreviousState] = useState(state);
+
+ return {
+ state,
+ setState,
+ previousState,
+ setPreviousState,
+ };
+}
diff --git a/libs/core/src/builtins/set.ts b/libs/core/src/builtins/set.ts
new file mode 100644
index 0000000..8c4a192
--- /dev/null
+++ b/libs/core/src/builtins/set.ts
@@ -0,0 +1,22 @@
+import { useCallback } from 'react';
+import { deepEqual } from '../utils/deep-equal';
+import { ReactlitContext, StateBase } from './types';
+import { InternalReactlitState } from './internal-state';
+
+export function useReactlitSet({
+ state,
+ setState,
+ setPreviousState,
+}: InternalReactlitState) {
+ return useCallback['set']>(
+ (key, value) => {
+ setState(key, (prev) => {
+ if (deepEqual(prev, value)) return prev;
+ return value;
+ });
+ setPreviousState(state);
+ return value;
+ },
+ [setState, state, setPreviousState]
+ );
+}
diff --git a/libs/core/src/builtins/types.ts b/libs/core/src/builtins/types.ts
new file mode 100644
index 0000000..e7dca15
--- /dev/null
+++ b/libs/core/src/builtins/types.ts
@@ -0,0 +1,65 @@
+import { Dispatch, ReactNode, SetStateAction } from 'react';
+import { DisplayArgs } from './display';
+import { ViewArgs } from './view';
+
+export type StateBase = Record;
+
+export interface ViewComponentProps {
+ stateKey: string;
+ value: T;
+ setValue: Dispatch;
+ display: (...args: DisplayArgs) => void;
+ view: (
+ ...args: ViewArgs
+ ) => R;
+}
+
+export type ViewComponent = React.FC>;
+
+export interface ViewDefinition {
+ component: ViewComponent;
+ getReturnValue?: (props: ViewComponentProps) => ReturnType;
+}
+
+export interface ReactlitContext {
+ view: (...args: ViewArgs) => R;
+ set: (key: K, value: T[K]) => T[K];
+ display: (...args: DisplayArgs) => void;
+ changed: (...keys: (keyof T)[]) => boolean;
+ trigger: () => void;
+ state: T;
+}
+
+export type ReactlitStateSetter = (
+ key: K,
+ value: SetStateAction
+) => void;
+
+export type ReactlitFunction<
+ T extends StateBase = any,
+ C extends ReactlitContext = ReactlitContext
+> = (ctx: C) => Promise;
+
+export type ReactlitProps = {
+ state?: T;
+ setState?: ReactlitStateSetter;
+ /**
+ * Render function to display a loading message
+ */
+ renderLoading?: (rendering: boolean) => ReactNode;
+ /**
+ * Render function to display an error message
+ */
+ renderError?: (props: {
+ error: any;
+ resetErrorBoundary: (...args: any[]) => void;
+ }) => ReactNode;
+ /**
+ * Whether to log debug messages to the console
+ */
+ debug?: boolean;
+ /**
+ * Function for the Reactlit rendering logic
+ */
+ children: ReactlitFunction;
+};
diff --git a/libs/core/src/builtins/view.ts b/libs/core/src/builtins/view.ts
new file mode 100644
index 0000000..7c0259e
--- /dev/null
+++ b/libs/core/src/builtins/view.ts
@@ -0,0 +1,74 @@
+import { useCallback } from 'react';
+import {
+ ReactlitContext,
+ StateBase,
+ ViewComponent,
+ ViewComponentProps,
+ ViewDefinition,
+} from './types';
+import { Wrapper } from '../wrappers';
+import { tail } from '../utils/tail';
+
+export function defineView(
+ component: ViewComponent
+): ViewDefinition {
+ return { component };
+}
+
+export function defineTransformView(
+ component: ViewComponent,
+ getReturnValue: (props: ViewComponentProps) => ReturnType
+): ViewDefinition {
+ return { component, getReturnValue };
+}
+
+export type ViewArgs = [
+ key: K,
+ ...wrappers: Wrapper[],
+ def: ViewDefinition
+];
+
+export function normalizeViewArgs<
+ T extends StateBase,
+ K extends keyof T & string,
+ V,
+ R
+>(args: ViewArgs) {
+ const [key, ...restArgs] = args;
+ const [wrappers, def] = tail(restArgs);
+ return { key, wrappers, def };
+}
+
+function makeViewFunction({
+ set,
+ display,
+ state,
+}: Pick, 'set' | 'display' | 'state'>) {
+ return function view(
+ ...args: ViewArgs
+ ) {
+ const { key, wrappers, def } = normalizeViewArgs(args);
+ const { component, getReturnValue } = def;
+ const props: ViewComponentProps = {
+ stateKey: key,
+ value: state[key] as V,
+ setValue: (value: any) => set(key, value),
+ display,
+ view,
+ };
+ display(key, ...wrappers, component(props));
+ return getReturnValue ? getReturnValue(props) : (state[key] as R);
+ };
+}
+
+export function useReactlitView({
+ set,
+ display,
+ state,
+}: Pick, 'set' | 'display' | 'state'>) {
+ return useCallback['view']>(
+ (...args: ViewArgs) =>
+ makeViewFunction({ set, display, state })(...args),
+ [state, set, display]
+ );
+}
diff --git a/libs/core/src/hooks/use-reactlit.spec.tsx b/libs/core/src/hooks/use-reactlit.spec.tsx
index fe6206d..c86bdb3 100644
--- a/libs/core/src/hooks/use-reactlit.spec.tsx
+++ b/libs/core/src/hooks/use-reactlit.spec.tsx
@@ -1,48 +1,32 @@
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
-import { useReactlit } from './use-reactlit';
-import { LayoutPlugin, makeLayoutPlugin } from '../plugins/layout';
+import { render, screen } from '@testing-library/react';
+import { definePlugin, useReactlit } from './use-reactlit';
import { useReactlitState } from './use-reactlit-state';
-import { defineLayout } from '../plugins/layout';
-import { TextInput } from '../reactlit.spec';
-
-export const TwoColumnLayout = defineLayout(2, ({ slots: [Slot1, Slot2] }) => (
-
-));
test('reactlit plugin', async () => {
+ let pluginRan = false;
+ const TestPlugin = definePlugin(({ display }) => ({
+ runPlugin: () => {
+ pluginRan = true;
+ return display('Hello from plugin');
+ },
+ }));
+
function PluginTest() {
const [state, setState] = useReactlitState({
firstName: 'John',
lastName: 'Doe',
});
- const Reactlit = useReactlit(LayoutPlugin);
+ const Reactlit = useReactlit(TestPlugin);
return (
{async (ctx) => {
- const [col1, col2] = ctx.layout(TwoColumnLayout);
- col1.view('firstName', TextInput);
- col1.display(`First Name: ${state.firstName}`);
- col2.display(`Last Name: ${state.lastName}`);
+ ctx.runPlugin();
}}
);
}
render( );
- expect(await screen.findByText('First Name: John')).toBeVisible();
- await userEvent.clear(screen.getByPlaceholderText('Enter firstName'));
- await userEvent.type(
- screen.getByPlaceholderText('Enter firstName'),
- 'Joseph'
- );
- expect(await screen.findByText('First Name: Joseph')).toBeVisible();
- expect(await screen.findByText('Last Name: Doe')).toBeVisible();
+ expect(pluginRan).toBe(true);
+ expect(await screen.findByText('Hello from plugin')).toBeVisible();
});
diff --git a/libs/core/src/hooks/use-reactlit.tsx b/libs/core/src/hooks/use-reactlit.tsx
index 3d8b2b6..06bae6a 100644
--- a/libs/core/src/hooks/use-reactlit.tsx
+++ b/libs/core/src/hooks/use-reactlit.tsx
@@ -1,11 +1,11 @@
import { useCallback, useMemo } from 'react';
+import { Reactlit } from '../reactlit';
import {
- Reactlit,
ReactlitContext,
ReactlitFunction,
ReactlitProps,
StateBase,
-} from '../reactlit';
+} from '../builtins/types';
export type ReactlitPlugin = (ctx: ReactlitContext) => C;
diff --git a/libs/core/src/index.ts b/libs/core/src/index.ts
index e03c77b..76fd3bb 100644
--- a/libs/core/src/index.ts
+++ b/libs/core/src/index.ts
@@ -1,10 +1,11 @@
export * from './reactlit';
+export * from './builtins/types';
+export { defineView, defineTransformView } from './builtins/view';
export * from './hooks/use-reactlit';
export * from './hooks/use-reactlit-state';
-export * from './utils/apply-wrapper';
-export * from './inputs/form.input';
+export * from './wrappers';
+export * from './inputs/layout.view';
export * from './plugins/data-fetching';
-export * from './plugins/layout';
// see https://github.com/mdx-js/mdx/issues/2487
import type { JSX as Jsx } from 'react/jsx-runtime';
diff --git a/libs/core/src/inputs/form.input.tsx b/libs/core/src/inputs/form.input.tsx
deleted file mode 100644
index be3e9c1..0000000
--- a/libs/core/src/inputs/form.input.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { Fragment, ReactNode, SetStateAction, useMemo } from 'react';
-import { defineView, ViewDefinition } from '../reactlit';
-import { ViewComponentProps } from '../reactlit';
-import { applyWrapper, Wrapper } from '../utils/apply-wrapper';
-import { isSetStateFunction } from '../hooks/use-reactlit-state';
-
-export type FormDefMap = {
- [K in keyof T]: ViewDefinition;
-};
-
-export interface FormInputProps {
- form: FormDefMap;
- wrapper?: Wrapper;
-}
-
-export function FormInputViewComponent({
- form,
- value,
- stateKey,
- setValue,
- wrapper,
-}: FormInputProps & ViewComponentProps) {
- const views = useMemo(() => {
- const views: ReactNode[] = [];
- for (const key in form) {
- const def = form[key];
- const props = {
- stateKey: `${stateKey}.${key}`,
- value: value?.[key],
- setValue: (v: SetStateAction) =>
- setValue({
- ...value,
- [key]: isSetStateFunction(v) ? v(value?.[key]) : v,
- }),
- };
- views.push({def.component(props)} );
- }
- return views;
- }, [form, value, setValue, stateKey]);
- return applyWrapper(<>{views}>, wrapper);
-}
-
-export function FormInput(
- form: FormDefMap,
- props?: Omit, 'form'>
-) {
- return defineView((viewProps) => (
-
- ));
-}
diff --git a/libs/core/src/inputs/layout.view.tsx b/libs/core/src/inputs/layout.view.tsx
new file mode 100644
index 0000000..80888e2
--- /dev/null
+++ b/libs/core/src/inputs/layout.view.tsx
@@ -0,0 +1,135 @@
+import { Fragment, useEffect, useRef } from 'react';
+import { normalizeDisplayArgs } from '../builtins/display';
+import {
+ ReactlitContext,
+ StateBase,
+ ViewComponentProps,
+} from '../builtins/types';
+import {
+ defineTransformView,
+ normalizeViewArgs,
+ ViewArgs,
+} from '../builtins/view';
+import tunnel from '../utils/tunnel';
+import { applySimpleWrapper, SimpleWrapper, Wrapper } from '../wrappers';
+
+export type Tunnel = ReturnType;
+
+export type Repeat<
+ T,
+ C extends number,
+ Result extends T[] = [],
+ Counter extends any[] = []
+> = Counter['length'] extends C
+ ? Result
+ : Repeat;
+
+export type LayoutSlot = Pick<
+ ReactlitContext,
+ 'display' | 'view'
+>;
+
+// during initialization we create empty layout slots, these are only temporary
+// until the state gets set up
+export function createEmptyLayoutSlot<
+ T extends StateBase = StateBase
+>(): LayoutSlot {
+ return {
+ display: () => <>>,
+ view(...args: ViewArgs) {
+ return undefined as R;
+ },
+ };
+}
+
+export function createLayoutSlot(
+ ctx: Pick, 'display' | 'view'>,
+ t: ReturnType
+): LayoutSlot {
+ const TunnelWrapper: Wrapper = ({ stateKey, children }) => {
+ return {children} ;
+ };
+ return {
+ display(...args) {
+ const { manualKey, wrappers, node } = normalizeDisplayArgs(args);
+ if (manualKey) {
+ ctx.display(manualKey, TunnelWrapper, ...wrappers, node);
+ } else {
+ ctx.display(TunnelWrapper, ...wrappers, node);
+ }
+ },
+ view(...args: ViewArgs) {
+ const { key, wrappers, def } = normalizeViewArgs(args);
+ return ctx.view(key, TunnelWrapper, ...wrappers, def);
+ },
+ };
+}
+
+export function LayoutViewComponent({
+ slots,
+ value,
+ setValue,
+ slotWrapper,
+}: {
+ slots: N;
+ slotWrapper?: SimpleWrapper;
+} & ViewComponentProps>) {
+ const tunnels = useRef([]);
+ useEffect(() => {
+ if (tunnels.current.length !== slots) {
+ tunnels.current = Array.from({ length: slots }, () => tunnel());
+ setValue(tunnels.current as Repeat);
+ }
+ }, [slots, setValue]);
+ if (!value) return null;
+ return (
+ <>
+ {(value as Tunnel[])
+ .map((t) => t.Out)
+ .map((Slot, index) => (
+
+ {applySimpleWrapper( , slotWrapper)}
+
+ ))}
+ >
+ );
+}
+
+export type LayoutViewType = Repeat | undefined;
+
+export function defaultLayoutState(
+ slots: N
+): LayoutViewType {
+ return undefined;
+}
+
+export function LayoutView(
+ slots: N,
+ slotWrapper?: SimpleWrapper
+) {
+ return defineTransformView<
+ Repeat | undefined,
+ Repeat, N>
+ >(
+ (viewProps) => (
+
+ ),
+ ({ value, display, view }) => {
+ const tunnels = (value ?? []) as Tunnel[];
+ if (tunnels.length !== slots) {
+ return Array.from({ length: slots }, createEmptyLayoutSlot) as Repeat<
+ LayoutSlot,
+ N
+ >;
+ }
+ const subContext = tunnels.map((t) =>
+ createLayoutSlot({ display, view }, t)
+ );
+ return subContext as Repeat, N>;
+ }
+ );
+}
diff --git a/libs/core/src/plugins/layout.tsx b/libs/core/src/plugins/layout.tsx
deleted file mode 100644
index c9bd53b..0000000
--- a/libs/core/src/plugins/layout.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import tunnel from 'tunnel-rat';
-import { ReactlitContext, StateBase, ViewDefinition } from '../reactlit';
-
-type Repeat<
- T,
- C extends number,
- Result extends T[] = [],
- Counter extends any[] = []
-> = Counter['length'] extends C
- ? Result
- : Repeat;
-
-export type LayoutSlot = Pick<
- ReactlitContext,
- 'display' | 'view'
->;
-
-type SlotComponent = React.ComponentType<{}>;
-
-type Tunnel = ReturnType;
-
-export type LayoutComponent = React.FC<{
- slots: Repeat;
-}>;
-
-type LayoutDefinition = {
- layout: React.ReactNode;
- tunnels: Repeat;
-};
-
-export function defineLayout(
- slots: N,
- component: LayoutComponent
-): LayoutDefinition {
- const tunnels = Array.from({ length: slots }, () => tunnel());
- return {
- layout: component({
- slots: tunnels.map((t) => t.Out) as Repeat,
- }),
- tunnels: tunnels as Repeat,
- };
-}
-
-export interface LayoutPluginContext {
- layout(
- definition: LayoutDefinition
- ): Repeat, N>;
-}
-
-function createLayoutSlot(
- ctx: ReactlitContext,
- t: ReturnType
-): LayoutSlot {
- return {
- display(...args) {
- const node = args.length === 1 ? args[0] : args[1];
- const manualKey = args.length === 1 ? undefined : args[0];
- const wrappedNode = {node} ;
- const passArgs =
- manualKey === undefined
- ? ([wrappedNode] as const)
- : ([manualKey, wrappedNode] as const);
- ctx.display(...passArgs);
- },
- view(key: K, def: ViewDefinition) {
- return ctx.view(key, {
- ...def,
- component: (props) => {
- return {def.component(props)} ;
- },
- });
- },
- };
-}
-
-export function makeLayoutPlugin(ctx: ReactlitContext) {
- return function layout({
- layout,
- tunnels,
- }: LayoutDefinition): Repeat, N> {
- const inputSlots: LayoutSlot[] = [];
- for (const t of tunnels as Tunnel[]) {
- inputSlots.push(createLayoutSlot(ctx, t));
- }
- ctx.display(layout);
- return inputSlots as Repeat, N>;
- };
-}
-
-export function LayoutPlugin(
- ctx: ReactlitContext
-) {
- return {
- layout: makeLayoutPlugin(ctx),
- };
-}
diff --git a/libs/core/src/reactlit.tsx b/libs/core/src/reactlit.tsx
index d587f1b..b09209e 100644
--- a/libs/core/src/reactlit.tsx
+++ b/libs/core/src/reactlit.tsx
@@ -1,114 +1,19 @@
import {
- Dispatch,
Fragment,
- ReactNode,
- SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
-import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
-import { useDeepMemo } from './hooks/use-deep-memo';
-import { useReactlitState } from './hooks/use-reactlit-state';
-import { deepEqual } from './utils/deep-equal';
-import { applyWrapper, Wrapper } from './utils/apply-wrapper';
-
-export interface ViewComponentProps {
- stateKey: string;
- value: T;
- setValue: Dispatch;
-}
-
-export type ViewComponent = React.FC>;
-
-export interface ViewDefinition {
- component: ViewComponent;
- getReturnValue?: (props: ViewComponentProps) => ReturnType;
-}
-
-export function defineView(
- component: ViewComponent
-): ViewDefinition {
- return { component };
-}
-
-export function defineTransformView(
- component: ViewComponent,
- getReturnValue: (props: ViewComponentProps) => ReturnType
-): ViewDefinition {
- return { component, getReturnValue };
-}
-
-export type StateBase = Record;
-
-export type DisplayArgs = [string, ReactNode] | [ReactNode];
-
-export interface ReactlitContext {
- view: (
- key: K,
- def: ViewDefinition
- ) => R;
- set: (key: K, value: T[K]) => T[K];
- display: (...args: DisplayArgs) => void;
- changed: (...keys: (keyof T)[]) => boolean;
- trigger: () => void;
- state: T;
-}
-export type ReactlitFunction<
- T extends StateBase = any,
- C extends ReactlitContext = ReactlitContext
-> = (ctx: C) => Promise;
-
-export type ReactlitStateSetter = (
- key: K,
- value: SetStateAction
-) => void;
-
-export type ReactlitProps = {
- state?: T;
- setState?: ReactlitStateSetter;
- /**
- * Render function to display a loading message
- */
- renderLoading?: (rendering: boolean) => ReactNode;
- /**
- * Render function to display an error message
- */
- renderError?: (props: {
- error: any;
- resetErrorBoundary: (...args: any[]) => void;
- }) => ReactNode;
- /**
- * Whether to log debug messages to the console
- */
- debug?: boolean;
- /**
- * Wrapper to apply around all displayed elements
- */
- wrapper?: Wrapper;
- /**
- * Function for the Reactlit rendering logic
- */
- children: ReactlitFunction;
-};
-
-function deltas(
- state: T,
- previousState: T | undefined
-): (keyof T)[] {
- return Object.keys(state).filter(
- (key: string) =>
- !previousState || !deepEqual(state[key], previousState[key])
- );
-}
-
-interface DisplayState {
- position: number;
- elements: [string, React.ReactNode][];
-}
+import { useReactlitChanged } from './builtins/changed';
+import { useReactlitDisplay } from './builtins/display';
+import { useInternalReactlitState } from './builtins/internal-state';
+import { useReactlitSet } from './builtins/set';
+import { ReactlitProps, StateBase } from './builtins/types';
+import { useReactlitView } from './builtins/view';
+import { useReactlitState } from './hooks/use-reactlit-state';
const defaultRenderError = ({ error }) => (
@@ -123,125 +28,18 @@ export function Reactlit
({
renderError = defaultRenderError,
debug,
children,
- wrapper,
}: ReactlitProps) {
const [defaultRawState, defaultSetState] = useReactlitState({} as T);
rawState = rawState ?? defaultRawState;
setState = setState ?? defaultSetState;
- const [renderState, setRenderState] = useState({
- position: 0,
- elements: [],
- });
- const state = useDeepMemo(() => rawState, [rawState]);
- const [previousState, setPreviousState] = useState(state);
+ const internalState = useInternalReactlitState(rawState, setState);
- const set = useCallback['set']>(
- (key, value) => {
- setState(key, (prev) => {
- if (deepEqual(prev, value)) return prev;
- return value;
- });
- setPreviousState(state);
- return value;
- },
- [setState, state, setPreviousState]
- );
-
- const display = useCallback['display']>(
- (...args: DisplayArgs) => {
- const node = args.length === 1 ? args[0] : args[1];
- const manualKey = args.length === 1 ? undefined : args[0];
- setRenderState(({ position, elements }) => {
- const key = manualKey ?? `${position}`;
- const keyIndex = elements
- .slice(0, position)
- .findIndex(([k]) => manualKey && k === manualKey);
- const element = (
-
- {applyWrapper(node, wrapper)}
-
- );
- const newEntry = [key, element] as [string, React.ReactNode];
-
- if (keyIndex !== -1) {
- return {
- position,
- elements: [
- ...elements.slice(0, keyIndex),
- newEntry,
- ...elements.slice(keyIndex + 1),
- ],
- };
- } else if (position < elements.length) {
- return {
- position: position + 1,
- elements: [
- ...elements.slice(0, position),
- newEntry,
- // for manual keys that haven't been found by the above case
- // we don't want to overwrite the index element because
- // it's likely a different element
- ...elements
- .slice(position + (manualKey ? 0 : 1))
- .filter((e) => !manualKey || e[0] !== manualKey),
- ],
- };
- } else {
- return {
- position: elements.length + 1,
- elements: [...elements, newEntry],
- };
- }
- });
- },
- [setRenderState, renderError, wrapper]
- );
-
- const view = useCallback['view']>(
- (
- key: K,
- { component, getReturnValue }: ViewDefinition
- ) => {
- const value = state[key] as V;
- const props: ViewComponentProps = {
- stateKey: key,
- value,
- setValue: (value: any) => set(key, value),
- };
- display(key, component(props));
- return getReturnValue ? getReturnValue(props) : (state[key] as R);
- },
- [state, set, display]
- );
-
- const changed = useCallback['changed']>(
- (...keys) => {
- const changedKeys = deltas(state, previousState);
- const selectedChangedKeys = keys.filter((k) =>
- changedKeys.includes(k as string)
- );
- const isChanged = selectedChangedKeys.length > 0;
- if (isChanged) {
- if (debug) {
- for (const k of selectedChangedKeys) {
- // eslint-disable-next-line no-console
- console.debug(
- `changed ${String(k)}: ${previousState?.[k]} -> ${state[k]}`
- );
- }
- }
- setPreviousState((prev) => {
- let newState = prev;
- for (const k of selectedChangedKeys) {
- newState = { ...newState, [k]: state[k] };
- }
- return newState;
- });
- }
- return isChanged;
- },
- [state, previousState, debug]
- );
+ const { state } = internalState;
+ const set = useReactlitSet(internalState);
+ const changed = useReactlitChanged(internalState, debug);
+ const { renderState, resetRenderPosition, finalizeRender, display } =
+ useReactlitDisplay({ renderError });
+ const view = useReactlitView({ set, display, state });
const [triggerCounter, setTriggerCounter] = useState(0);
const trigger = useCallback(() => {
@@ -268,7 +66,7 @@ export function Reactlit({
try {
// eslint-disable-next-line no-console
debug && console.debug('reactlit rendering:', childArgs.state);
- setRenderState(({ elements }) => ({ elements, position: 0 }));
+ resetRenderPosition();
await children(childArgs);
} catch (e: any) {
// eslint-disable-next-line no-console
@@ -279,10 +77,7 @@ export function Reactlit({
)
);
} finally {
- setRenderState(({ elements, position }) => ({
- position,
- elements: elements.slice(0, position),
- }));
+ finalizeRender();
renderLock.current = false;
if (renderAfter.current) {
renderAfter.current = false;
@@ -300,7 +95,8 @@ export function Reactlit({
trigger,
display,
setRendering,
- setRenderState,
+ resetRenderPosition,
+ finalizeRender,
renderError,
debug,
]);
diff --git a/libs/core/src/utils/apply-wrapper.tsx b/libs/core/src/utils/apply-wrapper.tsx
deleted file mode 100644
index 0ae99c0..0000000
--- a/libs/core/src/utils/apply-wrapper.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-export type Wrapper = (props: { children: React.ReactNode }) => React.ReactNode;
-
-export function applyWrapper(node: React.ReactNode, Wrapper?: Wrapper) {
- return Wrapper ? {node} : node;
-}
-
-export function combineWrappers(...wrappers: Wrapper[]): Wrapper {
- return ({ children }) =>
- wrappers.reduce((acc, Wrapper) => {acc} , children);
-}
diff --git a/libs/core/src/utils/tail.ts b/libs/core/src/utils/tail.ts
new file mode 100644
index 0000000..c622e6a
--- /dev/null
+++ b/libs/core/src/utils/tail.ts
@@ -0,0 +1,3 @@
+export function tail(args: [...T, U]): [T, U] {
+ return [args.slice(0, -1) as T, args.at(-1) as U];
+}
diff --git a/libs/core/src/utils/tunnel.tsx b/libs/core/src/utils/tunnel.tsx
new file mode 100644
index 0000000..3be4fda
--- /dev/null
+++ b/libs/core/src/utils/tunnel.tsx
@@ -0,0 +1,75 @@
+import React, { Fragment } from 'react';
+import { create, StoreApi } from 'zustand';
+import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
+
+// modified from tunnel-rat
+
+type Props = { childKey: string; children: React.ReactNode };
+
+type State = {
+ current: Array<{ childKey: string; node: React.ReactNode }>;
+ version: number;
+ set: StoreApi['setState'];
+};
+
+export default function tunnel() {
+ const useStore = create((set) => ({
+ current: new Array<{ childKey: string; node: React.ReactNode }>(),
+ version: 0,
+ set,
+ }));
+
+ return {
+ In: ({ childKey, children }: Props) => {
+ const set = useStore((state) => state.set);
+ const version = useStore((state) => state.version);
+
+ /* When this component mounts, we increase the store's version number.
+ This will cause all existing rats to re-render (just like if the Out component
+ were mapping items to a list.) The re-rendering will cause the final
+ order of rendered components to match what the user is expecting. */
+ useIsomorphicLayoutEffect(() => {
+ set((state) => ({
+ version: state.version + 1,
+ }));
+ }, []);
+
+ /* Any time the children _or_ the store's version number change, insert
+ the specified React children into the list of rats. */
+ useIsomorphicLayoutEffect(() => {
+ set(({ current }) => {
+ const existing = current.findIndex((c) => c.childKey === childKey);
+ return {
+ current:
+ existing !== -1
+ ? [
+ ...current.slice(0, existing),
+ { childKey, node: children },
+ ...current.slice(existing + 1),
+ ]
+ : [...current, { childKey, node: children }],
+ };
+ });
+
+ // remove the cleanup logic so that nodes stay in position, the key logic keeps things from getting too messy
+ // return () =>
+ // set(({ current }) => ({
+ // current: current.filter((c) => c.node !== children),
+ // }));
+ }, [children, version]);
+
+ return null;
+ },
+
+ Out: () => {
+ const current = useStore((state) => state.current);
+ return (
+ <>
+ {current.map((c) => (
+ {c.node}
+ ))}
+ >
+ );
+ },
+ };
+}
diff --git a/libs/core/src/utils/use-isomorphic-layout-effect.tsx b/libs/core/src/utils/use-isomorphic-layout-effect.tsx
new file mode 100644
index 0000000..00fb045
--- /dev/null
+++ b/libs/core/src/utils/use-isomorphic-layout-effect.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+// taken from tunnel-rat
+
+/**
+ * An SSR-friendly useLayoutEffect.
+ *
+ * React currently throws a warning when using useLayoutEffect on the server.
+ * To get around it, we can conditionally useEffect on the server (no-op) and
+ * useLayoutEffect elsewhere.
+ *
+ * @see https://github.com/facebook/react/issues/14927
+ */
+export const useIsomorphicLayoutEffect =
+ typeof window !== 'undefined' &&
+ (window.document?.createElement ||
+ window.navigator?.product === 'ReactNative')
+ ? React.useLayoutEffect
+ : React.useEffect;
+
+export function useMutableCallback(fn: T) {
+ const ref = React.useRef(fn);
+ useIsomorphicLayoutEffect(() => void (ref.current = fn), [fn]);
+ return ref;
+}
diff --git a/libs/core/src/wrappers.tsx b/libs/core/src/wrappers.tsx
new file mode 100644
index 0000000..7eff308
--- /dev/null
+++ b/libs/core/src/wrappers.tsx
@@ -0,0 +1,65 @@
+import {
+ cloneElement,
+ createElement,
+ Fragment,
+ isValidElement,
+ PropsWithChildren,
+ ReactElement,
+ ReactNode,
+} from 'react';
+
+export interface ReactlitWrapperProps {
+ position: number;
+ stateKey: string;
+}
+
+export type WrapperComponent = React.FC<
+ PropsWithChildren
+>;
+
+export type Wrapper = WrapperComponent | React.ReactElement;
+
+export type SimpleWrapperComponent = React.FC;
+
+export type SimpleWrapper = SimpleWrapperComponent | React.ReactElement;
+
+export function applySimpleWrapper(
+ children: ReactElement,
+ Wrap?: SimpleWrapper
+): ReactNode {
+ if (!Wrap) return children;
+ if (isValidElement(Wrap)) return cloneElement(Wrap, {}, children);
+ return Wrap({ children });
+}
+
+export function applyWrapper(
+ children: ReactElement,
+ Wrap?: Wrapper,
+ props?: ReactlitWrapperProps
+): ReactElement {
+ if (!Wrap) return children;
+ if (isValidElement(Wrap)) return cloneElement(Wrap, {}, children);
+ return Wrap({ children, ...props }) as ReactElement;
+ // return createElement(Wrap, props, children);
+}
+export function ApplyWrappers({
+ wrappers,
+ children,
+ ...props
+}: {
+ wrappers: Wrapper[];
+ children: ReactNode;
+} & ReactlitWrapperProps) {
+ const base = [...wrappers];
+ const wrappedContent = base.reverse().reduce(
+ (acc, W) => applyWrapper(acc, W, props),
+ // this extra Fragment wrapper at the end is necessary for some
+ // very mysterious reason to keep plain string nodes from shifting positions around
+ {children}
+ );
+ return wrappedContent;
+}
+
+export const FragmentWrapper: Wrapper = ({ children, stateKey }) => {
+ return {children} ;
+};
diff --git a/libs/radix/src/index.ts b/libs/radix/src/index.ts
index 3058839..ff151d2 100644
--- a/libs/radix/src/index.ts
+++ b/libs/radix/src/index.ts
@@ -10,3 +10,4 @@ export * from './inputs/text.input';
export * from './inputs/textarea.input';
export * from './inputs/switch.input';
export * from './radix-wrapper';
+export * from './label';
diff --git a/libs/radix/src/inputs.ts b/libs/radix/src/inputs.ts
index ef66426..040790f 100644
--- a/libs/radix/src/inputs.ts
+++ b/libs/radix/src/inputs.ts
@@ -1,4 +1,3 @@
-import { FormInput } from '@reactlit/core';
import { AsyncButton } from './inputs/async-button.input';
import { CheckInput } from './inputs/check.input';
import { RadioInput } from './inputs/radio.input';
@@ -17,7 +16,6 @@ export const Inputs = {
Switch: SwitchInput,
Radio: RadioInput,
Select: SelectInput,
- Form: FormInput,
Slider: SliderInput,
RangeSlider: RangeSliderInput,
AsyncButton: AsyncButton,
diff --git a/libs/radix/src/inputs/async-button.input.tsx b/libs/radix/src/inputs/async-button.input.tsx
index 88d885a..0df7c72 100644
--- a/libs/radix/src/inputs/async-button.input.tsx
+++ b/libs/radix/src/inputs/async-button.input.tsx
@@ -16,6 +16,8 @@ export const AsyncButtonViewComponent = ({
setValue,
value,
stateKey,
+ display,
+ view,
...props
}: AsyncButtonInputProps & ViewComponentProps) => {
return (
diff --git a/libs/radix/src/inputs/check.input.tsx b/libs/radix/src/inputs/check.input.tsx
index c45bbc6..ca03fbf 100644
--- a/libs/radix/src/inputs/check.input.tsx
+++ b/libs/radix/src/inputs/check.input.tsx
@@ -1,7 +1,7 @@
import { CheckboxGroup } from '@radix-ui/themes';
import { defineView, ViewComponentProps } from '@reactlit/core';
import { useMemo } from 'react';
-import { LabelType, renderLabel } from '../label';
+import { LabelType } from '../label';
export type CheckOptionsType = T[] | Record;
@@ -19,6 +19,8 @@ export const CheckInputComponent = ({
setValue,
label,
options,
+ display,
+ view,
...props
}: BaseCheckInputProps & ViewComponentProps) => {
const optionsEntries = useMemo(() => {
@@ -28,20 +30,13 @@ export const CheckInputComponent = ({
return Object.entries(options) as [string, T][];
}, [options]);
return (
- <>
- {renderLabel(label)}
-
- {optionsEntries.map(([label, value], i) => (
-
- {label}
-
- ))}
-
- >
+
+ {optionsEntries.map(([label, value], i) => (
+
+ {label}
+
+ ))}
+
);
};
diff --git a/libs/radix/src/inputs/radio.input.tsx b/libs/radix/src/inputs/radio.input.tsx
index 7d3c1ec..171a2dc 100644
--- a/libs/radix/src/inputs/radio.input.tsx
+++ b/libs/radix/src/inputs/radio.input.tsx
@@ -1,7 +1,7 @@
import { RadioGroup } from '@radix-ui/themes';
import { defineView, ViewComponentProps } from '@reactlit/core';
import { useMemo } from 'react';
-import { LabelType, renderLabel } from '../label';
+import { LabelType } from '../label';
export type RadioOptionsType = T[] | Record;
@@ -23,6 +23,8 @@ export const RadioInputComponent = ({
setValue,
label,
options,
+ display,
+ view,
...props
}: BaseRadioInputProps & ViewComponentProps) => {
const optionsEntries = useMemo(() => {
@@ -32,16 +34,13 @@ export const RadioInputComponent = ({
return Object.entries(options) as [string, T][];
}, [options]);
return (
- <>
- {renderLabel(label)}
-
- {optionsEntries.map(([label, value], i) => (
-
- {label}
-
- ))}
-
- >
+
+ {optionsEntries.map(([label, value], i) => (
+
+ {label}
+
+ ))}
+
);
};
diff --git a/libs/radix/src/inputs/select.input.tsx b/libs/radix/src/inputs/select.input.tsx
index 5ffa192..fc96ba9 100644
--- a/libs/radix/src/inputs/select.input.tsx
+++ b/libs/radix/src/inputs/select.input.tsx
@@ -1,7 +1,7 @@
import { Select } from '@radix-ui/themes';
import { defineView, ViewComponentProps } from '@reactlit/core';
import { useMemo } from 'react';
-import { LabelType, renderLabel } from '../label';
+import { LabelType } from '../label';
export type SelectOptionsType = T[] | Record;
@@ -20,6 +20,8 @@ export const SelectInputComponent = ({
setValue,
label,
options,
+ display,
+ view,
...props
}: BaseSelectInputProps & ViewComponentProps) => {
const optionsEntries = useMemo(() => {
@@ -29,19 +31,16 @@ export const SelectInputComponent = ({
return Object.entries(options) as [string, T][];
}, [options]);
return (
- <>
- {renderLabel(label)}
-
-
-
- {optionsEntries.map(([label, value], i) => (
-
- {label}
-
- ))}
-
-
- >
+
+
+
+ {optionsEntries.map(([label, value], i) => (
+
+ {label}
+
+ ))}
+
+
);
};
diff --git a/libs/radix/src/inputs/slider.input.tsx b/libs/radix/src/inputs/slider.input.tsx
index 2eacfcd..47c9ab3 100644
--- a/libs/radix/src/inputs/slider.input.tsx
+++ b/libs/radix/src/inputs/slider.input.tsx
@@ -1,6 +1,6 @@
import { Slider, SliderProps } from '@radix-ui/themes';
import { defineView, ViewComponentProps } from '@reactlit/core';
-import { LabelType, renderLabel } from '../label';
+import { LabelType } from '../label';
export type SliderInputProps = Omit & {
label?: LabelType;
@@ -12,19 +12,18 @@ export const SliderInputComponent = ({
setValue,
onChange,
label,
+ display,
+ view,
...props
}: SliderInputProps & ViewComponentProps) => {
return (
- <>
- {renderLabel(label)}
- {
- setValue(v[0]);
- }}
- {...props}
- />
- >
+ {
+ setValue(v[0]);
+ }}
+ {...props}
+ />
);
};
@@ -34,19 +33,18 @@ export const RangeSliderInputComponent = ({
setValue,
onChange,
label,
+ display,
+ view,
...props
}: SliderInputProps & ViewComponentProps<[number, number]>) => {
return (
- <>
- {renderLabel(label)}
- {
- setValue(v as [number, number]);
- }}
- {...props}
- />
- >
+ {
+ setValue(v as [number, number]);
+ }}
+ {...props}
+ />
);
};
diff --git a/libs/radix/src/inputs/switch.input.tsx b/libs/radix/src/inputs/switch.input.tsx
index e19b036..b692135 100644
--- a/libs/radix/src/inputs/switch.input.tsx
+++ b/libs/radix/src/inputs/switch.input.tsx
@@ -1,6 +1,6 @@
import { Switch, SwitchProps } from '@radix-ui/themes';
import { defineView, ViewComponentProps } from '@reactlit/core';
-import { LabelType, renderLabel } from '../label';
+import { LabelType } from '../label';
export type SwitchInputProps = Omit<
SwitchProps,
@@ -14,14 +14,11 @@ export const SwitchInputComponent = ({
stateKey,
setValue,
label,
+ display,
+ view,
...props
}: SwitchInputProps & ViewComponentProps) => {
- return (
- <>
- {renderLabel(label)}
-
- >
- );
+ return ;
};
export const SwitchInput = (props?: SwitchInputProps) =>
diff --git a/libs/radix/src/inputs/table.input.tsx b/libs/radix/src/inputs/table.input.tsx
index 1f5bcae..799c8b2 100644
--- a/libs/radix/src/inputs/table.input.tsx
+++ b/libs/radix/src/inputs/table.input.tsx
@@ -45,6 +45,8 @@ export function TableInputViewComponent({
getRowId,
maxHeight = '300px',
loading,
+ display,
+ view,
...props
}: TableInputProps & ViewComponentProps) {
const colDefs = useMemo[]>(() => {
diff --git a/libs/radix/src/inputs/text.input.tsx b/libs/radix/src/inputs/text.input.tsx
index 29160e2..f431a68 100644
--- a/libs/radix/src/inputs/text.input.tsx
+++ b/libs/radix/src/inputs/text.input.tsx
@@ -1,8 +1,8 @@
-import { Text, TextField } from '@radix-ui/themes';
+import { TextField } from '@radix-ui/themes';
import { defineView, ViewComponentProps } from '@reactlit/core';
import { isValidElement, ReactNode, useEffect, useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
-import { LabelType, renderLabel } from '../label';
+import { LabelType } from '../label';
export type TextInputProps = Omit & {
label?: LabelType;
@@ -20,6 +20,8 @@ export const TextInputComponent = ({
label,
debounceDelay = 200,
children,
+ display,
+ view,
...props
}: TextInputProps & ViewComponentProps) => {
const [rawValue, setRawValue] = useState(value ?? '');
@@ -30,28 +32,25 @@ export const TextInputComponent = ({
setRawValue(value ?? '');
}, [value]);
return (
-
- {renderLabel(label)}
- {
- setRawValue(e.target.value);
- debouncedSetValue(e.target.value);
- onChange?.(e);
- }}
- {...props}
- >
- {isValidElement(children)
- ? (children as ReactNode)
- : typeof children === 'function'
- ? children({ value, stateKey, setValue })
- : undefined}
-
-
+ {
+ setRawValue(e.target.value);
+ debouncedSetValue(e.target.value);
+ onChange?.(e);
+ }}
+ {...props}
+ >
+ {isValidElement(children)
+ ? (children as ReactNode)
+ : typeof children === 'function'
+ ? children({ value, display, view, stateKey, setValue })
+ : undefined}
+
);
};
-export const TextInput = (props: TextInputProps) =>
+export const TextInput = (props?: TextInputProps) =>
defineView((viewProps) => (
));
diff --git a/libs/radix/src/inputs/textarea.input.tsx b/libs/radix/src/inputs/textarea.input.tsx
index c0c3e62..e5668f6 100644
--- a/libs/radix/src/inputs/textarea.input.tsx
+++ b/libs/radix/src/inputs/textarea.input.tsx
@@ -1,8 +1,8 @@
-import { Text, TextArea, TextAreaProps } from '@radix-ui/themes';
+import { TextArea, TextAreaProps } from '@radix-ui/themes';
import { defineView, ViewComponentProps } from '@reactlit/core';
-import { useDebouncedCallback } from 'use-debounce';
-import { LabelType, renderLabel } from '../label';
import { useEffect, useState } from 'react';
+import { useDebouncedCallback } from 'use-debounce';
+import { LabelType } from '../label';
export type TextAreaInputProps = Omit & {
label?: LabelType;
@@ -15,6 +15,8 @@ export const TextAreaInputComponent = ({
setValue,
onChange,
label,
+ display,
+ view,
debounceDelay = 300,
...props
}: TextAreaInputProps & ViewComponentProps) => {
@@ -26,18 +28,15 @@ export const TextAreaInputComponent = ({
setRawValue(value ?? '');
}, [value]);
return (
-
- {renderLabel(label)}
-
+