vuejs / rfcs

RFCs for substantial changes / feature additions to Vue core
4.88k stars 546 forks source link

Add `v-using` property to scoped slots to provide helper components to scoped content #90

Open Aaron-Pool opened 4 years ago

Aaron-Pool commented 4 years ago

There have been several times I've thought that it would be really convenient if a component with scoped slots could provide components, as well as props data to the component that's consuming it.

Disclaimer: I realize that this can be done by passing a component definition through the scoped slot and using <component :is="x" />. However, I think this often hurts the semantic elegance and ease-of-readability of the template. It also makes the api slightly more obtuse, because v-slot is providing data and components without a distinction between the two:

Example:

Suppose I'm writing a library that exposes a reusable. super robust form builder component. All the consumer has to do is provide that data to edit, and a component DynamicFormField processes the data to determine it's display format and the component best suited to edit that data (a multi-select, a radio select, a textarea, a dropdown, etc), as well as the component best suited to label the field and display errors. The user just has to give me what data they want edited and the component does most of the logic work on their behalf. But in addition to choosing the best component for those things, there are several other variables for the consumer to choose that have to do with things the DynamicFormField has no way to deduce, just based on data. The style of the label, the error, and the form control can all be tweaked independently, based on the user context, by a given set of flags.

Right now, I could do it something like this:

<dynamic-form-field
  :value="myFormFieldValue"
  v-slot="{ formattedValue, FieldComponent, LabelComponent, ErrorComponent }">
  <component :is="FormLabelComponent" [bold/italic/uppercase/large/small/inline]>This is a label</component>
  <component :is="FormFieldComponent" [disabled/readonly/inline]>
  <component :is="FormErrorComponent" priority="[high/medium/low]">This is a label</component>
</dynamic-form-field

Ok, sure, that's fine. But it's messy and not super readable. For one, I have to use component and the actual component name gets relegated to a prop in each of the provided components, which, for a consuming-side api in a library that wants to be user-friendly, seems kind of obtuse. Two, I have to add Component to the name of every component the slot is providing to make it clear this is a component being provided, rather than data.

Solution: v-using, a directive which allows slotted components to declare (and provide) components which are defined on their scope, rather than just data.

<dynamic-form-field
  :value="myFormFieldValue"
  v-using="{ FormField, FormLabel, FormError }"
  v-slot="{ formattedValue }">
  <form-label [bold/italic/uppercase/large/small/inline]>This is a label</form-label>
  <form-field :value="formattedValue" [disabled/readonly/inline] />
  <form-error priority="[high/medium/low]">This is a label</form-error>
</dynamic-form-field>

Isn't that nice and clean? 😃

I realize I could also just have dynamic-form-field be non-scoped component, and pass the label, and stylistic props to it and have it splice them out amongst the different parts but

1) DyncamicFormField would end up having a million props, which is gross, and doesn't feel like an approachable API for someone writing a library. 2) Even if I pass down all the different variable flags as props, I'm still prevented from using directives on the field/label/error component children. And, particularly for things like a auto-focus directive on the field component, that's actually a pretty big loss in terms of re-usability.

posva commented 4 years ago

I don't think an extra syntax here is worth, this looks like a very niche case. Even with dynamic components, it's okay, although it's clearly something that will look better with JSX. Components can also be data FWIW I would rather expose these FormLabel, FormField, ... components and let the user place them as they want inside the slot. I would also provide simple props for the most basic use cases like the label and a few other things

Aaron-Pool commented 4 years ago

@posva but I can't "expose" those components. They're dynamic. The FormLabel and FormField components are chosen based on logic internal to DynamicFormField based on the components that best suite the data provided to DynamicFormField (if a boolean is provided, a toggle might be provided, if an array of booleans is given, then a multi-select might provided instead, etc). The purpose of DynamicFormField is that the user just provides the dataset they need to edit and the people making DynamicFormField can make the UX decisions, even after the fact, to choose the most suitable control to use to edit that data, without the consumer ever even needing to make any changes to the codebase on their side.

In general, I'm someone who writes Libraries in JSX, which are then end up being consumed by people using SFCs. There are a lot of unique patterns that can be achieved with this component+subcomponents packaging, where you can take props from the parent and dynamically generate or modify component instances and pass them to the consumer. The only problem is that, in SFC land, you lose so much semantic value because so much becomes a <component is> tag.

Aaron-Pool commented 4 years ago

You can even create really nice, maintainable abstractions like this:

<content-block type="secondary" v-using="{ ContentText, ContentHeader, ContentDivider }">
  <!-- these components will be swapped out based on the content type, but only the library team needs to know that, the general dev team just needs to use this api -->
  <content-header>This is a Header</content-header>
  <content-text>This is the body of a paragraph</content-text>
  <content-divider />
  <content-text>This is another paragraph</content-text>
</content-block>

Now, suppose I have 4 different types of content blocks, as long as someone uses this component, I can, down the line, change out or mix-and-match the actual component used for any of the individual pieces of ContentBlock. If I had, instead, exposed individual components like content-header-[urgent\primary\secondary] and content-text-[urgent\primary\secondary], and then we later decided that the content-text-primary and content-text-secondary don't need to be different. Then I have to either do a refactor to replace content-text-secondary with content-text-primary and even that ends up being weird because I'm using content-text-primary in a block called content type="secondary" or I have to have change content-text-secondary so that it is just a duplicate implementation to content-text-primary. Where as the way above instead could completely and cleanly consolidate implementation details like that to a single file. There's also the added benefit that we only have to import and register one component, rather than have to do four separate imports and registrations.

But, as it sits now, I'm hesitant to use it as a pattern because, like I said, it ends up resulting in component being everywhere.

vberlier commented 4 years ago

I think provide and inject are the right tool for the job here. You could make the ContentBlock component implicitly provide the necessary context to the inner components. The users would then simply import and use the components as they're normally used to but ContentHeader and the other inner components would be context-aware and retrieve the information they need with inject.

Aaron-Pool commented 4 years ago

@vberlier provide and inject have several unfortunate caveats that would make them inconvenient here.

1) they're not (intended to be) reactive 2) functional components have some unexpected behavior when it comes to provide/inject behavior, the most significant being that a functional component can't itself provide anything. Which would mean at least the wrapper component above couldn't be functional, even though it's functionality is a great candidate for a functional component.

yyx990803 commented 4 years ago

@Aaron-Pool in v3

Aaron-Pool commented 4 years ago

@Aaron-Pool in v3

  • Injection bindings can be reactive if you provide refs
  • If you need to provide something, just use a stateful component, nothing wrong with that.

Oh, I was actually thinking about this feature for v2, I suppose v2 is being considered "feature-complete" at this point?

yyx990803 commented 4 years ago

@Aaron-Pool we can think about porting the inject ref for reactivity to v2 for sure.