Input Components Demo

This page demo component based input fields.

Source Code of demo-inputs.tsx
(import statements omitted for simplicity, click to expand)
import { Style } from '../components/style.js'
import { o } from '../jsx/jsx.js'
import debug from 'debug'
import { title } from '../../config.js'
import { Routes } from '../routes.js'
import Menu from '../components/menu.js'
import { Switch } from '../components/router.js'
import DemoSelect from './demo-inputs/demo-select.js'
import DemoSingleFieldForm from './demo-inputs/demo-single-field-form.js'
import SourceCode from '../components/source-code.js'
const log = debug('demo-single-field-form.tsx')
log.enabled = true

let style = Style(/* css */ `
#demo-inputs .code-demo {
  display: flex;
  flex-wrap: wrap;
}
`)

let content = (
  <div id="demo-inputs">
    <h1>Input Components Demo</h1>
    <p>This page demo component based input fields.</p>

    {style}
    <link rel="stylesheet" href="/lib/prism/prism.css" />
    <script src="/lib/prism/prism.js"></script>

    <SourceCode page="demo-inputs.tsx" />

    <p>
      The example code snippets below are simplified for illustration. You can
      click "Source Code of [page]" to see the complete source code.
    </p>

    <fieldset>
      <legend>
        <Menu
          routes={[
            { url: '/inputs/select', menuText: '<Select/>' },
            {
              url: '/inputs/single-field-form',
              menuText: 'newSingleFieldForm()',
            },
          ]}
          separator=" | "
        />
      </legend>
      {Switch({
        '/inputs': DemoSelect,
        '/inputs/select': DemoSelect,
        '/inputs/single-field-form': DemoSingleFieldForm.content,
      })}
    </fieldset>
  </div>
)

let routes = {
  '/inputs': {
    title: title('Demo input components'),
    description: 'Demonstrate component-based input fields',
    menuText: 'Inputs',
    menuMatchPrefix: true,
    node: content,
  },
  '/inputs/select': {
    title: title('Demo <Select/> components'),
    description:
      'Demonstrate building select and option elements with <Select/>',
    node: content,
  },
  '/inputs/single-field-form': {
    title: title('Demo single-field-form component creator'),
    description: 'Demonstrate per-field saving with realtime update',
    node: content,
  },
  ...DemoSingleFieldForm.routes,
} satisfies Routes

export default { routes }

The example code snippets below are simplified for illustration. You can click "Source Code of [page]" to see the complete source code.

newSingleFieldForm() is a creation function. It helps you build simple form with per-field saving mechanism.

Example Source Codelet Username = newSingleFieldForm({ action: '/profile/update-username', label: 'Username', name: 'username', updateValue(attrs, context) { user.username = attrs.input.username if (context.type === 'ws') { context.ws.send([ 'update-text', '#greet_name', attrs.input.username ]) } }, renderUpdate(attrs, context) { return content }, }) let FavoriteFruit = newSingleFieldForm({ action: '/profile/update-fruit', label: 'Favorite fruit', name: 'fruit', onchange: 'submitForm(this.form)', submitButton: false, updateParser: object({ fruit: id() }), updateValue(attrs, context) { user.favorite_fruit_id = attrs.input.fruit if (context.type === 'ws') { context.ws.send([ 'update-text', '#fruit_card', fruits[user.favorite_fruit_id - 1]?.text, ]) } }, renderUpdate(attrs, context) { return content }, }) function Profile() { return ( <> <p> Welcome back, <span id="greet_name">{user.username}</span>. </p> <Username.Form value={user.username} /> <FavoriteFruit.Form value={{ options: fruits, selected: user.favorite_fruit_id, }} /> <span id="fruit_card"> {fruits[user.favorite_fruit_id - 1]?.text} </span> </> ) } let routes = { '/profile': { title: title('Profile Page'), description: 'View and update your public profile', node: <Profile/> }, ...Username.routes, ...FavoriteFruit.routes, } satisfies Routes export default { routes, content }
Preview

Welcome back, alice

cherry

The update message id is auto incremented to avoid duplication among different fields created by newSingleFieldForm(). However, if the <field.Form/> is used multiple times on screen, e.g. in mapArray loop, you should supply updateMessageKeyName attribute to newSingleFieldForm() and key attribute to <field.Form/>. Example see List Editing Demo

The possibility is endless. You can extend or modify the page and components to suit your need.

Source Code of demo-inputs/demo-single-field-form.tsx
(import statements omitted for simplicity, click to expand)
import { color, id, object } from 'cast.ts'
import code from '../../components/inline-code.js'
import { newSingleFieldForm } from '../../components/single-field-form.js'
import SourceCode from '../../components/source-code.js'
import Style from '../../components/style.js'
import { o } from '../../jsx/jsx.js'
import { Routes } from '../../routes.js'
import { Link } from '../../components/router.js'
let style = Style(/* css */ `
#fruit_card {
	padding: 0.5rem;
	border-radius: 0.5rem;
}
`)

let fruits = ['apple', 'banana', 'cherry'].map((text, index) => ({
  value: index + 1,
  text,
}))

let user = {
  username: 'alice',
  favorite_fruit_id: 3,
  favorite_color: '#c0ffee',
}

let Username = newSingleFieldForm({
  action: '/inputs/single-field-form/update-username',
  label: 'Username',
  name: 'username',
  updateValue(attrs, context) {
    user.username = attrs.input.username
    if (context.type === 'ws') {
      context.ws.send(['update-text', '#greet_name', attrs.input.username])
    }
  },
  renderUpdate(attrs, context) {
    return content
  },
})

let FavoriteFruit = newSingleFieldForm({
  action: '/inputs/single-field-form/update-fruit',
  label: 'Favorite fruit',
  name: 'fruit',
  onchange: 'submitForm(this.form)',
  submitButton: false,
  updateParser: object({ fruit: id() }),
  updateValue(attrs, context) {
    user.favorite_fruit_id = attrs.input.fruit
    if (context.type === 'ws') {
      context.ws.send([
        'update-text',
        '#fruit_card',
        fruits[user.favorite_fruit_id - 1]?.text,
      ])
    }
  },
  renderUpdate(attrs, context) {
    return content
  },
})

let FavoriteColor = newSingleFieldForm({
  action: '/inputs/single-field-form/update-color',
  label: 'Favorite color',
  name: 'color',
  oninput: 'submitForm(this.form)',
  submitButton: false,
  updateParser: object({ color: color() }),
  updateValue(attrs, context) {
    user.favorite_color = attrs.input.color
    if (context.type === 'ws') {
      context.ws.send([
        'update-props',
        '#fruit_card',
        { style: getTextStyle(attrs.input.color) },
      ])
    }
  },
  renderUpdate(attrs, context) {
    return content
  },
})

function getTextStyle(color: string) {
  let bg = inverseColor(color)
  return `color:${color};background-color:${bg}`
}

function inverseColor(color: string) {
  return (
    '#' +
    toColorPart(color.slice(1, 3)) +
    toColorPart(color.slice(3, 5)) +
    toColorPart(color.slice(5, 7))
  )
}
function toColorPart(hex: string): string {
  let code = parseInt(hex, 16)
  return ((code + 127) % 256).toString(16).padStart(2, '0')
}

let content = (
  <>
    {style}

    <p>
      {code('newSingleFieldForm()')} is a creation function. It helps you build
      simple form with per-field saving mechanism.
    </p>

    <div class="code-demo">
      <fieldset>
        <legend>Example Source Code</legend>
        <code class="language-tsx" style="padding: 0.5rem">
          {`
let Username = newSingleFieldForm({
  action: '/profile/update-username',
  label: 'Username',
  name: 'username',
  updateValue(attrs, context) {
    user.username = attrs.input.username
    if (context.type === 'ws') {
      context.ws.send([
        'update-text',
        '#greet_name',
        attrs.input.username
      ])
    }
  },
  renderUpdate(attrs, context) {
    return content
  },
})

let FavoriteFruit = newSingleFieldForm({
  action: '/profile/update-fruit',
  label: 'Favorite fruit',
  name: 'fruit',
  onchange: 'submitForm(this.form)',
  submitButton: false,
  updateParser: object({ fruit: id() }),
  updateValue(attrs, context) {
    user.favorite_fruit_id = attrs.input.fruit
    if (context.type === 'ws') {
      context.ws.send([
        'update-text',
        '#fruit_card',
        fruits[user.favorite_fruit_id - 1]?.text,
      ])
    }
  },
  renderUpdate(attrs, context) {
    return content
  },
})

function Profile() {
  return (
    <>
      <p>
        Welcome back,
        <span id="greet_name">{user.username}</span>.
      </p>
      <Username.Form value={user.username} />
      <FavoriteFruit.Form
        value={{
          options: fruits,
          selected: user.favorite_fruit_id,
        }}
      />
      <span id="fruit_card">
        {fruits[user.favorite_fruit_id - 1]?.text}
      </span>
    </>
  )
}

let routes = {
  '/profile': {
    title: title('Profile Page'),
    description: 'View and update your public profile',
    node: <Profile/>
  },
  ...Username.routes,
  ...FavoriteFruit.routes,
} satisfies Routes

export default { routes, content }
`.trim()}
        </code>
      </fieldset>
      <fieldset>
        <legend>Preview</legend>
        <UserProfile />
      </fieldset>
    </div>

    <p>
      The update message id is auto incremented to avoid duplication among
      different fields created by {code('newSingleFieldForm()')}. However, if
      the {code('<field.Form/>')} is used multiple times on screen, e.g. in{' '}
      {code('mapArray')} loop, you should supply {code('updateMessageKeyName')}{' '}
      attribute to {code('newSingleFieldForm()')} and {code('key')} attribute to{' '}
      {code('<field.Form/>')}. Example see{' '}
      <Link href="/name-list">List Editing Demo</Link>
    </p>

    <p>
      The possibility is endless. You can extend or modify the page and
      components to suit your need.
    </p>

    <SourceCode page="demo-inputs/demo-single-field-form.tsx" />
  </>
)

function UserProfile() {
  return (
    <>
      <p>
        Welcome back, <span id="greet_name">{user.username}</span>
      </p>
      <Username.Form value={user.username} />
      <FavoriteFruit.Form
        value={{
          options: fruits,
          selected: user.favorite_fruit_id,
        }}
      />
      <FavoriteColor.Form value={user.favorite_color} type="color" />
      <span id="fruit_card" style={getTextStyle(user.favorite_color)}>
        {fruits[user.favorite_fruit_id - 1]?.text}
      </span>
    </>
  )
}

let routes = {
  ...Username.routes,
  ...FavoriteFruit.routes,
  ...FavoriteColor.routes,
} satisfies Routes

export default { routes, content }