Improve editor UI

Improve editor UI

Val Studio lays a field out from its schema. .multiline() and s.code() change how a string is edited, preview() decides what a value looks like wherever it is previewed, and render({ as: "inline" }) moves an item's editor into its list row.

Multiline strings

Sometimes you want something that is not richtext but still multiple lines. Simple descriptions, for example. Use .multiline() to let the string hold line breaks, which turns the input into a growing text box and makes longer text easier to edit.

const articleSchema = s.object({
  title: s.string(),
  // Multi-line text box for longer descriptions
  description: s.string().multiline(),
  body: s.richtext(),
});

export default c.define("/content/article.val.ts", articleSchema, {
  title: "My Article",
  description: "This is a longer description that benefits from a multi-line input.\nIt can span multiple lines.",
  body: s.richtext([
    { tag: "p", children: ["Article content..."] },
  ]),
});

Code editor for strings

For strings that contain code, configuration, or structured data, use s.code({ language: "..." }). It is edited in a code editor with syntax highlighting, and - because a language is part of what the value is rather than how one field is drawn - it is its own schema type rather than a string. Being a type is also what keeps the value out of stega encoding, so no invisible edit markers are woven into your source.

const configSchema = s.object({
  title: s.string(),
  // JSON configuration with syntax highlighting
  apiConfig: s.code({ language: "json" }),
  // TypeScript code snippet
  customHook: s.code({ language: "typescript" }),
  // CSS styles
  customStyles: s.code({ language: "css" }),
});

export default c.define("/content/config.val.ts", configSchema, {
  title: "API Configuration",
  apiConfig: JSON.stringify({
    endpoint: "https://api.example.com",
    timeout: 5000,
    retries: 3
  }, null, 2),
  customHook: `export function useCustomHook() {
  const [state, setState] = useState();
  return state;
}`,
  customStyles: `.custom-class {
  color: blue;
  font-size: 16px;
}`,
});

Supported languages

language takes one of the following. Omit it for a plain monospaced editor with no highlighting.

  • JavaScript and TypeScript, including JSX and TSX

  • JSON and XML

  • HTML, CSS, Sass

  • Vue and Angular

  • Python, Java, Go, Rust, PHP, C++

  • SQL and Markdown

List view

When working with records or arrays of objects, you can use preview() to display items as a nice looking list with thumbnails. This makes it much easier for editors to navigate and edit collections of content. Declare it on the item schema — the schema of the value being previewed — and the record or array reads it from there.

Basic list view

The preview() callback determines what information is displayed in the list view. You can show a title, subtitle, and image for each item.

const teamMemberSchema = s
  .object({
    name: s.string(),
    position: s.string(),
    bio: s.string().multiline(),
    image: s.image(),
  })
  .preview(({ val }) => ({
    title: val.name,
    subtitle: val.position,
    image: val.image,
  }));

const teamSchema = s.record(teamMemberSchema);

export default c.define("/content/team.val.ts", teamSchema, {
  "john-doe": {
    name: "John Doe",
    position: "Software Engineer",
    bio: "John has been building web applications for over 10 years.",
    image: { path: "/public/val/john.jpg" },
  },
  "jane-smith": {
    name: "Jane Smith",
    position: "Product Designer",
    bio: "Jane specializes in user experience and interface design.",
    image: { path: "/public/val/jane.jpg" },
  },
});

List view benefits

Using list view makes it significantly easier for editors to browse and manage collections. Instead of navigating through a tree of items, editors see a visual list with thumbnails and key information at a glance.

Customizing list display

The preview() callback receives the value being previewed in the val parameter. You can return any combination of title, subtitle, and image. Both subtitle and image are optional. It only runs for the items that are actually on screen, so it stays cheap on large collections.

Editing items in place

render() is the other half. It decides how the field itself is laid out while you are looking at it, and it takes one option: { as: "inline" }. Put it on the item schema of an array or record and the list draws each item's own editor inside its (still sortable) row, instead of a preview row you click into. This is what a page-builder list is made of.

On a field that is not directly under an array or record it does nothing - an object's fields are already laid out in place. For a tagged union the natural place to write it is on the blocks, one per block type, and the list goes inline if any variant asks for it.

Inline list items

const heroBlock = s
  .object({
    type: s.literal("hero"),
    title: s.string(),
    image: s.image(),
  })
  .render({ as: "inline" });

const textBlock = s
  .object({
    type: s.literal("text"),
    body: s.richtext(),
  })
  .render({ as: "inline" });

const pageSchema = s.object({
  blocks: s.array(s.union("type", heroBlock, textBlock)),
});