Julho de 2018

Volume 33 – Número 7

O programador: Como ser MEAN: Dinamicamente Angular

Por Ted Neward | Julho de 2018

Ted NewardBem-vindos de volta, MEANers.

Na minha última coluna, "como ser MEAN: Programação reativa"(msdn.com/magazine/mt846724), eu examinei o módulo de reativo formulários do Angular, que oferece uma maneira diferente para construir um formulário e responder aos eventos que o usuário cria dentro dele. No final do artigo, eu pairam uma pergunta: E se eu tiver uma situação em que há um grande número de controles ou necessário alterar os controles criados com base sobre a natureza mutável do objeto modelo abaixo? Esse é um dos casos em que, francamente, depender do sistema "template" tradicional que emprega Angular não recortá-la. Se eu precisar criar um modelo para cada combinação possível de possibilidades, vai ser um dia muito longo.

Vamos supor que, por enquanto, que uma conferência deseja criar um sistema de sondagem para participantes avaliar os alto-falantes, fala, o local de conferência, o site da Web, etc. Eles querem ser capazes de distribuir novas perguntas muito rapidamente, potencialmente, mesmo durante a conferência, em si caso seja necessário surgir. Isso significa que, em seguida, o que eu quero páginas da Web que sabe como gerar campos com base nos tipos de pergunta que está sendo solicitados, e esses tipos de perguntas serão disponibilizados de uma fonte externa (como um serviço JSON ou até mesmo um arquivo).

Não é mágica. Em qualquer sistema GUI (baseado na Web ou não) que dá suporte à construção de controles por meio de uma construção de tempo de execução (por exemplo, sendo capaz de "new" para cima os controles em si), isso é algo bastante viável e razoável. É certamente factível no Angular: Posso criar um sistema que cria um formulário totalmente em objetos de modelo e os metadados associados (implícito ou explícito) contido nele.

Portanto, para continuar com o exemplo de questionário, se eu criar um serviço Angular simples que sabe como obter uma série de objetos "pergunta", um formulário Angular associado pode levar de alguns ou todos os objetos e construir a coleção correspondente de elementos de formulário para apresentar as perguntas e respostas, presumivelmente para armazenamento em algum lugar de captura. A chave para tudo isso será o FormGroup e FormControls Angular usa para representar esses controles em tempo de execução.

Modelo dinâmico

Vamos começar com uma classe base para todas as perguntas, o que ajudará a capturar alguns comportamentos comuns esperar (e será necessário) para todas as questões e seu controle relacionado. Eis o código para isso:

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')
  { }
}

Grande parte disso vai ser muito simples, porque a maior parte da classe aqui é apenas as propriedades (que o pessoal de padrões, às vezes, chamo um DTO ou objeto de transferência de dados), mas o elemento chave vai ser o campo controlType. Ele será o descritor que corresponde ao que HTML construções posso gerar. Atualmente, ele tem todas as duas possibilidades: uma caixa de texto (permitindo para entrada de texto em aberto) ou uma lista suspensa (um único item selecionado em um intervalo limitado de possibilidades).

Igualmente óbvio, pergunta é uma classe abstrata, pois espero que tipos derivados para ser criado aqui, um para cada tipo de pergunta. O código para TextboxQuestion tem esta aparência:

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

E o código para DropdownQuestion como este:

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 pergunta passa o conjunto de parâmetros de base até seu pai e cada adiciona uma coisa à combinação. No caso de TextboxQuestion, ele adiciona um parâmetro de tipo para a caixa de texto, caso eu quero indicar que se trata de uma caixa de texto de senha ou email. No caso de DropdownQuestion, ele adiciona uma matriz de pares chave/valor a ser usado como as possibilidades de lista suspensa.

No entanto, em seguida, eu preciso descobrir como transformar isso em objetos FormControl e FormGroup. Indiscutivelmente, acordo com a maneira Angular pensa sobre design, que pode ser um serviço autônomo, mas faz mais sentido para mim para torná-lo em uma parte da classe pergunta, como um método estático. (Se eu nunca adicionar um novo tipo de pergunta, ele é desse método que precisa ser atualizado, portanto, faz mais sentido para mim para manter todos eles agrupados dentro do mesmo módulo.) Do lado do código, criando os objetos FormControl requisito é bastante direto, da seguinte maneira:

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);
  }
  // ...
}

Esse método basicamente usa uma matriz de perguntas e as transforma em uma matriz de objetos FormControl aninhado dentro de um objeto FormGroup. Desse lado da coisas, observe que a pergunta real somente se o controle é necessário; qualquer outra lógica de exibição precisa ser capturada dentro do modelo.

Exibição dinâmica

Também preciso começar a pensar sobre os componentes de interface do usuário Angular envolvidos aqui; Fundamentalmente, uma pesquisa ou o questionário é composto por uma ou mais perguntas, portanto, vou usar isso como o modelo de trabalho: um QuestionnaireComponent usa um número de QuestionComponents e cada QuestionComponent terá como entrada um objeto de pergunta.

Parece um pouco mais simples iniciar na parte superior e trabalhar meu caminho para baixo, então, vamos fazer isso. Primeiro, eu tenho o AppComponent que exibirá o questionário, nesse caso, por conta própria, conforme mostrado na Figura 1.

Figura 1 o 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();
  }
}

Esse código oferece até o cenário perfeito do componente. Eu apenas usá-lo e ter um serviço que sabe como fornecer a entrada das necessidades do componente, para que o código permaneça claras, simples e intuitiva facilmente para qualquer desenvolvedor Angular.

Em seguida, vamos examinar o QuestionnaireComponent, conforme mostrado na Figura 2.

Figura 2 o 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);
  }
}

Novamente, a abordagem é bastante direto e simples. O QuestionnaireComponent pega uma matriz de perguntas como sua entrada e usa o FormGroup para corresponder ao formulário a ser criado no modelo. Figura 3 mostra isso.

Figura 3 Preparando para criar o formulário com 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>

Em termos gerais, a carga seria ser carregada por meio de HTTP por meio de um serviço Angular, presumivelmente para ser armazenado em um banco de dados, mas que está adotando o exemplo um pouco fora do escopo. Aqui, exibindo serve para demonstrar que os dados são validados, capturados e preparados para a distribuição.

É claro, ainda será necessário criar os elementos de pergunta individuais dentro do formulário, e que se encaixa no código QuestionComponent, mostrado aqui:

@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; }
}

Observe que a usa QuestionComponent como entrada (logicamente) o FormGroup para o qual ele pertence; Eu poderia tentar encontrar um meio de diferente obter o FormControl (para a implementação da propriedade isValid), mas dessa forma funciona e ajuda a manter as coisas simples.

O modelo para esse componente é onde acontece a verdadeira mágica da criação de formulário dinâmico. Graças a um ngSwitch criterioso em controlType do objeto pergunta, eu posso criar o elemento HTML bastante simples, conforme mostrado na Figura 4.

Figura 4 a construção do 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 você pode ver, é muito elegante enquanto essas coisas. Posso alternar na propriedade controlType e se isso for uma lista suspensa ou uma pergunta de tipo de caixa de texto, HTML diferente de compilação.

Por último, só preciso de um QuestionService que seja compatível com algumas perguntas, que, novamente, normalmente faria isso de algum recurso externo, como um arquivo ou uma API do lado do servidor. Nesse exemplo específico, o serviço recebe a pergunta da memória, conforme representado na Figura 5.

Figura 5 Obtendo perguntas 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, em um questionário real, algumas perguntas mais provavelmente, mas este exemplo obtém o ponto.

Conclusão

A pergunta real que pertencem a qualquer tipo de sistema como esse é sua extensibilidade: Pode adicionar novos questionários sem a necessidade de modificação significativa? Obviamente, o QuestionnaireService é a chave existe — desde que ele pode produzir diferentes matrizes de objetos de pergunta, eu tenho um número infinito de questionários, posso fazer nossa participantes da conferência. A única restrição é que os tipos de perguntas que posso fazer no momento, limitada a respostas de múltipla escolha ou open ended texto.

Que gera uma segunda pergunta: Muito difícil seria adicionar novos tipos de perguntas para o sistema, como um controle de classificações com valores numéricos discretos? Para fazer isso exigiria a criação de uma nova subclasse de pergunta (RatingsQuestion) com o intervalo numérico usar, um novo valor de enumeração de ControlType para o modelo a ser ativada e modificando o modelo QuestionComponent para ativar o novo valor de enumeração e Exiba o HTML adequadamente (no entanto, que ficaria). Todo o resto permaneceriam inalterado, que é o objetivo de qualquer tecnologia de componente — manter o cliente não reconhece as alterações estruturais, a menos que eles se optar por tirar proveito dos novos recursos.

Os leitores angular serão ser ansioso para dar esse conceito de todo uma rotação, portanto, vou transformar coisas aqui um fechamento. No entanto, é um pouco mais necessário, precisamos examinar antes de pode encerrar nossa cobertura Angular, portanto, alcançaremos da próxima vez. Até lá, feliz codificação!


Ted Newardé um consultor de politecnologia em Seattle, palestrante e mentor, atualmente trabalha como diretor de engenharia e relações com desenvolvedores na Smartsheet.com. Ele escreveu uma infinidade de artigos, autor e coautor de dezenas de livros e participa como palestrante pelo mundo. Entre em contato com ele em ted@tedneward.com ou leia seu blog em blogs.tedneward.com.

Graças ao seguinte especialista técnico: Garvice Eakins (Smartsheet)


Discuta esse artigo no fórum do MSDN Magazine