Vue: 改进了 UI 组件的 API,减少了转发属性和事件的样板文件

创建于 2017-06-27  ·  46评论  ·  资料来源: vuejs/vue

这个功能解决了什么问题?

在很多情况下,传递给 Vue 组件的属性不应添加到根元素,而应添加到子元素。 例如,在这个 UI 组件中,必须使用数量惊人的 props 来确保将属性添加到input元素,而不是包装器div

此外,通常希望将表单元素上的所有事件侦听器公开给父元素,如果元素不是根元素,目前这也需要大量样板(在这种情况下, .native修饰符可以解决问题)。

提议的 API 是什么样的?

编辑:从这里开始赶上讨论。

当前默认情况下,“公开”元素(可以添加任意属性的元素)始终是根元素。 新指令可用于定义不同的暴露元素。 指令名称的一些想法:

  • v-expose (可能是我个人最喜欢的)
  • v-expose-attrs (可能更清晰,但更长)
  • v-main
  • v-primary

如果v-expose被添加到一个元素,它将接受传递给它的组件的属性 - 这些属性将__不再__被传递给根元素。

其他可能不错的功能:

  • 如果指令在多个元素上定义,属性将在每个元素上重复
  • 在元素应该接受属性子集的情况下, v-expose可以接受字符串或字符串数​​组(例如v-expose="class"v-expose="['class', 'type', 'placeholder']" )。 在这种情况下,这些属性将添加到元素(同样,而不是添加到根元素),但所有其他属性将添加到根元素或具有无值v-expose的元素.
discussion feature request

最有用的评论

@chrisvfritz在渲染函数中如何工作?

我想也许更好的是:

  • 提供禁用根节点属性自动继承的选项
  • 将这些属性公开为$attributes ,例如(命名 tbd)
  • 使用 v-bind 将它们添加到任何你喜欢的地方,就像我们已经展示过的$props
v-bind="$attributes"

这将具有在 JSX/render 函数中几乎完全相同的额外好处

所有46条评论

嗯,我不知道,但对于像这样的组件,我认为你可以使用 JSX 或createElement来传播道具。

https://github.com/doximity/vue-genome

对我们来说,这将是一个伟大的。 我们将所有输入包装在一个标签中,用于样式和用户体验。 我同意我们可以改为使用 jsx,但模板对于每个人来说都更容易遵循。

@Austio ,不幸的是,这就是模板的回报......等等......也许我们可以想出一种方法将spread props 转换为 vue 模板?

我个人喜欢这个功能。 但这似乎打破了 v-bind 行为的一致性,就像有时我仍然需要为根元素绑定class属性。

那么如何使用一对指令作为 getter 和 setter,例如:

在组件内部,定义一个v-expose锚点:

<input v-expose="foo" />

使用时:

<the-component v-define:foo="{propA: '', propB: ''}"></the-component>

<!-- or maybe use v-bind for it directly -->
<the-component :foo="{propA: '', propB: ''}"></the-component>

@jkzing ,这看起来很棒,但同样,这看起来像一个基本的点差,并且存在潜在的问题,例如您如何定义@keyup.enter.prevent="myAction"

你不能只是<the-component :foo="{'@keyup.enter.prevent': myAction}"></the-component> ,这意味着你必须在运行时保留所有修饰符,如enterprevent (这是 vue-template-compiler 的一部分提款机)

@nickmessing

看起来像一个基本的点差

我们正在谈论的事情是为模板用户带来诸如传播之类的东西

<the-component :foo="{'@keyup.enter.prevent': myAction}"></the-component>

@是 v-on shortland,不代表prop (v-bind)。

@jkzing ,在描述的链接中也有很多v-on绑定

@nickmessing嗯...至于v-on绑定,它是另一个主题 IMO,如事件冒泡。 🤔

@jkzing ,这是v-expose afaik 的全部概念,使所有属性“转到”组件中的某个元素

@nickmessing ,无法确定最初的提案,但我认为不应将事件侦听器视为attribute

@jkzing ,可能不是,但考虑到<my-awesome-text-input />的常见示例,其中您可以拥有 >9000 个不同的道具,您只希望它们全部都能到达您的自定义组件中的<input />的代码。

我个人使用v-bind="$props"或者您可以过滤掉这些以排除您不想应用的道具。 通过这种方式,您可以一次在输入上应用多个道具。 确实 v-expose 可能很有用,因为对于包装器组件,例如输入,您必须指定所有这些 html props

所以这
https://github.com/almino/semantic-ui-vue2/blob/master/src/elements/Input.vue#L9
cane 减少到v-bind="$props"v-bind="filteredProps" ,其中filteredProps 可能是一些计算属性

@cristijora我们也在使用v-bind="someProps" 。 这个解决方案的问题是过多的属性将被添加为 HTML 属性。 如果v-bind=可以过滤掉组件不接受的所有属性,那就太好了。 使用动态<component>我们不知道要在计算属性中过滤掉哪些道具。 虽然可以提取options.props并使用lodash._pick

这对于指令真的可行吗?

@posva ,我不认为这本身就可以作为指令工作,但它可以是 vue 模板引擎的一部分,它可以在内部传播 + 一些事件传播

@posva我不认为是用户构建的指令,所以我可能使用了错误的语言。 我的意思只是一个“特殊属性”。

@chrisvfritz您对 API 的使用方式有什么想法吗(指定要公开的内容以及如何添加到子项)

我可以看到这与提供/注入概念的使用相似。

@Austio我可能不理解这个问题,但我在原始帖子中提供了一些关于 API 的想法。

嘿 Chris,意思是关于使用类似提供注入的其他想法,您可以在其中声明在父级中可公开的内容,然后在子级中使用它。

啊,我明白了。 我不确定是否需要这样做。 信息已经可以通过 props 和 slot 传递——甚至可以使用this.$parent访问父级的私有属性,尽管我认为最好避免这种模式。

@Austio是否有您正在考虑的特定用例?

@chrisvfritz在渲染函数中如何工作?

我想也许更好的是:

  • 提供禁用根节点属性自动继承的选项
  • 将这些属性公开为$attributes ,例如(命名 tbd)
  • 使用 v-bind 将它们添加到任何你喜欢的地方,就像我们已经展示过的$props
v-bind="$attributes"

这将具有在 JSX/render 函数中几乎完全相同的额外好处

@LinusBorg我喜欢你的想法。 😄 你的方法更直观。

作为旁注,我认为有了这个 API,Vue 的下一个主要版本甚至可以完全删除属性自动继承,以便跨组件通信可以在双方保持明确。

可以贬值或消除这种行为,是的。

如果这值得对库等中的许多组件进行可能需要的更改,则要决定并应与社区讨论,尤其是 UI 集合作者。

A 关于 prob posed 特性:此信息已通过context.data.attributes在功能组件中可用,因此此功能将为实例组件提供基本相同的功能。

对,就是这样。 我想到的主要目的是让 UI 组件作者(第 3 方和内部)的工作更简单。 目前有很多情况需要这样的事情:

<input
  v-bind:id="id"
  v-bind:accept="accept"
  v-bind:alt="alt"
  v-bind:autocomplete="autocomplete"
  v-bind:autofocus="autofocus"
  v-bind:checked="checked"
  v-bind:dirname="dirname"
  v-bind:disabled="disabled"
  v-bind:form="form"
  v-bind:formaction="formaction"
  v-bind:formenctype="formenctype"
  v-bind:formmethod="formmethod"
  v-bind:formnovalidate="formnovalidate"
  v-bind:formtarget="formtarget"
  v-bind:list="list"
  v-bind:max="max"
  v-bind:maxlength="maxlength"
  v-bind:min="min"
  v-bind:multiple="multiple"
  v-bind:name="name"
  v-bind:pattern="pattern"
  v-bind:placeholder="placeholder"
  v-bind:readonly="readonly"
  v-bind:required="required"
  v-bind:src="src"
  v-bind:step="step"
  v-bind:type="type"
  v-bind:value="value"
  v-on:keydown="emitKeyDown"
  v-on:keypress="emitKeyPress"
  v-on:keyup="emitKeyUp"
  v-on:mouseenter="emitMouseEnter"
  v-on:mouseover="emitMouseOver"
  v-on:mousemove="emitMouseMove"
  v-on:mousedown="emitMouseDown"
  v-on:mouseup="emitMouseUp"
  v-on:click="emitClick"
  v-on:dblclick="emitDoubleClick"
  v-on:wheel="emitWheel"
  v-on:mouseleave="emitMouseLeave"
  v-on:mouseout="emitMouseOut"
  v-on:pointerlockchange="emitPointerLockChange"
  v-on:pointerlockerror="emitPointerLockError"
  v-on:blur="emitBlur"
  v-on:change="emitChange($event.target.value)"
  v-on:contextmenu="emitContextMenu"
  v-on:focus="emitFocus"
  v-on:input="emitInput($event.target.value)"
  v-on:invalid="emitInvalid"
  v-on:reset="emitReset"
  v-on:search="emitSearch"
  v-on:select="emitSelect"
  v-on:submit="emitSubmit"
>

新的$attributes属性可以将其缩短为:

<input
  v-bind="$attributes"
  v-on:keydown="emitKeyDown"
  v-on:keypress="emitKeyPress"
  v-on:keyup="emitKeyUp"
  v-on:mouseenter="emitMouseEnter"
  v-on:mouseover="emitMouseOver"
  v-on:mousemove="emitMouseMove"
  v-on:mousedown="emitMouseDown"
  v-on:mouseup="emitMouseUp"
  v-on:click="emitClick"
  v-on:dblclick="emitDoubleClick"
  v-on:wheel="emitWheel"
  v-on:mouseleave="emitMouseLeave"
  v-on:mouseout="emitMouseOut"
  v-on:pointerlockchange="emitPointerLockChange"
  v-on:pointerlockerror="emitPointerLockError"
  v-on:blur="emitBlur"
  v-on:change="emitChange($event.target.value)"
  v-on:contextmenu="emitContextMenu"
  v-on:focus="emitFocus"
  v-on:input="emitInput($event.target.value)"
  v-on:invalid="emitInvalid"
  v-on:reset="emitReset"
  v-on:search="emitSearch"
  v-on:select="emitSelect"
  v-on:submit="emitSubmit"
>

尽管如此,我认为有某种方式也可以公开事件仍然很好。 也许一个空的v-on指令可以将父级上的所有事件侦听器转发到这个元素?

<input
  v-bind="$attributes"
  v-on
>

或者,如果最终确实有我们想要捆绑的多个问题,我们可能会回到v-expose

<input v-expose>

这已经变成了关于如何简化 UI 组件构建的更广泛的讨论,而不是特定的功能请求,所以我将重新标记这个问题。 🙂

我迟到了这个话题,但我也有一些想法。

v-bind当前解决方案和缺点

首先,我已经使用并喜欢v-bind="propObject"功能(如此强大)。 例如, bootstrap-vue有一个内部链接组件,可以在任何地方使用(按钮、导航、下拉列表等)。 组件枢轴成为本机锚点与基于路由器链接的hrefto以及vm.$router ,因此有相当多的属性可以有条件地传递给每个这些组件中。

我们的解决方案是将这些 props 放在一个 mixin 中,并将v-bind="linkProps"与一个计算对象一起使用。 这很好用,但仍然需要大量开销,将 mixin 添加到_所有其他使用链接组件的组件_。

v-expose使用v-bind可能性

我个人喜欢v-expose的概念,也许它可以像默认插槽 + 命名插槽一样工作,然后使用修饰符访问命名属性插槽。

默认属性 _"slot"_ 将始终将属性传递给组件本身(无更改),而其他命名目标可以由组件指定。 也许是这样的:

<template>
  <my-component 
    <!-- Nothing new here -->
    v-bind="rootProps"
    <!-- This binds the `linkProps` object to the named attribute slot `link` -->
    v-bind.link="linkProps"
  />
</template>

MyComponent.vue

<template>
  <div>
    <router-link v-expose="link" />
  </div>
</template>

事件代理

除了.native是一个非常强大的修饰符之外,我没有太多要添加的内容。 为我解决了很多问题。 尽管如此,Vue 开发人员似乎对它几乎一无所知(我看到大量 UI 库问题可以通过向开发人员公开此功能来解决)。 我在网站上放置了一个 PR,以在网站中添加更多文档和搜索支持,并可能针对谷歌搜索进行了优化。

来自另一个问题中关于 API 表面的争论,我必须重申,我不是v-expose想法的粉丝。 它引入了另一种“工作方式”,并且不适用于 JSX 而不在那里实现一些特殊的东西,等等。

我尊重 React 人员的一件事是他们对精简 API 的承诺,并尽可能多地使用该语言的功能。 本着这种精神,重新使用我们已经拥有的用于属性的 props 的模式似乎比引入另一个抽象要好得多。

<my-input
  type="file"
  mode="dropdown"
>
<template>
  <div>
    <input v-bind="$attributes">
    <dropdown v-bind="{ ...$props, $attributes.type }"/>
  </div>
</template

啊,我现在明白你在说什么了。 我喜欢它! 这是目前可用的吗? vm.$attributes会是加法吗?

重新阅读您的评论。 我正在追踪👍

是的, $attributes将是加法。

此外,我们需要一个选项来关闭将属性应用于根元素的当前默认行为,如下所示:
```html

如果我们决定这样做会导致重大更改,那么这可能会成为 Vue 3.0 中的默认设置。

@LinusBorg你对处理事件方面的事情有什么想法? 为了遵循相同的策略,我想我们还可以添加一个$listeners属性,它可能如下所示:

{
  input: function () { /* ... */ },
  focus: function () { /* ... */ },
  // ...
}

那么也许v-on可以接受一个对象,类似于v-bind 。 所以我们有:

<input v-bind="$attributes" v-on="$listeners">

我预见的一个问题是input / change事件,因为v-model对组件的工作方式与对元素的工作方式略有不同。 我也不知道我们是否想要$listeners$nativeListeners 。 我想如果$listeners可用,那么.native修饰符可能已过时。

此外,关于applyComponentAttrsToRoot选项,也许exposeRootEl会是一个好名字,当设置为false ,可以禁用自动应用的属性和.native事件转发?

能够通过Vue.config为整个应用程序以及单个组件禁用此功能也可能很好。

我最近对$listeners有一个类似的想法 - 它也可以通过以下方式在功能组件上使用

context.data.listeners

所以我们最终会得到$props$attributes$listeners ,这对我来说听起来不错。

还有 #5578 要求v-on="{...}"对象语法,就像我用于$attributes ,它很适合。

但我不确定 .native 修饰符。 为了使组件事件和本机侦听器都能工作,API 最终会复杂得多,而且使用是有问题的,因为应用于根元素的本机事件侦听器仍然会捕获所需的事件冒泡,所以它可能不会必须将其分配给模板中的特定元素。

一般来说,我会说对于低级组件库,当模板变得难以使用时,应该首选渲染函数。 但我同意以下内容是有价值的:

  1. 禁用“自动应用在 props 中找不到的绑定作为根元素的属性”的默认行为(相关问题:这是否也会影响classstyle绑定?)

  2. 公开一种更简单的方法来将组件上的外部绑定“继承”到不一定是根的内部元素上。 理想情况下,模板和渲染函数之间具有一致性。

ia kie like vue,简单的工具

只想说v2.4中的PR非常好! 👍

来自发行说明

结合这些,我们可以将这样的组件简化为:

<div>
  <input v-bind="$attrs" v-on="$listeners">
</div>

看起来不错,但事实并非如此,因为这些类型的组件旨在与 v-model 一起使用,据我所知,包装组件上的 v-model 无法正常工作。 例如,是否有任何示例说明如何将 v-model 从包装组件转发到输入?
我发现的最简单的方法:
https://jsfiddle.net/60xdxh0h/2/

也许功能组件与模板一起工作会更直接

这些类型的组件旨在与 v-model 一起使用,据我所知,包装组件上的 v-model 无法正常工作。

你为什么那么想? v-model 只是 prop 和事件侦听器的语法糖,两者都在 $attr/$props 中,因此可以轻松传递。

我想唯一需要了解子选项的事情是如果孩子更改了model默认值,那是真的。

但是,根据情况,可以阅读这些内容。

当然它是一种语法糖,但我的意思是它可能会令人困惑

结合这些,我们可以将这样的组件简化为:

当实际基于示例https://github.com/almino/semantic-ui-vue2/blob/master/src/elements/Input.vue 时,您不能直接传递侦听器来实现相同的控制。 (例如:你必须使用 v-on:input="emitInput($event.target.value)" )

不管怎样,这个PR很有价值,干得好!

@AlexandreBonaventure这是因为v-model在组件上的工作方式与在元素上的工作方式略有不同。 DOM 事件为回调提供一个事件对象,而组件事件直接提供一个值。 结果是v-model _does_ 工作,但绑定值是 DOM 的事件对象。 😕

我认为您是对的, v-model在这里工作是可取的,但我不确定解决它的最佳地点在哪里。 一些想法:

也许可以将不可枚举的属性添加到$listeners (例如__$listeners__: true ,以帮助v-on检测v-on="$listeners" 。然后在$listeners对象被传递给v-on ,每个监听器都可以被包装:

function (event) {
  listener(event.target.value)
}

一个缺点是现在我们正在丢弃数据。 例如,如果有人想访问keyCode ,他们就不走运了。 但是,如果v-on的对象语法支持修饰符,我们可以通过使.native禁用自动包装行为来解决此问题。

@yyx990803 @LinusBorg您对可行性有何看法? 我缺少任何边缘情况?

哦,我明白了,您指的是 rral 上的 v-model。 表单元素,我是在组件上考虑的。

不管有没有这个 PR,你都不能/不应该在道具上使用它。 在高级应用程序中,使用它是相当罕见的(尽管可以实现)。

@LinusBorg只是想确保我们在同一页面上。 给定带有此模板的CustomInput组件:

<div>
  <input v-bind="$attrs" v-on="$listeners">
<div>

您不希望下面的代码起作用吗?

<CustomInput v-model="myValue" />

我希望它能够工作——但我理解 alexandre 的方式是,他指的是元素上的 v-model,而不是组件——它最终只适用于变异的本地状态。

我试图说出@chrisvfritz在他的后

@LinusBorg在最新版本中这样做的问题在于它仍然被认为是一种反模式,并会触发关于改变状态的警告。

在 value 属性不是字符串的情况下,让上述工作非常有用。 以一个组合组件为例,我试图使用从我自己的库中导入的枚举作为选择选项的值:

<template>
    <select class="combo" v-model="value" v-on="$listeners"> 
      <option v-for="(item, key) in items" :value="item">{{key}}</option>
    </select>
</template>

<script>
export default {
    props: {
        items: {
            type: Object,
            required: true
        },

        value: {}
    }
}
</script>

这是我用于父项中的列表之一的示例:

            execList: {
                "None": ACT_EXEC_TYPES.NONE,
                "Function": ACT_EXEC_TYPES.FUNCTION,
                "Code": ACT_EXEC_TYPES.CODE
            }

以及我如何使用组合组件:

<combo :items="execList" v-model="selectedAction.execType"/>

我一直在努力使这项工作正常进行 2 天,但仍然感到非常沮丧。 问题是 $event.target.value 始终是一个字符串,而不是像:value那样进行评估。

@LinusBorg @AlexandreBonaventure @RobertBColton我刚刚打开了一个问题,我们可以在此集中讨论这个问题。

此页面是否有帮助?
0 / 5 - 0 等级