Skip to content
Merged
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
73 changes: 67 additions & 6 deletions apps/reactlit-examples/src/pages/hello-world-vanilla/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export default function HelloWorldVanilla() {
name: '',
pickedNumbers: [],
pickedColors: [],
chosenNumber: '',
chosenColor: '',
});
return (
<Reactlit state={appState} setState={setAppState}>
Expand All @@ -24,9 +26,14 @@ export default function HelloWorldVanilla() {
const picked = view(
'pickedNumbers',
Inputs.Check(['One', 'Two', 'Three'], {
className: 'border p-0.5 mr-1',
className: {
container: 'flex gap-2 items-center',
wrapper: 'flex gap-2',
item: {
input: 'border p-0.5 mr-1',
},
},
label: 'Pick any number',
containerClassName: 'flex gap-2',
})
);
display(<div>Picked: {picked.join(', ')}!</div>);
Expand All @@ -40,9 +47,14 @@ export default function HelloWorldVanilla() {
{ label: 'White', value: '#FFFFFF' },
],
{
className: 'border p-0.5 mr-1',
className: {
container: 'flex gap-2 items-center',
wrapper: 'flex gap-2',
item: {
input: 'border p-0.5 mr-1',
},
},
label: 'Pick any color',
containerClassName: 'flex gap-2',
valueof: (item) => item.value,
format: (item) => (
<span
Expand All @@ -56,12 +68,61 @@ export default function HelloWorldVanilla() {
{item.label}
</span>
),
keyof: (item) => item.label,
disabled: ['White'],
disabled: (item) => item.value === '#FFFFFF',
}
)
);
display(<div>Colors: {JSON.stringify(pickedColors)}!</div>);
const chosenNumber = view(
'chosenNumber',
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(<div>Chosen Number: {chosenNumber}!</div>);
const chosenColor = view(
'chosenColor',
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) => (
<span
style={{
color: item.value,
backgroundColor: '#aaa',
padding: '0rem 0.5rem',
borderRadius: '0.2rem',
}}
>
{item.label}
</span>
),
disabled: (item) => item.value === '#FFFFFF',
}
)
);
display(<div>Chosen Color: {JSON.stringify(chosenColor)}!</div>);
}}
</Reactlit>
);
Expand Down
2 changes: 2 additions & 0 deletions libs/vanilla/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { CheckInput } from './inputs/check.input';
import { RadioInput } from './inputs/radio.input';
import { TextInput } from './inputs/text.input';

export const Inputs = {
Text: TextInput,
Check: CheckInput,
Radio: RadioInput,
};
50 changes: 34 additions & 16 deletions libs/vanilla/src/inputs/check.input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,23 @@ export type CheckInputProps<T> = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
>,
'value' | 'disabled'
'value' | 'disabled' | 'className'
> & {
data: T[];
label?: string | React.ReactNode;
containerClassName?: string;
className?: {
container?: string;
wrapper?: string;
label?: string;
item?: {
wrapper?: string;
input?: string;
label?: string;
};
};
format?: (value: T) => string | React.ReactNode;
valueof?: (value: T) => string;
keyof?: (value: T) => string;
disabled?: string[];
disabled?: string[] | ((value: T) => boolean);
};

export const CheckInputComponent = <T,>({
Expand All @@ -28,43 +36,53 @@ export const CheckInputComponent = <T,>({
setValue,
onChange,
data,
containerClassName,
className,
format,
keyof,
valueof,
disabled,
label,
...props
}: CheckInputProps<T> & ViewComponentProps<(string | T)[]>) => {
return (
<div className="flex gap-2 items-center">
{label && <label htmlFor={stateKey}>{label}</label>}
<div className={containerClassName}>
<div className={className?.container}>
{label && (
<label htmlFor={stateKey} className={className?.label}>
{label}
</label>
)}
<div className={className?.wrapper}>
{data.map((item) => {
const isChecked = !!value.find((v) =>
valueof ? valueof(item) === v : item === v
);
const itemKey = `${stateKey}-${keyof?.(item) ?? item.toString()}`;
const itemKey = `${stateKey}-${valueof?.(item) ?? item.toString()}`;
let disabledValue = false;

if (typeof disabled === 'function') {
disabledValue = disabled(item);
} else if (Array.isArray(disabled)) {
disabledValue = disabled.includes(
valueof?.(item) ?? item.toString()
);
}

return (
<div key={itemKey}>
<div key={itemKey} className={className?.item?.wrapper}>
<input
type="checkbox"
checked={isChecked}
id={itemKey}
name={itemKey}
className={className?.item?.input}
onChange={(e) => {
const _value = valueof?.(item) ?? item;
if (e.target.checked) setValue([...value, _value]);
else setValue(value.filter((v) => v !== _value));
}}
disabled={
(keyof && disabled?.includes(keyof(item))) ||
disabled?.includes(valueof?.(item) ?? item.toString())
}
disabled={disabledValue}
{...props}
/>
<label htmlFor={itemKey}>
<label htmlFor={itemKey} className={className?.item?.label}>
{format?.(item) ?? item.toString()}
</label>
</div>
Expand Down
118 changes: 118 additions & 0 deletions libs/vanilla/src/inputs/radio.input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
defineTransformView,
defineView,
ViewComponentProps,
ViewDefinition,
} from '@reactlit/core';
import { DetailedHTMLProps } from 'react';

export type RadioInputProps<T> = Omit<
DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
>,
'value' | 'disabled' | 'className'
> & {
data: T[];
label?: string | React.ReactNode;
className?: {
container?: string;
wrapper?: string;
label?: string;
item?: {
wrapper?: string;
input?: string;
label?: string;
};
};
format?: (value: T) => string | React.ReactNode;
valueof?: (value: T) => string;
disabled?: string[] | ((value: T) => boolean);
};

export const RadioInputComponent = <T,>({
value,
stateKey,
setValue,
onChange,
data,
className,
format,
valueof,
disabled,
label,
...props
}: RadioInputProps<T> & ViewComponentProps<string | T>) => {
return (
<div className={className?.container}>
{label && (
<label htmlFor={stateKey} className={className?.label}>
{label}
</label>
)}
<div className={className?.wrapper}>
{data.map((item) => {
const isChecked = valueof ? valueof(item) === value : item === value;
const itemKey = `${stateKey}-${valueof?.(item) ?? item.toString()}`;
let disabledValue = false;

if (typeof disabled === 'function') {
disabledValue = disabled(item);
} else if (Array.isArray(disabled)) {
disabledValue = disabled.includes(
valueof?.(item) ?? item.toString()
);
}

return (
<div key={itemKey} className={className?.item?.wrapper}>
<input
type="radio"
checked={isChecked}
id={itemKey}
name={itemKey}
className={className?.item?.input}
onChange={(e) => {
const _value = valueof?.(item) ?? item;
if (e.target.checked) setValue(_value);
}}
disabled={disabledValue}
{...props}
/>
<label htmlFor={itemKey} className={className?.item?.label}>
{format?.(item) ?? item.toString()}
</label>
</div>
);
})}
</div>
</div>
);
};

export type RadioInputDefinition<T, P> = P extends { valueof: (v: T) => string }
? ViewDefinition<string | T, T>
: ViewDefinition<T>;

export const RadioInput = <T, P extends RadioInputProps<T>>(
data: P['data'],
{ valueof, ...props }: Omit<P, 'data'>
): RadioInputDefinition<T, P> => {
if (valueof) {
return defineTransformView<string | T, T>(
(viewProps) => (
<RadioInputComponent
data={data}
{...viewProps}
{...props}
valueof={valueof}
/>
),
({ value }) => data.find((d) => valueof(d) === value)
) as RadioInputDefinition<T, P>;
} else {
return defineView<T>((viewProps) => (
<RadioInputComponent data={data} {...viewProps} {...props} />
)) as RadioInputDefinition<T, P>;
}
};
Loading