-
-
Notifications
You must be signed in to change notification settings - Fork 33.7k
Proposal: Template inheritance using the slots mechanism #6811
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
Comments
Sorry but I really don't see what this provides over just: <!-- child.vue -->
<template>
<parent>
your stuff
</parent>
</template> |
@yyx990803 the names "Parent" / "Child" in the original example meant class-like inheritance, not a tree of components. I updated the example to make things more clear. The point here (as in #5401, and #6536, and in many places on the internet) is being able to extend a complex component for overriding something at script-level, while also doing small overrides at the template level. The simplest way to achieve this as per your example would be: <template>
<base>
..stuff
</base>
</template>
<script>
import Base from './Base'
export default {
extends: Base,
components: {
base: Base,
}
}
</script> ... which is a big bug waiting to happen. The non-simple but safe ways are:
This, as the author put it in #5401, is very cumbersome and fragile. I love Vue precisely because it allows doing OOP-like multiple inheritance with But this has been extensively discussed under #5401, where you seemed to agree there's an issue (although you obviously didn't agree with its severity as perceived by us). However you rejected the proposed solution because "having an extra composition model greatly muddles the structure of the program", which is a reasonable argument. So there, this proposal solves the issue and preserves the composition model while looking natural and clean. |
@yyx990803 @xlotlu I finally decided to add my two cents here because I am still a bit surprised and frustrated. The problem with the approaches I can think of using the existing mechanisms like mixins and slots is, if you want to change the Javascript AND the Markup, you always end up implementing the composition of elements at least two times. I don't think using scopes to define how a potential child component is able to merge additional content in to the parent's template, is bad at all. As the OP points out, I don't see how this feature would collide with the conventional scopes. This feature request is just about defining rules how the existing scopes could be used the other way round from within the child component to enable semantically correct component inheritance. It should be said that I am neither Vue nor Javascript expert and therefore I have certainly overlooked important points that speak against such a feature. |
If get it right, in your example a whole new instance of the parent component is created. In the proposed approach, the child component IS A parent actually and it is the only instance created in this situation. The parent either serves as a prototype for the child or is directly used at the same level of the markup tree, because they are equivalent and independent instances. Here you can see, what I want to do: |
Shameless copy-paste of my comment on the issue mentioned above #5401 Currently I'm running in the same kind of issue. Like Justineo (in #5401) said, introducing another language, like pug, only to be able to do this looks overkill for me. So introducing a native VueJs way of just filling in the slots would be amazing. btw, I can't include the CanvasBox as a component in the new component's template because there's logic in the CanvasBox's created hook for rendering to the HTML5 Canvas and using CanvasBox as component in the template would cause it to run this logic twice. So last but not least, I would also love a easy way of extending the template or at least filling in the slots of the component which is extended. |
I also want to make a proposal for in my eyes, a better syntax. It is as follows: <!-- Base.vue -->
<template>
<div>
<slot name="header">
<h3>Default header</h3>
</slot>
<slot><!-- yup, default --></slot>
<slot name="body">
<p>Default body</p>
</slot>
<slot name="footer">
<!-- no footer by default -->
</slot>
</div>
</template> <!-- SomeComponent.vue -->
<template slot="header">
<!-- Headery stuff -->
</template>
<template slot="body">
<!-- Body Stuff -->
</template>
<template slot="footer">
<!-- footer stuff -->
</template>
<script>
import Base from 'Base.vue';
export default {
name: 'SomeComponent',
extends: Base,
}
</script> |
@dvdbot this is so straightforward and obvious. There remains the problem of the default slot though, and the fact that currently multiple template blocks don't make sense to vue. (on a related note, that is a feature I wanted so I could define subcomponents locally, e.g.: <template>
<!-- main thing stuff ... -->
</template>
<template id="subthing-tpl">
<!-- sub thing stuff ... -->
</template>
<script>
import Thing from 'Thing'
import SubThing from 'SubThing'
export default Thing.extend({
components: {
subthing: SubThing.extend({
template: "#subthing-tpl",
})
}
})
</script> ... :) )
Incidentally, I encountered this in a similar scenario: I was using d3 to do the rendering, and vue as the data-handling backbone. |
why not default slot => <template slot=""> |
Currently using a cheaty workaround :P - which works :P |
I understand the arguments for having an actual "extend template" built into the language - using the parent version in the template initializes another extra "unnecessary" component, which isn't optimal for performance. I agree that it is weird that this isn't built into the language, both to avoid the extra component and to simplify props passing. However, the pattern for using the parent element as a slot is quite easy, and since this is one of the first Google results for the topic, here's a clean solution that has worked well for us: // Base Component
<template>
<div class="base-thing special-class">
<p>Special {{label}}</p>
<slot/>
</div>
</template>
<script>
export default {
name: "BaseComponent",
props: {
label: {
type: String,
required: true
},
}
}
</script> // Inheriting Component
<template>
<!-- $props passes all of the props on to the "parent" component -->
<base-component v-bind="$props">
<p>this is the extraProp: {{ extraProp }}</p>
<!-- $attrs and $listeners respectively passes any additional non-prop
attributes and any event listeners on to the child -->
<input v-model="someModel" v-bind="$attrs" v-on="$listeners">
</base-component>
</template>
<script>
import BaseComponent from './BaseComponent'
export default {
name: 'InheritingComponent',
components: {
BaseComponent
},
extends: BaseComponent,
inheritAttrs: false,
props: {
extraProp: {
type: String,
default: "Goodbye"
},
},
data() {
return {
someModel: 'test'
}
}
}
</script> Usage: <inheriting-component label="Hello, World!" extraProp="Nevermind" /> Output: <div class="base-thing special-class">
<p>Special Hello, World!</p>
<p>this is the extraProp: Nevermind</p>
<input type="text" value="test">
</div> |
The problem I'm running into when using your example @bbugh is that I've got logic in the created hook which draws something to the HTML5 Canvas - when using your example, the created hook is called on the base and the extending component, this is not what I want because the drawing is done twice - also I can't override the created hook without adding custom merge logic, but adding custom merge logic is global and not local for this component only, as far as I know and adding such custom merge logic may cause unfurseen side effects |
For now I cheated the props part by using the following: props: {
...BaseComponent.props
} And then using the v-bind="$props" |
Also not extending the BaseComponent |
@bbugh I covered this approach in my initial rebuttal of @yyx990803 's rejection: #6811 (comment). I called it "a big bug waiting to happen" for the same reason as @dvdbot: hooks and watchers. I wasn't aware of |
@xlotlu @dvdbot could I get your thoughts on this as a possible solution? anthonygore/vue-template-extension-loader#1 |
+1 IMHO, it would be great to have "template inheritance" as a part of a Vue core. In my case, I've extracted a few base components, for example, EditForm. Each child has its own fields, but they have the same buttons (Save, Save & Continue, Cancel). I have to copy-paste the same HTML code over and over now. Suggestion to use the parent as a separate component inside the child template - also doesn't work for me, and I don't like it. I want to have one child component on all levels - script and template, both. |
+1 for the "template inheritance". I use Nunjucks template engine which has this feature out-of-box. That's really convenient and powerful. In general there are two ways of reusing the code: the inheritance and the composition. Last week React announced React-hooks which are focused on the composition. Why doesn't Vue want to implement the first one? I have no idea...) |
@yyx990803 please, read my comment above and share your thoughts about it : ) Your comment here is just about composition but not inheritance (of templates). There is a difference. P.S: In my opinion "template inheritance" could be the new benefit of Vue. |
@yyx990803 why was this closed without further consideration? There's a lot of good comments and discussion about this topic, not just in here, but also in the referenced issues. Clearly this is something the community is very interested in, and it is not currently provided by vue. Component inheritance is not the same as nested parent/child components. |
Hi guys, If you know Chinses, please look it Vue Component 继承与复用
The Button of Child Component will be replaced OtherButton. We can do something in the OtherButton |
I also would like this to move forward, I have a base component that calls methods that are overriden by the child components, and this is not working now |
Coming from mostly using html templating languages like twig with minimal js, not being able to extend the parent component's template and override small portions in the child component feels really weird and awkward. The way it is right now if you want to change the js and template of a component directly, you need to first extend it to change the js, then wrap it to change the template. On the other hand if any of the solutions in this issue are implemented, you can get rid of the wrapper file completely, which is much cleaner. I get that it might not be that easy given how component extension is sort of buried in the js, but I think it's a necessary feature for ease-of-use in the long run. |
How to force the parent element to call child methods and use child data, props and ...? |
I ended up making a loader: https://github.com/mrodal/vue-inheritance-loader |
This should be simple and useful, something along the lines.
And at the end of the day if you wanna extend some templates you could put things behind or in front of it, if you wanna put something in between elements on the template it needs to contain a slot. |
Hi! What's the current way of extending a template? When extending a class, my child component just doesn't have any template at all... I might be missing something, but this is the closest issue I could find. Thanks! |
这种替换方法,使得组件数量增加,达不到扩展的目的.既然替换不如直接拆分,使用props传值 |
Although I still think having template inheritance in the tool box would not really do any harm, I must meanwhile also assist the ones saying you have probably not internalized the concept of composition if you're asking for this feature. That's at least what I found while becoming a more advanced vue developer during the last months. |
If I may say so, the corollary to this statement is that if one makes the case that template inheritance is useless because composition fits all scenarios, one probably has not internalized the concept of inheritance. Ten minutes spent within a good django or pug-based project might turn into an eye opener. The project that prompted this issue at the time ended up filled with idiosyncrasies and code that is hard to follow due to this glaring omission, forcing us indeed to use composition when inheritance would have simply been straightforward, clean, and avoided useless repetition. Such an example is MapBase.vue as it's used under BaseMap.vue, which in turn gets inherited by all the other '*Map.vue' files in the repository. And that's only one example. Yes, composition is great and useful and any developer worth their salt will be using it. That's why people invented functions. But inheritance and composition are complementary. That's why people invented classes. The funny thing is Vue does have inheritance, and that's precisely what makes it great. But frankly, sporting inheritance at the code level while making the case that it's useless at the template level is mind-boggling. |
I did not want and will not argue against the value of this feature in general and that it would make sense for certain cases to have the option to use full-component inheritance over composition. And I don't want to criticize anyone who has gone a long way of thought and come up with valid reasons to ask for such a feature. So please don't feel offended. I just wanted to remind people to always keep questioning themselves. Most developers (me included obviously) overrate themselves and are quite stuck in their methodologies. It's actually very hard to understand and accept a new philosophy that is fundamentally different from what you are used to. And that's what happened to me when I started with VueJs, even so I wouldn't say I was generally a bad developer. I was in fact a bad VueJs developer. ;)
That's what brought me to this topic and I have posted my thoughts on this before. |
i don't think he knows what inheritance really is... |
What does that refer to? |
You can do it with pug https://pugjs.org/language/inheritance.html |
Well, yes, because the Pug designers get how essential it is in any large-scale project worth its salt. But you can't use use Pug's inheritance with single-file components. |
@yyx990803 I think this is a valid use-case: <template>
<div class="modal fade" tabindex="-1" role="dialog" data-keyboard="false" data-backdrop="static">
<div class="modal-dialog modal-dialog-centered" :class="[size]" role="document">
<div class="modal-content">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="fa fa-times"></i>
</button>
<slot name="content">
<div class="modal-header">
<slot name="header">
<h5 class="modal-title">Modal</h5>
</slot>
</div>
<div class="modal-body">
<slot name="body"></slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button type="button" class="btn btn-primary" @click.prevent="submit">Save</button>
</slot>
</div>
</slot>
</div>
</div>
</div>
</template>
<script>
module.exports = {
props: {
size: {
'default': 'modal-md'
}
},
data: function() {
return {
shown: false
}
},
methods: {
show: function () {
$(this.$el).modal('show');
},
hide: function () {
$(this.$el).modal('hide');
},
submit: function () {
console.log('SUBMITTED MODAL');
this.hide();
}
}
};
</script>
<template>
<parent>
<div slot="body">
...
</div>
</parent>
</template>
<script>
const Modal = require('./modal').default;
module.exports = {
name: 'ExtendedModal',
extends: Modal,
components: {
parent: Modal,
},
methods: {
submit: function () {
console.log('OVERRIDE IS WORKING');
}
},
};
</script> The |
Checkout this loader, It only uses slot mechanism and you can override slots in child component |
Hi @SasanFarrokh, have you tested it? that module could save me lots of work, but neither the most simple attempt worked out. At first I thought I've a weird configuration that doesn't allow the loader to work, then tested it with the test inside the module and it worked, finally use Anyone has had any luck with this module? It's like the rule never catch the slot section in SFC. |
@chumager Did you config the loader correctly? Use the sample on the document, if still there is problems open an issue on the repository: |
@SasanFarrokh, It doesn't work for me, I've made some changes to avoid overload slots, it works fine except when there is async vNodes inside (the root or children), in that case it doesn't render, I've been reading to found the way to render vNodes with asyncFactory but I haven't found anything about it... Do you know how to do it? I've changed everything in my framework to work with this approach and now I NEED to accomplish this change. BTW your idea was AWESOME... |
Template inheritance I solved it as follows using vue-router. -- public/index.html <!DOCTYPE html>
<html lang="">
<head>
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<!-- Title -->
<title><%= htmlWebpackPlugin.options.title %> - Event</title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html> -- src/App.vue <template>
<div id="myapp">
<router-view />
</div>
</template> -- src/views/_layouts/master.vue <template>
<div>Master page content</div>
<router-view></router-view>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "master",
});
</script> -- src/views/home/Home.vue <template>
<div>Page content</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "Home",
});
</script> -- src/router/index.ts import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import Master from "../views/_layout/master.vue";
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "Home",
component: Master,
children: [
{
path: "/",
name: "xyz",
component: () => import("../views/home/Home.vue"),
},
],
},
{
path: "/about",
name: "About",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/About.vue"),
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router; And then execute |
@SasanFarrokh, this seems very promising! |
What problem does this feature solve?
Within a large application that makes heavy use of inheritance and mixins, the lack of template inheritance causes much jumping through hoops. The rationale under #5401 is eloquently written and captures the gist of it, so I won't repeat it here.
What does the proposed API look like?
My proposal for solving this issue is to simply extend upon the existing slots mechanism. That is (shamelessly copy/pasting from @simplesmiler's example under the issue above):
Thus the existing composition rules are preserved, while it feels natural to both Vue developers and people used to templates with block-based inheritance (like Django, Pug etc.)
The only rules are that
extends
attribute can be used only with single-file components,extends="#template-element"
,<template extends>
can allow multiple root elements.The text was updated successfully, but these errors were encountered: