Julio de 2018

Volumen 33, número 7

El programador ocupado: cómo dominar MEAN: Angular dinámicamente

Por Ted Neward | Julio de 2018

Ted NewardBienvenidos de nuevo, usuarios de MEAN.

En mi última columna, "cómo ser MEAN: Programación reactiva"(msdn.com/magazine/mt846724), examiné el módulo reactivo formularios de Angular, que ofrece otra forma de construir un formulario y responder a los eventos que el usuario se crea dentro de ese. Al final de ese artículo, que plantea una pregunta: ¿Qué ocurre si tengo una situación donde hay un número bastante grande de controles o necesitan cambiar los controles creados según la naturaleza cambiante del modelo de objetos bajo? Esto es uno de los casos en los que, Francamente, depender del sistema "plantilla" tradicional que emplea Angular no servirá. Si tengo que crear una plantilla para todas las combinaciones posibles de posibilidades, va a ser un día realmente largo.

Supongamos, por el momento, que desea crear un sistema de sondeo para los asistentes evaluar los altavoces, charlas, la sala de conferencias y el sitio Web de una conferencia, le da un nombre. Desean que sea capaz de implementar nuevas preguntas muy rápidamente, posiblemente incluso durante la conferencia de sí mismo la necesidad de debe producirse. Esto significa que, a continuación, que quiero páginas Web que sabe cómo generar campos según los tipos de preguntas que se le pida, esos tipos de pregunta, pero lo estará desde un origen externo (por ejemplo, un servicio JSON o incluso un archivo).

No es mágica. En cualquier sistema GUI (basada en Web o no) que admite la construcción de controles a través de una construcción en tiempo de ejecución (por ejemplo, para poder "new" seguridad de los propios controles), esto es algo bastante factible y razonable. Es ciertamente factible en Angular: ¿Puedo crear un sistema que se crea un formulario completamente fuera de los objetos de modelo y los metadatos asociados (implícito o explícito) en él.

Por lo tanto, para continuar con el ejemplo cuestionario, si creo un servicio Angular simple que sabe cómo obtener una serie de objetos "pregunta", un formulario Angular asociado puede tomar algunos o todos los objetos y construir la colección de elementos de formulario con correspondiente presentar las preguntas y capture las respuestas, supuestamente para el almacenamiento en algún lugar. La clave para todo esto será la FormGroup y FormControls que Angular se usa para representar los controles en tiempo de ejecución.

Modelo dinámico

Comencemos con una clase base para todas las preguntas, que le ayudarán a capturar algunos comportamientos comunes cuento con (y debe) para cualquier pregunta y su control relacionado. Este es el código para hacerlo:

export type ControlType = "textbox" | "dropdown";
export abstract class Question {
  constructor(public value: string = '',
    public key: string = '',
    public label: string = '',
    public required: boolean = false,
    public controlType: ControlType = 'textbox')
  { }
}

La mayoría de esto va a ser bastante sencillo, porque la mayoría de la clase aquí es solo las propiedades (lo que la gente de patrones a veces llamada a un DTO o el objeto de transferencia de datos), pero el elemento clave que se va a ser el campo controlType. Será el descriptor que corresponde a qué página HTML construcciones se puede generar. Actualmente, tiene todas estas dos posibilidades: un cuadro de texto (lo que permite la entrada de texto abierto) o una lista desplegable (un solo elemento seleccionado desde una variedad de posibilidades limitada).

Igualmente obvio, pregunta es una clase abstracta, ya que prevé tipos derivados para crearse en este caso, uno para cada tipo de pregunta. El código para TextboxQuestion tiene este aspecto:

export class TextboxQuestion extends Question {
  constructor(value: string = '',
    key: string = '',
    label: string = '',
    required: boolean = false,
    public type: string = '') {
    super(value, key, label, required, 'textbox');
  }
}

Y el código para DropdownQuestion similar al siguiente:

export class DropdownQuestion extends Question {
  constructor(value: string = '',
    key: string = '',
    label: string = '',
    required: boolean = false,
    public options: {key: string, value: string}[] = [])
  {
    super(value, key, label, required, 'dropdown');
  }
}

Cada pregunta pasa el conjunto de parámetros de la base hasta su elemento primario, y cada una agrega algo a la combinación. En el caso de TextboxQuestion, agrega un parámetro de tipo para el cuadro de texto en caso de desee indicar que se trata de un cuadro de texto de correo electrónico o contraseña. En el caso de DropdownQuestion, agrega una matriz de pares clave/valor que se usará como las posibilidades de la lista desplegable.

A continuación, sin embargo, tengo que saber cómo activar estos elementos en objetos FormControl y FormGroup. Sin duda, según la manera de Angular reflexiona sobre diseño, que podría ser un servicio independiente, pero tiene más sentido para mí para que sea una parte de la clase de pregunta, como un método estático. (Si alguna vez agrego un nuevo tipo de pregunta, es este método que debe actualizarse, por lo que tiene más sentido para mí para que sean agrupados dentro del mismo módulo). Desde el código, crear los objetos FormControl necesarios es bastante sencillo, como se indica a continuación:

export abstract class Question {
  public static toFormGroup(questions: Question[]): FormGroup {
    let group: any = {};
    questions.forEach(question => {
      group[question.key] =
        question.required ? new FormControl(question.value, Validators.required)
                          : new FormControl(question.value);
    });
    return new FormGroup(group);
  }
  // ...
}

Básicamente, este método toma una matriz de preguntas y convertirlos en una matriz de objetos FormControl anidadas dentro de un objeto FormGroup. Desde este lado de las cosas, tenga en cuenta que solo la pregunta es si es necesario; el control cualquier otra lógica de presentación debe capturarse dentro de la plantilla.

Presentación dinámica

También necesito empezar a pensar acerca de los componentes de interfaz de usuario de Angular implicados aquí; Básicamente, un cuestionario o extracción se compone de uno o más preguntas, por lo que usaré que como el modelo de trabajo: un QuestionnaireComponent usa un número de QuestionComponents, y cada QuestionComponent tendrá como entrada un objeto de pregunta.

Parece un poco más fácil iniciar desde la parte superior y trabajar hacia abajo, así que vamos a hacerlo. En primer lugar, tengo la denomina AppComponent que mostrará el cuestionario, en este caso por sí solo, como se muestra en figura 1.

Figura 1 la denomina AppComponent

@Component({
  selector: 'app-root',
  template: `
    <div>
      <h2>How was our conference?</h2>
      <app-questionnaire [questions]="questions"></app-questionnaire>
    </div>
  `,
  providers:  [QuestionService]
})
export class AppComponent {
  questions: Question[];
  constructor(service: QuestionService) {
    this.questions = service.getQuestions();
  }
}

Este código ofrece un escenario perfecto del componente. Simplemente usa y tiene un servicio que sabe cómo proporcionar la entrada de las necesidades del componente, para que el código esté claro, simple e intuitiva con facilidad a cualquier desarrollador Angular.

A continuación, echemos un vistazo a la QuestionnaireComponent, como se muestra en figura 2.

Figura 2 el QuestionnaireComponent

@Component({
  selector: 'app-questionnaire',
  templateUrl: './questionnaire.component.html'
})
export class QuestionnaireComponent implements OnInit {
  @Input() questions: Question[] = [];
  form: FormGroup;
  payload = '';
  ngOnInit() {
    this.form = Question.toFormGroup(this.questions);
  }
  onSubmit() {
    this.payload = JSON.stringify(this.form.value);
  }
}

Nuevamente, el enfoque es bastante simple y sencillo. El QuestionnaireComponent toma una matriz de preguntas como entrada y usa el FormGroup para que coincida con el formulario que se crea en la plantilla. Figura 3 muestra cómo hacerlo.

Figura 3 Preparación crear el formulario con FormGroup

<div>
  <form (ngSubmit)="onSubmit()" [formGroup]="form">
    <div *ngFor="let question of questions" class="form-row">
      <app-question [question]="question" [form]="form"></app-question>
    </div>
    <div class="form-row">
      <button type="submit" [disabled]="!form.valid">Save</button>
    </div>
  </form>
  <div *ngIf="payload" class="form-row">
    <strong>Saved the following values</strong><br>{{payload}}
  </div>
</div>

Por lo general, la carga podría cargarse a través de HTTP a través de un servicio Angular, supuestamente para almacenarse en una base de datos, pero el ejemplo está tardando un poco fuera del ámbito. En este caso, mostrar sirve para demostrar que los datos se valida, captura y preparados para su distribución.

Por supuesto, aún tengo que crear los elementos de la pregunta individuales dentro del formulario, y que se encuentra en el código QuestionComponent, que se muestra aquí:

@Component({
  selector: 'app-question',
  templateUrl: './question.component.html'
})
export class QuestionComponent {
  @Input() question: Question;
  @Input() form: FormGroup;
  get isValid() { return this.form.controls[this.question.key].valid; }
}

Tenga en cuenta que la toma de QuestionComponent como entrada el FormGroup a la que (lógicamente) pertenece; Podría intentar encontrar un medio diferente al que se va a obtener el control FormControl (para la implementación de la propiedad isValid), pero funciona de esta manera y ayuda a simplificar las cosas.

La plantilla para este componente es donde realiza la verdadera magia de la creación de forma dinámica. Gracias a un ngSwitch juiciosamente en controlType del objeto de la pregunta, puedo generar bastante sencilla, como se muestra en el elemento HTML figura 4.

Figura 4 creación del elemento HTML

<div [formGroup]="form">
  <label [attr.for]="question.key">{{question.label}}</label>
  <div [ngSwitch]="question.controlType">
    <input *ngSwitchCase="'textbox'" [formControlName]="question.key"
            [id]="question.key" [type]="question.type">
    <select *ngSwitchCase="'dropdown'" [formControlName]="question.key"
            [id]="question.key">
      <option *ngFor="let opt of question.options" [value]="opt.key">
        {{opt.value}}
      </option>
    </select>
  </div>
  <div class="errorMessage" *ngIf="!isValid">{{question.label}} is required</div>
</div>

Como puede ver, es bastante elegante con el tiempo. Cambiar en la propiedad controlType y, dependiendo de si esto es una lista desplegable o una pregunta del tipo de cuadro de texto, HTML diferentes de compilación.

Por último, sólo necesita un QuestionService que sirve a algunas preguntas, que, nuevamente, normalmente lo haría desde algún recurso externo, como un archivo o una API del lado servidor. En este ejemplo concreto, el servicio extrae la pregunta de la memoria, como se muestra en figura 5.

Figura 5 obtención de preguntas de QuestionService

@Injectable()
export class QuestionService {
  getQuestions() {
    return [
      new TextboxQuestion('', 'firstName',
        'Speaker\'s First name', true),
      new DropdownQuestion('', 'enjoyment',
        'How much did you enjoy this speaker\'s talk?',
        false,
        [
          {key: 'great', value: 'Great'},
          {key: 'good', value: 'Good'},
          {key: 'solid', value: 'Solid'},
          {key: 'uninspiring', value: 'Uninspiring'},
          {key: 'wwyt', value: 'What Were You Thinking?'}
        ]),
    ];
  }
}

Obviamente, en un cuestionario real, unas cuantas preguntas más puedan, pero en este ejemplo obtiene el punto.

Resumen

La gran pregunta que pertenecen a cualquier tipo de sistema como este es su extensibilidad: ¿Puedo agregar nuevo cuestionarios sin necesidad de modificaciones significativas? Obviamente, el QuestionnaireService es la clave no existe, por lo que siempre que pueden producir volver diferentes matrices de objetos de la pregunta, tengo un número infinito de cuestionarios puedo pedir nuestros asistentes al congreso. La única restricción es que los tipos de preguntas que puedo formular ahora mismo, se limitan a la opción múltiple o open ended texto de las respuestas.

Genera una segunda pregunta: ¿Lo difícil sería agregar nuevos tipos de preguntas en el sistema, como un control de clasificación con valores numéricos discretos? Hacerlo requeriría la creación de una nueva subclase de pregunta (RatingsQuestion) con el intervalo numérico para su uso, un nuevo valor de enumeración ControlType para la plantilla para activar y modificar la plantilla QuestionComponent para activar el nuevo valor de enumeración y Mostrar el código HTML en consecuencia (sin embargo, tendría un aspecto). Todo lo demás permanecerán intacto, que es el objetivo de cualquier tecnología de componente: mantener al cliente sin tener en cuenta los cambios estructurales, a menos que desean aprovechar las ventajas de las nuevas características.

Los lectores de angulares se se picazón proporcionar este concepto de un número, por lo que voy a poner las cosas a un cierre aquí. Sin embargo, hay un bit más necesario que se deberá pasar a través de antes de que podemos concluir nuestra cobertura de Angular, por lo que se alcanzará ese tiempo siguiente. Hasta entonces… ¡feliz codificación!


Ted Newardes un asesor politecnológico, orador y mentor, actualmente trabaja como director de ingeniería y relaciones con desarrolladores en Smartsheet.com. Ha escrito una gran cantidad de artículos, autor y coautor de una docena de libros y es orador en todo el mundo. Puede ponerse en contacto con él en ted@tedneward.com o leer su blog en blogs.tedneward.com.

Gracias al siguiente experto técnico: Garvice Eakins (Smartsheet)


Discuta sobre este artículo en el foro de MSDN Magazine