Open caikan opened 6 years ago
I would also use this feature pretty extensively. Writing powerful mixins that work with functional components becomes much more difficult when you don't have access to the $options object in the rendering context.
I forgot to ask, but could you please share specific scenarios where this would be useful? Also, keep in mind this is already possible by putting the object into a variable before exporting it
My use case it that I'm writing a mixin to handle reading and checking a "type" list that's custom property on my vue components. It checks attributes and applies specific css if an attribute name matches any of the given types specified in the vue instance "type" property. This works fine for full, non-functional components. But functional components can't access custom properties in the render function, so I can't access what type properties are set (because $options
doesn't exist on the context
object) when I'm actually composing styles for my component.
And I'm fine to do the export option, it just seems odd that $options
isn't present on the context
object, given all the other vue instance attributes which are.
Here is one scenario: https://github.com/vuejs/vue/issues/7492#issuecomment-379570456
In my project, I want to make vue route components "responsive".
const BaseResponsive = {
functional: true,
render(h, context) {
// The options of extended component can't be accessed in base render.
// My workaround is using injections, but looks weird.
let component = context.injections.components[getDeviceType()];
return h(component, context.data);
},
};
const routes = [
{
path: '/foo',
component: {
extends: BaseResponsive,
inject: {
components: {
default: {
desktop: {/* ... */},
mobile: {/* ... */},
},
},
},
},
},
{
path: '/bar',
component: {
extends: BaseResponsive,
inject: {
components: {
default: {
desktop: {/* ... */},
mobile: {/* ... */},
},
},
},
},
},
];
@caikan That's quite different, you need to import it or, in vue files, see #7492
What would you write on a mixin for functional components apart from props
?
My initial idea was to let functional components can be extended dynamically. Extended components have the same render logic but different options. Now I think I have found another workaround: using a factory function.
function createResponsiveComponent(options) {
return {
functional: true,
render(h, context) {
let component = options[getDeviceType()];
return h(component, context.data);
},
};
}
@posva , custom options are great options for writing mixins to encapsulate reusable behavior that doesn't rely on reactive data. It's would be nice to be able to use custom properties in mixins, so to not be forced to pollute the prop list with things that aren't going to change or be publicly exposed on the component API, just so those properties are accessible to a mixin.
I still don't get what you're trying to do. Can you share a piece of code, please?
Sure! Disclaimer though, I'm fairly new to vue. I have pretty extensive experience in other front end frameworks, but I'm new to adopting vue. There's a very real chance I'm doing something ridiculous and unintuitive, but the use case doesn't strike me that way, personally.
Suppose I have a mixin that takes a custom property "types" from a component definition, assuming its present, and checks for matching attributes on the component's host element. It then concatenates a string of styles, derived from the attributes specified on the component element that match a type specified in the component definition. Here's how I might do it (I'm using styled components. Hopefully you're familiar with the library):
So, suppose this is my component:
import styled from 'vue-styled-components';
import IsTyped from '@Composables/IsTyped';
import { Typography, Colors } from '@Constants/style';
let SmallLabel = {
functional: true,
name : 'SmallLabel,
mixins : [IsTyped],
render : function(h, context) {
let Label = styled.span`
// define base styles
font-size : ${Typography.size.medium};
color : ${Colors.black};
// now add any styles based on types provided in the attributes
${context.$options.typedStyles} // this will be set by my mixin
`;
return (<Label>{context.$slots.default}</Label>);
},
types : {
bright : `color : ${Colors.smoke};`,
dark : `color : ${Colors.sepia};`,
bold : `font-weight: ${Typography.weight.bold};`,
light : `font-weight: ${Typography.weight.bold};`
}
};
And here's my IsTyped
mixin:
import {keys, intersection, values} from 'lodash';
export default {
beforeMount: function() {
let styles = '';
// looks for intersection between a components attribute and specified types
intersection(keys(this.$options.types), values(this.$attrs))
.forEach((t) => {styles = styles.concat(this.types[t]);})
// concatenates the styles and then attaches them to the custom options of the component
this.$options.typedStyles = styles;
}
}
}
Unfortunately, this does not work. Because $options
is not on the context
object provided to the render function.
There is no lifecycle in functional components, they just call render. The only thing you can put in a mixin for functional components is props. edit: oh and inject. I may be missing some now that I think about it 🤔 Instead, you can set up functions to return an object of options and directly use that object in your render function, which also looks more straightforward IMO 😄
Ah, didn't realize lifecycle methods didn't exist in functional components (like I said, vue newb here). And yes, I actually ended up doing what you suggested in the end, and it probably is cleaner. I just figured I'd give you my original use case, in case it gave you additional perspective about the original request.
Here are two other use cases for this:
Libraries such as vuetify
rely on "detachable elements" (elements created and added to the dom programatically, outside the "inner dom tree" of the component). An example is menus.
As you can see in this snippet, vuetify will try to use the context's $options
to determine the scopeId
to apply on the created elements.
the missing $options
breaks the ability to add scoped css for this kind of elements.
i18n in functional components is not ideal since functional components do not have an i18n instance.
One way to make it work is to use a small translator
utility as in:
const getI18n = ({ parent: vm }) => {
do if (vm.$i18n) return vm.$i18n
while ((vm = vm.$parent))
}
const translator = (Component, ctx, locale) => {
const i18n = getI18n(ctx)
const { messages } = Component.options.i18n ?? {}
return (key, ...values) =>
i18n?._t(key, locale || i18n.locale, messages ?? i18n._getMessages(), null, ...values) ?? key
}
const XComponent = Vue.extend({
name: 'x-component',
functional: true,
i18n: {
messages: {
en: { foo: 'my foo' },
fr: { foo: 'le foo' },
},
},
props: {
locale: { type: String, default: null },
},
render(h, ctx) {
const t = translator(XComponent, ctx, ctx.props.locale)
return h('div', {}, t('foo'))
}
})
export default XComponent
As you can see from this example, having the ability to "find" $options
in ctx
would make the code a lot nicer
What problem does this feature solve?
Custom properties in the options of functional component can't be accessed easily.
What does the proposed API look like?
In the render function of functional component, options can be accessed by
context.options
, just likevm.$options
(https://vuejs.org/v2/api/index.html#vm-options)related issue #7492