Formularios
Estado: BetaSelect nativo
El <select> de siempre (lo que el admin manda y lo que en el teléfono abre el selector del sistema) con la superficie de los campos, un chevron propio que gira al abrir y el placeholder tenue mientras no hay valor. Agrupa opciones con un Hash y toma valor y error del form builder. Para una lista animada con descripciones está el Select con resorte.
- CSS
- Formularios
- Form builder
Quien suple aparece en el horario público.
Quien suple aparece en el horario público.
app/views/… (la demo de arriba)
<%# El equipo va agrupado (Hash → <optgroup>); el nivel recibe un Hash de
etiqueta → valor. El último es un f.select suelto con
Ui::SelectComponent.classes (chevron en CSS). %>
<div class="@container w-full max-w-3xl">
<div class="grid gap-x-8 gap-y-5 @xl:grid-cols-2">
<%= render Ui::SelectComponent.new(name: "clase[template_id]", id: "select-demo-template", label: "Plantilla de clase",
prompt: "Elige una plantilla", required: true,
options: [ [ "Vinyasa Flow · 60 min", 1 ], [ "Reformer Básico · 50 min", 2 ], [ "Barre Sculpt · 45 min", 3 ], [ "Yin & Meditación · 75 min", 4 ] ]) %>
<%= render Ui::SelectComponent.new(name: "clase[host_id]", id: "select-demo-teacher", label: "Quién la da", selected: 12,
hint: "Quien suple aparece en el horario público.",
options: {
"Titulares" => [ [ "Ana Robles", 11 ], [ "Mariana Ortiz", 12 ], [ "Sofía Treviño", 13 ] ],
"Suplentes" => [ [ "Lucía Gamboa", 21 ], [ "Renata Lira", 22, { disabled: true } ] ]
}) %>
<div class="hidden grid-cols-[1fr_auto] items-end gap-3 @xl:col-span-2 @xl:grid">
<%= render Ui::SelectComponent.new(name: "clase[level]", id: "select-demo-level", label: "Nivel", selected: "multinivel",
options: { "Todos los niveles" => "all_levels", "Multinivel" => "multinivel", "Int-Avanzado" => "int_avanzado" }) %>
<label class="grid gap-1.5 text-sm font-medium text-foreground" for="select-demo-sort">
Ordenar
<select id="select-demo-sort" name="sort" class="<%= Ui::SelectComponent.classes(size: :md, extra: "w-40") %>">
<option>Más recientes</option>
<option>Por fecha de clase</option>
<option>Por maestra</option>
</select>
</label>
</div>
</div>
</div>Qué hace
- Ui::SelectComponent.classes(invalid:, size:) viste un f.select o f.collection_select suelto; ahí el chevron va dibujado con gradientes que siguen los tokens.
- El placeholder se ve tenue con :has(option[value=""]:checked): sin JS.
- El chevron se aclara al pasar y gira mientras la lista del sistema está abierta (donde el navegador soporta :open).
- Las opciones y el selector del sistema siguen el modo del tema (color-scheme).
Props
| Nombre | Tipo | Default | Descripción |
|---|---|---|---|
| name | String | nil | Nombre del campo. Con form: + attribute: sale del builder. |
| options | Array | Hash | [] | Como options_for_select: [ "a" ], [ [ "Etiqueta", valor, { disabled: true } ] ]; un Hash de listas son grupos (<optgroup>). |
| label | String | nil | Etiqueta visible (sin ella queda oculta y nombrada). |
| selected | String | nil | Valor elegido al cargar (con form: sale del modelo). |
| include_blank | Boolean | String | nil | Opción vacía siempre presente (tenue mientras está elegida). |
| prompt | String | nil | Como include_blank pero solo mientras no hay valor (como en Rails). |
| form: + attribute: | FormBuilder, Symbol | nil | Usa f.select: nombre, id, valor y error del modelo. |
| hint / error | String | nil | Ayuda y error (el del modelo se toma solo). |
| required / optional / disabled | Boolean | false | Asterisco, Opcional o deshabilitado. |
| size | :sm | :md | :lg | :md | Alto del control. |
| **attrs | Hash | {} | id, data, aria, multiple… van al <select>; class: se suma a la superficie. |
Accesibilidad
- Es un <select> nativo, con teclado y lector de pantalla del sistema.
- Etiqueta, ayuda y error enlazados por Field (for, aria-describedby, aria-invalid).
- El texto tenue del placeholder conserva contraste AA (muted-foreground).