Props
Props are how a parent passes data to a component. You write them as HTML attributes, and the component reads them as ordinary variables.
Nothing to import, nothing to declare twice.
Passing Props
Add an attribute to the component tag.
A quoted value is passed as written; braces pass the value of an expression.
<script>
let who = 'Grace'
</script>
<!-- A quoted value is passed as written -->
<PropGreeting name="World" />
<!-- Braces pass the value of an expression -->
<PropGreeting name={who} />
PropGreeting is the small component in the next section. It
prints whatever name it is given:
Hello, World!
Hello, Grace!
Using Props
Inside the component, declare each prop with let.
The value you assign is the default, used whenever the parent passes nothing.
<script>
// `let` declares a prop. The value is the default, used when
// the parent passes nothing.
let name = 'friend'
</script>
<p>Hello, {name}!</p>
Rendered on its own, with no name passed, you get the default:
Hello, friend!
Only let declares a prop.
A const is static and never receives anything from the parent.
A prop passed as an expression stays live: when the parent's value changes, the component is given the new one.
The default is a starting point, not a one-off copy.
Props in Scripts
A prop is a normal variable in the script, so you can use it anywhere in the component - in an expression, a condition, or a class name.
This badge has two props and uses both:
<script>
// Two props. Each `let` is one prop, and its value is the default.
let label = 'Draft'
let tone = 'neutral'
</script>
<span class="badge badge-{tone}">{label}</span>
<style>
.badge {
display: inline-block;
padding: 0.15rem 0.6rem;
border: 1px solid currentColor;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 500;
}
.badge-neutral {
color: var(--text-secondary);
}
.badge-good {
color: var(--success);
}
.badge-late {
color: var(--error);
}
</style>And this is how a parent passes them in:
<PropBadge label="Shipped" tone="good" />
<PropBadge label="Overdue" tone="late" />
<!-- No props at all: both defaults are used -->
<PropBadge />
Every prop arrives as a string. count={3}
and count="3" both reach the component as
"3", so convert before doing maths:
Number(count) + 1 is 4, while count + 1 is
"31".
Objects and arrays do not survive the trip either - they arrive as the
text [object Object]. Share those with
unite() instead.
Props in Functions
Functions in the same script see props directly.
There is no this, and nothing to pass as an argument.
<script>
let name = 'friend'
let message = ''
// `name` is in scope here, like any other variable in this script.
function greet() {
message = 'Hello, ' + name + '!'
}
</script>
<button onclick="greet()">Greet</button>
<p>{message}</p>
<style>
button {
padding: 0.35rem 0.8rem;
border: 1px solid var(--border-hover);
border-radius: var(--radius-sm);
background: var(--bg-elevated);
color: var(--text-primary);
font: inherit;
cursor: pointer;
}
</style>
Click Greet and the handler reads name - here the default, since
this example is rendered with no props passed to it.
Booleans and Missing Values
Props are strings, so two ways of writing an attribute need an answer that a string on its own cannot give: an attribute with no value at all, and an expression that turns out to hold nothing.
No value means true. <Button disabled />
gives the component's let disabled the string
"true", which is truthy - so it switches a false
default on.
Write disabled="" if you really want an empty string; that is a
real value, and it is falsy.
An empty expression means "not passed".
<Greeting name={missing} />, where missing is
null or undefined, keeps the component's default
exactly as leaving the attribute off would. A field that has not loaded yet
cannot wipe your defaults.
false and "" are real values, so they do replace the
default.
All five cases, side by side:
<script>
let missing = null
</script>
<PropGreeting name="World" /> <!-- "World" -->
<PropGreeting name /> <!-- no value: the string "true" -->
<PropGreeting name="" /> <!-- an empty string is a real value -->
<PropGreeting /> <!-- not passed: the default, "friend" -->
<PropGreeting name={missing} /> <!-- nothing to pass: the default again -->
Hello, World!
Hello, true!
Hello, !
Hello, friend!
Hello, friend!
null does not clear a prop. Since it means
"not passed", setting the parent's variable to null leaves
the component showing the last real value it was given. Pass an empty
string when you mean "clear it".
Extra Props Are Forwarded
Passing a prop the component does not declare is not a mistake. It goes
straight through to the component's root element, so a component built from
one <button> accepts everything a
<button> accepts.
No spread syntax, no ...rest.
<script>
// Declared, so `variant` is used here and is not forwarded.
let variant = 'primary'
</script>
<button class="ui-btn ui-btn-{variant}"><slot /></button>
<style>
.ui-btn {
padding: 0.4rem 0.9rem;
border: 1px solid transparent;
border-radius: var(--radius-sm);
font: inherit;
cursor: pointer;
}
.ui-btn-primary {
background: var(--accent);
color: var(--bg-primary);
}
.ui-btn-quiet {
border-color: var(--border-hover);
background: var(--bg-elevated);
color: var(--text-primary);
}
.ui-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style><!-- `title` and `disabled` are not declared by the component,
so they land on its <button> element -->
<PropRestButton title="Sends the form">Submit</PropRestButton>
<PropRestButton variant="quiet" disabled>Not available</PropRestButton>
Attribute values forward: class, id,
style, aria- and data- attributes, and
valueless ones such as disabled.
Two things stay behind: props the component declares with let, and JAF's own
attributes (slot, key, noprops).
A third group is neither forwarded nor ignored - it is a build error on a component tag, naming the tag and the attribute:
-
Directives -
ref:,use:,class:,style:,transition:,animate:,in:,out:. Each of these decorated the wrapper element JAF used to render around a tag. There is no wrapper now, so the declaration has no element to land on. Put it on an element inside the component, or on an element of your own around the tag. (bind:andon:are the exceptions: they mean something on a component tag - a prop synced in both directions, and a handler attached to the child's root element.) -
file=- it names a path that only resolves once the app is running, and a component is resolved to a module at build time.
A Handler on a Component Tag Is Not a Prop
<Button onclick="save()"> compiles, and so does
<Button on:click|preventDefault={save}>. Neither is a prop: the child
never sees an onclick. The body joins the handler table of the component that
wrote it, so save() means your save and reads your
variables, and the child attaches it to its own root element with a real
addEventListener.
Changed in 0.1. The alpha forwarded the attribute text onto the child's root element and rewrote it at mount, which is why older notes tell you to pass a callback prop instead. Props are strings, so that never worked cleanly; the handler is compiled now.
Four shapes are still build errors, each named:
the child declares a variable of that name (let onclick), so the tag is passing
a prop rather than writing a handler; the tag writes noprops, or the child's
script is no-forward, so nothing forwards and the handler has nothing to attach
to; the child's template has no single root element; or the compiler could not read the
handler at all. See the on: directive for the mechanism.
Forwarding has to know which element to forward onto, and the compiler reads
that off the child's own template. A child whose template has two root elements, or
whose first root node is a block rather than an element, cannot receive a forwarded
prop: passing one is a build error naming the child. Give the child a single root
element, or declare the prop with let.
Class Merging
class and style are merged with what the root
element already has, rather than replacing it. A caller's
class="wide" on a root of class="ui-btn ui-btn-primary"
produces class="ui-btn ui-btn-primary wide".
Every other attribute is set outright, so a caller's title wins
over the component's own.
Turning Forwarding Off
In the component: no-forward
Add no-forward to the component's <script> tag
and nothing is ever forwarded, whoever calls it.
<!-- The button component again, with forwarding switched off -->
<script no-forward>
let variant = 'primary'
</script>
<button class="ui-btn ui-btn-{variant}"><slot /></button>
At the call site: noprops
Add noprops to a single tag and that one call forwards nothing.
<!-- This one call forwards nothing, so `title` is dropped -->
<PropRestButton title="Sends the form" noprops>Submit</PropRestButton>