regularjs/regular

Feature request: named include and scoped include.

Opened this issue · 14 comments

named include

在使用Tooltip或者Dropdown这样的组件时,我们希望将overlay部分的模板也写在组件的模板中,这样可以更直观的看到组件的View结构:

<Tooltip>
    <Button>hover me</Button>
    <div name="content">
        <Icon type="smile-o"></Icon> <span>你好</span> 
   </div>
</Tooltip>

在Tooltip内部:

{include this.$templates.content}

目前的解决方案是这样的,把模板作为组件的data传入:

<Tooltip content={~<Icon type="smile-o"></Icon> <span>你好</span>}>
    <Button>hover me</Button>
</Tooltip>

也可以把content模板挂在组件上然后传入。

不能说named include的写法更好,因为现在是可以用include this.foo实现引用指定名称模板这个功能的。Named include其实只是一个语法糖,但我觉得我们应该提供这样的写法,给开发者更多的选择。


scoped include

有时候需要在内嵌组件的属性中传入函数,比如Table组件。我们希望这个函数的参数可以是动态传入的:

<Table dataSource={dataSource}>
  <Column title="标题" key="title">
  <Column/>
  <Column title="作者"  key="author">
  <Column/>
  <Column title="删除"  key="delete">
     <Button r-click={this.delete(index)}></Button>
  <Column/>
</Table>

目前Regular在模板中是没法做到这个效果的,这里的index会一直指向实例data上的index。各大Regular组件库(MUI和NEK-UI)解决这个问题的办法是,在组件定义时将模板作为普通的字符串传入,在渲染之前通过插值动态生成一个模板字符串,把当前的局部变量写死在模板里,然后渲染。比如这样:

render: (text, record, index) => {
    return `<a href="#">${text}</a>`
}

或者将函数写成一个组件,把模板作为字符串传入:

<kl-table-template type="header">
            {'<a href={header.name+">+~!!@#$%^&*()"} on-click={this.emit("headerclick", header)}>I am && {header.name}</a>'}
            {'<anchor/>'}
</kl-table-template>

这样做原因是Regular没有在模板中支持scoped include。

Regular的$complie方法是可以传入extra这个变量的。Regular的list中的index就是在extra中传入的。现在的问题就是我们不能在模板中控制组件中部分模板编译时传入的参数。

Scoped include的使用大概是这样的:

<Table dataSource={dataSource}>
  <Column title="标题" key="title">
  <Column/>
  <Column title="作者"  key="author">
  <Column/>
  <Column title="删除"  key="delete">
     <Button  scoped r-click={this.delete(scope.index)}></Button>
  <Column/>
</Table>

Table组件内部

{include  index={index} item={this.arr[index]} }

include时传入参数可以在模板的作用域中通过scope对象访问到。这个对象的名称是可以通过scoped属性在组件模板中配置的。

<Button  scoped="item" r-click={this.delete(item.index)}></Button>

总结一下,include这个特性让Regular组件可以组合,所以是使用频率非常高的。Named include和scoped include可以让我们更优雅的写声明式的组件,将组件的View声明在模板中,而不是四散在各处。这个特性的实现成本也不是很大。

大家怎么看?

关于render 那个问题 也困扰了 我和一些同学蛮长时间, 一度有点摇摆是不是要使用类似virtual dom + jsx的方式去进行重构, mvvm这里天然的问题就是 模板和 JS 的上下文是隔离的 需要 viewmodel层做链接,这一层关系 限制了一些能力,而jsx 天然就是 公用上下文, 就我看来 vd最大的优势就在于此。

scoped 这个设计真的很好, 相当于提供了一个局部的动态绑定。但仍然有个不足就是,需要在文档中说明scope对象的内容

关于第一个 其实是个 body类参数传入的 语法糖, 确实用 {~ }在传入复杂实体的时候非常不美观, 我觉得可以考虑

scoped的用法的确不是很符合直觉,需要额外的说明。目前需要用到局部变量的地方可以手动调用compile来解决。

但我觉得还是需要有个办法来控制内嵌组件模板编译的时机,比如新增一个template组件,其实也是一个语法糖。

template中的内容可以在组件中通过 {include this.$templates.name} 组合。

<template name="foo">
    <div>bar</div>
</template>

声明了deferred属性的template里面的模板不会被解析。可以在组件里通过this.$templates.name拿到模板的AST,然后组件可以自己主动控制编译成Living DOM的时机。

<template name="foo" deferred> 
    <div>bar</div>
</template>

👍

我也喜欢 named include 的写法

如果引入这种写法的话,那是不是可以再增加一个 this.$templates.default 作为默认模板,和this.$body 作用相同,这样可以统一下写法,习惯 es6 的同学应该也很容易接受

不过建议把 name 换个名字或者单独弄一个标签, name 本身就是标准的 html 属性,用在 div 上会存在歧义

另外 scoped include 换成下面这种写法怎么样?

{#include this.$body with { index: index, item: this.arr[index] } }

#inc 之前好像就是跟一个表达式的,这样会不会好一点,和x=y的形式比,更贴近js语法

<Button scoped="item" r-click={this.delete(item.index)}></Button>

写成

<template context="item">
    <Button r-click={this.delete(item.index)}></Button>
</template>

可能会好理解点,context用来指定with后面变量的"挂载点",和原生js的with类似

  1. 如果 #inc 的时候没有 with ,那和现在的行为保持一致

  2. 如果 #inc 的时候 with 后有值,而且提供了 context的值,把 with 后面的变量挂载到 context
    对应的值上

  3. 如果 with 后有值,但开发者没提供 context 的值,那么查找变量的时候优先从 with 后的变量查找,再从宿主的data上查找,和原生 js 中的 with 行为类似

  4. 当然也可以手动关闭该特性,不切换查找的上下文

<template context={ false }>
    <Button></Button>
</template>

上面说的也可以微调下,比如默认关闭 context 特性,想要开启可以这样

<template context={ true } context-name="item">
    <Button></Button>
</template>

而且这种写法和上面说的 named include 也可以很好地结合

<template context={ true } context-name="item" slot="foo">
    <div>bar</div>
</template>

这里的 name 我觉得还是换成 slot 更容易理解,虽然可能和 vue 类似,但这同样也是 html标准,我们向标准靠拢并没有问题 😄

@fengzilong 是不是不与标准 冲突更好些? 理论上这是web component 是属于运行时的能力, 这样即使我们编译后产生了slot 或相关custom element 在浏览器端也能正常运行, 就和我们使用video标签一样

这方面的确欠考虑了,如果将来打算把 regular 和原生的 web components 结合使用的确会存在冲突,之前的想法是既然作为标准,这种设定肯定更容易被接受,如果我们能做到和标准保持一致的行为的话就没问题,相当于是在框架的层面对标准进行一定扩展

技术上确实不会麻烦, 关键还是要把 用法确定了。 之前发现一些不合理设计,感觉后期包袱很大。 内部实现觉得真的是小问题 @zxc0328 @fengzilong

iulo commented

有定论了吗?没有slot挺痛苦的

@iulo 看了下Vue的文档。如果是Vue的slot , 效果其实和 body param 配合 include是一样的。,如果你配合 title={~ <div>body param</div>} 使用 {#include title}。 理论会比Vue的slot 语法上更方便些

这里讨论的是scope include的问题

@fengzilong 既然显示声明了 this.$body 是伴随 {index: index, item: this.arr[index]} ,在外部使用时,其实是不需要再声明 scope param

比如这样使用

<CustomList >
 <td>第{index+1}项</td><td>{item.name}</td><td>{item.age}</td></tr>
 </CustomList>

同时CustomList内部

<tbody>
{#list list as item}
  <tr>
     {#include this.$body with { index: index, item: this.arr[index] } }
  <tr>
{/list}
</tbody>

template 是冗余的 . 如果使用body param也是一样

<CustomList row={~  <td>第{index+1}项</td><td>{item.name}</td><td>{item.age}</td>}  />

内部修改为

     {#include row with { index: index, item: this.arr[index] } }

scoped include的问题有结论了么?写table真的挺痛苦。

最终方案 ??

{#list table as item}
     {#include this.$body with  index: item_index + 1 , item:  item  }
{/list}

使用端, 包含Fragment和 Body嵌入 场景

<Table >
      <p on-click={this.clickContent($scope.item)}>{content}</p>
    </Table>

如果需要修改别名,(因为使用include 的粒度是组件),所以组件层面控制即可,而无需再定义一个<template/> 之类的节点控制

//类似这样,不一定接口是这样的

<Card $scope='scoped'  >
      <p on-click={this.clickContent(scoped.item)}>{content}</p>
</Card> 

@fengzilong

刚看到...我可以先按这个实现一版

有一点想要确认下,这里的 with index: item_index + 1 , item: item 需要用 with { index: item_index + 1 , item: item } 么?@leeluolee

暂时用了 `with { index: item_index + 1 , item: item },实现上方便一些,而且支持对象嵌套😅