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.74k stars 33.68k forks source link

add `options` property to the render context of functional component #7984

Open caikan opened 6 years ago

caikan commented 6 years ago

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 like vm.$options(https://vuejs.org/v2/api/index.html#vm-options)

related issue #7492

Aaron-Pool commented 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.

posva commented 6 years ago

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

Aaron-Pool commented 6 years ago

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.

caikan commented 6 years ago

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: {/* ... */},
          },
        },
      },
    },
  },
];
posva commented 6 years ago

@caikan That's quite different, you need to import it or, in vue files, see #7492

posva commented 6 years ago

What would you write on a mixin for functional components apart from props?

caikan commented 6 years ago

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);
    },
  };
}
Aaron-Pool commented 6 years ago

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

posva commented 6 years ago

I still don't get what you're trying to do. Can you share a piece of code, please?

Aaron-Pool commented 6 years ago

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.

posva commented 6 years ago

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 😄

Aaron-Pool commented 6 years ago

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.

matthieusieben commented 3 years ago

Here are two other use cases for this:

1. Detachable elements

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.

https://github.com/vuetifyjs/vuetify/blob/8bb752b210d25fbebcea12cd073d2ce4986f5e12/packages/vuetify/src/mixins/detachable/index.ts#L111-L117

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.

2. Functionnal i18n

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