Tipos de géneros de libros - Estructurar interfaz de usuario
- Obtener enlace
- X
- Correo electrónico
- Otras aplicaciones
Hola a todo@s, continuando con la serie de entradas en el blog La librería, en esta entrada se pretende enseñar a estructurar en componentes React personalizados, el interfaz de usuario realizado en la anterior entrada, con el fin de optimizar procesos de carga del componente SPfx, en el navegador del usuario final.
¿Cómo estructurar el interfaz de usuarios en componentes React personalizados?
He de informar que aun no soy un experto en cuanto a estructurar interfaz de usuarios en componentes React personalizados, por falta de puntos de experiencia, me he basado en un criterio muy simple y lógico a primera vista. He dividido el interfaz en componentes que se visualizan desde el principio y componentes que se visualizan en segundo plano.
He de informar que mientras desarrollaba esta entrada para el blog, me decidido cambiar los nombres de los eventos, para alinearme con los que se promulga en Office UI Fabric u otras páginas que he estado visitando.
Componente FormTypesOfBookGenres.tsx
El nuevo componente React personalizado que va a disponer el componente SPfx, es FormTypesOfBookGenres y estará alojado en el fichero FormTypesOfBookGenres.tsx.
Su funcionalidad es la de contener los controles no visibles inicialmente en el componente React ManagerTypesOfBookGenres y con los diferentes eventos definidos se ira visualizado según la lógica de negocio establecida.
Definir componente FormTypesOfBookGenres
Para definir inicial un nuevo componente React personalizado, solo es necesario los siguientes conceptos:
Importar el módulo de React.
Definir el componente FormTypesOfBookGenres.
Implementar el método Render.
A partir de aquí, posteriormente se irán agregando los componentes, eventos y funcionalidades de negocio al componente definido hasta ahora.
import * as React from 'react';
export class FormTypesOfBookGenres extends React.Component<any, any>{
public render(): React.ReactElement<any>{
console.warn("render - FormTypesOfBookGenres");
return null;
}
}
Módulos importados
Para el renderizado del nuevo componente React personalizado, se han importado la mayoría de las definiciones que se habían declarado en el componente ManagerTypesOfBookGenres.
import * as React from 'react';
import * as strings from 'ManagerTypesOfBookGenresWebPartStrings';
import styles from './ManagerTypesOfBookGenres.module.scss';
// Import components of Office UI Fabric
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
// Import custom components and classes
import { Constants } from "../Constants";
Método Render
Importados los módulos necesarios para implementar el renderizado del nuevo componente React, se procede a implementar el método Render.
public render(): React.ReactElement<any>{
console.warn("render - FormTypesOfBookGenres");
if ((this.state.stateManagerTypesOfBookGenres === null) || (!this.state.stateManagerTypesOfBookGenres.showPanel)) {
return null;
}
return (
<div>
<Panel isOpen={true}
type={PanelType.custom}
customWidth="350px"
headerText={this.state.stateManagerTypesOfBookGenres.panelNew ? strings.Components.Panel.PanelTitleNew : strings.Components.Panel.PanelTitleUpdate}
closeButtonAriaLabel={strings.Components.Actions.Close}
onRenderFooterContent={this._onRenderFooterContent}>
<TextField label={strings.Components.Fields.Title}
required={true}
name={Constants.ControlId.FieldTitle}
onChanged={value => this._onChangeInputText(value, Constants.ControlId.FieldTitle)}
value={this.state.formUpdating.Title}
onGetErrorMessage={this._onGetErrorMessageTitle}
errorMessage={this.state.errorMessage.Title}/>
</Panel>
<Dialog hidden={this.state.hideDialog}
dialogContentProps={{
type: DialogType.normal,
title: strings.Components.Dialog.DialogTitle,
subText: strings.Components.Dialog.DialogDescription}}>
<DialogFooter>
<div className={styles.listButton}>
<DefaultButton text={strings.Components.Actions.Accept}
onClick={this._onClickDeleteGenre} />
<DefaultButton text={strings.Components.Actions.Cancel}
onClick={this._onClickCloseDialog} />
</div>
</DialogFooter>
</Dialog>
</div>
);
}
En las primeras líneas de la función Render, se observa que se pregunta por la propiedad showPanel y si es favorable, se renderiza todo el componente React nuevo.
De esta forma se puede optimizar los procesos de carga de páginas, ya que no se tiene que renderizar componentes hasta que sean necesarios por parte del usuario final.
Hay que informar que hay otra forma de realizar este tipo de acción, pero a pesar de que el comportamiento era idéntico al que se ha elaborado al final, se descubrió posteriormente que en la función componentWillReceiveProps definida en el componente React, no se ejecutaba como debería ser, provocando errores de negocio en el componente SPfx.
El valor de la propiedad showPanel viene proporcionado por el componente React padre, bien a través del botón de crear o al realizar doble click en un tipo de género de libro, proporcionado en el listado. Más adelante en la entrada, se explicará cómo se realiza el proceso de pasar información del componente React padre al componente React hijo.
Método componentWillReceiveProps
Todo componente React dispone de un método llamado componentWillReceiveProps destinado a recoger la información que le proporciona su componente React padre y determinar de esta forma la configuración con la que se tiene que renderizar el componente React hijo.
El método componentWillReceiveProps se ejecuta antes que método render en el componente React hijo y el método solo se ejecuta una vez, mientras el componente React padre no vuelva a montar el componente React hijo.
public componentWillReceiveProps(_nextProps: any) {
console.warn("componentWillReceiveProps - FormTypesOfBookGenres");
let auxStateManagerTypesOfBookGenres = _nextProps.stateManagerTypesOfBookGenres;
let auxFormUpdating = {
Title: ""
};
if (auxStateManagerTypesOfBookGenres !== null) {
if ((!auxStateManagerTypesOfBookGenres.panelNew)
&& (auxStateManagerTypesOfBookGenres.selectItem !== null)) {
auxFormUpdating.Title = auxStateManagerTypesOfBookGenres.selectItem.Title;
}
}
this.setState({
stateManagerTypesOfBookGenres: auxStateManagerTypesOfBookGenres,
formUpdating: auxFormUpdating,
hideDialog:true
});
}
Cuando se invoca un componente React desde otro componente, se le pueden pasar uno o varios valores del componente React padre para la lógica de negocio del componente React hijo. En mi corta experiencia con los componentes SPfx, recomiendo pasar el componente state del componente React padre, para facilitar los desarrollos.
En este método se almacena en el componente state del componente React hijo, el componente state del componente React padre. Posteriormente, en el método render, se consultará esta propiedad para establecer la lógica de negocio del componente React hijo.
Eventos
Las definiciones de los eventos se han establecido en el constructor del componente React, después de la sección del componente state.
constructor(props: any) {
super(props);
this.state = {
…
…
};
this._onRenderFooterContent = this._onRenderFooterContent.bind(this);
this._onClickClosePanel = this._onClickClosePanel.bind(this);
this._onClickSaveGenre = this._onClickSaveGenre.bind(this);
this._onClickShowDialog = this._onClickShowDialog.bind(this);
this._onChangeInputText = this._onChangeInputText.bind(this);
this._onClickCloseDialog = this._onClickCloseDialog.bind(this);
this._onClickDeleteGenre = this._onClickDeleteGenre.bind(this);
this._onGetErrorMessageTitle = this._onGetErrorMessageTitle.bind(this);
}
En cuanto a la implementación de los eventos hay que destacar los siguientes aspectos:
-
El evento _onRenderFooterContent se ejecuta varias sin motivo aparente al montar el componente React. Se ha realizado pruebas con los ejemplos que proporciona Office UI Fabric y se reproduce el mismo comportamiento, por lo que se ha optado por no modificar la implementación inicial de nuestro evento.
Una vez montado el componente React, el comportamiento del evento es el esperado.
-
Se ha creado el evento _onGetErrorMessageTitle destinado a validar el contenido introducido en la caja de texto Título. Solo comprueba que no el valor de la caja de texto no esté vacío, pero se le puede agregar más funcionalidad si se desea, dependerá de la lógica de negocio requerida.
-
El evento de la caja de texto, se le proporciona dos parámetros, uno es el valor y otro es un identificador. Esto se ha realizado para poder reutilizar el evento _onChangeInputText para todas las cajas de texto que dispone el panel.
Este evento se encarga de actualizar en el componente state en la propiedad formUpdating el valor disponible en la caja de texto título, a través del identificador proporcionado.
Aquí no tiene mucha utilidad, pero si hubiera más cajas de texto, sería muy útil, para no repetir métodos o rutinas.
-
En algunos eventos del control, se hace referencia a la siguiente llamada this.props._closePanel();. El método _closePanel es un evento definido e implementado en el componente React ManagerTypesOfBookGenres, pero ha sido proporcionado al componente React hijo, para que cuando se invoque, el padre sea notificado de que debe cambiar el estado del panel para que no se renderice más, hasta nueva orden.
Esta es una forma con la que React permite devolver al componente React padre, información o peticiones de acciones desde el componente React hijo.
private _onRenderFooterContent() {
console.warn("_onRenderFooterContent - FormTypesOfBookGenres");
return (
<div className={styles.listButton}>
<DefaultButton text={strings.Components.Actions.Save}
onClick={this._onClickSaveGenre} />
{!this.state.stateManagerTypesOfBookGenres.panelNew &&
(<DefaultButton text={strings.Components.Actions.Delete} onClick={this._onClickShowDialog} className={styles.delete} />)
}
<DefaultButton text={strings.Components.Actions.Cancel}
onClick={this._onClickClosePanel} />
</div>
);
}
private _onClickClosePanel() {
this.props._closePanel();
}
private _onClickSaveGenre() {
this.props._closePanel();
}
private _onClickShowDialog() {
this.setState({ hideDialog: false });
}
private _onClickCloseDialog() {
this.setState({ hideDialog: true });
}
private _onClickDeleteGenre() {
this.props._closePanel();
}
private _onChangeInputText(value: any, key: React.ReactText) {
let auxFormUpdating = this.state.formUpdating;
auxFormUpdating[key] = value;
this.setState({ formUpdating: auxFormUpdating });
}
private _onGetErrorMessageTitle(value: string): string {
return value.trim() !== "" ? "" : strings.Components.ErrorMessages.MustEnter;
}
Componente state
El componente state va a disponer de las siguientes propiedades para almacenar información para su lógica de negocio:
Propiedad stateManagerTypesOfBookGenres: Está destinada a conservar el componente state de su componente React padre.
Propiedad hideDialog: Esta destinado a controlar la visualización del panel de dialogo que se muestra cuando se quiere eliminar un tipo de genero de libro.
Propiedad formUpdating: Esta destinado a almacenar los valores del formulario.
Propiedad errorMessage: Esta destinado controlar los mensajes de error en los procesos de validación, de los controles del formulario.
constructor(props: any) {
super(props);
this.state = {
stateManagerTypesOfBookGenres: null,
hideDialog: true,
formUpdating: {
Title: ""
},
errorMessage: {
Title: ""
}
};
…
…
}
Ficheros de recursos
Como se ha insertado el evento de validación en la caja de texto, se han introducido literales nuevos en los ficheros de recursos que se exponen a continuación:
-
Fichero mystrings.d.ts se ha agregado la categoría ErrorMessages dentro de la categoría Components, para definir ahí los mensajes de error que surgan durante el desarrollo y debido a la lógica de negocio del componente SPfx.
declare interface IManagerTypesOfBookGenresWebPartStrings { PropertyPaneDescription: string; Components: { … … ErrorMessages: { MustEnter: string; } } }
-
En los ficheros de idiomas en-us.js y es-es.js se han replicado la nueva estructura y se ha colocado los literales en el idioma correspondiente.
define([], function () { return { "PropertyPaneDescription": "In this pop-up panel of the web part SPfx 'Manager of types of genres of books', its characteristics are configured for its correct functioning.", "Components": { … … "ErrorMessages": { "MustEnter": "You must enter a value" } } } });
define([], function () { return { "PropertyPaneDescription": "En este panel emergente del web part SPfx ‘Gestor de tipos de géneros de libros’, se configuran sus características para su correcto funcionamiento.", "Components": { … … "ErrorMessages": { "MustEnter": "Debes introducir un valor" } } } });
Componente ManagerTypesOfBookGenres.tsx
Una vez creado e implementado el nuevo componente React destinado a mostrar el panel que contendrá el formulario de alta, baja y modificaciones de un tipo de genero de libro, hay que realizar los ajustes pertinentes en el componente React ManagerTypesOfBookGenres.
Módulos importados
Con la eliminación de diferentes controles de Office UI Fabric, debido a que se encuentra en otro componente React personalizado, se eliminan la importación de aquellos módulos que ya no son utilizados. En cambio, se importa el nuevo componente React FormTypesOfBookGenres.
import * as React from 'React';
import styles from './ManagerTypesOfBookGenres.module.scss';
import { IManagerTypesOfBookGenresProps } from './IManagerTypesOfBookGenresProps';
import * as strings from 'ManagerTypesOfBookGenresWebPartStrings';
// Import components of Office UI Fabric
import { CommandBarButton } from 'office-ui-fabric-React/lib/Button';
import { DetailsList, DetailsListLayoutMode, IColumn } from 'office-ui-fabric-React/lib/DetailsList';
// Import custom components and classes
import { Constants } from "../Constants";
import { MockWebApiCalls } from "./../WebApi/MockWebApiCalls";
import { FormTypesOfBookGenres } from "./FormTypesOfBookGenres";
Método Render
Teniendo en cuenta la nueva reestructuración de controles, la implementación del método Render, quedaría de la siguiente forma:
public render(): React.ReactElement<IManagerTypesOfBookGenresProps>{
console.warn("render - ManagerTypesOfBookGenres");
return (
<div className={styles.managerTypesOfBookGenres}>
<div className={styles.listButton}>
<CommandBarButton iconProps={{ iconName: 'Add' }}
text={strings.Components.Actions.AddGenre}
onClick={this._onClickNewGenre} />
</div>
<div>
<DetailsList items={this.state.listItemsGenres.value}
columns={_columns}
setKey="set"
layoutMode={DetailsListLayoutMode.justified}
onItemInvoked={this._onItemInvoked} />
</div>
<FormTypesOfBookGenres stateManagerTypesOfBookGenres={this.state}
_closePanel={this._closePanel} />
</div>
);
}
Se había comentado en apartados anteriores, que el control FormTypesOfBookGenres utilizaba configuraciones y eventos proporcionados por el componente React padre. Esto se realiza con las propiedades stateManagerTypesOfBookGenres y _closePanel, que se han establecido y los valores proporcionados.
Eventos
Como se han trasladado controles al nuevo componente React, los eventos implicados por los controles ausentes, que se disponían en el componente React, se han eliminados.
Las definiciones de los eventos que han sobrevivido a la reestructuración de controles siguen definidas en el constructor del componente React, después de la sección del componente state.
constructor(props: any) {
super(props);
this.state = {
…
…
};
this._onItemInvoked = this._onItemInvoked.bind(this);
this._onClickNewGenre = this._onClickNewGenre.bind(this);
this._closePanel = this._closePanel.bind(this);
}
En cuanto a la implementación de los eventos hay que destacar los siguientes aspectos:
El evento ._closePanel es el evento creado para pasar al componente React nuevo, para que infórmame a este componente de que debe ocultar el panel del formulario del interfaz de usuario.
El evento ._onItemInvoked, por defecto ya incorpora en su implementación el objeto seleccionado en el listado de tipos de géneros de libros. al mismo tiempo, el control facilita aún más el desarrollo, ya que el objeto _item que proporciona, es un objeto del interfaz de clase IItemGenre.
private _onItemInvoked(_item: any) {
this.setState({showPanel: true,
panelNew: false,
selectItem: _item
});
}
private _onClickNewGenre() {
this.setState({showPanel: true,
panelNew: true,
selectItem: null
});
}
private _closePanel() {
this.setState({ showPanel: false });
}
Componente state
El componente state del componente React, quedaría con la siguiente estructura de propiedades para la lógica de negocio:
Propiedad selectItem: Se encarga de almacenar el elemento seleccionado en el listado de tipos de géneros de libros, para los procesos de actualización o eliminación.
constructor(props: any) {
super(props);
this.state = {
listItemsGenres: null,
showPanel: false,
panelNew: true,
selectItem: null
};
…
…
}
¿Se ha conseguido eficiencia en nuestro diseño?
Viendo el ejercicio que se esta proponiendo en la serie La librería, notar una eficiencia con respecto al anterior código, es más costoso, pero la verdad es que se ha mejorado por las siguientes razones:
-
El renderizado del componente React FormTypesOfBookGenres tiende a cero, cuando su visualización no procede, por la implementación aplicada en el renderizado.
-
Cuando se modifica el contenido de la caja de texto del formulario, se modifica el componente state, provocando que se vuelve a renderizar el componente React FormTypesOfBookGenres.
Como la renderización del control DetailsList, encargado de lista los tipos de géneros de libros, está en otro componente React y no se ve afectado, los tiempos de carga se ven reducidos con respecto a la primera versión diseñada.
Lo que se pretendía con este ejercicio, es que dividir las renderizaciones de los controles y componentes que se están usando en el componente SPfx, para solo visualizarlos o procesarlos en los momentos precisos de la lógica de negocio.
- Obtener enlace
- X
- Correo electrónico
- Otras aplicaciones