在dap上编写TodoMVC。 第二部分

这是本教程的第二部分,也是最后一部分,在该部分中,我们使用简约的反应式ds js框架编写TodoMVC客户端。

第一部分的摘要:我们从服务器收到了JSON格式的待办事项列表,并从中建立了HTML列表,添加了针对每种情况编辑名称和完成标志的功能,并实现了有关这些编辑的服务器通知。

它仍然有待实现:删除任意案件,添加新案件,根据完成情况大规模安装/重置和过滤案件以及删除所有已完成案件的功能。 这就是我们要做的。 在本文中,我们将介绍客户端的最终版本。



我们上一次确定的选择权可以在这里得到解决

这是他的代码:

'#todoapp'.d("" ,'#header'.d("" ,'H1'.d("") ,'INPUT#new-todo placeholder="What needs to be done?" autofocus'.d("") ) ,'#main'.d("" ,'#toggle-all type=checkbox'.d("") ,'UL#todo-list'.d("*@ todos:query" ,'LI'.d("$completed=.completed $editing= $patch=; a!" ,'INPUT.toggle type=checkbox' .d("#.checked=.completed") .ui("$patch=($completed=#.checked)") ,'LABEL.view' .d("? $editing:!; ! .title") .e("dblclick","$editing=`yes") ,'INPUT.edit' .d("? $editing; !! .title@value") .ui("$patch=(.title=#.value)") .e("blur","$editing=") ,'BUTTON.destroy'.d("") ) .a("!? $completed $editing") .u("? $patch; (@method`PATCH .url:dehttp headers $patch@):query $patch=") ) ) ,'#footer'.d("" ,'#todo-count'.d("") ,'UL#filters'.d("" ,'LI'.d("") ) ,'#clear-completed'.d("") ) ) .DICT({ todos : "//todo-backend-express.herokuapp.com/", headers: {"Content-type":"application/json"} }) .FUNC({ convert:{ dehttp: url=>url.replace(/^https?\:/,'') } }) .RENDER() 


现在这里只有五十行,但是到本文结尾,将有两倍-多达100行。到服务器的HTTP请求将很多,所以请打开开发人员工具(在Chrome中,您记得Ctrl + Shift + I)-首先,“网络”选项卡很有趣,其次是“控制台”。 另外,请不要忘记查看每个页面版本的代码-在Chrome中为Ctrl +U。

在这里,我必须做些题外话。 如果您没有阅读本教程的第一部分 ,我仍然建议您从头开始。 如果您阅读了它,但什么都不懂,最好再读一遍。 正如我对前两篇文章的评论所显示的那样,准备不足的读者并不总是会立即理解dap的语法和原理。 不建议另一篇文章阅读给那些对si语法不满意的人。



本教程的第二部分将比第一部分更加复杂和有趣。 [待办事项:让令牌找到一个在互联网上爆炸大脑图片的男生]

在您的允许下,我将继续在第1部分中为各章编号。 在那里,我们数到了7。

8.制作状态变量的待办事项列表


要从列表中删除案例,有一个按钮BUTTON.destroy 。 删除包括向服务器发送DELETE请求,并实际上删除所有内容的相应UL#todo-list > LI元素UL#todo-list > LI 。 发送DELETE请求后,一切都变得清晰了:

  ,'BUTTON.destroy'.ui("(@method`DELETE .url:dehttp):query") 


但是,通过从屏幕上删除元素,可以进行选择。 可以简单地引入另一个状态变量,例如$deleted ,并使用CSS隐藏该元素,包括deleted CSS类。

  ,'LI'.d("$completed=.completed $editing= $patch= $deleted=; a!" //  $deleted   "" ... ,'BUTTON.destroy'.d("(@method`DELETE .url:dehttp):query $deleted=`yes") //  $deleted -     ) .a("!? $completed $editing $deleted") //   CSS  .deleted{display:none} 


它会工作。 但这会作弊。 此外,在此行的更下方,我们将具有活动和已完成案例的过滤器和计数器( #footer )。 因此,最好诚实地,“物理地”立即从待办事项列表中删除对象。 也就是说,我们需要具有修改数组本身的能力,这是我们最初从服务器收到的,这意味着该数组也必须成为状态变量。 我们称她$todos

$todos变量的范围是选择将访问此变量的所有元素的公共祖先。 它将由#headerINPUT#new-todo#footer计数器以及实际上是UL#todo-list 。 它们全部的共同祖先是模板的根元素#todoapp 。 因此,在其d规则中,我们将定义变量$todos 。 在同一位置,我们立即将数据从服务器上传到该服务器。 为了建立UL#todo-list ,我们现在也可以从中得到:

 '#todoapp'.d("$todos=todos:query" //   $todos      ... ,'UL#todo-list'.d("*@ $todos" //     $todos 


这很重要。 如果在测试过程中突然未加载待办事项列表,则很可能有人将它们全部删除了(这是一台公共服务器,任何事情都可能在那发生)。
在这种情况下,请转到功能完整的示例并创建一些案例进行试验。

我们看 。 在这里, $todos#todoapp元素的d规则中声明 ,并立即必要的数据初始化 。 一切似乎正常,但是出现了一个令人不快的功能。 如果服务器长时间响应请求(Chrome可以让您模拟这种情况:在开发人员工具的“网络”标签上,您可以选择模拟慢速网络的不同模式),那么直到请求完成之前,我们的新版本应用程序看起来都有些令人遗憾-除了CSS文物。 这样的图片绝对不会给用户增加热情。 尽管以前的版本没有受到此影响-在页面上接收到数据之前,只缺少列表本身,但是其他元素立即出现,而无需等待数据。

就是这个 您还记得:query转换器是异步的。 这种异步性表现为以下事实:在请求完成之前,只有当前规则的执行被阻止,也就是说,实际上需要请求的数据(逻辑上)的元素的生成。 其他元素的生成不会被阻止。 因此,当UL#todo-list访问服务器时,只有服务器被阻止,而#header#footer却没有被阻止,它们是立即绘制的。 现在,整个#todoapp正在等待请求完成。

9.延迟的数据加载


为了纠正这种情况并避免阻塞无关的元素,我们将推迟数据的初始加载,直到所有内容都绘制完成为止。 为此,我们不会立即将数据加载到$todos变量中,而是首先简单地使用“ nothing”将其初始化

 '#todoapp'.d("$todos=" //   $todos    "" 


因此,她不会阻止任何操作,整个模板都可以使用-尽管目前只有一个空的“待办事项列表”。 但是现在,在无聊的初始屏幕上,您可以通过将待办事项列表上载到$todos来安全地对其进行修改 。 为此,将此后代添加到#todoapp

  ,'loader' .u("$todos=todos:query") //  $todos,       .d("u") //   (u-)    


该元素的U规则看起来与我们拒绝的阻止规则完全相同,但是存在一个根本区别。
让我提醒您,d规则(从down )是在从上到下 ,从父级到后代构建模板时执行的元素生成规则。 和u-rules(来自up )是响应规则,用于响应从下到 ,从子级到父级弹出的事件。
因此,如果在d-rule中将某物(包括“ nothing”)分配变量,则意味着在该元素及其后代的范围内对其进行了声明和初始化 (嵌套范围在dap中实现,如JS中一样) ) 上规则中的分配表示对作用域中先前声明变量的修改 。 d规则中变量的声明和初始化允许父级将构造所需的信息传递给层次结构的后代,而修改则允许父级将对此信息的更新传递给该父级,从而对依赖于此信息的所有元素进行适当的重组。

loader元素是#todoapp的后代,在其u规则中会修改 $todos变量,将服务器中的数据加载到其中,这将导致该变量的所有使用者元素(仅它们,这很重要!)的自动再生。 变量的使用者是其d规则包含此变量作为右值的元素,即 那些在构建时读取此变量(考虑范围)的人。

现在,我们有一个$todos变量的使用者-非常UL#todo-list ,因此,它将在加载数据后重新构建。

  ,'UL#todo-list'.d("*@ $todos" //  ,   $todos 


因此, 现在我们有一个待办事项列表是#todoapp的状态变量,而不会阻止模板的初始呈现。

10.删除和添加待办事项


现在我们可以$todos各种方式修改$todos 。 让我们从删除项目开始。 我们已经有一个跨BUTTON.destroy的按钮,到目前为止,它只是发送服务器删除请求

  ,'BUTTON.destroy'.ui("(@method`DELETE .url:dehttp):query") 


必须确保也从$todos变量中删除了相应的对象-由于这将是一个修改,因此作为该变量的使用者的UL#todo-list将被自动重建,但没有删除的元素。

dap本身不提供任何特殊的数据处理方法。 操纵可以完美地用JS函数编写,并且dap规则只是向它们传递数据并获取结果。 我们编写了一个JS函数,以在不知道对象编号的情况下从数组中删除对象。 例如,这:

 const remove = (arr,tgt)=> arr.filter( obj => obj!=tgt ); 


您可能可以写出更有效的内容,但现在还不行。 我们的应用程序不太可能必须处理数百万个项目的待办事项列表。 重要的是该函数返回一个新的数组对象,而不仅仅是从元素中删除该元素。

为了使该功能可从dap规则访问,您需要将其添加到.FUNC部分,但在此之前确定我们要如何调用它。 在这种情况下,最简单的选择是从转换器中调用它,该转换器接受{ todos, tgt }对象并返回过滤后的数组

 .FUNC({ convert:{ dehttp: url => url.replace(/^https?\:/,''), //        remove: x => remove(x.todos,x.tgt) //     } }) 


但是没有什么阻止您直接在.FUNC内部定义此函数的(我已经说过.FUNC实际上是普通的JS方法,并且其参数是普通的JS对象?)

 .FUNC({ convert:{ dehttp: url => url.replace(/^https?\:/,''), remove: x => x.todos.filter( todo => todo!=x.tgt ) } }) 


现在我们可以从dap规则访问此转换器

  ,'BUTTON.destroy' .ui("$todos=($todos $@tgt):remove (@method`DELETE .url:dehttp):query") 


在这里,我们首先形成一个对象,该对象以JS表示法匹配{ todos, tgt:$ } ,并将其传递给.FUNC描述的:remove转换器,然后将过滤后的结果返回给$todos ,从而对其进行修改。 $是元素的数据上下文,该元素是构建模板的$todos数组中的业务对象。 @符号后,指示参数的别名。 如果不存在@ ,则使用参数自己的名称。 这类似于最近的ES6创新- 属性速记

同样,我们使用INPUT#new-todo元素和POST请求将新案例添加到列表中

  ,'INPUT#new-todo placeholder="What needs to be done?" autofocus' .ui("$=(#.value@title) (@method`POST todos@url headers $):query $todos=($todos $@tgt):insert #.value=") ... .FUNC({ convert:{ dehttp: url => url.replace(/^https?\:/,''), remove: x => x.todos.filter( todo => todo!=x.tgt ), //     insert: x => x.todos.concat( [x.tgt] ) //     } }) 


INPUT#new-todo元素对标准UI事件的反应规则(对于INPUT元素,将change事件视为标准dap)包括:从此元素的value属性读取用户输入,以该值作为.title字段形成本地$上下文,发送$上下文使用POST方法连接到服务器,通过添加上下文$作为新元素来修改$todos数组,最后清除INPUT元素的value属性。

在这里,一个年轻的读者可能会问:如果可以使用常规的push()完成操作,为什么在向数组添加元素时使用concat() push() ? 经验丰富的读者会立即了解问题所在,并在评论中写下答案。

我们看看发生了什么,正常添加和删除案例,相应的请求已正确发送到服务器(您一直保持“网络”选项卡处于打开状态,对吗?)。 但是,如果我们想更改新添加的案例的名称或状态该怎么办? 问题是要通知服务器这些更改,我们需要.url ,它将服务器分配给该业务。 创建业务时, .url分别不知道其.url ,因此无法形成正确的PATCH更改请求。

实际上,有关该案例的所有必要信息都包含在服务器对POST请求的响应中,不仅从用户输入而是从服务器的响应中创建一个新的业务对象,然后将此对象添加到$todos (提供了所有内容),这将是更正确的选择服务器信息,包括.url字段

  ,'INPUT#new-todo placeholder="What needs to be done?" autofocus' .ui("$todos=($todos (@method`POST todos@url headers (#.value@title)):query@tgt ):insert #.value=") 


我们看起来-好的,现在一切都在正确进行中。 向服务器发送有关编辑新创建案例的通知很正确。

可能有人会停下来,但是...但是,如果仔细观察,您仍然会注意到在输入新案件的名称与它出现在列表之间的时间之间存在一点延迟。 如果您打开模拟慢速网络,则该延迟清晰可见。 您可能已经猜到了,这是对服务器的请求:首先,我们从服务器请求一个新案例的数据,只有在接收到它们之后,我们才修改$todos 。 下一步,我们将尝试纠正这种情况,但是首先,我将把您的注意力转移到另一个有趣的地方。 如果我们稍微回溯到以前的版本 ,请注意:尽管请求也存在,但新案例会立即添加到列表中,而无需等待请求结束

  //    , :query   .ui("$=(#.value@title) (@method`POST todos@url headers $):query $todos=($todos $@tgt):insert #.value=") 


这是在dap中计算异步转换器的另一个功能:如果不使用异步转换器的结果(即,它没有分配给任何东西),那么您将无法等待其完成-规则不会被阻止。 这通常很有用:您可能已经注意到,从列表中删除案例时,它们会立即从屏幕上消失,而无需等待DELETE请求的结果。 如果您连续快速删除多个案例并在“网络”面板中跟踪请求,则这一点尤其明显。

但是,由于我们使用POST请求的结果-将其分配给$上下文-我们必须等待它完成。 因此,您需要在执行POST请求之前找到另一种修改$todos方法。 解决方案:仍然,首先创建一个新的业务对象,然后立即将其添加到$todos ,让列表绘制,直到渲染之后,如果该业务.url (即刚刚创建的业务),则执行POST请求,然后将其结果强加给这种情况的数据上下文。

因此,首先我们将一个仅包含.title的空白添加到.title

  ,'INPUT#new-todo placeholder="What needs to be done?" autofocus' .ui("$todos=($todos (#.value@title)@tgt):insert #.value=") 


UL#todo-list > LI元素生成规则已包含a! 在第一次绘制元素后开始一个规则。 在那里,我们可以在没有.url的情况下添加POST请求的启动。 要将其他字段注入上下文,dap具有&运算符

  .a("!? $completed $editing; ? .url:!; & (@method`POST todos@url headers $):query") 


我们看 。 另一件事! 即使网络速度较慢,待办事项列表也会立即更新,并且在绘制更新后的列表后,服务器通知和丢失数据的加载会在后台进行。

11.大家好!


#header元素中,有一个按钮用于批量安装/重置列表中所有情况的完成标志。 为了将值大规模分配给数组元素的字段,我们只需编写另一个转换器:assign $todos ,然后通过单击INPUT#toggle-all将其应用于$todos

  ,'INPUT#toggle-all type=checkbox' .ui("$todos=($todos (#.checked@completed)@src):assign") ... assign: x => x.todos && x.todos.map(todo => Object.assign(todo,x.src)) 


在这种情况下,我们只对.completed字段感兴趣,但是很容易看到,使用这种转换器,您可以大量更改数组元素的任何字段的值。
好的,在$todos数组中, $todos切换,现在我们需要将所做的更改通知服务器。 在原始示例中,这是通过针对每种情况发送PATCH请求来完成的-这不是一种非常有效的策略,但它不再取决于我们。 好的,对于每种情况,我们都会发送PATCH请求

  .ui("*@ $todos=($todos (#.checked@completed)@src):assign; (@method`PATCH .url:dehttp headers (.completed)):query") 


我们看一下 :单击一个普通的daw会使所有单个daw对齐,并且服务器会通过适当的PATCH请求得到通知。 规范

12.根据完成情况过滤案例


除了实际的待办事项清单,应用程序还应该能够按完成标志过滤案例,并显示已完成和未完成任务的计数器。 当然,对于过滤,我们将使用JS本身提供的相同filter()方法非常简单。

但是首先,您需要确保每个案例的.completed字段始终为true,并且当您单击案例的单个行时,将连同$completed变量一起更新。 以前,这对我们而言并不重要,但现在已经变得重要。

  ,'INPUT.toggle type=checkbox' .d("#.checked=.completed") .ui("$patch=(.completed=$completed=#.checked) $recount=()") //  .completed        


这里的重点是每个案例的数据上下文都是案例对象本身,位于$todos数组中。 不是单个副本或相关构造,而是对象本身。 并且所有对.title.title .completed字段的调用.completed url (包括读写)直接应用于此对象。 因此,为了使$todos数组过滤正常工作,我们需要确保不仅在屏幕上的daw中,而且还在.completed对象的.completed字段中反映.completed.completed

为了仅显示列表中具有必要完整性标志的情况,我们将根据所选过滤器简单过滤$todos 。 您猜到,所选过滤器是应用程序的另一个状态变量,我们将其称为: $filter 。 要根据选定的$filter器过滤$todos $filter让我们$todos缩略图并添加另一个转换器,形式为{list,filter} => filtered list ,我们将从“关联数组”(即普通JS)中获取名称和过滤功能。对象) todoFilters

 const todoFilters={ "All": null, "Active": todo => !todo.completed, "Completed": todo => !!todo.completed }; '#todoapp'.d("$todos= $filter=" //   $filter ... ,'UL#todo-list'.d("* ($todos $filter):filter" ... ,'UL#filters'.d("* filter" //  filter      .DICT ,'LI' .d("! .filter") .ui("$filter=.") //    "$filter=.filter" ) ... .DICT({ ... filter: Object.keys(todoFilters) //["All","Active","Completed"] }) .FUNC({ convert:{ ... filter: x =>{ const a = x.todos, f = x.filter && todoFilters[x.filter]; return a&&f ? a.filter(f) : a; } } }) 


我们检查 。 过滤器正常工作。 筛选器的名称显示在一起是有细微差别的,因为 在这里,我们从原始的DOM结构中稍微走了一步,然后脱离了CSS。 但是,稍后我们将返回到此。

13.完成和进行中案件的柜台。


要显示完成案例和活动案例的计数器,只需使用适当的过滤器过滤$todos并显示结果数组的长度

  ,'#footer'.d("$active=($todos @filter`Active):filter $completed=($todos @filter`Completed):filter" ,'#todo-count'.d("! (active $active.length)format") //  length    active ... ,'#clear-completed'.d("! (completed $completed.length)format") ) ... .DICT({ ... active: "{length} items left", completed: "Clear completed items ({length})" }) 


这种形式下,计数器会在启动时显示正确的值,但不会响应事务完成时的后续更改(单击DAWS时)。 事实是,点击寒鸦,改变每种情况的状态,不会改变$todos的状态-数组元素的修改不是数组本身的修改。 因此,我们需要有关需要重新注册案件的其他信号。 这样的信号可以是一个附加的状态变量,每次需要重新计数时都会对其进行修改。 称之为$recount我们将在d规则中声明一个共同祖先,当单击daws时将对其进行更新,并将#footer元素设为消费者-为此,请在其d规则中提及此变量

 '#todoapp'.d("$todos= $filter= $recount=" //  $recount     ... ,'INPUT.toggle type=checkbox' .d("#.checked=.completed") .ui("$patch=(.completed=$completed=#.checked) $recount=()") //  $recount    ... ,'#footer'.d("$active=($todos @filter`Active):filter $completed=($todos @filter`Completed):filter $recount" //  $recount 


现在一切正常,计数器已正确更新。

14.删除所有已完成的案件。


TodoMVC中案例的批量删除作为非原始的批量修改实现-通过多个请求。好吧,让我们感叹一下,耸耸肩,然后按DELETE请求执行每个完成的案例-现在我们已经全部拥有了$completed因此,$todos在删除完案后,应该已经$active

  ,'#clear-completed' .d("! (completed $completed.length)format") .ui("$todos=$active; *@ $completed; (@method`DELETE .url:dehttp):query") 


我们看:我们创建了一些不必要的事务,用daws标记它们并删除。“网络”选项卡将显示这种对批处理方法的恐惧。

15.地址栏中的状态


返回筛选器的选择。在原始示例中,选定的过滤器将显示在#后面的地址栏中。手动或在导航过程中更改地址栏中的#片段时,所选过滤器也会更改。这使您可以使用已选择的待办事项过滤器通过URL转到应用程序页面。例如,

您可以在一个元素(或其任何后代)的a-rule 中使用location.hash运算符urlhash进行写入,该运算符#todoapp将在每次更新时执行$filter

 .a("urlhash $filter") 


然后,您可以使用$filter地址栏中初始化变量,然后使用返回当前状态(不带#)的伪转换器通过hashchange事件进行更新。:urlhashlocation.hash

 .d("$todos= $filter=:urlhash $recount=" .e("hashchange","$filter=:urlhash") 


当地址栏中的#片段发生更改时,浏览器引发hashchange事件然而,由于某种原因,只windowdocument.body能听此事件。要从某个元素跟踪此事件#todoapp,您必须在其d规则listen添加一个运算符,以对该元素进行签名以中继来自该对象的事件window

 '#todoapp' .a("urlhash $filter") .e("hashchange","$filter=:urlhash") .d("$todos= $filter=:urlhash $recount=; listen @hashchange" 


我们看一下:切换过滤器,跟踪地址栏中的更改,通过#Active#All#Completed链接一切正常。但是回到原来。似乎在这里实现了过滤器的选择-通过单击链接。尽管不是很实用,但是为了完整起见,我们将做同样的事情。

  ,'UL#filters'.d("* filter" ,'LI'.d("" ,'A'.d("!! (`# .filter)concat@href .filter@") ) ) 


为了使选定的过滤器脱颖而出,请添加条件样式化运算符!?selected如果.filter上下文字段中的值等于变量的值,则该操作符将向元素添加CSS类$filter

  ,'A'.d("!! (`# .filter)concat@href .filter@; !? (.filter $filter)eq@selected") 


按照这种形式,我们的dap应用程序功能已经(据我所知)与原始功能完全一致

16.一些画龙点睛


我真的不喜欢原始形状的光标形状不会更改活动元素,因此我们将head这种样式添加到HTML文档中

  [ui=click]{cursor:pointer} 


因此,至少我们会看到您可以单击的位置。

哦,是的!仍然需要用大写字母写“待办事项”。但是在这里,也许我会允许自己最终展现一点想象力和创造力,而我不仅会写“待办事项”,还会写“ dap待办事项”

  ,'H1'.d("","dap todos") 


现在我们的应用程序可以被认为是完整的,并且可以举行本教程(如果您老实地读了这些内容)。

总结


也许,在阅读时,您会感觉到dap程序是通过反复试验编写的-它们全都是“让我们看看发生了什么”,“它似乎有用,但有细微差别”等。 实际上并非如此。在编写代码时,所有这些细微差别都非常明显且可预测。但是我认为通过这些细微差别的例子来说明为什么这个或那个决定出现在规则中,为什么这样做是这样,而不是其他方式,这将是有用的。

如他们所说,问问题。

Source: https://habr.com/ru/post/zh-CN481680/


All Articles