-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Comments
+1 to this! |
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. |
Would love to see this as well! |
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. |
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 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. |
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. |
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. |
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 :) |
That won't work – it needs to be an object literal for the sake of static analysis. |
Alright, I thought one could have used some es6 introspection functionalities on this. |
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 ? |
It's not about whether it's a POJO, it's about whether it's an object literal, by which I mean that the I'm going to close this though as I don't think there's a good argument in favour of implementing inheritance. |
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. |
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 <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>
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 <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
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. ConclusionI'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! |
|
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! |
I migrated from svelte to react for that reason. |
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. 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> |
For those who are here via Google looking to do this, you can do the following now:
props can be handled via |
Please do not use |
@dummdidumm updated the code sample! |
The problem with this solution is that you can't access the properties easily after. If the props contain, say, a property named |
@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 Now you have all your types. A clean up method purges keys like The 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> |
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
I however have problem using typescript.
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 |
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.
The text was updated successfully, but these errors were encountered: