Skip to content

Latest commit

 

History

History
274 lines (207 loc) · 9.33 KB

0000-slot-attributes.md

File metadata and controls

274 lines (207 loc) · 9.33 KB
  • Start Date: 2020-01-01
  • RFC PR: #15
  • Svelte Issue: (leave this empty)

Slot Attributes

Summary

Slot attributes allow a Component to assign attributes (eg, classnames) to the content of a slot if & when it holds content.

Motivation

I'll be using Bootstrap to help illustrate examples for this RFC, but the problem is most definitely not limited to Bootstrap by any means. I've run into this in every design system I've worked with. Bootstrap is chosen solely for its familiarity and simplicity.

The majority of design systems have conditional content slots for their elements – we know & expect this. However, when a Svelte developer goes to wire up the UX slots to actual <slot/>s, they'll run into problems:

1. Slot elements generally have CSS side-effects when empty.

Most of these slots' act as parent wrappers for what they contain. This means that there's almost always some level of padding, background-color, margin, etc applied to that parent, even when it contains no children. This, then, prevents the Svelte developer from including those parent containers within the Component directly:

<div class="card">
	<div class="card-header">
		<slot name="header"/>
	</div>

	<div class="card-body">
		<slot />
	</div>

	<div class="card-footer">
		<slot name="footer"/>
	</div>
</div>

See REPL example

As you will see, the .card-header and .card-footer carry their own paddings & background colors, rendering a visual effect even though they weren't defined/used by the Card's consumer.

Y Self-contained component styles Y Does not require internal knowledge Y&N Slot is configurable from consumer POV N Slot leaves no trace when unused/empty.

Pass/Fail Requirement
Self-contained component styles
Does not require internal knowledge
〰️* Slot is configurable from consumer POV
🚫 Slot leaves no trace when unused/empty

* You can't add additional classes to .card-footer (ideal) but still can to internals

This then forces us into the second problem...

2. External <slot/> access requires internal knowledge

In order to truly conditionally render parent classes (eg, card-header) we, the <Card> consumers, have to know how to recreate the <slot name=header/> in a way that the design element (eg, Bootstrap's .card) expected.

What this means is that we have to have internal knowledge of the Component's design mechanics AND it forces us to split the Component's markup across multiple boundaries.

Additionally, this forces <Card> to ship :global(.card-footer) style selectors since it does not actually contain any .card-footer elements in its markup.

<Card class="text-center">
	<!-- requires knowledge of "card-header" -->
	<div slot="header" class="card-header">Featured</div>

	<h5>Special title treatment</h5>
	<p>With supporting text below as a natural lead-in to additional content.</p>
	<button type="button" class="btn btn-primary">Go somewhere</button>

	<!-- requires knowledge of "card-footer" -->
  <div slot="footer" class="card-footer text-muted">
    2 days ago
  </div>
</Card>

See REPL example

Pass/Fail Requirement
🚫 Self-contained component styles
🚫 Does not require internal knowledge
Slot is configurable from consumer POV
Slot leaves no trace when unused/empty

Detailed Design

A <slot> can hold attributes like any other tag.
In English, this is the same as saying "this slot's (top-level) children will inherit these attributes". This is true for named & unnamed slots alike.

<!-- Card.svelte -->
<div class="card">
	<!-- when "header" is defined, it gain the "card-header" class -->
	<slot name="header" class="card-header" />

	<!-- when unnamed content is received, it gains the "card-body" class -->
	<slot class="card-body" />

	<!-- when "footer" is defined, it gain the "card-footer" class -->
	<slot name="footer" class="card-footer" />
</div>

Note: Example is restricted to class for simplicity.

As mentioned, the slot's attributes will be coerced/assigned to the incoming content. In this example, we have not defined any of our own class values, so it's a direct assignment:

<!-- input -->
<Card>
	<!-- define my header slot -->
	<div slot="header"><h4>My title</h4></div>

	<!-- pass default/body content -->
	<div>
		<h5>Special title treatment</h5>
		<p>With supporting text below as a natural lead-in to additional content.</p>
		<button type="button" class="btn btn-primary">Go somewhere</button>
	</div>

	<!-- define my footer slot -->
	<div slot="footer">2 days ago</div>
</Card>

<!-- output -->
<div class="card">
	<div class="card-header"><h4>My title</h4></div>
	<div class="card-body">
		<h5>Special title treatment</h5>
		<p>With supporting text below as a natural lead-in to additional content.</p>
		<button type="button" class="btn btn-primary">Go somewhere</button>
	</div>
	<div class="card-footer">2 days ago</div>
</div>

Now, when a <slot/> is unused, it has nothing to spread its attributes onto:

<!-- input -->
<Card>
	<div>
		<h5>Special title treatment</h5>
		<p>With supporting text below as a natural lead-in to additional content.</p>
		<button type="button" class="btn btn-primary">Go somewhere</button>
	</div>
</Card>

<!-- output -->
<div class="card">
	<div class="card-body">
		<h5>Special title treatment</h5>
		<p>With supporting text below as a natural lead-in to additional content.</p>
		<button type="button" class="btn btn-primary">Go somewhere</button>
	</div>
</div>

And finally, when there are attribute conflicts, we simply merge the <slot> values into the consumer values.
Again, the Component author has said "this <slot/> needs to have these attributes" – anything else is presumably relevant to the Consumer-side:

<!-- input -->
<Card>
	<!-- define my header slot w/ extra attr -->
	<div slot="header" aria-label="My title" ><h4>My title</h4></div>

	<!-- pass default/body content w/ extra class -->
	<div class="text-center">
		<h5>Special title treatment</h5>
		<p>With supporting text below as a natural lead-in to additional content.</p>
		<button type="button" class="btn btn-primary">Go somewhere</button>
	</div>

	<!-- define my footer slot w/ extra class -->
	<div slot="footer" class="text-muted">2 days ago</div>
</Card>

<!-- output -->
<div class="card">
	<div class="card-header" aria-label="My title"><h4>My title</h4></div>
	<div class="card-body text-center">
		<h5>Special title treatment</h5>
		<p>With supporting text below as a natural lead-in to additional content.</p>
		<button type="button" class="btn btn-primary">Go somewhere</button>
	</div>
	<div class="card-footer text-muted">2 days ago</div>
</div>
Pass/Fail Requirement
Self-contained component styles
Does not require internal knowledge
Slot is configurable from consumer POV
Slot leaves no trace when unused/empty

How we teach this

This would be a simple add-on tutorial for the Svelte website.

In plain English, we say "Any attributes (except name) on a defined <slot/> will be applied to its top-level children"

Drawbacks

These are pretty lame, but it's all I could muster:

  1. There's a built-in exception of name not being passed down – reserved for slot identification.
  2. This becomes an additional API to learn (lol)
  3. This is unique to Svelte – the closest is a React HoC using a fairly lengthy cloneElement loop to add properties to children.

Alternatives

Current alernatives and workarounds are:

  1. Do nothing – expect Component consumers to know which classes need to appear when defining the slot:

    <Card>
    	<div slot="header" class="card-header">
    		<h4>My title</h4>
    	</div>
    	<div slot="body" class="card-body">
    		<p>My content</p>
    	</div>
    	<div slot="footer" class="card-footer">
    		<Button>Submit</Button>
    	</div>
    </Card>
  2. Extract a Component's slots as separate Sub-Components:

    <Card>
    	<CardHeader>
    		<h4>My title</h4>
    	</CardHeader>
    	<CardBody>
    		<p>My content</p>
    	</CardBody>
    	<CardFooter>
    		<Button>Submit</Button>
    	</CardFooter>
    </Card>
  3. Limit all slotted content to plain text:

    This is a bit extreme – and a no-go for most designers – but it's possible to get what we want by replacing all <slot>s with props and a series of {#if} blocks.

    It semi-works for this example, but still requires one of the other two workarounds for non-plaintext content.

    <Card title="My title">
    	<p>My content</p>
    	<CardFooter>
    		<Button>Submit</Button>
    	</CardFooter>
    </Card>

Unresolved questions

  • Which attributes, if any others besides name, should not be allowed?