Dynamic components
Sometimes the element itself depends on the data: a heading whose level follows its depth in an outline, a section title that drops a level when it is nested, a card that collapses into a chip. <component is> renders one of a set of tags, or one of a set of components, chosen by a condition or by a prop.
Choosing a tag
is takes a conditional whose branches are tag names. The attributes and children apply to whichever tag renders:
import { ref } from "unframework";
export default function Section({ title }: { title: string }) {
const nested = ref(false);
return (
<article>
<component is={nested.value ? "h3" : "h2"} class="title">
{title}
</component>
<button type="button" onClick={() => (nested.value = !nested.value)}>
{nested.value ? "Lift" : "Nest"}
</button>
</article>
);
}
import { type CSSProperties, useRef, useState } from "react";
export interface SectionAttributes {
className?: string;
style?: CSSProperties;
}
export default function Section({
title,
className,
style,
}: { title: string } & SectionAttributes) {
const [nested, setNested] = useState(false);
const nestedRef = useRef(nested);
const Tag = nested ? "h3" : "h2";
return (
<article className={className} style={style}>
<Tag className="title">{title}</Tag>
<button
type="button"
onClick={() => {
nestedRef.current = !nestedRef.current;
setNested(nestedRef.current);
}}
>
{nested ? "Lift" : "Nest"}
</button>
</article>
);
}
- React, Qwik and Astro hold the chosen tag in a constant,
const Tag = nested ? "h3" : "h2", and render<Tag>. - Vue writes
<component :is>, Svelte<svelte:element this={…}>, and Solid<Dynamic component={…}>. - A strict Angular template can't choose an element by value, so Angular writes an
@switchwith one@caseper tag, each with the attributes and children (dynamic-component, emulated).
Every attribute must be valid on every candidate, since any of them may render: an attribute one tag doesn't take is UF3006, for that tag.
A prop typed as a union of tag names can choose the tag too, as a heading's level:
export default function Heading({ tag, text }: { tag: "h1" | "h2" | "h3"; text: string }) {
return (
<component is={tag} class="heading">
{text}
</component>
);
}
Choosing a component
The candidates can be components instead, imported or of the same module. Each one must declare every prop, model and slot you pass:
import { ref } from "unframework";
import Card from "./Card.uf.tsx";
import Chip from "./Chip.uf.tsx";
export default function Tag({ label }: { label: string }) {
const compact = ref(true);
return (
<section aria-label="Tag">
<component is={compact.value ? Chip : Card} label={label} />
<button type="button" onClick={() => (compact.value = !compact.value)}>
{compact.value ? "Expand" : "Compact"}
</button>
</section>
);
}
React, Qwik and Astro hold the chosen component in a constant, Vue writes <component :is="compact ? Chip : Card">, Svelte a $derived component, Solid <Dynamic>, and Angular an @switch with a @case per component. An attribute or a fill that one of the candidates doesn't declare is UF3035 or UF3038, as on any component.
A known set
is chooses from a set the compiler knows when it compiles: a conditional or a literal whose leaves are all tags or all components, or a prop typed as a union of tag literals. Angular writes every candidate, and the typed targets check each one, so a value only the run time knows, such as a string from an API, is UF3044, and so is a set that mixes tags and components. Write the candidates out in a conditional, or type the prop as a union of tags.
Context
provide and inject share a value with every descendant under an injection key, without passing it through each level. Each framework gets its own context, from React's createContext to Angular's InjectionToken.
Targets
What each of the seven targets writes, the capability matrix that declares where a framework differs, how each target takes a component's events, and how Angular hosts a component.