Skip to content

Template blocks for component inheritance #5401

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

Closed
simplesmiler opened this issue Apr 8, 2017 · 15 comments
Closed

Template blocks for component inheritance #5401

simplesmiler opened this issue Apr 8, 2017 · 15 comments

Comments

@simplesmiler
Copy link
Member

simplesmiler commented Apr 8, 2017

What problem does this feature solve?

In large applications, there are often families of components, within which components share most of the look/behavior, but vary slightly. This makes it desirable to reuse code between them. Some reuse can be achieved with slots, other is better with component inheritance. But while most of the parent component definition can be smart-merged with child definition, the template has to be either kept as is, or replaced entirely.

I have seen multiple approaches to reusing component templates:

  1. Write very granular appearance-only components with many slots. While this sounds good in theory, in practice the granularity needed often makes this approach into an obstruction rather than abstraction.

  2. Implement all required variation in a single component and make it configurable with slots and props. The downside is that you are stuck with a god-component, which is hard to maintain and extend further.

  3. Extend the component, and use <parent>...</parent> in the child template, using slots as "parts". The downside is that you have to "proxy" all props and slots down and events up. This is very cumbersome and fragile.

  4. Split the component in question further into "part" components, so that you can override only certain part. This gets tedious very quickly, especially when you need v-bind or v-on inside of the overridden part.

  5. Define "part" render functions among the methods, so that they can be overridden. The downside is that you can not really write such parts in template DSL, and the parts get disconnected from the main template, which makes it harder to understand.

What does the proposed API look like?

The Pug template engine (former Jade) implements a feature called blocks, which allows templates to extend other templates while overwriting (or extending) certain named blocks. I think this feature ported to Vue would fill the gap described above, and allow the templates to be more reusable.

A possible syntax:

<!-- parent.vue -->
<template>
  <div>
    <block name="header">
      <h3>Default header</h3>
    </block>
    <block name="body">
      <p>Default body</p>
    </block>
    <block name="footer">
      <!-- no footer by default -->
    </block>
  </div>
</template>
<!-- child.vue -->
<!-- there is no root element (only blocks), so the parent template is reused -->
<template>
  <block name="header">
    <h2>More pronounced header</h2>
  </block>
  <block name="footer">
    <p>Footer added by the child</p>
  </block>
</template>
@posva
Copy link
Member

posva commented Apr 9, 2017

How and where are child and parent connected in the proposed api example?

@ralphchristianeclipse
Copy link

@posva i think using the extend property

@yyx990803
Copy link
Member

First of all, I really don't think introducing a parallel composition mechanism is the way to solve this problem. In particular, when this is used in conjunction with slots it could lead to a lot of confusion.

Your solution (1), i.e. a structural component that exposes the outline + slots, is imo the proper abstraction for the problem - I'd like to see an actual example where it becomes obtrusive as you suggested.

Finally, template compilation for functional components, once supported, can probably alleviate this problem to some extent.

@Shyam-Chen
Copy link
Contributor

use posthtml?

@HerringtonDarkholme
Copy link
Member

TLDR: an example where structural component is verbose to proxy.

I agree that a new block inheritance will cause much confusion and that structural component should be the solution for this.

But I also have sympathy with @simplesmiler for extending component, namely the point 3 in OP's issue. I have a realistic example at hand. I don't know much about functional component with template, so I won't include it in this example.

Consider a generic map component, say google map clone.

  • It accepts many props: width, height, zoom level, lat/lng,
  • It emits many events: onZoomIn, onZoomOut, onPinClicked, onMapMoved, ....
  • It exposes many slots: a header, a zoom control, a view type control (switching satellite/terrain), some pins on map, a popup (when user clicks a pin), a legend.

The map component is very powerful. Arguably it is so powerful as to be a god component, but these features are cohesive enough to be organized into one component. So I choose to implement the map by structural component with default slot, rather than other methods in OP issue like splitting into smaller/representational components.

Then users will have to proxy all the props/events/slots.

For example, this is an imaginary implementation for component vue-map.

<template>
<div>
  <slot name="header">implementation.....</slot>
   <slot name="zoom-control">
      <button @click="zoomIn"></button>
   </slot>
   // template slot for pin
   <template v-for="pin in pins"> 
      <slot name="pin" lat="pin.lat" lng="pin.lat">
          <div class="default pin">I'm default pin!</div>
       </slot>   
   </template>
   <slot >
   // more to go....
</div>
</template>

<script>
export default {
  props: ['width', 'height', 'pins', ....],
  methods: {
    zoomIn() { this.$emit('zoomIn'); } // will emit more events
  }
}
</script>

Now, we want to create another generic map component, but for VIP users: the only difference is that pins on VIP map will have shiny borders and filaments.

<template>
<vue-map 
     @zoomIn="reemit" @zoomOut="reemit"  @mapMove="reemit" v-bind="$props">  // proxy
   <template scope="pin" slot="pin">
     <div class="bling-bling">
        <slot name="pin" lat="pin.lat" lng="pin.lng"></slot> // proxy
      </div>
   </template>
  <template slot="header"><slot name="header"></slot></template> // proxy
  <template slot="zoom-control"><slot name="zoom-control"></slot></template> // proxy
   // a lot to proxy
</vue-map>
</template>

It is quite verbose to proxy all events/slots. Proxying slot is error-prone, maybe due to the same name of original slot and new proxy slot.

We already have v-bind="$prop" to reduce the verbosity of proxying props, but it seems we cannot concisely proxy events or slots in template. (Similar discussion #4703).

Indeed, we can sidestep tedious works by using render functions and JSX. But it goes to OP's point 5: mixing template and render function is hard for understanding.

@simplesmiler
Copy link
Member Author

@yyx990803 with (1) components tend to become so granular that they end up being a div with a class, thus making the abstraction so thin that it might as well not even be there. That's what I meant by "obstructiveness".

I would have liked to avoid large examples, but that would defeat the purpose, so please bear with me.


Say you need to develop a panel component that would encapsulate the look of panels in the app. Generic panel has a title in the header and slot for content. But panels may have a lot of variation:

  1. Some panels need subtitle along with the title, some need an icon before or after the title, some need action buttons or other widgets aligned to the opposite side of the title.

  2. Some panels need their header shaded with color, some need header the same color as body (with or without a separator between them).

  3. Some panels need their content to be padded (e.g. text), some panels need their content to be flush with the panel borders (tables, charts, images, etc.).

To accommodate for this variation you decide to go with granular appearance components. Your panel when used, starts to look like this (not accounting for layout styles like flexbox):

<panel>
  <panel-header slot="panel-header" color-scheme="primary">
    <panel-title title="Settings" subtitle="subtitle" icon-left="cog"></panel-title>
    <action-list :actions="actions" @action="handleAction"></action-list>
  </panel-header>
  <panel-body slot="panel-body" flush>
    ...
  </panel-body>
</panel>

All is good so far.


Then you recognize, that among all the possible variation there are common use cases. So you decide to write components for these common use cases to avoid code duplication. This way you end up with text-panel, chart-panel, confirm-panel, etc. And this is where things start to go a little bit south.

If you want to write a specialized panel that behaves exactly like an existing one, but has some altered behavior/interface (e.g. confirm-panel with an extra button) then either:

  1. You need to put this variation into the parent component (e.g. in form of a slot or a prop).
  2. You need to redefine the child template completely, because there is no way to reuse parent template.

Of course you could define components that encapsulate common variations of parts of the panel. But at this point you will already have three levels of abstraction (appearance, common part, common panel). And every level requires you to proxy props, slots and events.

@ralphchristianeclipse
Copy link

Maybe you can try pug like this in vue

https://pugjs.org/language/inheritance.html

@maelgrove
Copy link

I'm also interested in this. In my specific case, I don't have very granular components, however several 'pages' where I need different 'layouts'.

Currently, I'm solving this using appearance-only components, as OP states it. I have a DefaultLayoutComponent which imports further stuff like SidebarComponent etc. as well as a BlankLayoutComponent which has nothing but a router-view as it's template. Composition is done with child components inside the vue-router configuration. This 'top-down' approach has a major drawback though: It results in a god object (the router configuration) which defines the entire composition of the application, instead as having modular components which could inherit certain aspects of their parents (e.g. template or imported components).

@yyx990803
Copy link
Member

I still feel that having an extra composition model greatly muddles the structure of the program and only makes the mental model more complex then it should be. Although the proposed solution may make certain use cases a bit more manageable, I'm not really convinced of the tradeoff.

@elsurudo
Copy link

@yyx990803 I think that without such a mechanism, component composition via inheritance is inherently broken, since it allows no affordances DRYing up your template. The fact that people are using the Pug template language workaround is proof enough of this, IMO. The downside, of course, is that it introduces another dependency, and a whole new templating language, just for this feature. I would urge you to re-open discussion on this!

@danielsvane
Copy link

Yeah, it feels like an extreme solution to change rendering engine, to be able to set the content of the breadcrumb from a child template. I do have workarounds, but it's not pretty.

@Justineo
Copy link
Member

Actually I'm with @simplesmiler @HerringtonDarkholme. We might need a way to reuse render logic in templates without messing up data context. Another parallel mechanism will definitely increase complexity but I think it's better than introducing something like pug to reuse template pieces after all...

@ThaDaVos
Copy link

Currently I'm running in the same kind of issue.
I'm extending a component which only purpose is to render something to a HTML5 Canvas (Parent of this component) - this component is called CanvasBox - now I want to extend this component to add separate logic for a specific use case - for this use case I also need to add a few elements to the CanvasBox's slots - so extending on the template - or at least filling in the slots without proxying all the props, events etc.

Like @Justineo 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.

@ThaDaVos
Copy link

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>

@mrodal
Copy link

mrodal commented Jan 27, 2019

I made a loader for this: https://github.com/mrodal/vue-inheritance-loader
It uses syntax similar to some of the ideas you shared. I would like to hear what you guys think, and where could it be improved!

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