Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export node from Component (or Component delegating) #7980

Closed
adiguba opened this issue Oct 27, 2022 · 2 comments
Closed

Export node from Component (or Component delegating) #7980

adiguba opened this issue Oct 27, 2022 · 2 comments

Comments

@adiguba
Copy link
Contributor

adiguba commented Oct 27, 2022

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

@brunnerh
Copy link
Member

Related: sveltejs/rfcs#60

@adiguba
Copy link
Contributor Author

adiguba commented Oct 27, 2022

Didn't see this before.
Should I close this ?

@adiguba adiguba closed this as completed Dec 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants