Escrevendo TodoMVC no dap. Parte 2

Esta é a segunda parte final do tutorial em que escrevemos o cliente TodoMVC usando a estrutura ds js reativa minimalista.

Resumo da primeira parte : recebemos uma lista de tarefas do servidor no formato JSON, construímos uma lista HTML, adicionamos a capacidade de editar o nome e o sinal de conclusão de cada caso e implementamos uma notificação do servidor sobre essas edições.

Resta realizar: exclusão de casos arbitrários, adição de novos casos, instalação / redefinição em massa e filtragem de casos com base na conclusão e na função de excluir todos os casos concluídos. É isso que faremos. A versão final do cliente, a qual abordaremos neste artigo, pode ser vista aqui .



A opção que escolhemos da última vez pode ser atualizada aqui .

Aqui está o código dele:

'#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() 


Agora, existem apenas cinquenta linhas aqui, mas até o final do artigo, haverá duas vezes mais - até 100. Haverá muitas solicitações HTTP para o servidor; portanto, abra as ferramentas do desenvolvedor (no Chrome, como você se lembra, Ctrl + Shift + I) - haverá Primeiro, a guia Rede é interessante e, em segundo lugar, o console. Além disso, não se esqueça de visualizar o código de cada versão da nossa página - no Chrome, é Ctrl + U.

Aqui eu tenho que fazer uma pequena digressão. Se você não leu a primeira parte do tutorial, ainda recomendo começar com ele. Se você o leu, mas não entendeu nada, é melhor lê-lo novamente. Como mostram os comentários nos meus dois artigos anteriores, a sintaxe e o princípio da dap nem sempre são imediatamente entendidos por um leitor despreparado. Outro artigo não é recomendado para leitura para pessoas que se sentem desconfortáveis ​​com a aparência de sintaxe não semelhante a si.



Esta segunda parte do tutorial será um pouco mais complicada e interessante que a primeira. [TODO: peça ao token para encontrar uma imagem de cérebro explodindo de um aluno na internet] .

Com sua permissão, continuarei a numerar os capítulos com a parte 1. Lá contamos até 7. Então,

8. Fazendo uma lista de tarefas da variável de estado


Para remover um caso da lista, existe um botão BUTTON.destroy . A remoção consiste em enviar uma solicitação DELETE para o servidor e remover de vista o elemento UL#todo-list > LI correspondente UL#todo-list > LI com todo o conteúdo. Com o envio de uma solicitação DELETE, tudo fica claro:

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


Mas com a remoção de um elemento da tela, são possíveis opções. Pode-se simplesmente introduzir outra variável de estado, digamos $deleted , e ocultar o elemento com CSS, incluindo a classe CSS deleted

  ,'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} 


E funcionaria. Mas seria trapaça. Além disso, mais adiante, teremos filtros e contadores de casos ativos e concluídos (o que está em #footer ). Portanto, é melhor remover imediatamente o objeto da lista de tarefas com honestidade, "fisicamente". Ou seja, precisamos da capacidade de modificar a própria matriz, que recebemos inicialmente do servidor - o que significa que essa matriz também deve se tornar uma variável de estado. Vamos chamá-la de $todos .

O escopo da variável $todos é selecionar o ancestral comum de todos os elementos que acessarão essa variável. E será acessado por INPUT#new-todo partir de #header e contadores de #footer e, na verdade, UL#todo-list . O ancestral comum de todos eles é o elemento raiz do modelo, #todoapp . Portanto, em sua regra d, definiremos a variável $todos . No mesmo local, enviamos imediatamente os dados do servidor para ele. E para criar a lista UL#todo-list , agora também faremos dela:

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


É importante. Se durante o teste repentinamente a lista de tarefas a carregar não for possível - é bem possível que alguém tenha excluído todos eles (este é um servidor público e tudo pode acontecer lá).
Nesse caso, vá para um exemplo completo e crie alguns casos para experimentar.

Nós olhamos . Aqui $todos declarado na regra d do elemento #todoapp e é inicializado imediatamente com os dados necessários. Tudo parece funcionar, mas um recurso desagradável apareceu. Se o servidor responder à solicitação por um longo tempo (o Chrome permite simular essa situação: na guia Rede das ferramentas do desenvolvedor, você pode escolher diferentes modos de simular redes lentas), nossa nova versão do aplicativo até que a solicitação seja concluída parece um pouco triste - não há nada além de CSS artefatos. Essa imagem definitivamente não adicionará entusiasmo ao usuário. Embora a versão anterior não sofresse com isso - até os dados serem recebidos na página, apenas a lista estava ausente, mas outros elementos apareceram imediatamente, sem aguardar os dados.

Aqui está a coisa. Como você se lembra, o conversor :query é assíncrono. Essa assincronia é expressa no fato de que, até que a solicitação seja concluída, apenas a execução da regra atual é bloqueada, ou seja, a geração de um elemento que realmente precisa dos dados solicitados (o que é lógico). A geração de outros elementos não está bloqueada. Portanto, quando UL#todo-list acessou o servidor, apenas ele foi bloqueado, mas não o #header e o #footer , que foram desenhados imediatamente. Agora, todo o #todoapp está aguardando a conclusão da solicitação.

9. Carregamento atrasado de dados


Para corrigir a situação e evitar o bloqueio de elementos não envolvidos, adiaremos o carregamento inicial de dados até que tudo já esteja desenhado. Para fazer isso, não carregaremos imediatamente os dados na variável $todos , mas, primeiro, simplesmente os inicializamos com "nothing"

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


Portanto, ela não bloqueará nada e todo o modelo funcionará - embora por enquanto com uma "lista de tarefas" vazia. Mas agora, com uma tela inicial chata, você pode modificar com segurança $todos fazendo o upload de uma lista de tarefas. Para fazer isso, adicione este descendente a #todoapp

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


Este elemento possui uma regra-u que se parece exatamente com a regra de bloqueio que rejeitamos, mas há uma diferença fundamental.
Deixe-me lembrá-lo de que a regra-d (de baixo para baixo ) é a regra de geração de elemento que é executada quando o modelo é criado de cima para baixo , de pai para descendente; e u-rules (de cima para baixo ) são regras de reação que são executadas em resposta a um evento que aparece de baixo para cima , de filho para pai.
Portanto, se algo (incluindo "nada") for atribuído a uma variável na regra d , isso significa sua declaração e inicialização no escopo deste elemento e seus descendentes (escopos aninhados são implementados no dap, como em JS ) A atribuição em up-rules significa uma modificação de uma variável declarada anteriormente no escopo. A declaração e a inicialização de variáveis ​​na regra d permitem que o pai transmita aos descendentes pela hierarquia as informações necessárias para a construção, e a modificação permite que o pai transmita atualizações dessas informações para o topo e, assim, inicie a reestruturação correspondente de todos os elementos que dependem dela.

O elemento loader , descendente de #todoapp , em sua regra-u modifica a variável $todos , carregando dados do servidor para ele, o que causa a regeneração automática de todos os elementos de consumo dessa variável (e somente eles, o que é importante!). Os consumidores de uma variável são elementos cujas regras d contêm essa variável como um rvalor, ou seja, Quem essa variável (levando em consideração o escopo) ao construir.

Agora temos um consumidor da variável $todos - a própria UL#todo-list , que, consequentemente, será reconstruída após o carregamento dos dados.

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


Portanto, agora temos uma lista de tarefas a fazer é uma variável de estado em #todoapp , sem bloquear a renderização inicial do modelo.

10. Excluindo e adicionando tarefas


Agora podemos modificar $todos todas as formas. Vamos começar removendo os itens. Já temos um botão cruzado BUTTON.destroy , que até agora simplesmente envia as solicitações de remoção do servidor

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


É necessário garantir que o objeto correspondente também seja excluído da variável $todos - e como isso será uma modificação, UL#todo-list , como consumidor dessa variável, será reconstruída automaticamente, mas sem o elemento excluído.

Por si só, o dap não fornece nenhum meio especial para manipular dados. As manipulações podem ser perfeitamente escritas em funções em JS, e as regras dap simplesmente fornecem dados para elas e capturam o resultado. Nós escrevemos uma função JS para remover um objeto de uma matriz sem saber seu número. Por exemplo, isto:

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


Você provavelmente pode escrever algo mais eficaz, mas não é sobre isso agora. É improvável que nosso aplicativo precise trabalhar com listas de tarefas de milhões de itens. O importante é que a função retorne um novo objeto de matriz e não apenas remova o elemento do que é.

Para tornar essa função acessível a partir das regras dap, você precisa adicioná-la à seção .FUNC , mas antes disso, decida como queremos chamá-la. A opção mais simples nesse caso é, talvez, chamá-lo do conversor, que aceita o objeto { todos, tgt } e retorna uma matriz filtrada

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


mas não há nada que o impeça de definir essa função dentro do .FUNC (eu já disse que .FUNC é na verdade um método JS comum e seu argumento é um objeto JS comum?)

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


Agora podemos acessar este conversor a partir das regras dap

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


Aqui, primeiro formamos um objeto que, na notação JS, corresponde a { todos, tgt:$ } , passa-o para o conversor :remove descrito em .FUNC e retorna o resultado filtrado para $todos , modificando-o. Aqui $ é o contexto de dados do elemento, esse objeto de negócios da matriz $todos no qual o modelo é construído. Após o símbolo @ , o alias do argumento é indicado. Se @ ausente, o nome do argumento será usado. Isso é semelhante à recente inovação ES6 - abreviação de propriedades .

Da mesma forma, adicionamos um novo caso à lista usando o elemento INPUT#new-todo e a solicitação 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] ) //     } }) 


A regra de reação do elemento INPUT#new-todo a um evento de interface do usuário padrão (para os elementos INPUT , o evento de change é considerado o dap padrão) inclui: ler a entrada do usuário a partir da propriedade value desse elemento, formando um contexto $ local com esse valor como um campo .title , enviando $ context para o servidor usando o método POST, modificando a matriz $todos adicionando o contexto $ como um novo elemento e finalmente limpando a propriedade value do elemento INPUT .

Aqui, um jovem leitor pode perguntar: por que usar concat() ao adicionar um elemento a uma matriz, se isso pode ser feito usando push() regular push() ? Um leitor experiente entenderá imediatamente qual é o problema e escreverá sua resposta nos comentários.

Nós olhamos o que aconteceu: os casos são adicionados e excluídos normalmente, as solicitações correspondentes são enviadas ao servidor corretamente (você mantém a guia Rede aberta o tempo todo, certo?). Mas e se quisermos mudar o nome ou o status de um caso recém-adicionado? O problema é que, para notificar o servidor sobre essas alterações, precisamos de .url , que atribui o servidor a esse negócio. Quando criamos o negócio, não conhecíamos seu .url , respectivamente, não podemos formar a solicitação PATCH correta para alteração.

De fato, todas as informações necessárias sobre o caso estão contidas na resposta do servidor à solicitação POST, e seria mais correto criar um novo objeto de negócios não apenas a partir da entrada do usuário, mas da resposta do servidor, e adicionar esse objeto a $todos - com todas as informações fornecidas. informações do servidor, incluindo o campo .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=") 


Olhamos - tudo bem, agora tudo está sendo resolvido corretamente. As notificações ao servidor sobre a edição de casos recém-criados dão certo.

Pode-se parar com isso, mas ... Mas se você olhar atentamente, ainda poderá notar um pequeno atraso entre inserir o nome do novo caso e o momento em que ele aparecer na lista. Esse atraso é claramente visível se você ativar uma rede lenta simulada. Como você deve ter adivinhado, a questão é uma solicitação ao servidor: primeiro, solicitamos dados para um novo caso do servidor e somente após recebê-los, modificamos $todos . O próximo passo, tentaremos corrigir essa situação, mas primeiro voltarei sua atenção para outro ponto interessante. Se voltarmos um pouco para a versão anterior , observamos: embora a solicitação também esteja lá, o novo caso é adicionado à lista instantaneamente, sem esperar que a solicitação termine.

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


Esse é outro recurso da elaboração de conversores assíncronos no dap: se o resultado do conversor assíncrono não for usado (ou seja, não estiver atribuído a nada), você não poderá esperar pela conclusão - e a execução da regra não será bloqueada. Isso geralmente é útil: você pode ter notado que, ao excluir casos da lista, eles desaparecem da tela instantaneamente, sem aguardar o resultado da solicitação DELETE. Isso é especialmente perceptível se você excluir rapidamente vários casos seguidos e rastrear solicitações no painel Rede.

Mas, como usamos o resultado da solicitação POST - atribua-a ao contexto $ - precisamos esperar que ela seja concluída. Portanto, você precisa encontrar outra maneira de modificar $todos antes de executar a solicitação POST. Solução: ainda, primeiro crie um novo objeto de negócios e adicione-o imediatamente a $todos , deixe a lista ser .url e somente depois, após a renderização, se o negócio não tiver .url (ou seja, o negócio acabou de ser criado), execute uma solicitação POST e impor seu resultado no contexto de dados deste caso.

Então, primeiro basta adicionar um espaço em branco contendo apenas .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 regra de geração de elemento UL#todo-list > LI já contém a! iniciando uma regra após o primeiro elemento ser desenhado. Lá, podemos adicionar o lançamento de uma solicitação POST na ausência de .url . Para injetar campos adicionais no contexto, dap possui o operador &

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


Nós olhamos . Outra coisa! Mesmo com uma rede lenta, a lista de tarefas é atualizada instantaneamente e a notificação do servidor e o carregamento de dados ausentes ocorrem em segundo plano, após o desenho da lista atualizada.

11. Daw todos!


No elemento #header , há um botão para instalação em massa / redefinição do sinal de conclusão para todos os casos na lista. Para a atribuição em massa de valores aos campos dos elementos da matriz, basta escrever outro conversor :assign , e aplicá-lo a $todos , clicando em INPUT#toggle-all

  ,'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)) 


Nesse caso, estamos interessados ​​apenas no campo .completed , mas é fácil ver que, com esse conversor, você pode alterar massivamente os valores de qualquer campo dos elementos da matriz.
Ok, na matriz $todos as $todos trocadas, agora precisamos notificar o servidor sobre as alterações feitas. No exemplo original, isso é feito enviando solicitações PATCH para cada caso - não uma estratégia muito eficaz, mas não depende mais de nós. Ok, para cada caso, enviamos uma solicitação PATCH

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


Observamos : Um clique em um daw comum alinha todos os daws individuais e o servidor é notificado por solicitações de PATCH apropriadas. Norma

12. Filtrando casos com base na conclusão


Além da lista de tarefas pendentes, o aplicativo também deve poder filtrar casos pelo sinal de conclusão e mostrar contadores de tarefas concluídas e inacabadas. Obviamente, para filtrar, seremos triviais usar o mesmo método filter() fornecido pelo próprio JS.

Mas primeiro você precisa garantir que o campo .completed de cada caso seja sempre verdadeiro e, quando você clicar, o daw individual do caso será atualizado junto com a variável $completed . Anteriormente, isso não era importante para nós, mas agora será.

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


O ponto importante aqui é que o contexto de dados de cada caso é o próprio objeto de caso, que fica na matriz $todos . Não é uma cópia única ou construção relacionada, mas o próprio objeto. E todas as chamadas para os campos .title , .completed , .completed url - leitura e gravação - se aplicam diretamente a esse objeto. Portanto, para filtrar a matriz $todos para funcionar corretamente, precisamos garantir que a conclusão do caso seja refletida não apenas na madrugada na tela, mas também no campo .completed do objeto de .completed .

Para mostrar apenas os casos com o sinal necessário de completude .completed na lista, filtraremos simplesmente $todos acordo com o filtro selecionado. O filtro selecionado é, você adivinhou, outra variável de estado de nosso aplicativo, e nós o chamaremos: $filter . Para filtrar $todos acordo com o $filter selecionado $filter vamos seguir a miniatura e apenas adicionar outro conversor, do formulário {list, filter} => a lista filtrada , e pegaremos os nomes e as funções de filtragem da "matriz associativa" (ou seja, JS comum objeto) 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; } } }) 


Nós verificamos . Os filtros funcionam corretamente. Há uma nuance em que os nomes dos filtros são exibidos juntos, porque aqui nos afastamos um pouco da estrutura DOM do original e saímos do CSS. Mas voltaremos a isso um pouco mais tarde.

13. Contadores de casos concluídos e ativos.


Para mostrar os contadores de casos concluídos e ativos, basta filtrar $todos com os filtros apropriados e mostrar os comprimentos das matrizes resultantes

  ,'#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})" }) 


Nesse formulário, os contadores mostram os valores corretos no momento da inicialização, mas não respondem a alterações subseqüentes na conclusão dos assuntos (ao clicar nas daws). O fato é que cliques em gralhas, alterando o estado de cada caso individual, não alteram o estado de $todos - uma modificação de um elemento da matriz não é uma modificação da própria matriz. Portanto, precisamos de um sinal adicional sobre a necessidade de registrar novamente os casos. Esse sinal pode ser uma variável de estado adicional, que é modificada toda vez que é necessária a recontagem. Chame de $recount .Declararemos um ancestral comum na regra d, atualizaremos quando clicarmos no daws e #footertornaremos o elemento um consumidor - para isso, basta mencionar essa variável em sua regra 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 


Agora tudo funciona como deveria, os contadores são atualizados corretamente.

14. Exclusão de todos os casos concluídos.


A exclusão em lote de casos no TodoMVC é implementada como não kosher como modificação em lote - por várias solicitações. Bem, vamos dar um suspiro, encolher os ombros e executar por solicitação DELETE para cada caso concluído - e já temos todos eles $completed. Assim, $todosapós a remoção dos casos concluídos, o que já deveria estar em$active

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


Olhamos : criamos alguns assuntos desnecessários, marcamos com daws e excluímos. A guia Rede mostrará o horror dessa abordagem para operações em lote.

15. Status na barra de endereço


Voltar para a seleção de filtros. No exemplo original, o filtro selecionado é refletido na barra de endereço após #. Ao alterar o fragmento # na barra de endereços manualmente ou durante a navegação, o filtro selecionado também muda. Isso permite que você vá para a página do aplicativo por URL com o filtro de tarefas já selecionado.

Você pode escrever com um location.hashoperador urlhash, por exemplo, na regra de um elemento #todoapp(ou qualquer de seus descendentes), que será executado a cada atualização$filter

 .a("urlhash $filter") 


E você pode inicializar a variável com um $filtervalor da barra de endereço e depois atualizar pelo evento hashchange usando um pseudo-conversor :urlhashque retorna o estado atual location.hash(sem #)

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


O evento hashchange é gerado pelo navegador quando um fragmento # na barra de endereço é alterado. No entanto, por alguma razão única window, e document.bodypode ouvir este evento. Para rastrear esse evento a partir de um elemento #todoapp, você precisará adicionar um operador à regra d listenque assina o elemento para retransmitir eventos do objetowindow

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


Observamos : alternar filtros, rastrear alterações na barra de endereço, seguir os links com #Active , #All , #Completed . Tudo funciona. Mas voltando ao original. Parece que a escolha do filtro é implementada - clicando nos links. Embora não seja muito prático, faremos o mesmo por uma questão de integridade.

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


E para destacar o filtro selecionado, adicione um operador de estilização condicional !?que adicionará uma classe CSS ao elemento selectedse o valor no campo de .filtercontexto for igual ao valor da variável$filter

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


Em deste modo a funcionalidade dos nossos Dap-aplicações já estão completamente (tanto quanto eu posso dizer) corresponde ao que faz o original .

16. Alguns retoques finais


Não gosto muito disso no original, a forma do cursor não muda sobre os elementos ativos, por isso, adicionaremos headesse estilo ao nosso documento HTML

  [ui=click]{cursor:pointer} 


Então, pelo menos, veremos onde você pode clicar.

Ah sim! Resta escrever a palavra "todos" em letras maiúsculas. Mas aqui talvez eu me permita finalmente mostrar um pouco de imaginação e criatividade e, em vez de apenas "todos", escreverei "dap todos"

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


Uau. Agora, nosso aplicativo pode ser considerado completo e o tutorial realizado (se você ler honestamente essas linhas).

Em conclusão


Talvez, durante a leitura, você tenha tido a impressão de que o programa dap é escrito por tentativa e erro - tudo isso é "vamos ver o que aconteceu", "parece funcionar, mas há uma nuance", etc. Este não é realmente o caso.Todas essas nuances são bastante óbvias e previsíveis ao escrever código. Mas pensei que seria útil mostrar pelo exemplo dessas nuances por que essa ou aquela decisão está presente nas regras e por que é feita dessa maneira e não de outra maneira.

Faça, como eles dizem, perguntas.

Source: https://habr.com/ru/post/pt481680/


All Articles