Este artículo muestra cómo de sencillo es utilizar Web Components de terceros en la parte de visualización de cualquier página.
La parte de edición, para ser integrada en PHPRunner es más compleja, pero en el siguiente artículo vamos a realizar algunos ejemplos, aunque soy consciente de que para los usuarios que son noveles en PHPRunner va a ser más difícil, si no hay alguien que le facilita la labor, escribiendo plugins de PHPRunner, que facilita su utilización. Voy a realizar un ejemplo, para ver si alguien se anima a construirlo para el colectivo de usuarios, porque yo no dispongo de tiempo para hacer esta labor.
Los Web Components que voy a utilizar el el ejemplo son los de Awesome. Seguro que hay otros muchos, pero estos, tienen buena documentación y son sencillos de utilizar.
Objetivo
Utilizar web components de Awesome para mostrar en un ejemplo básico cómo se pueden utilizar estos para variar el interface / UI de usuario de una aplicación de PHPRunner. En este caso vamos a modificar la visualización de contenidos.
Antes de entrar en la codificación, se ha hecho un ejemplo de HTML, para ver la sencillez de utilización de estos componentes. Los podéis revisar y para ver qué se ha escrito, podéis «ver el código fuente de la página» para que observéis lo simple que es.
DEMO: https://fhumanes.com/web_components/
La integración con PHPRunner se ha hecho cambiando la visualización de 2 campos:
- Campo «Status», que es un campo de tipo checkBox y se muestra cómo Switch
- Campo «Images», que muestra como Carrousel de miniaturas y de tamaño completo.
DEMO: https://fhumanes.com/wc_test1/
Solución Técnica
Cuando escojáis una librería de Web Components, a mi me gusta, probar su funcionamiento con ejemplos hechos en HTML. De esta forma se reafirma que lo que has leído es correcto y probar, verificar, corregir y ampliar se desarrolla rápido.
En primer ejercicio es el resultado de esas pruebas, por ello es todo HTML y el CSS y JS de la librería de Awesome.
La integración con PHPRunner la hecho para que sea un ejemplo simple (el de Check) y otro un poco más complejo (el del Carrusel). En ambos casos se ha modificado la visualización de estos campos, no así la edición, que se ha dejado la de PHPRunner.
He escogido la versión 10.91 de PHPRunner, porque creo que es la más utilizada y simple, pero de igual forma funcionaría con versiones anteriores y posteriores a esta.
Para incluir el fichero JS y CSS de la librería de Awesome he utilizado la modificación de la cabecera de las páginas.

<link rel="stylesheet" href="https://ka-f.webawesome.com/[email protected]/styles/webawesome.css" /> <script type="module" src="https://ka-f.webawesome.com/[email protected]/webawesome.loader.js"></script>
En este caso, porque la librería carga dinámicamente las partes según se estén utilizando. Si no fuera así, se podría cargar con SNIPPET, en las páginas en donde se incluyeran los Web Components.
Los cambios de visualización de los campos se han hecho definiendo «custom view», la visualización de los campos e incluyendo codificación.
- Campo «Status»

if ( $value === '0') {
$checked = '';
} else {
$checked = 'checked';
}
$html=<<<EOT
<wa-switch size="l" disabled $checked></wa-switch>
EOT;
$value=$html;
- Campo «Images»
$id = $data['id'];
// get information about uploaded files
$fileArray = my_json_decode($value);
$html = "<div id=\"image_$id\" style=\"width: 250px\"><wa-carousel pagination navigation mouse-dragging loop>";
$dialog = <<<EOT
<wa-dialog label="Muestra las Imaganes del registros: $id" id="dialog-$id" class="dialog-width" style="--width: 75%; --height: 100vh" >
<wa-carousel pagination navigation mouse-dragging loop>
EOT;
// rename each file. In this example - convert to lowercase.
for($i = 0; $i < count($fileArray); $i++)
{
$fileTH = $fileArray[$i]["thumbnail"]; // URL miniatura
$fileNM = $fileArray[$i]["name"]; // URL fichero grande
$html .= <<<EOT
<wa-carousel-item>
<img
alt="fichero miniatura de la imagen cargada"
src="./$fileTH"
/>
</wa-carousel-item>
EOT;
$dialog .= <<<EOT
<wa-carousel-item>
<img
alt="fichero de la imagen cargada"
src="./$fileNM"
/>
</wa-carousel-item>
EOT;
}
$html .= "</wa-carousel><wa-button id=\"visualizar-$id\" appearance=\"filled\">Visualizar</wa-button></div>";
$dialog .= "</wa-carousel><wa-button appearance=\"filled\" slot=\"footer\" variant=\"brand\" data-dialog=\"close\">Cerrar</wa-button></wa-dialog>";
$html .= <<<EOT
<script>
const dialog_$id = document.querySelector('#dialog-$id');
/* const openButton_$id = dialog_$id.nextElementSibling; */
const openButton_$id = document.querySelector('#visualizar-$id');
openButton_$id.addEventListener('click', () => (dialog_$id.open = true));
</script>
EOT;
$value=$dialog.$html;
En este caso utilizamos 2 web components «wa-carousel» y «wa-dialog«.
«wa-carousel» se utiliza para mostrar el carrusel de las miniaturas y las imágenes completas.
«wa-dialog», para mostrar una venta modal con las imágenes en grande.
Una parte importante a observar, es que se ha dado «id» a las partes del HTML construido, requerido para el JavaScript que interactúa entre ellos. Como debe ser un «id» único, se ha utilizado el campos «id» del registro que obligatoriamente es único.
Espero que este ejemplo os sirva para entender el funcionamiento y analizar las posibilidades de modernización del interface de PHPRunner. Si no en todas las páginas, en las que se considere imprescindible en vuestras aplicaciones.
Terminado este artículo, seguí revisando implementaciones de terceros y encontré Material Web de Google. Me gusta muchísimo este UI y por ello, lo he estado probando y aquí explico mis pruebas.
🌐 Qué es Material Web
Material Web es la implementación oficial de Material Design en forma de Web Components. A diferencia de versiones anteriores (Material para Angular, Material UI para React, etc.), esta librería está construida directamente sobre estándares del navegador, sin depender de ningún framework.
Esto significa que sus componentes:
- funcionan en cualquier proyecto HTML
- se integran en React, Svelte, Vue, Angular o Vanilla JS
- no requieren compilación especial
- son totalmente reutilizables y encapsulados
🧩 Por qué es importante para los frameworks modernos
✔️ 1. Un único código funciona en todos los frameworks
- Material para Angular
- Material Components for Web (MDC)
- Material UI (React) mantenido por la comunidad
- Variantes no oficiales para Vue, Svelte, etc.
Un único componente funciona en todos los frameworks.
✔️ 2. Los frameworks modernos ya están optimizados para Web Components
React, Vue, Angular y Svelte han ido añadiendo soporte nativo para Custom Elements. Hoy, Material Web se integra sin hacks ni adaptadores en:
- Svelte 5 (mi caso)
- React 18+
- Vue 3
- Angular 17+
✔️ 3. Es la forma más estable de usar Material Design
Material Web es la implementación oficial de Material Design.
🏢 Quién mantiene y evoluciona Material Web
Material Web está desarrollado y mantenido por el equipo oficial de Material Design de Google
Esto implica:
- soporte continuo
- actualizaciones alineadas con Material Design
- accesibilidad certificada
- rendimiento optimizado
- documentación oficial y ejemplos reales
Además, el repositorio es público y activo:
- Google revisa PRs
- Google publica releases
- Google marca la hoja de ruta
Esto lo diferencia de otras librerías UI que dependen de comunidades externas.
🚀 Por qué es relevante esta solución
Material Web es hoy:
- la implementación más moderna de Material Design
- la más compatible con cualquier stack
- la más fácil de integrar en proyectos PHP, ASP, JAVA, Vanilla, etc.
- una buena forma de aprender Web Components reales
🌀Pero……
- el producto está en mantenimiento.
- no hay planes de evolución, ni de incorporar nuevos componentes.
Como resumen particular, creo que esta implementación es muy buena y aunque no tenga asegurado su futuro, que me gustaría mucho, es lo que suele ocurrir con todos los temas de implementación de soluciones.
DEMO: https://fhumanes.com/wc-material/ y https://fhumanes.com/wc-material/index2.html

Es muy sencillo la personalización de los colores y del UI, siendo además muy fácil de definir los campos y sus validaciones.
Los 2 ejemplos es porque he utilizado 2 formas de incluir la librería en la página. En el primer caso se utiliza un Bundle que he construido con NODE.js y que también os por si deseáis re-elaborarlo vosotros o simplificarlo, pues en mi caso he incluido todo los elementos de la solución.
Para hacer el Bundle (librería a utilizar en local) dejo del proyecto Vanilla JS, realizado con NODE.js, se instala con cualquier otro proyecto:
- una vez instalado Node.js
- situarse en el directorio que dees y ahí recuperar el fichero ZIP que os facilito.
- se instala con npm install
Os explico lo ficheros más relevantes.
{
"name": "material-bundle",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@material/web": "^2.4.1",
"vite": "^8.0.16"
}
}
// src/material.js import '@material/web/all.js';
/** * @license * Copyright 2023 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview A convenience bundle import that includes all components. * * WARNING: This import is intended for prototyping and development builds only. * Import only the individual components used for production. */ // LINT.IfChange(imports) // go/keep-sorted start import './button/elevated-button.js'; import './button/filled-button.js'; import './button/filled-tonal-button.js'; import './button/outlined-button.js'; import './button/text-button.js'; import './checkbox/checkbox.js'; import './chips/assist-chip.js'; import './chips/chip-set.js'; import './chips/filter-chip.js'; import './chips/input-chip.js'; import './chips/suggestion-chip.js'; import './dialog/dialog.js'; import './divider/divider.js'; import './elevation/elevation.js'; import './fab/branded-fab.js'; import './fab/fab.js'; import './field/filled-field.js'; import './field/outlined-field.js'; import './focus/md-focus-ring.js'; import './icon/icon.js'; import './iconbutton/filled-icon-button.js'; import './iconbutton/filled-tonal-icon-button.js'; import './iconbutton/icon-button.js'; import './iconbutton/outlined-icon-button.js'; import './list/list.js'; import './list/list-item.js'; import './menu/menu.js'; import './menu/menu-item.js'; import './menu/sub-menu.js'; import './progress/circular-progress.js'; import './progress/linear-progress.js'; import './radio/radio.js'; import './ripple/ripple.js'; import './select/filled-select.js'; import './select/outlined-select.js'; import './select/select-option.js'; import './slider/slider.js'; import './switch/switch.js'; import './tabs/primary-tab.js'; import './tabs/secondary-tab.js'; import './tabs/tabs.js'; import './textfield/filled-text-field.js'; import './textfield/outlined-text-field.js'; // go/keep-sorted end // LINT.ThenChange(:exports) // LINT.IfChange(exports) // go/keep-sorted start export * from './button/elevated-button.js'; export * from './button/filled-button.js'; export * from './button/filled-tonal-button.js'; export * from './button/outlined-button.js'; export * from './button/text-button.js'; export * from './checkbox/checkbox.js'; export * from './chips/assist-chip.js'; export * from './chips/chip-set.js'; export * from './chips/filter-chip.js'; export * from './chips/input-chip.js'; export * from './chips/suggestion-chip.js'; export * from './dialog/dialog.js'; export * from './divider/divider.js'; export * from './elevation/elevation.js'; export * from './fab/branded-fab.js'; export * from './fab/fab.js'; export * from './field/filled-field.js'; export * from './field/outlined-field.js'; export * from './focus/md-focus-ring.js'; export * from './icon/icon.js'; export * from './iconbutton/filled-icon-button.js'; export * from './iconbutton/filled-tonal-icon-button.js'; export * from './iconbutton/icon-button.js'; export * from './iconbutton/outlined-icon-button.js'; export * from './list/list.js'; export * from './list/list-item.js'; export * from './menu/menu.js'; export * from './menu/menu-item.js'; export * from './menu/sub-menu.js'; export * from './progress/circular-progress.js'; export * from './progress/linear-progress.js'; export * from './radio/radio.js'; export * from './ripple/ripple.js'; export * from './select/filled-select.js'; export * from './select/outlined-select.js'; export * from './select/select-option.js'; export * from './slider/slider.js'; export * from './switch/switch.js'; export * from './tabs/primary-tab.js'; export * from './tabs/secondary-tab.js'; export * from './tabs/tabs.js'; export * from './textfield/filled-text-field.js'; export * from './textfield/outlined-text-field.js'; // go/keep-sorted end // LINT.ThenChange(:imports) //# sourceMappingURL=all.js.map
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: 'src/material.js',
name: 'MaterialBundle',
fileName: 'material-bundle',
formats: ['es']
},
rollupOptions: {
output: {
// No queremos dividir en chunks
manualChunks: () => 'everything.js'
}
}
}
});
En (1) véis que línea 12 indica la versión de Material Web, que he instalado.
En (2) es el fichero que indica los elementos que vamos a incluir en la librería. Podemos seleccionar los elementos que formarán la librería.
En (3) es el fichero que viene en la instalación y que recoge el 100% de los elementos que tiene la libreria.
En (4) es el fichero de configuración de VITE y son las instrucciones de construcción de la librería. En este caso crea 2 ficheros en el directorio DIST.
Para lo que necesitéis, podéis contactar a través del email.
Como siempre, os dejo los fuentes del ejemplo para que lo podáis modificar y probar, todo lo que necesitéis.
