vuejs / vue

This is the repo for Vue 2. For Vue 3, go to https://github.com/vuejs/core
http://v2.vuejs.org
MIT License
207.71k stars 33.68k forks source link

Initing child components with no associated dom #4547

Closed miljan-aleksic closed 7 years ago

miljan-aleksic commented 7 years ago

Hi!

in several components I find myself doing few workaround to accomplish the child components be a simple data representation.

Example 1 - Table

<vk-table>
  <vk-table-column header="Name">Cell Content</vk-table-column>
  <vk-table-column header="Name">Cell Content</vk-table-column>
</vk-table>

The vk-table-column purpose is to collect the data but leverage the rendering to vk-table which will iterate over the children as necessary to accomplish the Header, Rows, Cell combination.

Example 2 - Tabs

<vk-tabs>
  <vk-tab label="Name">Tab Content</vk-tab>
  <vk-tab label="Name">Tab Content</vk-tab>
</vk-tabs>

Similar as in previous example but additionally the Tabs content should be rendered using single Transition.

To the point

In both examples, the children can't be used out the default way to accomplish the special rendering. There are several workarounds but are tricky, unsupported and of course not best practice, so I would like to start a discussion about solving this particular need.

A solution could be having a way to initiate those $children with no render or template set as out of document components. They would be accessible early in the parent for immediate use during rendering.

LinusBorg commented 7 years ago

This should be solvable with scoped Slots

And if that syntax doesn't suit you, you can pass component options as props as well, and simply use them in a dynamic component:

// parent
<child :component="{template: ....}" />
//child
<template>
  <component :is="component"/>
</template>
<script>
  props: ['component']
</script>
miljan-aleksic commented 7 years ago

ScopedSlots are a great addition and they could be used to customize the output of each child, but they don't solve this situation. They are simple functions to be used during the childs rendering, but the idea here is to leave that to the parent. To make this more clear, let's take the example 1:

<vk-table>
  <vk-table-column>
    <template slot="header" scope="props">
      My Customi Header Inner Content
    </template>
    <template slot="cell" scope="props">
      My Custom Cell Inner content
    </template>
  </vk-table-column>
</vk-table>

Cool, now we know what the inner content for each column should be look like. With that information in the parent, ideally, we would do:

<table>
  <thead>
    <tr>
      <th v-for="col in $childrens">
        { col.$scopedSlots.header(col) || col.header }
      </th>
    </tr>
  </thead>
  <tbody>
    <tr v-for="row in rows">
      <td v-for="col in $childrens">
        { col.$scopedSlots.cell({ row, col }) }
      </td>
    </tr>
  </tbody
</table>

Note: for clarity and brevity I am mixing JSX with template declaration.

The above example could not work because $childrens is not available unless <slot></slot> is used somewhere, which in this case doesn't have place, there is no need for a default child rendering.

yyx990803 commented 7 years ago

I'm afraid I have trouble understanding what you are trying to achieve - looks like you are simply trying to pass down data, but want to use templates to do that. Why not just pass down an array as a prop?

miljan-aleksic commented 7 years ago

Ok, let me put all the cards on the table. This is an adapted extract of a real component workflow:

VkTable

<template>
<table
  { this.$slots.default }
  <thead>
    <tr>
      { this._l(this.$children, col => col.headerRender.call(col._renderProxy, h)) }
    </tr>
  </thead>
  <tbody>
    { this._l(this.data, (row, rowIndex) =>
      <tr>
        { this._l(this.$children, col =>
          col._cellRender.call(col.renderProxy, h, { row, rowIndex })
         ) }
      </tr>
    )}
  </tbody>
</table>
</template>

VkTableColumn

render (h) {
  return (<col></col>)
},
headerRender (h) {
  const scopedSlot = this.$scopedSlots && this.$scopedSlots.header
  return (<th>{ scopedSlot ? scopedSlot() : this.header }</th>)
},
cellRender (h, { row, rowIndex }) {
  const cell = this.cell
  const scopedSlot = this.$scopedSlots && this.$scopedSlots.cell
  return (<td>{ scopedSlot ? scopedSlot({ row, rowIndex }) : row[cell] }</td>)
}

VkTableColumnSelect

extends: VkTableColumn,
headerRender (h) {
  // using a similar approach as default renders instead a Selectable Checkbox
},
cellRender (h) {
  // using a similar approach as default renders instead a Selectable Checkbox
}

Usage

<VkTable>
  <VkTableColumnSelect />
  <VkTableColumn header="Simple Header" cell="dataKey" />
  <VkTableColumn>
    <template slot="header" scope="props">Custom Header inner content</template>
  <VkTableColumn>
<VkTable>

Conclusions

yyx990803 commented 7 years ago

Please take a step back and describe what use case you are trying to achieve. The code samples are quite confusing without explanation on what the design constraints are.

miljan-aleksic commented 7 years ago

I have created a jsfiddle describing the use case behind VkTable.

LinusBorg commented 7 years ago

Since that is, once again, just code, and no explanation of you goal (I assume you are just too far "in the zone" to understand that we can't follow you easily with a few lines of code), I'll try to summarize what I understood as your design goal and you can tell me if my guess is right:

Much like, e.g. Recharts (for React) does it, in part, for Charts:

<LineChart width={500} height={300} data={data}>
    <XAxis dataKey="name"/>
    <YAxis/>
    <CartesianGrid stroke="#eee" strokeDasharray="5 5"/>
    <Line type="monotone" dataKey="uv" stroke="#8884d8" />
    <Line type="monotone" dataKey="pv" stroke="#82ca9d" />
    <Tooltip /> <!-- this is not rendered in place, it rather defines a behaviourn -->
  </LineChart>

Getting close?

miljan-aleksic commented 7 years ago

Sorry to be so deep "in the zone" LOL. Yes, @LinusBorg, you summarized it pretty well, thanks :)

pohnean commented 7 years ago

Linus i think u are on spot with what miljan is saying (as an interface to parent component). Ive actually thought about the exact same components before (a datagrid and data column definitions), and tabs.

I've managed to implement the tabs with the use of render functions, but it was not very straightforward to implement... i had to filter out the 'tab' component's vnodes and get its 'label' propdata in order to render the tab header.

Not straightfoward but definitely doable in vuejs2 with the use of render functions

miljan-aleksic commented 7 years ago

@pohnean is right, all this is doable with workarounds and Vue2, but being a quite common issue (judging the forum) it would be nice to have an official solution.

We could think of those like functional/declarative components. They should accept props and composedSlots, but with no render of any kind.

LinusBorg commented 7 years ago

My gut feeling is that this is not a good idea as an additional API. Those components essentially are an empty shell which we only need to get their propsData into the main component.

This is has nothing in common with what it means when we talk about a "component" in Vue in any other situation.

So I have a bad feeling about introducting an API that essentially says:

"if you set this setting, then <someComponent/> will not behave like a component at all, it's basically a fancy way to pass props".

Addtionally, the vnode API is part of the public API, so using it is not automatically a "hack".

If you want to use templates, the good news is, you can! You can access this.$slots in beforeMount() and beforeUpdate(), using their vnode's content to prepare all the data nessessary for the following render function (which can be created by vue-loader from a template)

miljan-aleksic commented 7 years ago

Thank you @LinusBorg and don't worry, I was expecting this kind of answer. A similar one I got when exposed a template dilema, back then in v1, which was now finally solved in v2.1 with ScopedSlots. Probably at the time I was also too far into the zone and didn't managed to provide a good presentation or any valid solution. So, I hope this will arise again naturally, evolve, and eventually get solved.

Meantime, I will keep with my workarounds. You can close this ticket and if you have the time, perhaps you are up to a challenge related to this topic. Basically, without the workarounds, I can't find a way to apply a simple Tab transition. Details in the forum.

LinusBorg commented 7 years ago

I will close this now. Further discussion is fine, but I think it's clear that such a feature will not happen anytime soon.

maksnester commented 6 years ago

I found this issue by searching what is the this._l thing (still don't have an answer though).

I think that I want to implement exactly the same thing.

I like the way how Table component from element library can be used - a very straightforward. So I wanted to implement something similar but my own and with less features, well you know... :) Just some simple table component with neat api.

But I was really confused by reading element source code, there are plenty of jsx and some this._l, this._renderProxy. And it seems like there is just no way to achieve similar component interface without all those hacks.

LinusBorg commented 6 years ago

This issue is more than a year old.

Today, I would use the provide/inject for such functionality.

This issue is closed. Please use forum.vuejs.org for further questions.

amrdraz commented 5 years ago

I found this issue by searching what is the this._l thing (still don't have an answer though).

I think that I want to implement exactly the same thing.

I like the way how Table component from element library can be used - a very straightforward. So I wanted to implement something similar but my own and with less features, well you know... :) Just some simple table component with neat api.

But I was really confused by reading element source code, there are plenty of jsx and some this._l, this._renderProxy. And it seems like there is just no way to achieve similar component interface without all those hacks.

@Alendorff as far as I understood Vue.prototype._l is a short hand for renderList the helper used by v-for

so vue template code

<td v-for="(column, index) in columns" calss="table_cell" />

in jsx would be

{ _l(columns, (column, index) => <td calss="table_cell" /> ) }