-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Refactor Mixins page into a more thorough Component Composition page #1221
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
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great, Chris. The heavy use of template literals may put off people programming directly in older browsers, as I can't remember anything that would have trouble running in IE in the guide (outside of single-file components).
Using this strategy, all notes in the app maintain unified behavior, but _you_ control the API for creating them. This can be especially useful when you want to: | ||
|
||
- Create many variations of a component, abstracting away the specific implementation details. | ||
- Give designers, less experienced developers, or members of a very large team the ability to create new component variations, but strictly control what they're allowed to change. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would this strictly control what they're allowed to change? They could change the returned object, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It restricts the changes that can be made to component options through the "normal" means (i.e. the API exposed by the function's arguments). Changing the object generated by a factory function built to abstract away the concern would be considered not normal and easier to clearly identify as a smell.
There are additional measures one could take if you're worried about this, such as freezing the returned object - but really, nothing can keep you safe from a colleague that doesn't respect exposed APIs. 😅 They could just as easily overwrite built-in browser methods or delete internal Vue properties.
@dennythecoder Thanks for the review! 🙂
For pages beyond the Essentials, I've decided to make an exception for multi-line templates, simply because they're so much more readable with template literals. My hope is that users reading the more advanced content will also have a slightly more advanced knowledge of JavaScript, but do you think I should add a note at the beginning of the page about them just in case? |
A note wouldn't hurt. |
src/v2/guide/composition.md
Outdated
order: 301 | ||
--- | ||
|
||
Vue offers many strategies for building components and sharing functionality between them. Below, we'll provide a description of each strategy, followed by more in-depth explanations of when and how to apply them. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not clear on how this is different from just creating components. Maybe change the name or make the intro more clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. I'll qualify with "advanced strategies" to make it clear that this isn't just building basic components.
src/v2/guide/composition.md
Outdated
Vue offers many strategies for building components and sharing functionality between them. Below, we'll provide a description of each strategy, followed by more in-depth explanations of when and how to apply them. | ||
|
||
## Overview | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this overview structure.
src/v2/guide/composition.md
Outdated
You might use it to display an error like this: | ||
|
||
``` html | ||
<info-note type="danger"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be <app-note>
, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! Whoops. 😅
src/v2/guide/composition.md
Outdated
``` | ||
|
||
This is the simplest option and sometimes it's appropriate, when two components only _incidentally_ share a lot in common. In this case, however, we probably want all the notes in our application to work mostly the same. For example, if we later added a close button to `AppNote`, we'd now have to add the exact same code to `ErrorNote` and any other variations we created. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A close button on a note seems a little weird to me. What about changing note to announcement, or making it an expand/collapse button instead? You could expand/collapse both note details and error details.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I like the announcement idea. I agree a close button makes more sense in that context.
|
||
### Mixins vs Extension | ||
|
||
**Mixins** work [like extension](#Merging-of-Instance-Properties), but allow you to define an array of configs instead of only one. For example, these two options do exactly the same thing: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be "like extensions"? (plural)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not actually in this case - "extension" refers to the strategy. Does that make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I see what you're saying.
|
||
## Custom Merge Strategies | ||
|
||
Plugins often define custom options, which use the default strategy of overwriting any existing value. If you're the author a plugin where this is undesirable, you can define a **custom merge strategy** for your options with `Vue.config.optionMergeStrategies`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know anything about Vue plugins! Assuming I don't need to worry about this section for my own development.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is correct. 😄
## Wrapper Components | ||
|
||
As you've seen, advanced composition with extends and mixins can quickly grow complicated. One simpler alternative is creating **wrapper components**, which provide an interface for another component typically used as its root element. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This took me several reads to grok. Particularly "provide an interface for another component typically used as its root element." No advice for making it clearer, unfortunately. 🙁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I'll try to come up with a way to rephrase/simplify.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... which provide an interface for another component typically used as its root element.
What about:
... which can be used to wrap the current component while assigning the necessary default values using the template.
} | ||
``` | ||
|
||
Instead of inheriting and building on the interface of a base component, this strategy does **not** provide direct access to the wrapped component. So unlike the `ErrorNote` using extension, this one has its own props (currently none) and its own slots (currently just the default slot). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of inheriting and building on the interface of a base component, this strategy does not provide direct access to the wrapped component.
Not completely sure what this means ("interface" and "direct access").
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would you think about rewriting this paragraph as below?
Unlike extension, where props and slots were inherited from
AppNote
intoErrorNote
, this strategy does not allow passing props or slots directly to the base component (AppNote
). Instead,ErrorNote
acts as a middleman, strictly controlling whatAppNote
receives. It has its own props (currently none) and its own slots (currently just the default slot).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like it!
|
||
Above, a developer has hijacked the component for a new kind of note that is not actually an error. The wrapper version of this component more strictly controls the API, so does not allow this kind of abuse. | ||
|
||
<p class="tip">In the example above, <code>type="success"</code> <em>would</em> actually affect the wrapped <code>AppNote</code> component, but as an attribute rather than a prop. To prevent attributes from automatically being added to the root element of a component, use the <a href="../api/#inheritAttrs"><code>inheritAttrs: false</code></a> option.</p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So it would give it type="success"
but it would be the HTML type
attribute instead of the type
prop you defined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'll add "HTML" before the first "attribute" just to be extra clear.
{{ headingContent }} | ||
</dynamic-heading> | ||
``` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this whole section, I struggle because I don't get why you'd want this. The linked render function example, where you are dynamically applying anchor tags, is clearer to me. Can you use that here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was previously including more render function code here and simplified the examples so as not to dive too deeply into render functions, but I think it should be possible now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have an example with SVG avatars that I was working on, but I'm not sure if that would leave out people who don't know SVG
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sdras That'd be something I'd worry about too, but I'd still love to see it as an example to learn from. It could possibly even be adapted into something simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update on this, @codebryo might be up for writing a cookbook entry to do a bit more of a deep dive into this, so if we get that, we can keep this example simple and link his up for further clarification :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will actually write that entry :)
I'd like to give this a thorough review but is really short on time this week and next. Let's hold till I come back from ReactiveConf on 29th. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really well done and I think will be really useful! One thing it made me think about is the fidelity of the examples. I assume we're just giving them enough information to get going, but I wonder- if people are unfamiliar with these concepts, do they need more? Or alternatively, do they need to see the same example, composed with each different option to see how it changes in more of a 1:1? Either way, this is a great addition! Awesome work.
As you've seen, advanced composition with extends and mixins can quickly grow complicated. One simpler alternative is creating **wrapper components**, which provide an interface for another component typically used as its root element. | ||
|
||
For example, the `ErrorNote` component we [created earlier with extension](#An-Example-Use-Case) could be refactored to: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we make mention of Higher Order Components for people coming from a React background? It might be a comparison that would help some people get the idea a bit faster
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, will do.
{{ headingContent }} | ||
</dynamic-heading> | ||
``` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have an example with SVG avatars that I was working on, but I'm not sure if that would leave out people who don't know SVG
``` | ||
|
||
Check out [VueFire's source](https://github.com/search?utf8=%E2%9C%93&q=%22Vue.mixin%22+repo%3Avuejs%2Fvuefire+path%3A%2Fsrc&type=Code) for a complete example. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think a small example with console.logs (all of a sudden every component is console logging) would be sufficient to clear it up, Katie?
I only ask if we should add more to this to make it more clear because global mixins verge on anti-pattern
</AppNote> | ||
` | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should have one more example for styling with a scoped slot because that's pretty useful for theming. That could also go in the cookbook.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was reluctant to include it, because I prefer other patterns for most use cases, but between you, @Akryum, and others, I've been convinced we need a section for it. 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I did some work on this today, but I just couldn't make scoped props fit because unlike all the other strategies on this page, they're not about sharing functionality between components. Instead, they're really more for communication between parent-child components, so the examples I came up with will be in a new page on advanced communication between components instead.
|
||
Nothing is removed, prop definitions with the same name are replaced, and any new props are added. | ||
|
||
### Merging of Event Handlers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's more accurate to just use "Merging of Lifecycle Hooks" here. Event handlers may be confused with handlers added via this.$on
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point!
|
||
- JavaScript's [`Function.prototype.apply`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply). | ||
|
||
- Vue's internal `_render` method, which will either reference a user-provided render function or one created from a compiled, user-provided template. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm in general against relying on any internal methods that start with an underscore - these methods may change behavior, be renamed or removed without notice (not connected to semver at all). Even with the warning tip below this is still in my opinion something that should not be demonstrated in the official docs.
Hacking the render function of the host component is also IMO prone to problems / conflicts if multiple mixins rely on similar strategies.
In this case it's more explicit to let the host component just add <div v-if="!isClosed">
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. When talking with other people I often see them already abusing some internals of Vue and whenever something breaks there, it's hard to figure out for them as they relied on the hack completely.
I don't see it a good pattern to encourage people to go down that route.
|
||
We'll demonstrate the last advantage in the next section. | ||
|
||
### Computed Components |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really a fan of computed components - creating component definitions on the fly is always inefficient, especially when it involves dynamically generating template strings. I'd just suggest people stick to render functions and remove this section.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree. This is one of the most asked questions on the forum, in a sense.
there seem to be many people out there who are stuck with backends that send string templates to the frontend, and they all end up trying to insert them into their apps with v-html and wonder why the template syntax is not being processed.
The solution for these people is to create a dynamic component from the template string that they received, often by using a computed property. It's actually kind of a selling point we have over Angular or React - we can integrate with such a backend (even if it's not without it's downsides) - these other libs simply can't.
So I strongly vote for keeping this in the guide, maybe even change it a bit to be a better fit for the usecase I described, and just mention the downsides of that approach.
That would give me a piece of documentation that I can refer these people to. :-P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you point to an explicit example in the Forum that you refer to? I would be curious if there isn't another way of succeeding. I am not sure the current example covers the use case you refer to in an approachable way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will look one up tomorrow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one, but maybe not the best one:
https://forum.vuejs.org/t/innerhtml-compilation-vue-2/8780
I'll dig for some more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @LinusBorg , The "dynamic router links" one is probably the example that mostly drives home the point. I would probably still try to direct users in the direction of using "funcitonal" components for this kind of behavior, yet if we are going to add this, I would recommend using the router example as it's more clear for what it tries to achieve.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, great content @chrisvfritz and very helpful regarding the overview of different strategies.
|
||
- JavaScript's [`Function.prototype.apply`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply). | ||
|
||
- Vue's internal `_render` method, which will either reference a user-provided render function or one created from a compiled, user-provided template. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. When talking with other people I often see them already abusing some internals of Vue and whenever something breaks there, it's hard to figure out for them as they relied on the hack completely.
I don't see it a good pattern to encourage people to go down that route.
``` | ||
|
||
Check out [VueFire's source](https://github.com/search?utf8=%E2%9C%93&q=%22Vue.mixin%22+repo%3Avuejs%2Fvuefire+path%3A%2Fsrc&type=Code) for a complete example. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think console.log
clears it up for sure. As addition though, I think it's fine to reference another project where maybe Global Mixins are used in a appropriate way. Maybe not VueFire though as the documentation there is not ideal, and it also requires people to understand Firebase to fully grock what's going on. (I recently failed with VueFire and switched to VueFirestore)
## Wrapper Components | ||
|
||
As you've seen, advanced composition with extends and mixins can quickly grow complicated. One simpler alternative is creating **wrapper components**, which provide an interface for another component typically used as its root element. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... which provide an interface for another component typically used as its root element.
What about:
... which can be used to wrap the current component while assigning the necessary default values using the template.
|
||
Instead of inheriting and building on the interface of a base component, this strategy does **not** provide direct access to the wrapped component. So unlike the `ErrorNote` using extension, this one has its own props (currently none) and its own slots (currently just the default slot). | ||
|
||
This is useful when we'd really like to _remove_ options from a base interface. For example, in the case of `ErrorNote`, it'd be very strange for it to be used like this: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to be careful here. With all the changes we now avoided using the word "interface" as reference to the components API/props. Can we just replace interface
with component
?
{{ headingContent }} | ||
</dynamic-heading> | ||
``` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will actually write that entry :)
Awesome work so far. One thing I was thinking about is that patterns involving scoped slots could also fit nicely into this section. There's currently a big trend in React land to leave HOCs (wrapper components) and move to render props (scoped slots) to compose components in various scenarios. Michael Jackson from ReactTraining gave a great talk about how Render Props are superios to HOCs in many ways and use cases: https://www.youtube.com/watch?v=BcVAq3YFiuc And I think the same holds true for Vue scoped Slots vs. Mixins / Wrapper components, in a way even more so since HOCs are evern more notoriously tricky to get right in Vue with it's bigger API surface. So while I think this doesn't have to be added before we publish this, we might put it on the list of possible additions to do later? |
@LinusBorg After merging #1482, I'll be coming back to re-evaluate this page and there will definitely be some scoped slots love. 🙂 |
@chrisvfritz just stumbled upon this. Is this still relevant? |
@chrisvfritz we've now got |
Since this is a pretty significant addition, I'd like some team and community review before merging.