Description
Describe the problem
Caution : I am not an English speaker. Sorry in advance in case of misunderstanding.
A lot of component are wrapper that enhance HTML node.
But using a component will restricts access to the node, and we must use event bubbling and restProps to compensate, with limited functionality :
Button.svelte :
<button class="btn" on:click on:mouseover on:mouseout on:focus on:blur {...$$restProps}>
<i class='icon'></i>
<span><slot/></span>
</button>
But there are a lot of limitations :
- If the event bubbling is not declared on the component, the handler cannot be used. Here for exemple it's impossible to add a touchstart/touchstop/... event on the button, because these events are not bubbling...
- $$restProps allow us to pass any attributes to the button, but without any checks or autocompletion from the compiler/EDI. The attributes are just passed without controls...
- We cannot set a class-name via
class="xx"
- Others directives cannot be used, like class:name, style:property, use:action, transition:fn/in:fn/out:fn or animate:fn
- We can't directly bind an attribute of the node
Describe the proposed solution
Components should be able to delegate this to their base node, by exemple with a special directive export:this :
Button.svelte :
<button class="btn" export:this>
<i class='icon'></i>
<span><slot/></span>
</button>
export:this should only be used on the root node of the component.
When a component has an export:this on his root node, it will be associated with the element (for exemple via an attribute of the SvelteComponent class).
Then, the following rule are applied :
Events
<Button on:click={clickHandler} on:touchStart={touchHandler}>click me</Button>
When an event is attached to the component, it will be added at once :
- to the component via $on() (like normal components, for use with dispatchEvent)
- directly to the node marked via export:this
Bonus : prefixing the directive with an '@' should apply the handler only on the node.
<Button @on:click={clickHandler}>click me</Button>
Class
<Button class="red">a red button</Button>
<style>
.red {
background: red;
color: white;
}
</style>
Using a class attribute on the class should add it to the class of the exported node.
If the component already use a class attribute on the node, it should be merged.
Exemple in this case the button will finally have the following class : "btn svelte-XXXXX red svelte-YYYYY"
(where svelte-XXXXX and svelte-YYYYY are the class-marker defined by the Component and the caller).
class:name
<script>
let active = true;
</script>
<Button class:active>click here</Button>
The class "active" will be added/removed on the exported node.
Nothing special here except a possible conflict if the component and the caller use the same classname.
I think that the call must be orderer (first the component, and after the caller).
Maybe the compiler could detect it and generate a warning, but i don't think it's such a big deal.
style:property
<Button style:border="3px solid #000">click here</Button>
Same thing here that for class:name. Call should be ordered and a possible warning from the compiler in case of conflicts.
use:action
<Button use:mysuperaction={args}>click here</Button>
The action will be applied on the node.
Nothing special here since we can already use several actions on the same node.
transition:fn / in:fn / out:fn
<Button transition:fade>click here</Button>
Here it is more complex.
I don't know if it is possible to merge several transitions, or what it will result...
But I think there's no need to complicate that : I think it's best to use this only on the caller.
So if a node is marked with export:this, then we can't define a transition on it on the component.
=> Only the caller can do this
Button.svelte :
<!-- ERROR : An element that uses export:this cannot use the transition directive -->
<button class="btn" export:this transition:fade>
...
</button>
<Button transition:fade>click here</Button> <!-- OK -->
animate:fn
{#each actions as a(a.id)}
<Button animate:flip>{a.name}</Button>
{/each}
Same rule as for transitions : only the caller can put an animate directive.
It's even more obvious here...
<!-- ERROR : An element that uses export:this cannot use the animate directive -->
<button class="btn" export:this animate:flip>
...
</button>
Unknow props
<Button title="hello">click here</Button>
All unknow props will be directly passed to the node.
EDI should allow autocompletion with all the props of the Component, AND all the attributes of the node.
Bonus : prefixing the attribute with an '@' should pas it directly to the node, event if the component has a similar props.
<Button @title="hello" @disabled>click here</Button>
Read-only binding
There are a number of read-only bindings :
- All nodes :
clientWidth
,clientHeight
,offsetWidth
,offsetHeight
- Media element :
duration
,buffered
,played
,seekable
,seeking
,ended
- Input type files :
files
Unless I'm mistaken, I don't see any problem with these binds being performed multiple times.
So this should be allowed both in the component and from the caller :
Button.svelte :
<button class="btn" export:this
bind:offsetWidth={width}
bind:offsetHeight={height}>
...
</button>
And the caller :
<Button bind:offsetWidth={width} bind:offsetHeight={height}>click here</Button>
Two-way binding
However, I don't think it's possible to use several two-way binding on the same attribute.
I think that the component should have priority.
Input.svelte :
<script>
let text = ...;
</script>
<input export:this bind:value={text}>
If an node marked with export:this has a two-way binding, the caller should not use a binding :
<!-- ERROR : "value" cannot be binded -->
<Input bind:value={value} />
In fact, it shouldn't be able to affect the attribute at all :
<!-- ERROR : "value" cannot be set -->
<Input value={value} />
These restrictions are not so problematic, since we could use the classic binding instead, simply by exporting a variable of the same name.
Input.svelte :
<script>
export let value;
</script>
<input export:this bind:value={value}>
And then :
<Input bind:value={value} /> <!-- OK -->
Here we use the binding to the 'value' field of the component, witch is binded to the node...
Delegate the component
As for other directive, the export:this directive should be usable on a component (if it's the root element) :
Button.svelte :
<button class="btn" export:this>
<slot/>
</button>
IconButton.svelte
<script>
import Button from './Button.svelte';
</script>
<Button class="with-icon" export:this>
<i class="icon"></i>
<slot/>
</Button>
Caller :
<IconButton class="my-button"
on:click={click} transition:fade
disabled title="Title">
click
</IconButton>
Named Delegation
We can even extend that for any node/component, even if it's not the root node/component, using a name to differentiate them.
ConfirmPanel.svelte
<div class="panel" export:this>
<div class="main">
<slot/>
<div>
<div class="actions">
<button class="cancel" export:this="cancel">Cancel</button>
<button class="valid" export:this="valid">Ok</button>
</div>
</div>
In order to modify the named delegate éléments, we can use a prefix like "name@" (where name is the name of the exported element).
Exemple :
<ConfirmPanel
class="panel-class" on:click={clickOnPanel}
cancel@class="red" cancel@on:click={clickOnCancel}
valid@class="blue" valid@on:click={clickOnValid}>
...
</ConfirmPanel>
Or a specific tag :
<ConfirmPanel>
<delegate:default class="panel-class" on:click={clickOnPanel} />
<delegate:cancel class="red" on:click={clickOnCancel} />
<delegate:valid class="blue" on:click={clickOnValid} />
...
</ConfirmPanel>
Alternatives considered
The current solution is to use event bubbling, specifics props and/or restProps to compensante.
<script>
export let clazz = '';
export let red = false;
export let border = null;
export let color = null;
export let background = null;
export let offsetWidth;
export let offsetHeight;
</script>
<button class="btn ${clazz}" class:red
style:border style:color style:background
on:click on:mouseover on:mouseout on:focus on:blur
bind:offsetWidth bind:offsetHeight
{...$$restProps}>
<slot />
</button>
But :
- it's more verbose, and need specific code in order to simulate each functionnality.
- There is no support for actions, transition or animate.
This could advantageously be replaced by this, while increasing the possibilities :
<button class="btn" export:this>
<slot />
</button>
Importance
would make my life easier