Skip to content

Instantly share code, notes, and snippets.

@otakustay
Last active March 3, 2017 16:43
Show Gist options
  • Select an option

  • Save otakustay/4cf4872b6607ae2f4a81e0e542971481 to your computer and use it in GitHub Desktop.

Select an option

Save otakustay/4cf4872b6607ae2f4a81e0e542971481 to your computer and use it in GitHub Desktop.
Slot event

实际上我们要解决的场景一共有3种:

  1. 子组件从一开始就被设计为与某个固定的父组件挂钩,如上面的CheckListCheckItem的关系,是强耦合的
  2. 父组件需要某些子组件的通知来完成自己的逻辑,但子组件是任意的,如上面Dialog里的close这个slot,是半透明的
  3. 父组件通过slot容纳子组件,有可能需要子组件的某些通知,但并不关心子组件是什么,且需要让子组件的通知了对有被更上层接收到,如上面的TogglePanel(完全透明)和PersistForm(带有自己逻辑的透明),是透明的

我的设计是,让所有在slot中的组件的事件均会调用parentComponent#onSlotEvent,提供当前的事件对象和slot的名称,由父组件选择要如何处理

强耦合

这种情况比较容易处理,无论是使用事件还是让子组件自己找到父组件进行注册都可以,因为两者均知道对方是什么有哪些行为,所以很容易就能形成一种协议来进行通信

半透明

这种情况下,通过onSlotEvent能接收到通知,然后完成自己的逻辑即可,这个逻辑可能还包含继续触发自己的事件

全透明

这一情况下,父组件需要有目的性地在onSlotEvent中调用this.fire(event)将事件直接代理出去

一些补充

事实上,全透明才是比较罕见的情况,不然我们也不会直到今天才开始讨论这个问题了

这个方案的一个特点在于:

  1. 无论是哪一种情况,决定权都在父组件的实现上,任何一个组件对外的接口就是其数据+其事件,不会出现一个组件定义的事件有click,结果它的子组件的keypress从这个组件上触发了出来这种不符合接口定义的情况,保证了组件本身的静态类型特征
  2. 子组件对父组件是没有任何的感知的(不允许在代码中使用parent或parentComponent),这保证了整个结构是单向的,不会因为组件开发者双向的逻辑编写导致最终调用、事件流的混乱
  3. 没有任何隐式地穿透parentComponent的逻辑,事件和数据的传播均被设计为以组件为边界,拥有足够的封装性(Shadow DOM也是这一套逻辑)
class App extends Component {
template = `
<san-persist-form key="formData">
<input name="name" on-change="update" />
<san-calendar name="age" on-change="update" />
<!-- 因为这个TogglePanel的slot是transparent的,所以PersistForm可以在onSlotChildAttach/onSlotChildDetach里拿到这个CheckList -->
<san-toggle-panel title="更多信息">
<san-check-list on-change="update">
<!-- 但CheckList的slot不是transparent的,所以外面是无法拿到CheckItem的,这说明CheckList想封装自己的内部结构 -->
<san-check-item text="1" value="1"></san-check-item>
...
</san-check-list>
</san-toggle-panel>
</san-persist-form>
<footer>
<san-button on-click="submit" text="提交"></san-button>
</footer>
<san-dialog visible="{=isDialogVisible=}" on-close="cancelSubmit">
<slot name="body">是否真的要提交?</slot>
<slot name="footer">
<san-button on-click="postData" text="确定"></san-button>
</san-dialog>
`;
submit() {
this.data.set('isDialogVisible', true);
}
cancelSubmit() {
...
}
postData() {
...
}
}
class CheckItem extends Component {
template = `<li on-click="fire('click')" data-ui-value="{{value}}>{{text}}</li>`
}
class CheckList extends Component {
// 假设这里能ref到
template = `
<ul ref="list">
<slot></slot>
</ul>
`;
childClick = event => {
let value = this.data.get('value');
let childValue = event.target.data.get('value');
if (value.includes(childValue)) {
this.data.remove('value', childValue);
}
else {
this.data.push('value', childValue);
}
// 统一变成自己的change事件
this.fire('change');
};
onSlotChildAttached(child) {
child.on('click', this.childClick);
}
onSlotChildDetached(child) {
child.un('click', this.childClick);
}
}
class PersistForm extends Component {
template = `
<h3>
{{title}}
<slot name="close">
<!-- 因为可能会被替代掉,所以这里不写on-close -->
<span{{closeText}}
</slot>
</h3>
<div>
<slot name="body"></slot>
</div>
<footer>
<slot name="buttons"></slot>
</footer>
`;
close = () => {
this.data.set('visible', false);
};
onSlotChildAttached(child, slotName) {
// 对于关闭按钮,不会再把事件送出去,而是变成close事件
if (slotName === 'close') {
child.on('click', this.close);
}
// 其它slot的事件存心不送出去,要用dialog就别想拿到事件了,就这么设计的爱用不用
}
onSlotChildDetached(child, slotName) {
if (slotName === 'close') {
child.un('click', this.close);
}
}
}
class PersistForm extends Component {
template = `
<div>
<!--
如果是transparent的话,在调用当前组件的onSlotChildAttached/onSlotChildDetached之后,还会继续去调用parent的同样方法,
调用parent时slotName会变成parent里声明的那个(即**仿佛这个组件不存在**)
-->
<slot attach="transparent"></slot>
</div>
`;
onInit() {
this.data.set('persistData', JSON.parse(localStorage.get(this.data.get('key'))));
}
toggle() {
this.data.set('collapsed', !this.data.get('collapsed'));
}
persistData = event => {
if (event.type === 'change') {
// 这个只处理了Component,还需要考虑下Element的问题
this.data.get('persistData').set(event.target.data.get('name'), event.target.data.get('value'));
localStorage.set(this.data.get('key'), this.data.get('persistData'));
}
}
onSlotChildAttached(child) {
// 注册事件
child.on('change', this.persistData);
// 灌数据
child.data.set('value', this.data.get('persistData')[child.data.get('name')]);
}
onSlotChildDetached(child) {
child.un('change', this.persistData);
}
}
class TogglePanel extends Component {
// 假设这里能ref到
template = `
<h3 on-click="toggle">{{title}}</h3>
<div style="display: {{collapsed | yesOrNo('none', '')}}"
<slot attach="transparent"></slot>
</div>
`;
toggle() {
this.data.set('collapsed', !this.data.get('collapsed'));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment