Demo Form

Sanitized user input

Hint: Try Bob and <b>o</b>

Demo more input types

Most often on:

Code Snippet

HTML code is escaped by default

HTML Code
<p>
  <i title="with realtime updates">Live</i> <b>Rich-Text</b> Editor
  <span style="color:red; background-color:lightgreen;">
    <button onclick="alert('running js')">Will it popup?</button>
  </span>
</p>

But it is possible to display sanitized HTML

HTML Preview

Live Rich-Text Editor Will it popup?

Source Code of demo-form.tsx
(import statements omitted for simplicity, click to expand)
import { Raw } from '../components/raw.js'
import { Style } from '../components/style.js'
import { MessageException } from '../../exception.js'
import { o } from '../jsx/jsx.js'
import type { attrs } from '../jsx/types'
import sanitizeHTML from 'sanitize-html'
import { Script } from '../components/script.js'
import debug from 'debug'
import SourceCode from '../components/source-code.js'
import { Context, getContextFormBody } from '../context.js'
import {
  checkbox,
  color,
  int,
  object,
  optional,
  ParseResult,
  string,
  values,
} from 'cast.ts'
import { renderError } from '../components/error.js'
import { Link } from '../components/router.js'
import { apiEndpointTitle, title } from '../../config.js'
import { Routes } from '../routes.js'
import { toRouteUrl } from '../../url.js'
const log = debug('demo-form.tsx')
log.enabled = true

function sanitize(html: string) {
  return sanitizeHTML(html, {
    allowedAttributes: { '*': ['style', 'title', 'width', 'height'] },
    allowedStyles: {
      '*': {
        'width': [/.*/],
        'height': [/.*/],
        'color': [/.*/],
        'background-color': [/.*/],
        'font-size': [/.*/],
        'text-align': [/.*/],
      },
    },
  })
}

let username = ''
let code = /* html */ `
<p>
  <i title="with realtime updates">Live</i> <b>Rich-Text</b> Editor
  <span style="color:red; background-color:lightgreen;">
    <button onclick="alert('running js')">Will it popup?</button>
  </span>
</p>
`.trim()

const style = Style(/* css */ `
#DemoForm form > label {
  color: green;
}
#DemoForm label {
  margin-top: 1rem;
  display: block;
  text-transform: capitalize;
}
#DemoForm .inline-label-container,
#DemoForm .radio-group
{
  margin-top: 1rem;
}
#DemoForm .inline-label-container label,
#DemoForm .radio-group label
{
  display: inline;
}
#DemoForm .radio-group label::before {
  width: 1.5em;
  display: inline-block;
  content: "";
}
#DemoForm label::after {
  content: ": ";
}
#DemoForm .btn-group input {
  border-radius: 0.25em;
  padding: 0.3em;
  margin: 0.3em;
}
#DemoForm [type=reset] {
  background-color: lightcoral;
  color: darkred;
}
#DemoForm [type=submit] {
  background-color: lightgreen;
  color: darkgreen;
}
#DemoForm fieldset fieldset {
  display: inline-block
}
`)

function SetKey(_attrs: attrs, context: Context) {
  type Params = {
    key: string
  }
  if (context.type === 'ws' && context.args) {
    const match = context.routerMatch
    if (!match) {
      console.error('Assert Error: missing routerMatch in ws context')
      return
    }
    const key = (match.params as Params).key
    const value = context.args[0] as string
    switch (key) {
      case 'username':
        username = value
        throw new MessageException(['update-in', '#username-out', username])
      case 'code':
        throw new MessageException([
          'batch',
          [
            ['update-in', '#code-out', value],
            ['update-in', '#preview-out', Raw(sanitize(value))],
          ],
        ])
      default:
        console.log('unknown key in DemoForm, key:', key)
        break
    }
  }
  return DemoForm()
}

function Submit(_attrs: attrs, context: Context) {
  let body = getContextFormBody(context)
  let input: ParseResult<typeof formBody> | undefined = undefined
  let err = null
  try {
    input = formBody.parse(body, {
      name: 'form body',
    })
  } catch (error) {
    err = error
  }
  return (
    <>
      {err ? (
        renderError(err, context)
      ) : (
        <p>
          Received:{' '}
          <pre>
            <code>{JSON.stringify(input, null, 2)}</code>
          </pre>
        </p>
      )}
      <p>(The content is not saved on server)</p>
      <div>
        <Link href="/form">Retry</Link>
      </div>
    </>
  )
}

const validation = {
  username: { minLength: 3, maxLength: 64 },
  password: { minLength: 6 },
  level: { min: 1, max: 100 },
}
const formBody = object({
  username: string(validation.username),
  password: string(validation.password),
  level: int(validation.level),
  gender: optional(values(['female', 'male', 'rainbow', 'na'])),
  color: color(),
  happy: checkbox(),
  contact: optional(values(['tel', 'text', 'video', 'face'])),
})

function DemoForm() {
  return (
    <div id="DemoForm">
      <div>
        <div style="display: inline-flex; flex-direction: column">
          <h1>Demo Form</h1>
          <form
            method="POST"
            action={toRouteUrl(routes, '/form/submit')}
            onsubmit="emitForm(event)"
          >
            {style}

            <h2>Sanitized user input</h2>

            <div class="inline-label-container">
              <label for="username">username</label>
              <span id="username-out" title="Live preview of username" />
            </div>
            <input
              type="text"
              name="username"
              id="username"
              minlength={validation.username.minLength}
              maxlength={validation.username.maxLength}
              oninput="emit('/form/live/username',this.value)"
            />
            <div title="Tips to try html-injection">
              Hint: Try <code>Bob</code> and <code>{'<b>o</b>'}</code>
            </div>

            <h2>Demo more input types</h2>

            <label for="password">password</label>
            <input
              type="password"
              name="password"
              id="password"
              minlength={validation.password.minLength}
            />

            <label for="level">level</label>
            <input
              type="number"
              name="level"
              id="level"
              step="1"
              min={validation.level.min}
              max={validation.level.max}
            />
            {Script(`level.defaultValue=1`)}

            <label for="gender">gender</label>
            <select name="gender" id="gender">
              <option value="female">Female</option>
              <option value="male">Male</option>
              <option value="rainbow">Non-binary</option>
              <option value="na">Prefer not to say</option>
            </select>

            <label for="color">color</label>
            <input name="color" id="color" type="color" />

            <div class="inline-label-container">
              <label for="happy">happy?</label>
              <input name="happy" id="happy" type="checkbox" />
            </div>

            <div class="radio-group">
              <div style="margin-bottom:0.5em">Most often on:</div>
              <label for="tel">tel</label>
              <input name="contact" id="tel" type="radio" value="tel" />
              <label for="text">text</label>
              <input name="contact" id="text" type="radio" value="text" />
              <label for="video">video</label>
              <input name="contact" id="video" type="radio" value="video" />
              <label for="face">face</label>
              <input name="contact" id="face" type="radio" value="face" />
            </div>

            <div class="btn-group">
              <input type="reset" value="Reset" />
              <input type="submit" value="Submit" />
            </div>
          </form>
        </div>

        <div style="display: inline-block; width: 3rem"></div>

        <div style="display: inline-flex; flex-direction: column">
          <h2>Code Snippet</h2>

          <p>HTML code is escaped by default</p>
          <fieldset>
            <legend>HTML Code</legend>
            <pre>
              <code id="code-out">{code.trim()}</code>
            </pre>
          </fieldset>

          <p>But it is possible to display sanitized HTML</p>
          <fieldset>
            <legend>HTML Preview</legend>
            <div id="preview-out">{Raw(sanitize(code))}</div>
          </fieldset>

          <label for="html-input">HTML Input</label>
          <textarea
            id="html-input"
            style="width:37em;height:10em;"
            oninput="emit('/form/live/code',this.value)"
          >
            {code}
          </textarea>
        </div>
      </div>

      <SourceCode page="demo-form.tsx" />
    </div>
  )
}

let routes = {
  '/form': {
    title: title('Demo Form'),
    description: 'Demonstrate form handling with ts-liveview',
    menuText: 'Form',
    node: <DemoForm />,
  },
  '/form/submit': {
    title: apiEndpointTitle,
    description: 'submit demo form',
    node: <Submit />,
    streaming: false,
  },
  '/form/live/:key': {
    title: apiEndpointTitle,
    description: 'set form field in realtime',
    node: <SetKey />,
    streaming: false,
  },
} satisfies Routes

export default { routes }