rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

Breaking up templates in Vue3

Open arpowers opened this issue 5 years ago • 41 comments

This is a follow up to a discussion in @yyx990803's workshop yesterday.

Specifically I wanted to register a vote for an ability to break up and organize template "chunks" along with reorganized functions/options in Vue 3's composition API.

Use case.. here is the code for the landing page you see at fiction.com

As you can see, the code is nearly 1000 lines long. The composition API will definitely help with this. However, the template alone clocks in at 150 lines. In this there are discrete areas of functionality like figures vs grids, etc..

The initial suggestion was to break things out into multiple components, however, this comes at a cognitive cost in jumping around to different files. As well as what you might call "file bloat"...

Im not sure of suggestions for the direct implementation details, however please consider its feasibility if you haven't already.

arpowers avatar Nov 11 '19 15:11 arpowers

What ideas you have in mind? Is the goal of this to split template into multiple parts within the same file? If so, I think it is an interesting idea!

Would something like "named template" work?

<template>
  <use-template name="header" />
  <use-template name="main" />
  <use-template name="footer" />
</template>

<template name="header">
  <header>
    ...
  </header>
</template>


<template name="main">
  <header>
    ...
  </header>
</template>


<template name="footer">
  <header>
    ...
  </header>
</template>

Just throwing ideas. This is sort of replicating how you can assign virtual dom to variables in a render function; which could be quite useful if we figure a nice way to do it.

ycmjason avatar Nov 11 '19 16:11 ycmjason

I always wished for something like this 👍

The official answer for this has usually been to use additional components, but sometimes you want something lighter directly within the same file.

JosephSilber avatar Nov 11 '19 17:11 JosephSilber

Building off of @ycmjason's proposal, what if each named <template name="Foo"> behaved just like a named component (<Foo/>) that you could just use in your main <template>? They could even be marked as functional components if you wanted to keep a clean separation.

This would be nice for situations where you'll never reuse these one-off components and you don't want the overhead of putting them in separate .vue files.

<template>
  <Header :user="user" />
  <Main />
  <Footer />
</template>

<template name="Header" functional>
  <header>
    <h1>Hello, {{ props.user }}!</h1>
  </header>
</template>

<template name="Main">
  <main>
    ...
  </main>
</template>

<template name="Footer">
  <footer>
    ...
  </footer>
</template>

chriscalo avatar Nov 11 '19 17:11 chriscalo

So a functional template would be reusable in other files?

The syntax idea sounds great to me.

arpowers avatar Nov 11 '19 21:11 arpowers

I really like the idea of using functional component. However, Vue 3 is going to remove support for <template functional> (see here), so it doesn't look like to me that <template functional> adhere to Vue 3's philosophy much.

I think these named templates should be treated as "partials" that form the bigger part of <template>. An obvious way to do this is to make them inherit variables binding from wherever they are used. But this approach reminds me of the old php days which makes me feel 🤢.

The following code example will make you 🤢

<template>
  <div v-for="i in 5"><use-template name="water" /></div>
</template>

<template name="water">
  <div>{{ i }}</div>
</template>

I am definitely more inclined to having named templates to have their own scoped and things passed in as "props". But we just need to find the right syntax to do this?

ycmjason avatar Nov 12 '19 09:11 ycmjason

You can have implicit (not declared) props in Vue 3, so you could totally pass props to template-only components.

Akryum avatar Nov 12 '19 09:11 Akryum

@Akryum but would this mean that, if vue team decided to go down this route, that each named template would create a new instance of vue component?

ycmjason avatar Nov 12 '19 09:11 ycmjason

But I think Vue 3 component will be very cheap to create if they are template only? Combining with @Akryum's suggestion on using implicit props, we might be able to do:

<template>
  <Header :user="user" />
  <Main />
  <Footer />
</template>

<template name="Header">
  <header>
    <h1>Hello, {{ $props.user }}!</h1>
  </header>
</template>

<template name="Main">
  <main>
    ...
  </main>
</template>

<template name="Footer">
  <footer>
    ...
  </footer>
</template>

ycmjason avatar Nov 12 '19 10:11 ycmjason

See https://github.com/vuejs/rfcs/pull/25

Akryum avatar Nov 12 '19 10:11 Akryum

+1 to having to pass props to these private components. Probably best if they aren't magically passed props from the parent scope.

They could explicitly access parent to get at its data, props, methods, etc.

chriscalo avatar Nov 12 '19 12:11 chriscalo

I also wanted to add that since the whole point of this feature is better grouping stuff together in these huge components:

Not sure if it can get to something like this:

<template name="header">
  <header>
    ...
  </header>
</template>
<script>
function myHeaderStuffForCompositionAPI () {
  // ... 
}
</script>

arpowers avatar Nov 12 '19 19:11 arpowers

What's the advantage of this over having separate components that are just a <template> section?

I don't understand the cognitive costs referenced in the description as I generally find smaller, focused files easier to mentally consume and understand... and this approach seems more like file bloat than separate components would since you're ending up with one large file.

When you're in your main template and you see <SomeTag ...> there's no indication if that's a component (under some-tag.vue) or a template name="some-tag" (or name="SomeTag") in the current file and if you have to scroll to somewhere else in your file to find it and understand it and then go back to where you left off in the same file to continue reading through the entire template -- this seems more painful than file switching and returning to exactly where you left off.

Explicitly passing props also makes this sound like separate components... or rather, it sounds like you want a way to define multiple, separate components in a single file?

michaeldrotar avatar Nov 12 '19 20:11 michaeldrotar

@michaeldrotar

Sometimes the smaller parts of a large component does not make sense on its own. So it would be nice if those smaller parts could be local to the larger component. Regarding cognitive costs, it is down to personal preferences and ways of working. I don't think we are claiming superiority of named template over components in its own file.

I agree that <SomeTag ...> can be confusing of where it came from. It would be nice if we can distinguish between registered components and local components. Perhaps with some prefix? <$SomeTag ...>? (Again, just throwing ideas.)

ycmjason avatar Nov 12 '19 21:11 ycmjason

Understood this approach would be optional, I'm just struggling to understand the use case -- to understand what problem it's solving.

The idea of simply separating one's layout pieces is not a very comprehensive use case... and if this is something where that's the only use case -- something most projects basically do once and rarely revisit -- then this is creating alot of work for something that's very narrowly focused.

Maybe better use cases would better illustrate the concern?

I can think of two times where I've personally considered something somewhat related to this idea:

Complex Components Consider something like a material card -- it may be useful to have simpler patterns of two-way communication between the parent mdc-card component and its child mdc-card/actions, mdc-card/action-button, etc children. I think implicit prop access might be a strong point here, and it'd provide a reason for why mdc-card/whichever-child can't be used on its own: because it has to share state with its parent.

Complex Loops I've ran into scenarios where I'm iterating some items in the template and the data model doesn't provide exactly what I need so I need some computed stuff... these are definitely cases where creating a separate component feels like overkill if I just need one computed and if it's something that's not reused anywhere else (and then I'm splitting my styles across files when they really aren't designed to be used independently). Alternatively I could create a computed that transforms the entire data set to one that's "view-ready", but this can also be alot of work with large sets and then I have trouble naming things... things and viewReadyThings..? Like.. urgh.

But that said... Both of these scenarios are more of a sub-component then merely a sub-template use case so I don't know if these are way out of scope for what the author originally intended... and while it might be quick and dirty in the moment, having multiple template and script blocks seems like a decision I'd end up regretting at some point.

michaeldrotar avatar Nov 12 '19 22:11 michaeldrotar

Angular ng-template and ngTemplateOutlet system is one of the few things I miss since I moved to Vue. The Angular syntax for those is pretty verbose (as always), but I guess there are ways to make it simpler keeping the modularity and code tidyness they provide. Vue large components would benefit for that kind of API, some proposals in this issue are somewhat pretty similar. Of course, clear and complete documentation of how that feature work doesn't exists in Angular official documentation 💚 But here some links anyway to take inspiration. Official docs A random guide

IlCallo avatar Nov 13 '19 00:11 IlCallo

@IlCallo is this sort of a way to separate functionality and presentation? I'm not familiar with Angular but that's the gist I'm getting from the guide's <toggle> example.. that the toggle provides the methods and events so I'm assuming the layoutTemplate provides the presentation.

If so, would the composition API be another way to approach that? I see myself building pieces of functionality that components would use in whatever presentation they want. Then I could have a vertical-form-checkbox and a side-checkbox (or whatever) that both use the same checkbox functionality where the only differing code is whatever is necessary to make the different presentations for each component work properly... template, styles, minimal wiring.

michaeldrotar avatar Nov 13 '19 00:11 michaeldrotar

Not really, presentation and functionality are often mixed in web components. Templates are used pretty much everywhere in Angular (if you dig enough deep), but in this kind of usage it's just too keep your code cleaner and allow structure reuse without having to create a new component, for those cases where it would be an overkill.

I can paste an usage example if you want some real world code where it came in handy.

IlCallo avatar Nov 13 '19 00:11 IlCallo

Relevant tweet: https://twitter.com/adamwathan/status/1194250642791043073?s=21

Four years in, I think my only real @vuejs feature request is some sort of "multiple components in the same file" solution. I'd create many more tiny "private" components if this was easy.

Has there ever been any interesting discussion around this with syntax suggestions? 🤔

chriscalo avatar Nov 13 '19 00:11 chriscalo

But your template code is actually 136 lines long. That's not a lot for such a component. The largest part of your SFC is CSS, which should be imported from a separate file in this case. The same thing applies to script declaration, where it's mostly about data structure, which should also be normalized.

As for the template code the suggestion of braking it up into multiple components does solve your issue. Not sure what you mean by cognitive cost when working with component composition. It's a standard paradigm in component frameworks that it's indeed created to decrease cognitive load and split your work into manageable parts. Merging multiple components into a single one defeats that purpose.

Moreover, introducing reusable templates will actually increase mental overhead for a developer. Imagine you have a scoped slot and within that slot you want to reuse a scoped slot variable. When you're working with that external template there's no easy way to tell where that variable came from, because it's not declared in component options, but rather is hidden somewhere in the markup.

<!-- container -->
<div v-slot="{ foo }">
  <template src="./bar.template.html" />
</div>
<!-- bar.template.html -->
<div>
  {{ foo }}
</div>

Also imagine a situation where your reusable template declares a slot. How you'd be able to tell what slots does your component have without peeking at every single reusable template?

<template>
  <template name="foo" />
</template>

<template name="foo">
  <slot />
</template>

To recap:

  • We already have component composition
  • Reusable templates are the same thing as a component, you'll have to (and you will) navigate to them anyway, even if they're in the same file
  • Those templates introduce mental overhead

I strongly vote against this feature since we already have a mechanism to reuse templates, which is a component composition. This feature would introduce more problems than solve.

CyberAP avatar Nov 13 '19 12:11 CyberAP

@CyberAP

Isn't the whole angle with the composition API about "code organization"?

With that perspective you could say the same thing: "why create the composition API when you could just break things into separate components?"

Also, nothing introduces more friction for me in my projects than hopping between files and folders. Maybe I'm the only one...

However! I do agree with you that if there are substantial technical tradeoffs and "footguns" with this, then it might not be worth it. I just felt it warranted a discussion.

arpowers avatar Nov 13 '19 16:11 arpowers

(You're not the only one, @arpowers.)

When we're talking about plain functions, this is a widely-accepted best practice: when one function is starting to do too much or becomes hard to follow, it might be time to break some of that functionality into another function and call it from the first. And in doing so, there's no best practice that says we need to automatically move that function to another file. If it's only going to be referenced from this file, it's probably best to leave it here until the moment comes where it's needed elsewhere.

To others who don't have this pain or see the value in this proposal: it doesn't have to be useful to you to have value for others. As stated above, I have been in many situations where I wanted to break out a chunk of template for readability's sake even though I have no intention of using it anywhere other than in this component.

chriscalo avatar Nov 13 '19 16:11 chriscalo

The composition API solves a need that doesn't have a great solution currently.. there are half measures like mixins and component composition, but they have major drawbacks. I wouldn't argue that this is the same thing.

I also don't think it can be an argument of someone not having to use this if they don't want to -- everything takes time to build so time spent on one feature is time away from another... plus the cost of maintenance after it's built. The core of Vue should be hyper-focused to features that everyone needs. Niche features can be implemented in 3rd party libs.

To address the use case in the original post (cause I'm just noticing it now).. I kind of have to disagree that this should all be in one file, regardless of whether or not it could have multiple <template> blocks... the template is only 136 lines, compared to 132 lines of script and 640 lines of css. I think the template is the least of the worries.

I'd break up the data into separate files: benefits, features, and compares. Data should always be separate from code, and it doesn't need to be in a database -- json files work great for this.. or even a js file that returns an object. Then you can use it from anywhere and non-tech team members can add new items or fix spelling mistakes without wading through a sea of code.

The css is definitely way heavier than the markup.. sometimes it's worthwhile to break something into a component simply because of the css.. especially once you're doing animations and transitions and making things responsive. You already basically have it split into components: .header, .benefits, .features, .compare, .alpha-program.

The fiction site is beautiful and you clearly know how to organize and structure code.. I have been trying to understand the motivation behind this idea but it feels like the issue is perhaps with your code editor making it hard to switch between files moreso than anything else..? 🤔 Saying that creating files or hopping between files is painful or time-consuming is a tooling issue, not a framework issue.

To the other people that agree with this proposal, please list some actual use cases. Even I tried to list some with my experiences with complex components and for loops, but my use cases are better suited to private sub-components than to breaking up templates.

michaeldrotar avatar Nov 13 '19 22:11 michaeldrotar

The problem working across files happens when you go from say 5 components to 25 components as you try and break things out discretely as possible.

It gets harder to remember where things are and what things do without following the dependency chain each time.

Feel free to close this issue if it's too controversial to deal with right now. Let's note that it would be "purely additive" hence shouldnt be an issue to add in 3.X release.

arpowers avatar Nov 13 '19 23:11 arpowers

I think that's a great point, and I think most people struggle with that at some point... I've definitely been at both extremes during my career -- either ending up with massive components that are everything in a page or tiny components that are barely more than a few lines of markup.

My goal is no longer to make components as discrete as possible, or even as reusable as possible.

There are 2 criteria I usually use:

repetition If I've done the same thing more than a few times, it might be ready to make it a component -- if I do it too soon, I inevitably make some poor assumptions and structure it poorly, so I want to have the same pattern on a few different screens in different scenarios.

complexity/size Once a component becomes so large that I can't digest what it does with a cursory scroll through the file, then it's time to break up the pieces.

If we look at something like the composition api, part of it's goal is to get the props, computeds, methods, etc that are all related to the same feature into the same block of code together so that the code looks like XXX YY ZZZ instead of XYZ XZ XYZ. I think it's the same deal for components -- once a component has 3+ logical "sections" than it's easy to split those into logical components just so it's easier to read one piece at a time.

michaeldrotar avatar Nov 14 '19 00:11 michaeldrotar

I don't know fragment is a reserved name already but having something like this would be great

    <template>
      <div>
        <FolderTree :data="{files}" />
      </div>
    </template>

    <fragment name="FolderTree" :data="{files}">
      <ul class="pl-4">
        <li class v-for="(file, index) of files" :key="index">
          <div>
            <input type="checkbox" name :id="file._id" />
            <span class="ml-2">{{file.name}}</span>
          </div>
          <div v-if="file.isOpen">
            <FolderTree />
          </div>
        </li>
      </ul>
    </fragment>

Akumzy avatar Nov 20 '19 15:11 Akumzy

I genuinely think that this would just increase confusion, without adding anything beneficial to the framework.

anthonypenna avatar Nov 20 '19 18:11 anthonypenna

I'm all for @ycmjason's idea (https://github.com/vuejs/rfcs/issues/96#issuecomment-552822532). That seems easy to support, easy to optimize, and very similar to how you would approach the issue if you were writing JSX.


I was curious if something similar could be hacked together currently. I ended up with something like this:

<template>
  <div class="hello">
    <Define name="Link" v-slot="{ url, name }">
      <li>
        <a :href="url">
          <strong>{{ prefix }}:</strong>
          {{ name }}
        </a>
      </li>
    </Define>
    <Define name="Header" v-slot="{ title }">
      <h3>{{ title }}</h3>
    </Define>
    <Define name="LinkGroup" v-slot="{ title, links }">
      <Header title="Big Tech Companies"/>
      <ul>
        <Link v-for="{ url, name } in links" :key="name" v-bind="{ url, name }"/>
      </ul>
    </Define>

    <h1>{{ msg }}</h1>
    <Header title="Big Tech Companies"/>
    <ul>
      <Link url="http://google.com" name="Google"/>
      <Link url="http://apple.com" name="Apple"/>
    </ul>

    <LinkGroup title="Frameworks" :links="links"/>
  </div>
</template>

<script>
import Define from "./Define";

export default {
  name: "HelloWorld",
  components: { Define },
  props: {
    msg: String
  },
  data() {
    return {
      prefix: "Go to",
      links: [
        { name: "Vue", url: "https://vuejs.org" },
        { name: "React", url: "https://reactjs.org/" },
        { name: "Angular", url: "https://angular.io/" }
      ]
    };
  }
};
</script>

see Define.js in the sandbox for the functional renderless component that enables this: https://codesandbox.io/s/vue-template-47veo

It relies on the parent mounting after the children so the children can register functional components on the parent before render. So yeah, it's a hack. No idea on how easily it would break, but clearly no ones should do this. This is just for kicks.

But in terms of the ergonomics of this type of pattern, I guess i could see it being useful for groups of tightly coupled components that have repetitive patterns and specific characteristics (not very generic or useful elsewhere). I can't say I've ever really run into cases where I would find this useful, though. Ordinarily companion components grow and benefit from separate files in my experience (promoting private components to separate files would be super easy in @ycmjason's example, which I like).

This type of pattern could probably be natively supported (i.e. compiler could optimize for it, like hoisting the defined component if not referencing anything from parent scope). The only advantage of this approach, though, is access to the parent scope. Otherwise, it's more clunky and and less accessible to newer devs compared to the simplicity of @ycmjason's example.


Update: I was also able to get it to work like this, which might look a little cleaner

<template>
  <div class="hello">
    <Components>
      <template #Link="{ url, name }">
        <li>
          <a :href="url">
            <strong>{{ prefix }}:</strong>
            {{ name }}
          </a>
        </li>
      </template>
      <template #Header="{ title }">
        <h3>{{ title }}</h3>
      </template>
      <template #LinkGroup="{ title, links }">
        <Header title="Big Tech Companies"/>
        <ul>
          <Link v-for="{ url, name } in links" :key="name" v-bind="{ url, name }"/>
        </ul>
      </template>
    </Components>

    <h1>{{ msg }}</h1>
    <Header title="Big Tech Companies"/>
    <ul>
      <Link url="http://google.com" name="Google"/>
      <Link url="http://apple.com" name="Apple"/>
    </ul>

    <LinkGroup title="Frameworks" :links="links"/>
  </div>
</template>

However, I'm not sure this hack would work at all in the Vue 3 template compiler. Based on the output render function from the compiler, it looks like it tries to resolve the components all at once, before the renderless component would have a chance to register the nested components. So, Vue 3 would have to have a special built-in component like <components> from where it would take the slots and create components that are then available to rest of the component render function (or this functionality could be added to the existing <component> built-in perhaps). If it were supported in Vue 3 you could envision this:

<template>
  <components>
    <template #Link="{ url, name }">
      <li>
        <a :href="url">
          <strong>{{ prefix }}:</strong>
          {{ name }}
        </a>
      </li>
    </template>
    <template #Header="{ title }">
      <h3>{{ title }}</h3>
    </template>
    <template #LinkGroup="{ title, links }">
      <Header title="Big Tech Companies"/>
      <ul>
        <Link v-for="{ url, name } in links" :key="name" v-bind="{ url, name }"/>
      </ul>
    </template>
  </components>

  <h1>{{ msg }}</h1>
  <Header title="Big Tech Companies"/>
  <ul>
    <Link url="http://google.com" name="Google"/>
    <Link url="http://apple.com" name="Apple"/>
  </ul>

  <LinkGroup title="Frameworks" :links="links"/>
</template>

<script>
import { reactive, ref } from "vue";

// rather than creating a new component with state, just compose more state here
function usePrefix() {
    return { prefix: ref("Go to") }
}

function useLinks() {
  const links = reactive([
    { name: "Vue", url: "https://vuejs.org" },
    { name: "React", url: "https://reactjs.org/" },
    { name: "Angular", url: "https://angular.io/" }
  ]);

  return { links };
}

export default {
  name: "HelloWorld",
  props: {
    msg: String
  },
  setup() {
    return {
      ...usePrefix(),
      ...useLinks()
    };
  }
};
</script>

I want to say that I would like something like this. There is no need for more templates in different parts of the file. You aren't shoving multiple components in the file. It's still a single file component. But, it allows you to easily create reusable templates, and if they require new pieces of state or logic, it's really easy to drop that in using composition. No need to create a new component with a single data attribute or computed. If needed, it's easy to copy the template and associate composition functions to a new file. And if unrestricted access to parent scope is a concern, you can maybe add a scoped prop to the template so that only a subset of the parent data is passed to the scoped slot (eg data.Link by default for the Link slot).

However, this is a change to how the template compiler works. So even though slots are already idiomatic to the framework, this would not be an insignificant change. And it's not clear to me if there would be any side effects from adding this ability. The other option of just including fragment template that use $props seems much simpler and more of a vue loader change, rather than affecting the framework itself. However, I find it a little limited. The above would feel much more useful and natural to me.

aztalbot avatar Nov 21 '19 01:11 aztalbot

I've hit a similar issue a couple of times, and breaking out the repeated markup didn't really make sense as it was tightly coupled to the parent component and couldn't be used elsewhere.

I found JSX to be really useful - you can easily make small components (with logic too if you need) in the same file.

Not sure that the template syntax would add much given that JSX exists

SamWoolerton avatar Jan 14 '20 00:01 SamWoolerton

@SamWoolerton interesting thought, would it be possible to get a link to somewhere this is done in JSX + Vue? It would def be a good study.

arpowers avatar Jan 14 '20 00:01 arpowers

@arpowers Here's a basic overview From memory, the Vue CLI has support baked in so no config required either - just write component in the script section, register and use as normal

JSX is a bit different with event handlers etc but not that bad, especially if you're mainly doing this for repeated markup

samwoolertonLW avatar Jan 14 '20 00:01 samwoolertonLW