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

Component inheritance #192

Closed
ghost opened this issue Dec 13, 2016 · 24 comments
Closed

Component inheritance #192

ghost opened this issue Dec 13, 2016 · 24 comments

Comments

@ghost
Copy link

ghost commented Dec 13, 2016

The documentation does not indicate if component inheritance can be done with Svelte.

Is it possible for a component at definition time (before the compilation, not at instantiation time) to inherit the default data, methods, helpers, hooks, components and events from another component or to overwrite them ?

Is the HTML part of a component and the style a property that could be inherited or overwritten too ?

Thanks for the nice repository.

@Datamance
Copy link

+1 to this!

@Swatinem
Copy link
Member

At least in the react community, composition and higher-order-components are preferred instead of inheritance. I opened #195 for some features that could make HOC a lot simpler in svelte.

@nithiz
Copy link

nithiz commented Dec 14, 2016

Would love to see this as well!

@ghost
Copy link
Author

ghost commented Dec 15, 2016

I think there are three relevant concepts here: modules, composition and inheritance.

Modules allow for separating, organizing and avoiding the duplication of code. Composition allows for nesting or mixing components together, and inheritance allows for a meaningful and hierarchical organization of components (which helps too for the non-duplication of code).

To me, they all are helpful and have their own usage, they are complementary. But some trouble may come when one uses them for the finality of the others, because one mixes their purposes which gets confusing. Inheritance makes the components better self-contained and there is less guessing about what they are. Because components are objects, they are instantiated, so inheritance makes sense.

High-order components seem more like an attempt to do inheritance based on composition principles (from top to bottom). But composition and inheritance have different purposes.

That's why I like, personally, to have both in my code. This allows for a better thinking about the application, and finally, a better organization too.

@Rich-Harris
Copy link
Member

I'm of the 'favour composition over inheritance' school – in my experience, inheritance is more trouble than it's worth. Your components become less predictable (because you have to understand the inheritance chain before you can get a handle on how a component behaves – just reading the code in a single file no longer suffices), and you have to either mess around with transpilers (which not everyone wants to do) or implement a non-standard super.method() equivalent.

That said, I'd be curious to know more about where you'd want to use inheritance so we can understand the problem more fully.

@ghost
Copy link
Author

ghost commented Dec 16, 2016

What comes to mind is the following: prototyping the DOM helpers, observe method, and other things that are common to all components into a base class component could be a way of introducing inheritance that would reduce code duplication (#9) as well as answer to the need for scaling up the application with many components that are hierarchically related, but not necessarily "composited".

In simpler words, the idea would be to extend a base component class like so:

<div>Hello!</div>
<script>
export default class Hello extends Base {
    data () {
    },
    methods:{
    	sayHello(){}
    }
    render(){}
  };
</script>

Then, the Hello component itself could be extended like so:

<div>Hello Plus!</div>
<script>
import Hello from 'Hello.html'

export default class HelloPlus extends Hello {
    methods:{
    	sayHelloPlus(){}
    }
    render(){}
  };
</script>

Like the importing of nested components, one would import the component to extend, so there would not be more separation than with nested components. In both cases, one would import a component either for compositing either for extending either for both...(#51)

One component could extend another by simply modifying its HTML, CSS, just adding some methods to it or even completely changing it while keeping the parent component events and data. There would be much flexibility.

That is just an idea that came naturally in mind when looking at the structure of the components while reading the documentation and the examples.

@Rich-Harris
Copy link
Member

Exporting a class doesn't really work, because classes can only have methods. It also introduces a hard requirement to transpile your code. Realistically, inheritance would work more like this...

<script>
  import Base from './Base.html';

  export default {
    base: Base,

    // ...
  };
</script>

...but without a real world use case I'm still not convinced it's desirable. It doesn't really add any expressive power, because you could easily share methods like so:

<script>
  import sayHello from './sayHello.js';

  export default {
    methods: {
      sayHello,
      sayHelloPlus () {}
    }
  };
</script>

This is much clearer in my opinion.

@ghost
Copy link
Author

ghost commented Dec 17, 2016

Yes, one meaned more something like that:

<div>Hello {{who}}! {{what}}</div>

<script>
import helpers from './helpers.js';
import Base from './Base.html';

export default class Hello extends Base {
   constructor(what, who){
    this.helpers = helpers;
    this.data = {
        what:what,
        who:who
   };
    this.events = {};
   },
  sayHello() {},
  render(){}
  };
</script>

and

<div>Hello Plus {{who}} ! 
<super duber="cool">{{what}}</super>
</div>

<script>
import super from 'super.html'
import Hello from 'Hello.html'
export default class HelloPlus extends Hello {
  sayHelloPlus() {}
}
</script>

That is much more concise :)

@Rich-Harris
Copy link
Member

That won't work – it needs to be an object literal for the sake of static analysis.

@ghost
Copy link
Author

ghost commented Dec 17, 2016

Alright, I thought one could have used some es6 introspection functionalities on this.

@ghost
Copy link
Author

ghost commented Dec 18, 2016

So, unleashing the power of POJOs, that would give the nicer and more functional:

Hello.html:

<div>Hello {{who}}! {{what}}</div>

<script>
import helpers from './helpers.js';
import Base from './Base.html';

export default Object.create(Base).assign({
    data() {
      return {
        what:"",
        who:""
      }
   },
   helpers : helpers,
   events: {},
   sayHello() {},
   render(){}
  });
</script>

and HelloPlus.html:

<div>Hello Plus {{who}} ! 
<zuper duber="cool">{{what}}</zuper>
</div>

<script>
import zuper from 'zuper.html'
import Hello from 'Hello.html'

export default Object.create(Hello).assign({
  sayHelloPlus() {},
  components: {
      zuper
  }
});
</script>

Not sure how that would work into the svelte internals though :)

I'm wondering now, as it is simply a POJO, perhaps does it already work out of the box ?

@Rich-Harris
Copy link
Member

It's not about whether it's a POJO, it's about whether it's an object literal, by which I mean that the declaration property of the default export is a node of type ObjectExpression. See http://astexplorer.net/#/FiqmTmmpOG.

I'm going to close this though as I don't think there's a good argument in favour of implementing inheritance.

@ghost
Copy link
Author

ghost commented Dec 18, 2016

Alright, so that is quite restrictive... As there is no inheritance and no will to implement it, I won't bother you more with it. Thanks for the time explaining.

@Gin-Quin
Copy link

Gin-Quin commented Jul 9, 2021

By his "components as modules" design rather than a "components as classes" design Svelte is not well-suited for component inheritance, but there are really good arguments for component inheritance in general.

When talking about inheritance a lot of people say "composition is better" but that's not true - both are differents and you should not just pick a side. Saying composition is better is like saying: "we should always use 'has' and not 'is'".

"has" and "is" are quite close indeed (in english you say "I am X years old", in french you say "I have X years") but both have their use cases where they shine better than the other. And sometimes inheritance elegantly solves issues that composition cannot (at least not with the elegancy). I am a Svelte user and I recently stumbled into a situation where I need component inheritance.

So, what can inheritance give?

Let's say we have this Button.svelte component:

<script>
  export let value = false;
  export let outlined = false;
  export let text = false;
  export let block = false;
  export let disabled = false;
  export let icon = null;
  export let small = false;
  export let light = false;
  export let dark = false;
  export let flat = false;
  export let iconClass = "";
  export let color = "primary";
  export let href = null;
  export let fab = false;
  export let type = "button";

  // ... more code
</script>

<button
  use:ripple
  class={classes}
  {...props}
  {type}
  {disabled}
  on:click={() => (value = !value)}
  on:click
  on:mouseover
>
  {#if icon}
    <Icon class={iClasses} {small}>{icon}</Icon>
  {/if}
  <slot />
</button>

This non-working code is a part of the Button component from the Smelte UI-kit library.

My Button works fine. That's cool. But what if I want to have my own button component slightly different?

Let's say I want an additional "pushed" property that tells whether my button is pushed or not. That immediately "feels" like inheritance: I want a new component that is a Button - with just one more property.

Since there is no way to do inheritance let's use composition and create our "PushButton.svelte" component:

<script>
  import Button from "./Button.svelte"
  
  // our new property
  export let pushed = false;

  // all other Button properties that we have to repeat
  export let value = false;
  export let outlined = false;
  export let text = false;
  export let block = false;
  export let disabled = false;
  export let icon = null;
  export let small = false;
  export let light = false;
  export let dark = false;
  export let flat = false;
  export let iconClass = "";
  export let color = "primary";
  export let href = null;
  export let fab = false;
  export let type = "button";
</script>

<Button
  bind:value
  bind:outlined
  bind:text
  bind:block
  bind:disabled
  bind:icon
  bind:small
  bind:light
  bind:dark
  bind:flat
  bind:iconClass
  bind:color
  bind:href
  bind:fab
  bind:type
/>

Immediately we see why composition does not do inheritance's job as well as inheritance. We have to repeat all the props, Which is 1. very annoying and 2. a bad design.

With an imaginary inheritance syntax our PushButton.svelte file could be:

<script>
  import Button from "./Button.svelte"
  export default class PushButton extends Button {
    pushed = false
 } 
</script>

and that's all. No need to repeat every property. The sveltish html and css would be inherited as well. DRY.

Present workarounds

  • Use {...$$props}. It works very fine with Javascript but unfortunately not at all with Typescript: you lose all your precious property type informations that will save you hours of programming.
  • Use an object of properties instead of many single properties. But if you use a component from an external library chances are extremely high they didn't do that.

Also the previous solutions work with props but not with slots. What if you have an intermediate component that need to pass an arbitrary number of arbitrary named slots from a parent component to a child component? You just cannot do that. With inheritance you could.

Conclusion

I'm not asking for component inheritance in Svelte since I think that would need quite a refactoring. My point is just to explain why component inheritance is actually a good thing. Especially when you are developing component libraries. I actually need it in my current project and it's quite frustrating to feel stuck. I think I will have no other choice than to "copy-paste these props and slots one by one".

As a final note I want to say that Svelte and SvelteKit are awesome. Keep the good work!

@milahu
Copy link
Contributor

milahu commented Jul 10, 2021

Present workarounds

  • maintain a fork of the Button.svelte file, for example with patch-package - cost: code duplication

@Gin-Quin
Copy link

That's a workaround for patching a library rather than for inheritance. You may want to use PushButton as well as Button without replacing it!

@Morbden
Copy link

Morbden commented Aug 5, 2021

I migrated from svelte to react for that reason.
In react, I can use inheritance and logic nodes(React.Fragment), which helps keep the html semantics.

@sebastianconcept
Copy link

sebastianconcept commented May 9, 2022

Composition over inheritance is not composition and never inheritance.

Having both and being a good software designer that knows when to use which one is the correct level of software literacy.

PS: I love Svelte and I think it captures a fantastic project spirit.
PS2: I do have use cases for Component inheritance and I'm pushing design workarounds to make composition to be enough like exporting a protocol var where a child component can get all these methods from a parent in one shot, for granular state from the parent that can't happen but, with good refactoring, that is way less needed:

  let protocol;
  
  const privateProtocol = {
    answerOfHandshake,
    answerOfCallback,
  };

  function onViewMounted() {
    // This component's View is mounted, we can now extend its protocol to 
    // handle the handshake and callbacks answers in its own unique and specialized way.
    protocol = { ...protocol, ...privateProtocol };
  }
 
  ...
  
<div class="container">
  <View
    bind:id
    {handshakeOptions}
    bind:protocol
    children={{ introspector, workspace }}
    viewType="Inspector"
    bind:socket
    on:viewmounted={onViewMounted}
  >
    <div class="tree">
      <Introspector
        id={introspector.id}
        name={introspector.name}
        bind:roots
        on:nodeselected={onNodeSelected}
      />
    </div>
    <div class="display">
      <div>{inspecteeDisplayString}</div>
    </div>
    <div class="workspace">
      <WorkspaceArea id={workspace.id} name={workspace.name} />
    </div>
  </View>
</div>

@smblee
Copy link

smblee commented Jul 8, 2022

For those who are here via Google looking to do this, you can do the following now:

<script lang="ts">
    import type { ComponentProps } from 'svelte';
	interface $$Props extends  ComponentProps<Button> {}
</script>


<Button whateverButtonProp={...} {...$$restProps} on:click on:any-action-you-wanna-forward>
	<slot />
</Button>

props can be handled via ...$$restProps, prop types can be done via interface $$Props extends ButtonProps {}, actions unfortunately need to be specified explicitly like on:click (theres no such thing as ...$$restActions atm.), and slots also need to be specified explicitly.

@dummdidumm
Copy link
Member

Please do not use Button["$$prop_def"], that key is private API which may break in unexpected ways. The latest release of Svelte contains a utility function CompentProps, use that instead: ComponentProps<Button>

@smblee
Copy link

smblee commented Jul 8, 2022

@dummdidumm updated the code sample!

@ClementNerma
Copy link

The problem with this solution is that you can't access the properties easily after. If the props contain, say, a property named name: string, I can't access name anywhere in the <script> tag. Or am I missing something?

@blujedis
Copy link

@dummdidumm @Rich-Harris if either of you might be gracious and comment on the below. If you see any trouble spots please share, feelings are irrelevent here :)

Using a helper class I pass in a config that when parsed auto builds your types based on that config. You then build your defaults specifying any required props and the result extends $$Props.

Now you have all your types. A clean up method purges keys like $$slots and $$scope that way your resulting props that turn things on and off for that component, add/remove styles, classes etc can be parsed out and the rest won't contain those internal props.

The .addClass() and .addFeature() (among others) methods accept a key and a value that is either a bool or a value that represents a nested object's key. The builder class will then pick those values to build things up. (only one level of nesting currently)

Lots more to this but would love a comment, see what others think. Allows for sharing configs with good typing, theming with variants etc. More to explain but I'll leave it at that. I'll be releasing this soon along with a Svelte UI library that allows realtime theming using Tailwind. You import our plugin, init your color palette and using SvelteStore its live. Again more on that soon!

Here's what a simplified component might look like. As you can tell it's concise and the types are just buttery!!!

NOTE: this came about out of a need to share option props with strict typing in a consistent and concise way.

<script lang="ts">
  import { Builder, shared } from '$lib/theme';
  import type { PickElement, Theme } from '$lib/types';
  
  type ElementProps = PickElement<'button', 'size'>; // helper type picks html button's props.
  type Defaults = typeof defaults; // gets type from below builder.defaults()

  // extend our interface making our props avail.
  interface $$Props extends ElementProps, Defaults {
   // add any other custom props you might like 
   // the consuming parent will have access  to all.
  } 
  
  // nothing more than an object of key value options. If a value is not
  // an object the types parsed will be  of type boolean if an object
  // the type parsed will be that of a key in the nested object.
  // NOTE: this is tailwind but these classes could just as easily come from the
  // component's <style></style> classes. 
  const features = {
    full: 'w-full',
    size: {
      none: '',
      xs: 'px-2.5 py-1.5 text-xs',
      sm: 'px-3 py-2 text-sm',
      md: 'px-4 py-2 text-sm',
      lg: 'px-4 py-2 text-base',
      xl: 'px-6 py-3 text-base',
      '2xl': 'px-8 py-4 text-base'
    },
   // properties that are arrays results in a type that requires one of the values.
    theme: ['primary', 'secondary'...] as const 
  };
  
  const builder = new Builder(features);
  
   // note below how it knows that size requires one of the nested keys for it's default value.
  // whereas "full" accepts a bool to toggle it on or off.
  const defaults = builder.defaults({
    full: false,
    theme: 'primary',
  }, 'size'); // while you likely would set a default size, bad example I'm setting 'size' to be a required prop.
  
  const props: $$Props = {
    ...defaults,
    ...$$props
  };
  
  const {size, full, ...rest} = builder.cleanProps(props);
  
  rest.class = builder
    .addFeature('size', size) // the size's value will be picked from 'features.size.md' in this case
    .addFeature('full', full)  // if full is true then `features.full` value will be used.
    .addClass('inline-flex items-center font-medium focus:outline-none text-sm border-1') // just add these classes
    .addUserClass(rest.class) // ensures this is last classes to be added so can override.
    .addFilters() // classes that should be filtered out/excluded. Useful when sharing base configs
    .bundle();
  </script>
  <button {...rest} >
    <slot />
  </button>

@ftognetto
Copy link

Hi all, I too ran into this problem and before arriving at this issue I had found a way to declare the component properties by extending those of the child with

interface $$Props extends ComponentProps<Child> {}

I however have problem using typescript.
The child component exposes many properties among which some are mandatory, and therefore going to write

<Child
	{...$$restProps}
>

The compiler rightly complains that "Property 'x' is missing in type" ... ts(2741).

Has anyone had the same problem and found a solution? Thanks in advance

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