#8 通过Actions触发变化

来源:互联网 发布:网络风险有哪些 编辑:程序博客网 时间:2024/06/03 09:35

英文原版:https://guides.emberjs.com/v2.14.0/components/triggering-changes-with-actions/

你可以把组件想象成一个带有UI功能的盒子。到目前为止,我们已经学习了父组件如何把属性传递给子组件,也学习如何在javascript代码和模版中使用这些组件。

但是,如果把数据流转的方向颠倒一下呢?数据是如何流转到父组件去呢?其实在Ember中,组件是通过actions来传递事件和变化。

接下来让我们来通过一个简单的例子来观察一下一个组件是如何通过action来与父组件交流的。

假设我们正在制作一个应用,并且用户可以创建账户。我们同时也需要构建一个界面来使得用户可以删除他们的账户。由于我们并不想使用户由于误操作而删除了账户,所以我们需要做一个按钮通过让用户点击确认来触发一些action。

那么,我们来构建了一个“确认按钮“组件,并且希望它可以被复用。

创建组件

定义的组件的名称为button-with-confirmation,使用命令:

ember generate component button-with-confirmation

我们打算这么使用这个组件:

app/templates/components/user-profile.hbs{{button-with-confirmation  text="Click OK to delete your account."}}

同时我们也需要在其他模版中使用这个组件:

app/templates/components/send-message.hbs{{button-with-confirmation  text="Click OK to send your message."}}

设计action

当实现组件的action时,你需要考虑以下2个因素:

  1. 如果是在父组件中,你需要决定如何处理这个action。在这里,我们想要让这个action在某一个处被使用时删除账户,再另一处被使用时发送一条消息。
  2. 如果是在当前组件中,要对发生的事情作出反应,并告诉外界。在这里,我们需要在用户点击按钮并确认之后触发外界的action (删除账户或发送消息)。

接下来我们一步一步来实现。

实现action

在父组件中,我们首先定义当用户点击按钮并确认后要做什么。对应于上面的第一个因素,那么要做的就是找到并删除账户。

在Ember中,每一个组件都有一个叫做actions的属性,它里面你可以定义函数,这些函数可以通过用户交互在组件内触发或者被子组件调用触发

让我们来看一下父组件的javascript文件。在这个下面的例子中,假设我们现在有一个父组件名叫user-profile,它向用户展示账户的配置信息。

我们将会在父组件实现一个action –> userDidDeleteAccount(),该组件会被注入一个模拟的登录服务,当action被调用时会调用这个服务的deleteUser( ) 方法。

app/components/user-profile.jsimport Ember from 'ember';export default Ember.Component.extend({  login: Ember.inject.service(),  actions: {    userDidDeleteAccount() {      this.get('login').deleteUser();    }  }});

好了,我们已经实现这个action,但是现在我们还没有告诉Ember我们什么时候会触发它。接着往下看。

设计子组件

下一步,我们会在子组件中实现用户点击按钮和确认的逻辑。

app/components/button-with-confirmation.jsimport Ember from 'ember';export default Ember.Component.extend({  actions: {    launchConfirmDialog() {      this.set('confirmShown', true);    },    submitConfirm() {      // trigger action on parent component      this.set('confirmShown', false);    },    cancelConfirm() {      this.set('confirmShown', false);    }  }});

这个组件模版中需要有一个按钮和一个用来显示确认对话框的div,其中div基于confirmShown属性展示。

app/templates/components/button-with-confirmation.hbs<button {{action "launchConfirmDialog"}}>{{text}}</button>{{#if confirmShown}}  <div class="confirm-dialog">    <button class="confirm-submit" {{action "submitConfirm"}}>OK</button>    <button class="confirm-cancel" {{action "cancelConfirm"}}>Cancel</button>  </div>{{/if}}

给组件传递action

现在,我们需要让定义在父组件user-profile中的userDidDeleteAccount( ) action函数可以被子组件button-with-confirmation调用。我们将通过向传递属性那样来传递action给子组件。

app/templates/components/user-profile.hbs{{button-with-confirmation  text="Click here to delete your account."  onConfirm=(action "userDidDeleteAccount")}}

这段代码表示“将父组件中的action 函数赋值给了子组件的onConfirm属性“。注意了,action助手会返回父组件中名为userDidDeleteAccount的函数。

在send-message模版中,我们同样这么操作:

app/templates/components/send-message.hbs{{button-with-confirmation  text="Click to send your message."  onConfirm=(action "sendMessage")}}

好了,现在我们在子组件中可以通过onConfirm属性来调用父组件的action函数了:

app/components/button-with-confirmation.jsimport Ember from 'ember';export default Ember.Component.extend({  actions: {    launchConfirmDialog() {      this.set('confirmShown', true);    },    submitConfirm() {      //call the onConfirm property to invoke the passed in action      this.get('onConfirm')();    },    cancelConfirm() {      this.set('confirmShown', false);    }  }});

this.get(‘onConfirm’)将会返回被传入的父组件中的action函数,再它的后面加上 ( ),这个函数就会被调用。

只要你记得如何传递属性,那么要记住如何传递action也比较简单,仅仅是额外使用的action助手。

组件中的action可以使得事件处理逻辑不那么纵横交错,它可以帮你更容易的构建一个可复用的组件。

收尾工作

action通常可以处理一些异步任务,比如发送ajax请求,由于action就是一个可以由父组件传入的函数,所以action也可以在被调用的时候返回值。常见的场景就是让action返回一个promise。

在我们的button-with-confirmation组件中,我们想要显示一个遮罩,并且让它直到处理完必要的数据后才消失。这个效果将通过在promise被resolve的时候实现。基于这个条件,我们定义了一个属性,用它来控制遮罩的显示和隐藏。

app/components/button-with-confirmation.jsimport Ember from 'ember';export default Ember.Component.extend({  actions: {    launchConfirmDialog() {      this.set('confirmShown', true);    },    submitConfirm() {      // call onConfirm with the value of the input field as an argument      let promise = this.get('onConfirm')();      promise.then(() => {        this.set('confirmShown', false);      });    },    cancelConfirm() {      this.set('confirmShown', false);    }  }});

传递参数

有时候,父组件调用action时会提供上下文,然而子组件确没办法提供。考虑一下,对于我们例子,button-with-confirmation组件是在send-message组件中被定义使用的。被传入子组件的sendMessage( ) action函数的可能会期望获得一个表示消息类型的参数。

app/components/send-message.jsexport default Ember.Component.extend({  actions: {    sendMessage(messageType) {      //send message here and return a promise    }  }});

然而,button-with-confirmation组件却不太关心消息的类型。在这种情况下,在父模版中,可以在action被传入子组件时提供一个参数。比如,假设我们想通过button来发送一个类型为info的消息。

app/templates/components/send-message.hbs{{button-with-confirmation text="Click to send your message." onConfirm=(action "sendMessage" "info")}}

button-with-confirmation组件中的submitConfirm( ) action中的代码不用做改变。它仍然在没有传入参数的情况下被调用。

app/components/button-with-confirmation.jsconst promise = this.get('onConfirm')();

然而表达式(action “sendMessage” “info”)仍然完美的完成了它的使命。你会发现就算在子组件中调用此action的时候虽然没有传入参数,但是在父组件中,当该action被执行的时候,接收的参数是有值的。

到目前为止,传入button-with-confirmation组件的action携带了一个参数用来指明messageType。 假设我们现在想扩展sendMessage( ) action,让它可以接收两个参数:

app/components/send-message.jsexport default Ember.Component.extend({  actions: {    sendMessage(messageType, messageText) {      //send message here and return a promise    }  }});

我们想要这个action在button-with-confirmation组件中被调用时带有这两个参数。我们已经看到了如果我们要提供messageType的值,我们需要在模版中做什么,这个参数会在调用onConfirm( )时直接传入被调用的action 函数中。如果我们随后在调用onConfirm时传入一个参数,那么这个参数就会被传入action并作为action的第二个参数

在我们的例子中,这个被传入onConfirm( )的值,会作为messageText参数的值。然而,需要注意的是在我们button-with-confirmation组件的内部却不会关注这个参数到底是作为什么来使用。见下面的代码:

app/components/button-with-confirmation.jsexport default Ember.Component.extend({  actions: {    //...    submitConfirm() {      // call onConfirm with a second argument      let promise = this.get('onConfirm')(this.get('confirmValue'));      promise.then(() => {        this.set('confirmShown', false);      });    },    //...  }});

为了使得confirmValue的值是我们所需要的消息,我们将会把这个属性绑定到input控件上,并且当点击按钮之后它就会被显示。为了实现这个,我们首先改造一下组件,使用块方式来调用组件,并且将confirmValue的值yield到“confirmDialog”元素中:

app/templates/components/button-with-confirmation.hbs<button {{action "launchConfirmDialog"}}>{{text}}</button>{{#if confirmShown}}  <div class="confirm-dialog">    {{yield confirmValue}}    <button class="confirm-submit" {{action "submitConfirm"}}>OK</button>    <button class="confirm-cancel" {{action "cancelConfirm"}}>Cancel</button>  </div>{{/if}}

通过这样改造,我们现在得到了一个封装了输入文本的send-message组件。

app/templates/components/send-message.hbs{{#button-with-confirmation    text="Click to send your message."    onConfirm=(action "sendMessage" "info")    as |confirmValue|}}  {{input value=confirmValue}}{{/button-with-confirmation}}

当用户输入在input控件输入消息时,这些消息就会被同步更新到confirmValue属性上。然后一旦用户点击了确认,submitConfirm action就会被触发,并且调用onConfirm()和传入参数,紧接着send-message组件中的sendMessage action就会被执行,并且接收2个参数:messageType和messageText。

直接在组件协作器上调用Actions

可以不通过组件内的action,直接在模版中调用某对象的action。例子,在send-message组件中,我们注入一个服务用来处理sendMessage逻辑。

app/components/send-message.jsimport Ember from 'ember';export default Ember.Component.extend({  messaging: Ember.inject.service(),  // component implementation});

我们可以告诉action,让它直接调用messaging服务的sendMessage action,我们要做的就是指定action的target属性,让该属性指向messaging服务对象即可。

app/templates/components/send-message.hbs{{#button-with-confirmation    text="Click to send your message."    onConfirm=(action "sendMessage" "info" target=messaging)    as |confirmValue| }}  {{input value=confirmValue}}{{/button-with-confirmation}}

由于提供了target属性,那么action助手将会直接在target所指向的messaging服务对象上调用它提供的sendMessage action函数。

app/services/messaging.jsimport Ember from 'ember';export default Ember.Service.extend({  actions: {    sendMessage(messageType, text) {      //handle message send and return a promise    }  }});

将对象的一部分作为action的参数

组件通常不知道父组件要处理一个action需要哪些信息,而是将可能的信息都传递过去。例子,我们的user-profile组件会去通知父组件—system-preferences-editor, 用户的账户已经被删除了,并且将完整的user profile对象传过去。

app/components/user-profile.jsimport Ember from 'ember';export default Ember.Component.extend({  login: Ember.inject.service(),  actions: {    userDidDeleteAccount() {      this.get('login').deleteUser();      this.get('didDelete')(this.get('login.currentUserObj'));    }  }});

然而system-preferences-editor组件所需的所有信息,仅仅是被删除的账户的ID。为了应对这种情况, action助手提供了value属性,它使得父组件可以深入到被传入的参数中,仅仅把它需要的值拿出来。

app/templates/components/system-preferences-editor.hbs{{user-profile didDelete=(action "userDeleted" value="account.id")}}

现在,当system-preferences-editor组件处理userDeleted action的时候,它所接收到的仅仅是id,而不会是整个对象了。

app/components/system-preferences-editor.jsimport Ember from 'ember';export default Ember.Component.extend({  actions: {    userDeleted(idStr) {      //respond to deletion    }  }});

穿越多层组件的action调用

当你的组件包含了很多层模版的时候,常常会需要将一个action在多层进行处理。通过action助手,父组件可以将action传递给子组件,并且你不需要在子组件中编写额外的javascript 代码。

例子, 现在我们把删除账户的操作从user-profile组件中挪到它的父组件system-preferences-editor中去。

首先我们先把deleteUser action从user-profile.js中转移到system-preferences-editor.js中。

app/components/system-preferences-editor.jsimport Ember from 'ember';export default Ember.Component.extend({  login: Ember.inject.service(),  actions: {    deleteUser(idStr) {      return this.get('login').deleteUserAccount(idStr);    }  }});

接着,在system-preferences-editor模版中将deleteUser aciton 传递到user-profile组件的deleteCurrentUser属性上。

app/templates/components/system-preferences-editor.hbs{{user-profile  deleteCurrentUser=(action 'deleteUser' login.currentUser.id)}}

注意了,deleteUser被【单引号】包裹,那是因为这个action定义在system-preferences-editor组件中。【单引号】表明这个action应该应该从本地调用,而不是从父组件调用。

对于user-profile.hbs模版,我们将它更改为 直接调用deleteCurrentUser。

app/templates/components/user-profile.hbs{{button-with-confirmation  onConfirm=(action deleteCurrentUser)  text="Click OK to delete your account."}}

注意了,deleteCurrentUser于之前的相反,它没有带引号。引号用来初始化组件的传递树,但是在每个子层级你都通过action助手传递了实际的对函数的引用。

现在,当你确认删除后,action会直接在system-preferences-editor提供的上下文中进行处理。

本章完

原创粉丝点击