TSX
Velt compiles TSX, the JSX syntax of .tsx files, for server-side rendering: an element
is a call into a JSX provider, which renders HTML (or builds whatever its client needs). The
default provider is velt/jsx. The syntax and the typing are TypeScript's
jsx: "react-jsx" mode; where Velt differs, this page says so. The provider interface is the
JSX provider contract. For a walkthrough, see
Server-rendered pages.
import { renderToStringSync } from "velt/jsx";
function Greeting(props: { name: string }): JSX.Element {
return <p class="greeting">Hello, {props.name}!</p>;
}
console.log(renderToStringSync(<Greeting name="Ada & Bo" />));
// <p class="greeting">Hello, Ada & Bo!</p>Where JSX is allowed
JSX is allowed in .vlt and .tsx files. In a .ts file it is an error, as in TypeScript
("JSX is not allowed in a .ts file"; rename the file to .tsx). Velt has no <T>expr
casts, so in .vlt and .tsx files a < where an expression starts is an element or a
generic arrow function (<T>(x: T): T => x, Functions).
The provider
The import source of a module decides its provider, in this order:
- a
/** @jsxImportSource <source> */comment among the comments before the first token (Velt also reads// @jsxImportSource <source>, buttscreads only the block comment, so a file shared with TypeScript uses that one); jsx: { importSource: "<source>" }in the package'spackage.vlt;velt/jsx.
So /** @jsxImportSource velt/jsx */ (or jsx: { importSource: "velt/jsx" }) names the default
explicitly, and /** @jsxImportSource velt/jsx/generic */ selects std's provider without
precompiled templates (velt/jsx).
A module with JSX imports <source>/jsx-runtime implicitly. Its exports are the namespace
JSX in that module's types (JSX.Element, JSX.IntrinsicElements, …), as if the module had
import * as JSX from "<source>/jsx-runtime". A module without JSX loads no provider.
Elements
<div class="a">…</div>,<br />and<>…</>(a fragment) are expressions of typeJSX.Element.- A lower-case name (
div,my-widget,clipPath) or a namespaced one (svg:rect) is an intrinsic element: a tag ofJSX.IntrinsicElements(velt/jsxlists the HTML, SVG 2 and MathML Core elements). - A capitalized or dotted name (
Card,ui.Card) is a component: a value in scope. - A generic component takes type arguments on the opening tag,
<List<number> items={xs} />; the closing tag has none (</List>).
Attributes
name="text"is a string (entities such as&are decoded),name={expr}any value, and a barenameistrue. A component's prop may also be given an element,icon=<Star />(an intrinsic element's attribute can't: an element is noJSX.AttrValue).{...obj}spreads the fields ofobj, whose type must be an object type, at compile time; a later attribute replaces a spread field with the same name. Two written attributes with the same name are an error ("JSX elements cannot have multiple attributes with the same name.").key={k}(astringor a number) is not an attribute: it goes to the provider separately.- An intrinsic element's attributes are checked against its field of
JSX.IntrinsicElements: an unknown name is an error, with a suggestion, and each value must have the field's type and convert toJSX.AttrValue. Names with-or:(data-*,aria-*,xlink:href), and every attribute of a tag with-or:(custom elements), are only checked againstJSX.AttrValue. A field that is not optional is a required attribute.
const link = <a hreff="/docs">Docs</a>;
// error: Property 'hreff' does not exist on type 'JSX.IntrinsicElements["a"]'. Did you mean 'href'?Children
- Text between tags follows JSX's whitespace rules: a line break between two pieces of text, with the indentation around it, becomes one space; a line with only whitespace disappears; and spaces within a line stay. Entities are decoded.
{expr}is one child, converted toJSX.Child.{...xs}is one child too: the arrayxs.{}and{/* comment */}are no children.- What
true,falseandnullrender is the provider's choice (velt/jsx: nothing). - A provider that writes some tags without an end tag lists them (
jsxVoidElements;velt/jsx: HTML's void elementsbr,img,input, …), and children of those tags are an error.{}, comments and an explicit end tag (<br></br>) are allowed. &&returns an operand, as in JavaScript (truthiness):{flag && <b>new</b>}is the element orfalse(whichvelt/jsxrenders as nothing), and{user && <p>{user.name}</p>}on a nullable object is the element ornull. As in React,{count && <b>n</b>}renders0whencountis0.
import { renderToStringSync } from "velt/jsx";
const flag = true;
const items = ["a", "b"];
console.log(renderToStringSync(
<ul class="list">
{items.map((x) => <li>{x}</li>)}
{flag ? <li>new</li> : null}
{flag && <li>also new</li>}
</ul>,
));
// <ul class="list"><li>a</li><li>b</li><li>new</li><li>also new</li></ul>const line = <p>one<br>two</br></p>;
// error: <br> is a void element and cannot have childrenComponents
A component is a function from its props to JSX.Element. Its attributes form an object literal
checked against the props type: a missing required field and an unknown field are errors, with
TypeScript's wording. Its children go into the field that
JSX.ElementChildrenAttribute names (children by default): one child is passed as itself,
several as an array, each checked against the field's type; children for props without that
field are an error.
import { renderToStringSync } from "velt/jsx";
function Section(props: { title: string; children: string[] }): JSX.Element {
return (
<section>
<h2>{props.title}</h2>
{props.children.map((c) => <p>{c}</p>)}
</section>
);
}
console.log(renderToStringSync(<Section title="Notes">{"one"}{"two"}</Section>));
// <section><h2>Notes</h2><p>one</p><p>two</p></section>function Section(props: { title: string }): JSX.Element {
return <h2>{props.title}</h2>;
}
const s = <Section />;
// error: Property 'title' is missing in type '{}' but required in type '{ title: string }'.- The compiler does not call a component: it passes it, uncalled, to the provider with its
props and a name (
"<module path>#<Name>"), as TypeScript's runtime does.velt/jsxcalls it as the element is created; another provider may call it later, around something, or not on the server at all. - An async component returns
Promise<JSX.Element>. Withvelt/jsxit starts when its element is created, so siblings load concurrently, and rendering awaits it (renderToString,renderToStream). A provider without async support reports an error. - Generic components infer their type arguments from the props and the children, as in TypeScript, or take them on the tag.
- A provider may declare attributes that every component accepts, as TypeScript's
JSX.IntrinsicAttributes(sigx marks islands this way:<Counter client:load start={1} />). They are checked astscchecks them, so a misspelledclient:lodis an unknown property with a suggestion. The provider receives them separately from the props. See the contract. - A component gets its props as JavaScript passes them: the same objects, not copies. A layout
takes its content as
children, async components still loading in it included, and streaming sends the layout's top first, asexamples/apps/ssr-blog's<Page>does:
import { renderToString } from "velt/jsx";
function Layout(props: { title: string; children: JSX.Element[] }): JSX.Element {
return (
<main>
<h1>{props.title}</h1>
{props.children}
</main>
);
}
async function Posts(): Promise<JSX.Element> {
return <ul><li>first</li></ul>;
}
console.log(await renderToString(<Layout title="News"><Posts /><p>more</p></Layout>));
// <main><h1>News</h1><ul><li>first</li></ul><p>more</p></main>How elements are compiled
An element becomes calls of the provider's functions, chosen by what it exports (the contract):
- Generic:
jsx(tag, names, values, children, key)for an intrinsic element,Fragment(children, key)andjsxComponent(C, props, key, name)(orjsxAsyncComponent). - Precompiled, when the provider exports
jsxTemplate,jsxEscapeandjsxAttr(velt/jsxdoes): a tree of intrinsic elements becomes one template. Static tags, attributes and text are escaped at compile time into constant strings, dynamic text and attributes are folded into them, and components, fragments and other elements are its slots. A list such as{rows.map((r) => <tr>…</tr>)}whose rows are templates without slots is built from strings too (with the provider'sjsxList), as a hand-written template literal would be. Both lowerings render the same bytes.
Differences from TypeScript
- The provider's types are the namespace
JSXof itsjsx-runtimemodule, not a globalJSXnamespace or React's types;JSX.IntrinsicElementscan't be extended by declaration merging (a provider builds its own: the contract). {flag && <x />}with abooleanflagis an error (writeflag ? <x /> : null); with a nullable object on the left it works as in TypeScript.- Children of a void element (
<br>x</br>) are a compile error ("<br> is a void element and cannot have children"), where React fails at run time. velt/jsxhas no event-handler attributes: it renders on the server, soonClickis a compile error (a client-side provider declares its own handlers).{...xs}as a child passes the arrayxsas one child.numberis JavaScript's number (f64). A provider whoseJSX.ChildandJSX.AttrValueshould also take Velt's integer types writesi64 | f64, asvelt/jsxdoes.- Class components and
refare not supported (compile errors);JSX.LibraryManagedAttributesis ignored.