SolidJSS
SolidJS15mo ago
13 replies
agentsmith

Reactivity with shadcn-solid textfield

I created my own NumberInput using https://shadcn-solid.com/docs/components/textfield. I see they have a number field, but I wanted the more typical html number input.

My implementation has 4 number inputs that are clamped based on min/max values. I'm testing by manually typing -1 in the field. Technically a 1 and then I'm adding a - to it since it won't input otherwise.

The clamp works for 2 of the inputs, but fails for the other 2. It seems to be losing the reactivity somehow. I've tried adding createMemo around the value to ensure the reactivity is present although I don't believe that shouldn't be necessary since I'm passing the accessor. I might see about creating a minimal example for this, but for now here's the code for the NumberInput and the implementation using is.

import { createMemo } from 'solid-js'
import {
  TextField,
  TextFieldLabel,
  TextFieldDescription,
  TextFieldRoot,
} from '@/components/ui/textfield'

import type { Component, Accessor } from 'solid-js'
import type { JSX } from 'solid-js/jsx-runtime'

type NumberValue = number | Accessor<number>
type NumberSetter = ((v: number) => void) | ((prev: number) => number)

interface NumberInputProps {
  id: string
  label: JSX.Element
  info?: JSX.Element
  class?: string
  min?: number
  max?: number
  value?: NumberValue
  onInput?: NumberSetter
}

export const NumberInput: Component<NumberInputProps> = (props) => {
  // const getValue = (): string => {
  //   console.log('getValue:', props.value)
  //   if (props.value === undefined) return ''
  //   return typeof props.value === 'function' ? props.value().toString() : props.value.toString()
  // }
  const value = createMemo(() => {
    console.log('getValue:', typeof props.value === 'function' ? props.value() : props.value)
    if (props.value === undefined) return ''
    return typeof props.value === 'function' ? props.value().toString() : props.value.toString()
  })

  const clampValue = (value: number): number => {
    let clampedValue = value

    if (props.min !== undefined) {
      clampedValue = Math.max(props.min, clampedValue)
    }

    if (props.max !== undefined) {
      clampedValue = Math.min(props.max, clampedValue)
    }

    return clampedValue
  }

  const handleInput = (e: Event) => {
    const target = e.target as HTMLInputElement
    const newValue = clampValue(Number(target.value))
    console.log('setValue:', target.value, newValue)

    props.onInput?.(newValue)
  }

  return (
    <TextFieldRoot
      class={props.class ?? 'w-24'}
      // defaultValue={getValue()}
    >
      <TextFieldLabel for={props.id}>{props.label}</TextFieldLabel>
      <TextField
        id={props.id}
        type="number"
        min={props.min}
        max={props.max}
        value={value()}
        onInput={handleInput}
      />
      <TextFieldDescription>{props.info}</TextFieldDescription>
    </TextFieldRoot>
  )
}

export default NumberInput
Was this page helpful?