logo Buffalo slack logo
Formularios
Frontend

Forms

Buffalo usa el paquete github.com/gobuffalo/tags para facilitar la construcción de formulario.

Plush incluye dos helpers de este paquete que genera un formulario con estulo de Boostrap v3. Estos helpers son form y form_for.

Ambos tipos de helpers de formulario tienen las siguientes caracteristicas en común:

  • Configuración automática del token de autenticidad CSRF
  • Soporte para todos los métodos HTTP (PUT, POST, DELETE, etc…)
  • Manejo de errores
  • Soporte de formularios multipart
  • Tipos de entrada personalizables
  • Paso de atributos de etiquetas HTML

Formularios básicos

El helper form se puede usar para generar formularios HTML. Dado que este tipo de formulario no está amarrado a ningún “modelo” en particular, toda la información se debe pasar como opciones al formulario y sus métodos.

// templates/talks/edit.html

<%= form({action: talkPath({id: 3}), method: "PUT"}) { %>
  <div class="row">
    <div class="col-md-12">
      <%= f.InputTag({name:"Title", value: talk.Title }) %>
    </div>

    <div class="col-md-6">
      <%= f.TextArea({value: talk.Abstract, hide_label: true }) %>
    </div>

    <div class="col-md-6">
      <%= f.SelectTag({name: "TalkFormatID", value: talk.TalkFormatID, options: talk_formats}) %>
      <%= f.SelectTag({name: "AudienceLevel", value: talk.AudienceLevel, options: audience_levels }) %>
    </div>

    <div class="col-md-12">
      <%= f.TextArea({name: "Description", value: talk.Description, rows: 10}) %>
    </div>
    <div class="col-md-12">
      <%= f.TextArea({notes:"Notes", value: talk.Notes, rows: 10 }) %>
    </div>

  </div>
<% } %>
// OUTPUT
<form action="/talks/3" method="POST">
  <input name="authenticity_token" type="hidden" value="e0c536b7a1a7d752066727b771f1e5d02220ceff5143f6c77b">
  <input name="_method" type="hidden" value="PUT">
  <div class="row">
    <div class="col-md-12">
      <div class="form-group">
        <input class=" form-control" name="Title" type="text" value="My Title">
      </div>
    </div>
    <div class="col-md-6">
      <div class="form-group">
        <textarea class=" form-control">some data here</textarea>
      </div>
    </div>

    <div class="col-md-6">
      <div class="form-group">
        <select class=" form-control" name="TalkFormatID">
          <option value="0" selected>Talk</option>
          <option value="1">Lightning Talk</option>
          <option value="2">Workshop</option>
          <option value="3">Other</option>
        </select>
      </div>
      <div class="form-group">
        <select class=" form-control" name="AudienceLevel">
          <option value="All" selected>All</option>
          <option value="Beginner">Beginner</option>
          <option value="Intermediate">Intermediate</option>
          <option value="Advanced">Advanced</option>
        </select>
      </div>
    </div>

    <div class="col-md-12">
      <div class="form-group">
        <textarea class=" form-control" name="Description" rows="10">some data here</textarea>
      </div>
    </div>

    <div class="col-md-12">
      <div class="form-group">
        <textarea class=" form-control" notes="Notes" rows="10">some data here</textarea>
      </div>
    </div>
  </div>
</form>

Formularios de modelos

El helper form_for se puede usar para generar formularios HTML para un modelo específico. Esto hace que el código sea más fácil de escribir, y mantiene un nivel de “consistencia” a través de tu aplicación.

El helper form_for se comporta similar al helper form, con algunas diferencias claves.

La primera diferencia es que el form_for toma un “modelo” como el primer argumento. Este “model” sólo necesita ser un struct, no tiene que estar respaldado por una base de datos.

La segunda diferencia está en la etiqueta que llama al modelo directamente. Estas etiquetas, como InputTag, toma el nombre del atributo del modelo para el que deseas contruir un campo, luego toma un grupo opcional de opciones como segundo argumento.

// models/talk.go
type Talk struct {
  ID            int          `json:"id" db:"id"`
  CreatedAt     time.Time    `json:"created_at" db:"created_at"`
  UpdatedAt     time.Time    `json:"updated_at" db:"updated_at"`
  UserID        int          `json:"user_id" db:"user_id"`
  Title         string       `json:"title" db:"title"`
  Description   nulls.String `json:"description" db:"description"`
  Notes         nulls.String `json:"notes" db:"notes"`
  ParentID      nulls.Int    `json:"parent_id" db:"parent_id"`
  Abstract      string       `json:"abstract" db:"abstract"`
  AudienceLevel string       `json:"audience_level" db:"audience_level"`
  IsPublic      nulls.Bool   `json:"is_public" db:"is_public"`
  TalkFormatID  int          `json:"talk_format_id" db:"talk_format_id"`
}
<%= form_for( talk, {action: talkPath({id: 3}), method: "PUT"}) { %>
  <div class="row">
    <div class="col-md-12">
      <%= f.InputTag("Title") %>
    </div>
    <div class="col-md-6">
      <%= f.TextArea("Abstract", {hide_label: true}) %>
    </div>


    <div class="col-md-6">
      <%= f.SelectTag("TalkFormatID", {options: talk_formats}) %>
      <%= f.SelectTag("AudienceLevel", , {options: audience_levels}) %>
    </div>

    <div class="col-md-12">
      <%= f.TextArea("Description", {rows: 10}) %>
    </div>

    <div class="col-md-12">
      <%= f.TextArea("Notes", {rows: 10}) %>
    </div>
  </div>
<% } %>
// OUTPUT
<form action="/talks/3" id="talk-form" method="POST">
  <input name="authenticity_token" type="hidden" value="cd998be98a99b452481c43fd3e4715e4e85333a45b982ac999">
  <input name="_method" type="hidden" value="PUT">
  <div class="row">
    <div class="col-md-12">
      <div class="form-group">
        <label>Title</label>
        <input class="form-control" id="talk-Title" name="Title" type="text" value="My Title">
      </div>
    </div>
    <div class="col-md-6">
      <div class="form-group">
        <textarea class="form-control" id="talk-Abstract" name="Abstract">some data here</textarea>
      </div>
    </div>

    <div class="col-md-6">
      <div class="form-group">
      <label>TalkFormatID</label>
        <select class="form-control" id="talk-TalkFormatID" name="TalkFormatID">
          <option value="0" selected>Talk</option>
          <option value="1">Lightning Talk</option>
          <option value="2">Workshop</option>
          <option value="3">Other</option>
        </select>
      </div>
      <div class="form-group">
        <label>AudienceLevel</label>
        <select class=" form-control" id="talk-AudienceLevel" name="AudienceLevel">
          <option value="All" selected>All</option>
          <option value="Beginner">Beginner</option>
          <option value="Intermediate">Intermediate</option>
          <option value="Advanced">Advanced</option>
        </select>
      </div>
    </div>

    <div class="col-md-12">
      <div class="form-group">
        <label>Description</label>
        <textarea class=" form-control" id="talk-Description" name="Description" rows="10">some data here</textarea>
      </div>
    </div>

    <div class="col-md-12">
      <div class="form-group">
        <label>Notes</label>
        <textarea class=" form-control" id="talk-Notes" name="Notes" rows="10">some data here</textarea>
      </div>
    </div>
  </div>
</form>

Etiqueta Select

Para construir tus etiquetas <select> dentro de los formularios Tags proporciona 3 formas convenientes de agregar tus opciones <select>: form.SelectOptions, map[string]interface{} o []string, todas ellas pasando un campo options a las opciones de form.SelectTag como:

<%= f.SelectTag("TalkFormatID", {options: talkFormats}) %>

o

<%= f.SelectTag("TalkFormatID", {options: ["one", "two"]}) %>

La cual usará el mismo valor del atributo value y el cuerpo de la opción, o:

<%= f.SelectTag("TalkFormatID", {options: {"one": 1, "two": 2}}) %>

La cual te permite definir el mapa de opciones dentro de la vista.

Interfaz Selectable

Otra alternativa para las opciones de select es pasar una lista de estructuras que implementen la interfaz form.Selectable.

Que consiste en 2 métodos:

//Selectable allows any struct to become an option in the select tag.
type Selectable interface {
  SelectValue() interface{}
  SelectLabel() string
}

Al implementar esta interfaz, tags llamará a SelectValue y SelectLabel para obtener la opción Value y Lavel del implementador.

Selected

Tags agregará el atributo selected a la opcion que tiene el mismo valor al que se recibe en la opción value del form.SelectTag, asi no tienes que morar la opción que tienen el mismo valor que la seleccionada manualmente, p.e:

<%= f.SelectTag("TalkFormatID", {options: {"one": 1, "two": 2}, value: 2}) %>

Produce:

<div class="form-group">
  <label>TalkFormatID</label>
  <select class="form-control" id="talk-TalkFormatID" name="TalkFormatID">
    <option value="1">one</option>
    <option value="2" selected>two</option>
  </select>
</div>

Y similarmente con el slice form.SelectOptions:

<%= f.SelectTag("TalkFormatID", {options: talkFormats, value: 2}) %>

Etiqueta Checkbox

Tags proporciona una forma conveniente de construir un elemento HTML input con type="checkbox":

<%= f.CheckboxTag("IsPublic") %>

Que produce:

<div class="form-group">
  <label>
    <input class="" id="talk-IsPublic" name="IsPublic" type="checkbox" value="true" checked="">
    IsPublic
  </label>
</div>

Puedes cambiar facilmente el contenido del label con:

<%= f.CheckboxTag("IsPublic", {label: "Is the talk public?"}) %>

Que produce:

<div class="form-group">
  <label>
    <input class="" id="post-IsPublic" name="IsPublic" type="checkbox" value="true" checked="">
     Is the Talk public?
  </label>
</div>

Valores de Checkbox no marcado

Por defecto, cuando un checkbox no está “marcado” no se enviará ningun valor al servidor. A menudo, es útil enviar un valor indicando que el checkbox no está marcado. Esto se puede establecer pasando un valor unchecked.

<%= f.CheckboxTag("IsPublic", {unchecked: false}) %>
<div class="form-group">
  <label>
    <input id="widget-IsPublic" name="IsPublic" type="checkbox" value="true">
    <input name="IsPublic" type="hidden" value="false"> IsPublic
  </label>
</div>

Cuando se envie el formulario, se enviará la etiqueta hidden y el servidor verá el valor falso.

Manejando Errores

Tanto los ayudantes form como form_for tienen soporte para manejar los errores del paquete github.com/gobuffalo/validate.

En una acción simplemente establece un valor de tipo *validate.Errors en el contexto como errors y los helpers de formulario lo recogerán y añadirán mensajes de error a las etiquetas de formulario correspondientes.

// actions/widgets.go
func (v WidgetsResource) Create(c buffalo.Context) error {
  tx := c.Value("tx").(*pop.Connection)
  widget := &models.Widget{}
  if err := c.Bind(widget); err != nil {
    return err
  }
  // Validate the data from the html form
  verrs, err := tx.ValidateAndCreate(widget)
  if err != nil {
    return errors.WithStack(err)
  }
  if verrs.HasAny() {
    c.Set("widget", widget)
    // Make the errors available inside the html template
    c.Set("errors", verrs)
    return c.Render(422, r.HTML("widgets/new.html"))
  }
  c.Flash().Add("success", "Widget was created successfully")
  return c.Redirect(302, "/widgets/%s", widget.ID)
}
// templates/widgets/new.html
<%= form_for(widget, {action: widgetsPath(), method: "POST"}) { %>
  <%= f.InputTag("Name") %>
  <button class="btn btn-success" role="submit">Save</button>
  <a href="<%= widgetsPath() %>" class="btn btn-warning" data-confirm="Are you sure?">Cancel</a>
<% } %>
// OUTPUT
<form action="/widgets" id="widget-form" method="POST">
  <input name="authenticity_token" type="hidden" value="AI0pb5YFBw2xU/EfcS6FaEOwTLWaGv58Y+w0ArfJoknfqu7l/j6tRLWybbcm+YZqXbBmi7f80l3Sf0WfnR7COA==">
  <div class="form-group has-error">
    <label>Widget</label>
    <input class=" form-control" id="widget-Widget" name="Widget" type="text" value="">
    <span class="help-block">Widget can not be blank.</span>
  </div>
  <button class="btn btn-success" role="submit">Save</button>
  <a href="/widgets" class="btn btn-warning" data-confirm="Are you sure?">Cancel</a>
</form>

Uso de helpers de formulario que no son de Bootstrap

Los helpers por defecto, form y form_for generan formularios compatibles con Boostrap 3. Si esto no es para ti, puedes usar fácilmente la version no-Boostrap de estos helpers.

Requiere la versión de Plush v3.4.8 o superior
// actions/render.go
func init() {
  r = render.New(render.Options{
    // ...
    // Add template helpers here:
    Helpers: render.Helpers{
      "form":     plush.FormHelper,
      "form_for": plush.FormForHelper,
    },
    // ...
  })
}
// templates/widgets/new.html
<%= form_for(widget, {action: widgetsPath(), method: "POST"}) { %>
  <%= f.InputTag("Name") %>
  <%= f.InputTag("Body") %>
  <button class="btn btn-success" role="submit">Save</button>
  <a href="<%= widgetsPath() %>" class="btn btn-warning" data-confirm="Are you sure?">Cancel</a>
<% } %>
// OUTPUT
<form action="/widgets" id="widget-form" method="POST">
  <input name="authenticity_token" type="hidden" value="jN3nYOhCTqxZvmYnO9v1maso2VMs8fslj3rmKg1TS281W6JKpMd6Uezqp1dd3VBu2su41nKRBkd5AWDyCM4BzQ==">
  <input id="widget-Name" name="Name" type="text" value="">
  <input id="widget-Body" name="Body" type="text" value="">
  <button class="btn btn-success" role="submit">Save</button>
  <a href="/widgets" class="btn btn-warning" data-confirm="Are you sure?">Cancel</a>
</form>

Preguntas frecuentes

¿Cómo asignar un formulario a un modelo/estructura?

Consulta la página Vinculación de Peticiones para mas información sobre la vinculación de peticiones.

¿Puedo cambiar el nombre de la variable f en mi plantilla?

Por defecto, el valor del formulario dentro del bloque tiene por nombre f, sin embargo, este lo puedes cambiar cuando creas el formulario y pasas la opción var.

<%= form({var: "xyz"}) { %>
  <%= xyz.InputTag({name: "Foo"}) %>
<% } %>

Como puedo crear un formulario Multipart?

<%= form({multipart: true}) { %>
<% } %>
<form enctype="multipart/form-data" method="POST">
</form>

Puedo solo usar mi propio formulario (Sin usar el helper de formulario)?

Si, puedes crear tu propio formulario. Los formularios son proporcionados para qeu Buffalo genere tus recursos son simplemente un marcador de pocision para que puedas ponerte en marcha rápidamente. Sin embargo, es importante tener en cuenta que pedir a Buffalo que genere tus recursos, usando generadores suministrados, tambien generará las rutas relacionadas con los CRUD’s. Esto es importante ya que la ruta asociada a la acción UPDATE hace uso del método PUT y no es un valor válido para un método de formulario HTML según el HTML Standard. Dicho esto, debes asegurarte de estructurar tu formulario (para editar un recurso) para usar el método POST para conectar con el método HTTP, mientras usas una entrada oculta para indicar tu intención de hacer uso del método PUT del lado del servidor. Un ejemplo de esto sería el siguiente:

<form method="POST" ...>
  <input type="hidden" name="_method" value="PUT" />
...

Cómo puedo manejar los tokens CSRF si utilizo mi propio formulario?

Si decides usar tus propios formularios, necesitarás una forma de proporcionarle al formulario el token de autenticidad. Hay dos formas de resolver este problema.

La primera forma es usar el authenticity_token directamente en el formulario, puesto que ya está en el contexto.

<form method="POST" ...>
  <input name="authenticity_token" type="hidden" value="<%= authenticity_token %>">
</form>

Otra forma es escribir el helper que genere esa linea de codigo por ti.

"csrf": func(ctx plush.HelperContext) (template.HTML, error) {
  tok, ok := ctx.Value("authenticity_token").(string)
  if !ok {
    return "", fmt.Errorf("expected CSRF token got %T", ctx.Value("authenticity_token"))
  }
  t := tags.New("input", tags.Options{
    "value": tok,
    "type":  "hidden",
    "name":  "authenticity_token",
  })
  return t.HTML(), nil
},

Ahora que has definido un helper para usarlo en tus plantillas puedes usar tu helper dentro de tu formulario con <%= csrf() %>. Así que tu formulario personalizado debería terminar viéndose así:

<form method="POST" ...>
  <%= csrf() %>
</form>