Noviembre de 2018
Volumen 33, número 12
Tecnología de vanguardia: componentes personalizados de Blazor
Por Dino Esposito
La experiencia de programación con Blazor conduce naturalmente a un amplio uso de componentes. En Blazor, un componente es una clase .NET que implementa alguna lógica de representación de UI a partir de algún estado. Los componentes de Blazor están cerca de la idea de los componentes web de la próxima especificación de W3C y de implementaciones análogas en marcos de aplicaciones de página única (SPA). Un componente de Blazor es una combinación de código HTML, C# y JavaScript interoperable, y CSS que, en conjunto, actúan como un único elemento con una interfaz de programación común. Un componente de Blazor puede activar eventos y exponer propiedades, en gran parte, del mismo modo que un elemento DOM HTML.
En mi artículo reciente sobre Blazor, presenté un par de componentes dedicados. El primero proporcionaba una UI completa que permitía escribir texto, ejecutar una consulta y desencadenar un evento externo que recuperaba datos como argumento. El segundo componente del ejemplo actuaba como una cuadrícula a medida no personalizable que controlaba el evento, extraía los datos y completaba la tabla interna. El campo de entrada que recopilaba el texto de consulta se podía completar automáticamente mediante la extensión de Bootstrap de escritura anticipada. En este artículo, parto de este punto y describo el diseño y la implementación de un componente totalmente basado en Blazor para la funcionalidad de escritura anticipada. Al final de la tarea, tendrá un archivo CSHTML reutilizable que no dependerá de nada, excepto de la agrupación principal de Bootstrap 4.
Interfaz pública de escritura anticipada
El objetivo es producir un componente nativo de Blazor que se comporte como una versión extendida del componente typeahead.js clásico (consulte twitter.github.io/typeahead.js). El componente final se encuentra en medio de un cuadro de texto y una lista desplegable. A medida que los usuarios escriben en el campo de entrada de texto, el componente consulta una dirección URL remota y obtiene sugerencias de valores posibles que escribir. La escritura anticipada de JavaScript proporciona sugerencias, pero no obliga a los usuarios a aceptarlas. En otras palabras, el texto personalizado diferente de las sugerencias sigue siendo aceptable.
Sin embargo, en algunos casos, quiere que los usuarios escriban texto y, a continuación, seleccionen una de las entradas válidas de una lista. Considere, por ejemplo, un campo de entrada donde debe proporcionar un nombre de país o de cliente. En el mundo, hay más de 200 países y, a menudo, en un sistema de software, hay cientos de clientes o más. ¿Realmente es recomendable usar una lista desplegable? Un componente de escritura anticipada más inteligente podría permitir que los usuarios escribieran, por ejemplo, la palabra "Unido" y, a continuación, presentar una lista de países coincidentes, como Emiratos Árabes Unidos, Reino Unido y Estados Unidos. Si se escribe cualquier texto sin formato, el código borra el búfer automáticamente.
Otro problema relacionado: Si el texto sin formato no es una opción, es probable que también necesite un código. Idealmente, le gustaría escribir el nombre de un país o cliente y que se completara a partir del formulario de hospedaje el identificador del país o el identificador único del cliente. En HTML puro y JavaScript, necesita algún script adicional que agregue un campo oculto y administre selecciones a partir del complemento de escritura anticipada. Un componente nativo de Blazor tendrá todas estas funcionalidades encubiertas. Veamos cómo escribir un componente de este tipo.
Diseño del componente
El componente de escritura anticipada está pensado para ser un campo de entrada adicional que se pueda usar en un formulario HTML. Está formado por dos campos de entrada estándares: uno de tipo texto y otro oculto. El campo de entrada oculto presentará un atributo NAME que hará que sea completamente interoperable si se usa dentro de un formulario. A continuación se incluye un marcado de muestra para usar el componente nuevo:
<typeahead style="margin-top: 40px;"
class="form-control"
url="/hint/countries1"
selectionOnly="true"
name="country"
placeholder="Type something"
onSelectionMade="@ShowSelection" />
Como puede ver, el componente refleja algunos atributos específicos de HTML, como estilo, clase, nombre y marcador de posición junto con atributos personalizados, como Url, SelectionOnly y eventos como onSelectionMode. El atributo Url define el extremo remoto para buscar sugerencias, mientras que el atributo booleano SelectionOnly controla el comportamiento del componente, y si la entrada solo debe proceder de una selección o si el texto escrito libremente es válido como entrada. Observemos el marcado de Razor y el código C# relacionado del componente en la Figura 1. Encontrará detalles completos en el archivo typeahead.cshtml del proyecto de muestra en bit.ly/2ATgEKm.
Figura 1 Marcado del componente de escritura anticipada
<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>
La Figura 2 enumera las propiedades que define y admite el componente.
Figura 2 Propiedades del componente TypeAhead
Nombre | Descripción |
Clase | Obtiene y define la colección de clases CSS que se aplicará a los elementos HTML internos del componente. |
Nombre | Obtiene y establece el valor del atributo NAME cuando se incorpora el componente en un formulario HTML. |
Placeholder | Obtiene y establece el texto que actuará como marcador de posición para los elementos HTML que se van a representar. |
SelectedText | Obtiene y define el texto para mostrar. Sirve como valor inicial y como texto seleccionado, tanto si lo ha escrito el usuario como si se ha elegido en un menú desplegable. |
SelectedValue | Obtiene y define el valor seleccionado. Esto puede o no coincidir con el valor de SelectedText. El valor seleccionado está enlazado con el campo oculto, mientras que SelectedText está enlazado con el campo de texto. |
SelectionOnly | Valor booleano que determina si se permite texto sin formato o si los usuarios deben elegir una de las sugerencias proporcionadas. |
Style | Obtiene y define la colección de estilos CSS que se aplicarán a los elementos HTML internos del componente. |
Dirección URL | Dirección del extremo remoto para buscar sugerencias. |
Es importante tener en cuenta que el diseño HTML del componente de escritura anticipada es más complejo que un par de campos de entrada. Se ha diseñado para recibir sugerencias en el formato de una clase TypeAheadItem definida como se muestra aquí:
public class TypeAheadItem
{
public string MenuText { get; set; }
public string Value { get; set; }
public string DisplayText { get; set; }}
Cualquier sugerencia mostrada está formada por un texto de presentación (por ejemplo, el nombre del país) que establece el campo de entrada, un texto de menú (por ejemplo, un texto más enriquecido basado en HTML) que aparece en la lista desplegable y un valor (por ejemplo, el código de país) que identifica de forma exclusiva el elemento seleccionado. El atributo de valor es opcional, pero tiene una finalidad fundamental en caso de que el componente de escritura anticipada se use como lista desplegable inteligente. En este caso, el rol del atributo Value es el mismo que el del atributo value del elemento HTML Option. El código que se encuentra en el extremo remoto al que hace referencia el atributo Url debe devolver una matriz de entidades TypeAheadItem. La Figura 3 proporciona un ejemplo de un punto de conexión que devuelve la lista de nombres de países que coinciden con una cadena de consulta.
Figura 3 Devolver una lista de nombres 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);
}
Hay un par de cosas que se deben tener en cuenta aquí, y ambas están relacionadas con la expresividad. Debe haber algún tipo de intimidad entre el servidor de sugerencias y el componente de escritura anticipada. Como desarrollador, tiene la última palabra sobre eso. En el código de ejemplo descrito en este artículo, el punto de conexión de servicio web devuelve una colección de objetos TypeAheadItem. Sin embargo, en una implementación personalizada, puede tener un punto de conexión que devuelva una colección de propiedades específicas de la aplicación y hacer que el componente decida dinámicamente qué propiedad se debe utilizar para el texto y cuál para el valor. Sin embargo, el objeto TypeAheadItem proporciona una tercera propiedad (MenuText), que contiene una cadena HTML que se debe asignar al elemento de lista desplegable, como se muestra en la Figura 4.
Figura 4 Componente de escritura anticipada de Blazor en acción
MenuText se define con la concatenación del nombre del país y del continente, junto con el nombre de la capital justificado a la derecha en el control. Puede usar cualquier formato HTML que se adapte a sus necesidades visuales.
Sin embargo, tener el marcado determinado estáticamente en el servidor no es lo ideal. Un enfoque mucho mejor sería enviar los datos relevantes al cliente y dejar que el marcado se especificara como parámetro de plantilla en el componente. Los componentes con plantilla son una fantástica característica próxima de Blazor que cubriré en una futura columna.
Además, tenga en cuenta que, en la Figura 4, el usuario está escribiendo el nombre de un continente y recibe sugerencias para todos los países del mismo continente. De hecho, la implementación del punto de conexión de países coincide con la cadena de consulta ("oce" en la captura de pantalla) con la concatenación del país y el nombre del continente, como en la coincidencia de la variable LINQ que se observa en el fragmento de código superior.
Este es el primer paso de una manera evidentemente más eficaz de buscar datos relacionados. Por ejemplo, puede dividir la cadena de consulta por comas o espacios para obtener una matriz de cadenas de filtro y, a continuación, combinarlas con los operadores OR o AND. Otra mejora es la plantilla del elemento de lista desplegable que, en el ejemplo actual, está codificada de forma rígida en la implementación del punto de conexión, pero se puede proporcionar como plantilla HTML junto con la cadena de consulta.
La conclusión es que el componente de escritura anticipada presentado aquí usa el complemento de escritura anticipada de JavaScript de Twitter únicamente como punto de partida. Los componentes de Blazor permiten a los desarrolladores ocultar fácilmente los detalles de la implementación y, en última instancia, elevan el nivel de abstracción del código que escriben los desarrolladores web.
La mecánica del componente
En la Figura 1, ha examinado el marcado asociado al componente de escritura anticipada de Blazor. Se basa en Bootstrap 4 y cuenta con algunos estilos CSS personalizados definidos en el mismo archivo de origen CSHTML. Si quiere que los desarrolladores personalicen estos estilos, solo tiene que proporcionarles documentación. En cualquier caso, los desarrolladores que quieran usar el componente de escritura anticipada no tienen que conocer los estilos CSS personalizados como, por ejemplo, los menús desplegables.
El componente se articula como un grupo de entrada de Bootstrap 4 formado por un campo de entrada de texto, un campo oculto y un botón de lista desplegable. En el campo de entrada de texto es donde el usuario escribe cualquier cadena de consulta. El campo oculto es donde se almacena el valor de la sugerencia aceptada para reenviarse mediante cualquier formulario HTML de host. Solo el campo oculto tiene el conjunto de atributos de nombre HTML definido. El botón de lista desplegable proporciona el menú con sugerencias. La lista de elementos de menú se completa cuando se escribe texto nuevo en los campos de entrada. El botón no está visible de forma predeterminada, pero su ventana de lista desplegable se muestra mediante programación siempre que sea necesario. Para ello, se aprovechan las funcionalidades de enlace de datos de Blazor. Se define una variable booleana interna (_isOpen) que determina si la clase CSS "show" de Bootstrap 4 se debe agregar a la sección desplegable del botón. Este es el código:
<div class="dropdown-menu
dropdown-menu-right
scrollable-menu
@(_isOpen ? "show" : "")"> ...
</div>
El operador de enlace de Blazor se usa para enlazar la propiedad SelectedText con la propiedad de valor del campo de entrada de texto y la propiedad SelectedValue con la propiedad valor del campo oculto.
¿Cómo se desencadena la consulta remota para las sugerencias? En HTML 5 estándar, tendría que definir un controlador para el evento de entrada. El evento de cambio no resulta adecuado para los cuadros de texto, ya que solo se desencadena una vez que se ha perdido el foco, lo que no se aplica en este caso concreto. En la versión de Blazor usada para el artículo (versión 0.5.0), puede asociar cierto código C# al evento de entrada, pero, en realidad, aún no funciona según lo previsto.
Como solución temporal, adjunté el código que rellena la lista desplegable al evento de desenfoque y agregué cierto código JavaScript para controlar el evento de entrada que llama al desenfoque y al enfoque a nivel de DOM. Como puede ver en la Figura 1, el método TryAutoComplete que se ejecuta en respuesta al evento de desenfoque realiza la llamada remota, toma una matriz JSON de objetos TypeAheadItem y rellena la colección de elementos internos:
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;
}
Cuando sucede esto, la lista desplegable se completa con elementos de menú y se muestra, como se indica a continuación:
@foreach (var item in Items)
{
<a class="dropdown-item"
onclick="@(() => TrySelect(item))">
@((MarkupString) item.MenuText)
</a>
}
Tenga en cuenta la conversión a MarkupString, que es el equivalente de Blazor para Html.Raw en Razor de MVC de ASP.NET. De forma predeterminada, se codifica cualquier texto que procesa Razor, excepto cuando la expresión se convierte al tipo MarkupString. Por lo tanto, si quiere que el HTML se muestre, debe pasar a través de la conversión de MarkupString. Cada vez que se hace clic en un elemento de menú, se ejecuta el método TrySelect de la siguiente manera:
void TrySelect(TypeAheadItem item)
{
_isOpen = false;
SelectedText = item.DisplayText;
SelectedValue = item.Value;
OnSelectionMade?.Invoke(item);
}
El método recibe el objeto TypeAheadItem asociado con el elemento en que se hizo clic. A continuación, se cierra la lista desplegable, se define _isOpen como false y se actualizan los valores de SelectedText y SelectedValue según corresponda. Por último, llama a StateHasChanged para actualizar la interfaz de usuario y genera el evento SelectionMade personalizado.
Conectar con el componente de escritura anticipada
Una vista de Blazor que utiliza el componente de escritura anticipada enlazará partes de su interfaz de usuario con el evento SelectionMade. De nuevo, para que los cambios surtan efecto, es necesario invocar el método StateHasChanged con el código siguiente:
void ShowSelection(TypeAheadItem item)
{
_countryName = item.DisplayText;
_countryDescription = item.MenuText;
this.StateHasChanged();
}
En el fragmento de código, los datos que vienen con el evento se enlazan con las propiedades locales de la vista y, una vez que se actualiza el DOM, la vista se actualiza automáticamente (consulte la Figura 5).
Figura 5 Vista actualizada
Resumen
Los front-ends modernos están formados, cada vez más, por componentes. Los componentes elevan el nivel de abstracción del lenguaje de marcado y proporcionan una forma más limpia de crear contenido web. Al igual que otros marcos del lado cliente, Blazor tiene su propia definición de componentes personalizados para acelerar y simplificar el desarrollo. El código fuente de este artículo se puede encontrar en bit.ly/2ATgEKm, junto con la columna del mes pasado acerca de cómo utilizar el complemento de escritura anticipada de JavaScript.
Dino Esposito ha escrito más de 20 libros y más de 1000 artículos en su carrera de 25 años. Autor de “The Sabbatical Break”, un espectáculo de estilo teatral, Esposito se dedica a escribir software para un mundo más verde como estratega digital de BaxEnergy. Puede seguirle en Twitter: @despos.
Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Daniel Roth