Novembro de 2018

Volume 33 – Número 12

Atualização - Componentes Personalizados do Blazor

De Dino Esposito

Dino EspositoA experiência de programação do Blazor leva naturalmente a um uso extensivo de componentes. No Blazor, um componente é uma classe .NET que implementa uma lógica de renderização de interface do usuário com base em algum estado. Os componentes do Blazor estão próximos da ideia dos componentes da Web a partir da próxima especificação W3C e implementações análogas em estruturas de aplicativo de página única (SPA). Um componente do Blazor é uma combinação de HTML, C# e código interoperável do JavaScript e CSS, que juntos atuam como um único elemento com uma interface de programação comum. Um componente do Blazor pode disparar eventos e expor as propriedades em grande parte da mesma maneira que faz um elemento do DOM do HTML.

No meu artigo recente sobre o Blazor, apresentei alguns componentes dedicados. O primeiro forneceu uma interface do usuário abrangente para digitar algum texto, executar uma consulta e acionar um evento externo com os dados recuperados como um argumento. O segundo componente no exemplo atuou como uma grade não personalizável feita sob medida, que manipularia o evento, pegaria os dados e preencheria a tabela interna. O campo de entrada que coleta o texto da consulta ofereceu preenchimento automático por meio da extensão de preenchimento automático de Bootstrap. Neste artigo, posso criar a partir desse ponto e discutir o design e a implementação de um componente totalmente baseado em Blazor para a funcionalidade de preenchimento automático. No final da tarefa, você terá um arquivo CSHTML reutilizável sem dependências, exceto o pacote principal do Bootstrap 4.

A Interface Pública de Preenchimento Automático

A meta é produzir um componente nativo Blazor que se comporta como uma versão estendida do componente clássico typeahead.js (confira twitter.github.io/typeahead.js). O componente final se encontra no meio de uma caixa de texto e de uma lista suspensa. Conforme os usuários digitam no campo de entrada de texto, o componente consulta uma URL remota e obtém as dicas de valores possíveis para inserir. O preenchimento automático do JavaScript fornece sugestões, mas não força os usuários a aceitá-las. Em outras palavras, o texto personalizado, diferente das sugestões, é ainda aceitável.

No entanto, em alguns casos, você pode querer que os usuários inseriram o texto e, em seguida, selecionem a partir de uma lista de entradas válidas. Imagine, por exemplo, em um campo de entrada onde precisa fornecer um nome de país ou um nome de cliente. Há mais de 200 centenas de países no mundo e muitas vezes centenas (ou mais) de clientes em um sistema de software. Você realmente deve usar uma lista suspensa? Um componente de preenchimento automático mais inteligente pode permitir que os usuários digitem, digamos, a palavra "Unidos" e, em seguida, apresentar uma lista de correspondência de países, como Emirados Árabes Unidos, Reino Unido e Estados Unidos. Se qualquer texto livre é digitado, o código automaticamente limpa o buffer.

Outro problema relacionado: Quando o texto livre não é uma opção, você provavelmente precisará também ter um código. O ideal é que você digite o nome do país ou do cliente e tenha publicado no formulário de hospedagem o ID do país ou o identificador exclusivo do cliente. Em HTML e JavaScript puro, você precisa de um script extra que adicione um campo oculto e gerencie as seleções do plug-in de preenchimento automático. Um componente nativo do Blazor terá todos esses recursos ocultos. Vamos ver como escrever esse componente.

Design do Componente

O componente Typeahead deve ser um campo de entrada adicional para uso dentro de um formulário HTML. Ele é feito de um par de campos de entrada padrões, um do tipo texto e outro oculto. O campo de entrada oculto apresentará um atributo NAME que irá torná-lo totalmente interoperável se usado em um formulário. Veja uma amostra de marcação para usar o novo componente:

<typeahead style="margin-top: 40px;"
           class="form-control"
           url="/hint/countries1"
           selectionOnly="true"
           name="country"
           placeholder="Type something"
           onSelectionMade="@ShowSelection" />

Como você pode ver, o componente espelha alguns atributos específicos ao HTML como Estilo, Classe, Nome e o Espaço Reservado lado a lado com atributos personalizados, como Url, SelectionOnly e eventos como onSelectionMode. O atributo Url define o ponto de extremidade remoto para chamar as dicas, enquanto o atributo booliano SelectionOnly controla o comportamento do componente e se a entrada só deve vir de uma seleção ou se o texto digitado livremente é permitido como entrada. Vamos dar uma olhada na marcação do Razor e C# relacionado do componente na Figura 1. Todos os detalhes podem ser encontrados no arquivo typeahead.cshtml do projeto de exemplo em bit.ly/2ATgEKm.

Figura 1 - Marcação do Componente Typeahead

<div class="blazor-typeahead-container">
  <div class="input-group">
    <input type="text" class="@Class" style="@Style"
           placeholder="@Placeholder"
           oninput="this.blur(); this.focus();"
           bind="@SelectedText"
           onblur="@(ev => TryAutoComplete(ev))" />
    <input type="hidden" name="@Name" bind="@SelectedValue" />
    <div class="input-group-append">
      <button class="btn btn-outline-secondary dropdown-toggle"
              type="button" data-toggle="dropdown"
              style="display: none;">
      </button>
      <div class="dropdown-menu dropdown-menu-right
                  scrollable-menu @(_isOpen ? "show" : "")"
         style="width: 100%;">
        <h6 class="dropdown-header">@Items.Count item(s)</h6>
        @foreach (var item in Items)
        {
          <a class="dropdown-item"
           onclick="@(() => TrySelect(item))">
            @((MarkupString) item.MenuText)
          </a>
        }
      </div>
    </div>
  </div>
</div>

A Figura 2 lista as propriedades com suporte e definidas pelo componente.

Figura 2 - Propriedades do Componente TypeAhead

Name Descrição
Classe Obtém e define a coleção de classes da CSS a ser aplicada aos elementos HTML internos do componente.
Name Obtém e define o valor do atributo NAME quando o componente é incorporado em um formulário HTML.
Espaço reservado Obtém e define o texto para servir como um espaço reservado para os elementos HTML em renderização.
SelectedText Obtém e define o texto de exibição. Isso serve como o valor inicial e o texto selecionado, seja digitado pelo usuário ou escolhido em um menu suspenso.
SelectedValue Obtém e define o valor selecionado. Esse pode ou não corresponder ao valor de SelectedText. O valor selecionado está associado ao campo oculto, enquanto o SelectedText está associado ao campo de texto.
SelectionOnly Valor booliano que determina se o texto livre é permitido ou os usuários são forçados a selecionar somente uma das dicas fornecidas.
Style Obtém e define a coleção de estilos da CSS a ser aplicada aos elementos HTML internos do componente.
URL Endereço do ponto de extremidade remoto para chamar as dicas.

É importante observar que o layout HTML do componente Typeahead é mais complexo do que apenas alguns dos campos de entrada. Ele foi projetado para receber as dicas na forma de uma classe de TypeAheadItem definida conforme mostrada aqui:

public class TypeAheadItem
{
  public string MenuText { get; set; }
  public string Value { get; set; }
  public string DisplayText { get; set; }}

Qualquer dica sugerida é feita de um texto de exibição (como o nome do país) que define o campo de entrada, um texto de menu (como um texto mais avançado baseado em HTML) que aparece na lista suspensa e um valor (como o código do país) que identifica exclusivamente o item selecionado. O atributo value é opcional, mas serve para uma finalidade essencial no caso de o componente Typehead ser usado como uma lista suspensa inteligente. Nesse caso, a função do atributo Value é a mesma que do atributo value do elemento HTML Option. O código localizado no ponto de extremidade remoto referenciado pelo atributo Url deve retornar uma matriz de entidades TypeAheadItem. A Figura 3 fornece um exemplo de um ponto de extremidade que retorna a lista de nomes de país que correspondem a uma cadeia de caracteres de consulta.

Figura 3 - Retorno de uma lista de nomes de país

public JsonResult Countries(
  [Bind(Prefix = "id")] string filter = "")
{
  var list = (from country in CountryRepository().All();
    let match =
      $"{country.CountryName} {country.ContinentName}".ToLower()
    where match.Contains(filter.ToLower())
    select new TypeAheadItem()
    {
      Value = country.CountryCode,
      DisplayText = country.CountryName,
      MenuText = $"{country.CountryName} <b>{country.ContinentName}</b>
        <span class='pull-right'>{country.Capital}</span>"
    }).ToList();
  return Json(list);
}

Há algumas coisas a serem observadas aqui e elas têm a ver com expressividade. Deve haver algum tipo de intimidade entre o servidor de dica e o componente de preenchimento automático. Como desenvolvedor, a última palavra é sua. No código de exemplo discutido neste artigo, o ponto de extremidade de serviço Web retorna uma coleção de objetos TypeAheadItem. Em uma implementação customizada, no entanto, você pode fazer com que o terminal retorne uma coleção de propriedades específicas do aplicativo e fazer com que o componente decida dinamicamente qual propriedade deve ser usada para o texto e qual para o valor. O objeto TypeAheadItem, no entanto, fornece uma terceira propriedade — MenuText — que contém uma cadeia de caracteres HTML a ser atribuída ao item de lista suspensa, conforme mostrado na Figura 4.

Componente Typeahead do Blazor em Ação
Figura 4 - Componente Typeahead do Blazor em Ação

O MenuText é definido como a concatenação do nome de país e o nome de continente, juntamente com o nome da capital corretamente justificado no controle. Você pode usar qualquer formatação de HTML que atenda às suas necessidades visuais.

No entanto, ter a marcação determinada estaticamente no servidor não é ideal. Uma abordagem muito melhor seria enviar todos os dados relevantes para o cliente e permitir que a marcação seja especificada como um parâmetro de modelo para o componente. Não por acaso, os componentes de modelo são um novo recurso do Blazor que abordarei em uma coluna futura.

Além disso, observe que na Figura 4 o usuário está digitando o nome de um continente e recebe as dicas para todos os países daquele continente. A implementação do ponto de extremidade Países, na verdade, corresponde à cadeia de caracteres de consulta ("oce" na captura de tela) em relação à concatenação do nome do país e do continente, como a correspondência da variável LINQ que você vê no snippet de código acima.

Essa é a primeira etapa de um modo, obviamente, mais avançado para pesquisar dados relacionados. Você pode, por exemplo, dividir a cadeia de caracteres de consulta por vírgulas ou espaços para obter uma matriz de cadeias de caracteres de filtro e, em seguida, combiná-las por operadores OR ou AND. Outra melhoria é o modelo do item da lista suspensa que, no exemplo atual, é embutido no código na implementação do terminal, mas pode ser fornecido como um modelo HTML junto com a cadeia de caracteres de consulta.

O resultado final é que o componente Typeahead apresentado aqui usa o plug-in typeahead do JavaScript do Twitter apenas como um ponto de partida. Os componentes do Blazor permitem que os desenvolvedores ocultem facilmente os detalhes da implementação, aumentando o nível de abstração do código que os desenvolvedores da Web escrevem.

A Mecânica do Componente

Na Figura 1, você examinou a marcação por trás do componente Typeahead do Blazor. Ele se baseia no Bootstrap 4 e conta com alguns estilos da CSS personalizados definidos no mesmo arquivo de origem CSHTML. É possível que os desenvolvedores personalizem esses estilos, basta fornecer a documentação para eles. De qualquer forma, os desenvolvedores que desejam usar o componente Typeahead não precisam saber sobre os estilos da CSS personalizados, como o menu rolável.

O componente é articulado como um grupo de entrada de Bootstrap 4 composto de um campo de entrada de texto, um campo oculto e um botão suspenso. O campo de entrada de texto é onde o usuário digita qualquer cadeia de caracteres de consulta. O campo oculto é onde o valor da dica aceito é armazenado para ser encaminhado por meio de qualquer host do formulário HTML. Somente o campo oculto tem o atributo de nome HTML definido. O botão suspenso fornece as dicas no menu. A lista de itens de menu é preenchida sempre que o novo texto é digitado no campo de entrada. O botão não está visível por padrão, mas sua janela suspensa é mostrada programaticamente sempre que necessário. Isso é feito aproveitando os recursos de associação de dados do Blazor. Uma variável booliana interna é definida (_isOpen) que determina se a classe da CSS “show” do Bootstrap 4 deve ser adicionada à seção de lista suspensa do botão. Eis o código:

<div class="dropdown-menu
            dropdown-menu-right
            scrollable-menu
            @(_isOpen ? "show" : "")"> ...
</div>

O operador de ligação do Blazor é usado para associar a propriedade SelectedText à propriedade value do campo de entrada de texto e a propriedade SelectedValue à propriedade value do campo oculto.

Como disparar a consulta remota para obter dicas? No HTML 5 simples, você deve definir um manipulador para o evento de entrada. O evento de alteração não é apropriado para as caixas de texto, porque é acionado somente quando o foco é perdido, o que não se aplica neste caso específico. Na versão do Blazor usada para o artigo (versão 0.5.0), é possível anexar alguns C# code ao evento de entrada, mas ele ainda não funciona conforme o esperado.

Como solução temporária, anexei o código que preenche a lista suspensa para o evento de desfoque e incluí um código JavaScript para manipular o evento de entrada que só chama desfoque e foco no nível do DOM. Como você pode ver na Figura 1, o método TryAutoComplete que é executado em resposta ao evento de desfoque coloca a chamada remota, pega uma matriz JSON de objetos TypeAheadItem e preenche a coleção de itens interna:

async Task TryAutoComplete(UIFocusEventArgs ev)
{
  if (string.IsNullOrWhiteSpace(SelectedText))
  {
    Items.Clear();
      _isOpen = false;
    return;
  }
  var actualUrl = string.Concat(Url.TrimEnd('/'), "/", SelectedText);
  Items = await HttpExecutor.GetJsonAsync<IList<TypeAheadItem>>(actualUrl);
    _isOpen = Items.Count > 0;
}

Quando isso acontece, a lista suspensa é preenchida com os itens do menu e exibida dessa forma:

@foreach (var item in Items)
{
  <a class="dropdown-item"
    onclick="@(() => TrySelect(item))">
    @((MarkupString) item.MenuText)
  </a>
}

Observe a conversão para MarkupString, que é a contraparte do Blazor para Html.Raw no ASP.NET MVC Razor. Por padrão, qualquer texto processado pelo Razor é codificado, exceto quando a expressão é convertida no tipo MarkupString. Portanto, se desejar que o HTML seja exibido, você deve passar pela conversão de MarkupString. Sempre que um item de menu é clicado, entra em execução o método TrySelect, da seguinte forma:

void TrySelect(TypeAheadItem item)
{
  _isOpen = false;
  SelectedText = item.DisplayText;
  SelectedValue = item.Value;
  OnSelectionMade?.Invoke(item);
}

O método recebe o objeto TypeAheadItem associado ao elemento clicado. Em seguida, ele fecha a lista suspensa, definindo _isOpen como false e atualiza SelectedText e SelectedValue, conforme apropriado. Por fim, ele chama StateHasChanged para atualizar a interface do usuário e gera o evento SelectionMade personalizado.

Como conectar ao componente Typeahead

Uma exibição do Blazor que usa o componente Typeahead associará partes da sua interface do usuário ao evento SelectionMade. Novamente, para que as alterações entrem em vigor, o método StateHasChanged deve ser invocado com este código:

void ShowSelection(TypeAheadItem item)
{
  _countryName = item.DisplayText;
  _countryDescription = item.MenuText;
  this.StateHasChanged();
}

No snippet de código, os dados que vêm com o evento estão associados às propriedades locais do modo de exibição e depois que o DOM é atualizado a exibição é atualizada automaticamente (confira Figura 5).

O Modo de Exibição Atualizado
Figura 5 - O Modo de Exibição Atualizado

Conclusão

Os front-ends modernos da Web são cada vez mais feitos de componentes. Os componentes aumentam o nível de abstração da linguagem de marcação e fornecem uma maneira muito mais limpa para criar conteúdo da Web. Assim como outras estruturas do lado do cliente, o Blazor tem sua própria definição de componentes personalizados para acelerar e simplificar o desenvolvimento. O código-fonte para este artigo pode ser encontrado no bit.ly/2ATgEKm, lado a lado com a coluna do mês passado sobre como usar o plug-in de preenchimento automático do JavaScript.


Dino Esposito é autor de mais de 20 livros e de 1.000 artigos em seus 25 anos de carreira. Autor de “The Sabbatical Break”, um show de estilo cênico, Esposito se ocupa escrevendo software para um mundo mais ecológico, como estrategista digital da BaxEnergy. Siga-o no Twitter: @despos.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Daniel Roth


Discuta esse artigo no fórum do MSDN Magazine