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.

source
<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:

output

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.

source
<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:

output

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>

And this is how a parent passes them in:

source
<PropBadge label="Shipped" tone="good" />
<PropBadge label="Overdue" tone="late" />

<!-- No props at all: both defaults are used -->
<PropBadge />
output
Shipped Overdue Draft

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>

Click Greet and the handler reads name - here the default, since this example is rendered with no props passed to it.

output

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:

source
<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 -->
output

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>
source
<!-- `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>
output

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: and on: 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.

source
<!-- 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.

source
<!-- This one call forwards nothing, so `title` is dropped -->
<PropRestButton title="Sends the form" noprops>Submit</PropRestButton>