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

Allow hyphenated properties to be defined in a component #3852

Closed
ahopkins opened this issue Nov 5, 2019 · 33 comments
Closed

Allow hyphenated properties to be defined in a component #3852

ahopkins opened this issue Nov 5, 2019 · 33 comments
Milestone

Comments

@ahopkins
Copy link

ahopkins commented Nov 5, 2019

Is your feature request related to a problem? Please describe.

Given the usage of the following hypothetical component:

<Icon data-tooltip="foobar" />

I receive a warning that data-tooltip has not been declared as a property on the Icon component. However, there is no way to do that since it is hyphenated.

Describe the solution you'd like

Inside my component, it would be nice if I could declare the property like this:

export let dataTooltip = null

Describe alternatives you've considered

I know that the properties are also available in $$props. But that doesn't really solve the problem.

const dataTooltip = $$props['data-tooltip']

While this would give me access to the value, it doesn't remove the in browser warning.

How important is this feature to you?

It's annoying. Important? Well, certainly not a deal killer. And for now I just am using the form: <Icon tooltip="foobar" />. And then I am passing the property to the hyphenated version inside the component.

The reason this came about was I was trying to just use a spread operator and pass props thru. But, here I cannot because I don't want to keep seeing those warnings all over the place.

@Conduitry
Copy link
Member

A component's 'unknown prop' warning is supposed to be suppressed if the component uses $$props at all (#2881). Is this not what you're seeing? Do you have a repro?

@ahopkins
Copy link
Author

Thanks for the response. I believe that I was still getting it, but I'll go back and post a snippet when I get back to a computer.

@ghost
Copy link

ghost commented Nov 12, 2019

In this specific case, the property looks like HTML5 user-defined data.

Maybe take hyphenated names and make the second word of the hyphen a key inside the first name?

Such as:

<Foo data-test="some data">
//... inside Foo
<script>
export let data;
</script>
{data.test}

@ahopkins
Copy link
Author

True, but I am not sure that would be the only use case.

@antony
Copy link
Member

antony commented Nov 25, 2019

@dkondrad @ahopkins yeah I think this is a bad example. Ignoring data- prefixes, something as simple as:

<Profile avatarType="xxx">

where I'd really prefer to declare it as:

<Profile avatar-type="xxx">

@ahopkins
Copy link
Author

ahopkins commented Nov 26, 2019

@antony I agree. It just so happened that my example I was using something that required a data- property.

I agree with your example.

@Conduitry
Copy link
Member

@ahopkins Did you ever get a reproduction for the 'unknown prop' warning persisting despite the component referencing $$props?

@Conduitry Conduitry added the awaiting submitter needs a reproduction, or clarification label Nov 26, 2019
@ahopkins
Copy link
Author

ahopkins commented Dec 9, 2019

Of course I cannot recreate it now ...

myCredibility -= 1

As stated, as soon as $$props is referenced, the warning goes away. Perhaps I had this on an older build of svelte?

@ahopkins
Copy link
Author

@Conduitry I am not sure that this was ready to be closed. The warning was sort of a side matter.

I think the issue (as displayed by @antony) is still a valid feature request. Unless you are saying this is not something that you would entertain?

@Conduitry
Copy link
Member

Yeah, fair, we can re-open this.

@antony
Copy link
Member

antony commented Dec 12, 2019

This needs to be done before Rich comes along and does this:

<Profile avatar_type="xxx">

But seriously... not sure what the behaviour should be if we did this:

<Profile avatarType="xxx"  avatar-type="xxx">
export let avatarType

I suppose we could consider avatar-type to be equivalent to avatarType and thus act as if the same prop had been declared twice, which is currently to throw an error saying "Attributes need to be unique (5:18)" - perhaps this message needs to say something like "Attributes need to be unique, excluding hyphens" or something far more meaningful than that.

Another weird edge case:

<Profile avatar--type="xxx">

I'd suggest that the above is left intact, thus ending up as the (un-addressable) prop avatar--type.

@ahopkins
Copy link
Author

Not knowing the feasibility of this... I like that suggestion.

@morewry
Copy link

morewry commented Feb 6, 2020

And what about when it's compiling to a custom element? When adopting Svelte, I tried to keep the hyphenated attribute names I'd previously had when writing CEs from scratch and couldn't figure it out in the time I had, so gave up. Do web components get access to $$props set via hyphenated attributes? I wasn't aware of $$props at all, so I didn't try it.

@antony antony removed the awaiting submitter needs a reproduction, or clarification label Feb 14, 2020
@dkniffin
Copy link

dkniffin commented Jun 2, 2020

@morewry It looks like they don't. I just tried it.

I'd really like to see this feature implemented. Without it, it seems the only option is to pass in attributes as snake_case, which is inconsistent with the rest of the DOM elements. I can't even seem to get camelCase to work.

@Ciantic
Copy link

Ciantic commented Jun 5, 2020

The solution was already outlined by Mr @Rich-Harris in #875, just make the camelCase to kebab-case translation possibility.

@osamamaruf
Copy link

We are making transition from using polymer web components and upgrading our existing component library to use svelte web components where applicable. Inability to define hyphenated props is a blocker for us.

@oranmor
Copy link

oranmor commented Jul 28, 2020

@osamamaruf I've faced this problem too.
As temporary solution I've written my own wrapper for CustomElement, where all camelCased attributes are converted to dash-case:

import MyCustomComponent from './MyCustomComponent.svelte';

class MyCustomComponentWrapper extends MyCustomComponent {
  static get observedAttributes() {
    return (super.observedAttributes || []).map(attr => attr.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase());
  }

  attributeChangedCallback(attrName, oldValue, newValue) {
    attrName = attrName.replace(/-([a-z])/g, (_, up) => up.toUpperCase());
    super.attributeChangedCallback(attrName, oldValue, newValue);
  }
}

customElements.define('my-custom-component', MyCustomComponentWrapper);

MyCustomComponent.svelte

<script>
  export let someDashProperty;
</script>

<svelte:options tag={null} />

{someDashProperty}

Then you can use it in this way:

<my-custom-component some-dash-property="hello"></my-custom-component>

Hope it helps until fix is coming

@nolanlawson
Copy link
Contributor

Just wanted to say +1 for automatically converting propLikeThis to attr-like-this.

I've been looking around for some "best practices" for publishing custom elements, and besides this article, I've also looked at the pattern used by the same author's chessboard-element, and it considers lowercase-kebab attributes to be equivalent to camel-cased props.

@oranmor's temporary solution works great in the meantime, though!

@benkeil
Copy link

benkeil commented Aug 26, 2020

@oranmor do you have a "full" example? I don't understand how you implement the MyCustomComponent.

@oranmor
Copy link

oranmor commented Aug 26, 2020

@benkeil in fact it is a full example.
You can see complete working repo here: https://github.com/oranmor/svelte-custom-element-dash-properties-example

@arackaf
Copy link
Contributor

arackaf commented Jan 2, 2021

Here's one possible workaround for accessing hyphenated attributes sent into a Svelte wc. Have a rootEl variable, and bind it to the outermost container in your wc. Then

  onMount(() => {
    tick().then(() => {
      const host = rootEl.parentNode.host;
      const x = host.getAttribute("some-attr");

Obviously that won't be reactive. But it might be good enough depending on use case. The tick() call is necessary because of this issue: #2227

@Mouvedia
Copy link

But seriously... not sure what the behaviour should be if we did this:

<Profile avatarType="xxx"  avatar-type="xxx">
export let avatarType

This is problematic for some SVG elements which have camelcased attributes' name.
That would require a whitelist.
e.g. viewBox

@Th1nkK1D
Copy link

Hi, I tweaked @oranmor awesome workaround a bit to inject a wrapper during build-time, leaving the original Svelte component untouched, and no corresponded wrapper component file needed. (Very useful if we have multiple WCs)

First, I create a customElements.define mock with @oranmor workaround

/* src/utils/custom-element.js */

export const customElements = {
  define: (tagName, CustomElement) => {
    class CustomElementWrapper extends CustomElement {
      static get observedAttributes() {
        return (super.observedAttributes || []).map((attr) =>
          attr.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase(),
        );
      }

      attributeChangedCallback(attrName, oldValue, newValue) {
        super.attributeChangedCallback(
          attrName.replace(/-([a-z])/g, (_, up) => up.toUpperCase()),
          oldValue,
          newValue === '' ? true : newValue, // [Tweaked] Value of omitted value attribute will be true
        );
      }
    }

    window.customElements.define(tagName, CustomElementWrapper); // <--- Call the actual customElements.define with our wrapper
  },
};

Then I used esbuild inject option to inject the above code to the top of the built file

/* esbuild.js */

import { build } from 'esbuild';
import esbuildSvelte from 'esbuild-svelte';
import sveltePreprocess from 'svelte-preprocess';

// ...
    build({
      entryPoints,
      ourdir,
      bundle: true,
      inject: ['src/utils/custom-element.js'], // <--- Inject our custom elements mock
      plugins: [
        esbuildSvelte({
          preprocess: [sveltePreprocess()],
          compileOptions: { customElement: true },
        }),
      ],
    })
// ...

When build, my original Svelte components like this:

<!-- src/components/navbar/navbar.wc.svelte -->

<svelte:options tag="elect-navbar" />

<!-- Svelte Component ... -->

Will have an output like this:

// components/navbar.js

(() => {
  // src/utils/custom-element.js
  var customElements = {
    define: (tagName, CustomElement) => {
      // Our mocked customElements.define logic ...
    }
  };

  // Svelte compiled code ...

  customElements.define("elect-navbar", Navbar_wc); // <--- This code compiled by Svelte will called our mocked function instead of actual customElements.define
  var navbar_wc_default = Navbar_wc;
})();

This idea should be adaptable with other bundlers as well. I want to share this since it's might be useful, or it might be a bad solution. So, please feel free to leave feedback. Here is the Svelte WC collection project I'm working on using the above technique (+ WindiCSS and Storybook).

@sawden
Copy link

sawden commented Jun 28, 2021

2 years and the workaround is still the best solution? 😕

@Acmion
Copy link

Acmion commented Nov 4, 2021

It should also be noted that the workaround is for "custom elements" (=web components). A "custom element" is not the same thing as a Svelte component.

I for one do not really care about web components, but would like to have all Svelte components support hyphenation in properties and component names.

@roonie007
Copy link

For those who needs this so bad, I just created a vite plugin that adds kebab-case support for Svelte components vite-plugin-svelte-kebab-props

@joelhickok
Copy link

If not using the Vite plugin, you can also watch $$props on the child component. This does work, and you can reassign the $$props properties to internal component variables.

let myValue = null

$: {
    console.log($$props)
    myValue = $$props['my-value']
}

@DoisKoh
Copy link

DoisKoh commented Jan 13, 2023

For me it's not about warnings; I've been trying to avoid using $$props because of the optimization problem but I'm forced to because of this. I can't create a standardized custom attribute that I can use with normal HTML elements (following the data-* convention) and also use it with my Svelte components.

@willnationsdev
Copy link

willnationsdev commented Jul 23, 2023

Based on the current documentation, if you are defining a custom element, I would think that you're able to do this now by specifying an attribute name in the props section of the customElement part of <svelte:options>:

<svelte:options customElement={{
  tag: "my-tag",
  props: {
    myValue: { reflect: true, attribute: "my-value" }
  }
}} />

<script>
  export let myValue = "";
</script>

I haven't actually tried this, and it obviously isn't an automatic solution for all properties matching up with all attributes, but it's at least some kind of proper solution to the problem with a direct binding, assuming you are using Svelte for custom elements. Still, an idiomatic and automatic solution that works with both custom elements and traditional Svelte components would be ideal.

Perhaps there could be a svelte:options property for optionally mapping & syncing property names to attribute names using a lambda function, similar to extend, but have it also come with the option of just specifying a string instead of a lambda where the string tells the Svelte compiler to use a given strategy/implementation behind-the-scenes?

<svelte:options attributes: (propName) => attributeName />
<svelte:options attributes: "kebab" />

Then, if a value for attributes is set at all, it becomes implicit that all exported properties will generate corresponding attributes and be reflected back to the DOM's attributes, etc.

@unikitty37
Copy link

How do I use the $$props solution in conjunction with TypeScript?

If I have this in Thingy.svelte:

<script lang="ts">
  export let name: string
  let myValue: string | undefined = undefined

  $: myValue = $$props['my-value']
</script>

and then invoke it with

<Thingy name="Bob" my-value="Stuff" />

TypeScript will complain that my-value isn't in the type signature.

@Caellian
Copy link

Caellian commented Jan 9, 2024

@unikitty37

I'm assuming value of $$props is any? If so:

$: myValue = $$props['my-value'] as SomeType

Otherwise:

$: myValue = $$props['my-value'] as unknown as SomeType

@unikitty37
Copy link

@Caellian Unfortunately I can't test this in the REPL as sveltejs/sites#156 still hasn't been implemented — but isn't that going to set the type of myValue within Thingy.svelte, while still leaving the error on the invocation of <Thingy name="Bob" my-value="Stuff" />?

I can't see how that cast will affect the signature of $$props, which is what TypeScript was complaining about…

@dummdidumm
Copy link
Member

Declaring hyphenated attributes will be possible in Svelte 5 using the $props rune.
For custom elements you can also get this today as outlined in #3852 (comment)

@dummdidumm dummdidumm added this to the 5.0 milestone Feb 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests