Cómo crear componentes de Vue en un tema de WordPress

Índice
  1. PHP → JavaScript
  2. Sentando las bases
  3. El componente de calificación de estrellas
  4. El formulario de comentarios
  5. Filtrar recetas
  6. Conclusión

¿Estás intrigado por el título y solo quieres ver algo de código? Vaya directamente.

Este tutorial fue escrito para Vue 2 y utiliza “plantillas en línea”. Vue 3 ha dejado de usar esta característica, pero existen alternativas (como poner sus plantillas en etiquetas de script) a las que podría traducir la idea.

Hace unos meses, estaba creando un sitio web de WordPress que requeriría un formulario con un montón de campos condicionales favorables. Se requerían diferentes opciones e información para las distintas elecciones que podíamos realizar en el formulario, y nuestro cliente necesitaba control total sobre todos los campos 1 . Además, el formulario debería aparecer en varios lugares de cada página, con configuraciones ligeramente diferentes.

Y la instancia del encabezado del formulario debía ser mutuamente excluyente con el menú de hamburguesas, de modo que al abrir uno se cerrara el otro.

Y el formulario tenía contenido de texto relevante para SEO.

Y queríamos que la respuesta del servidor presentara algunos lindos comentarios animados.

(Uf.)

Todo parecía lo suficientemente complejo como para no querer manejar todo ese estado manualmente. Recordé haber leído el artículo de Sarah Drasner “Reemplazar jQuery con Vue.js: no es necesario ningún paso de compilación”, que muestra cómo reemplazar los patrones clásicos de jQuery con microaplicaciones simples de Vue. Parecía un buen punto de partida, pero rápidamente me di cuenta de que las cosas se complicarían en el lado PHP de WordPress.

Lo que realmente necesitaban eran componentes reutilizables .

PHP → JavaScript

Me encanta el enfoque estático de las herramientas Jamstack, como Nuxt , y estaba buscando hacer algo similar aquí: enviar el contenido completo desde el servidor y mejorar progresivamente en el lado del cliente.

Pero PHP no tiene una forma integrada de trabajar con componentes. Sin embargo, admite requirearchivos -ing dentro de otros archivos 2 . WordPress tiene una requirellamada de abstracción get_template_part, que se ejecuta en relación con la carpeta del tema y es más fácil trabajar con ella. Dividir el código en partes de la plantilla es lo más parecido a los componentes que proporciona WordPress 3 .

Vue, por otro lado, se trata de componentes, pero solo puede hacer su trabajo después de que la página se haya cargado y esté ejecutando JavaScript .

El secreto de esta unión de paradigmas resulta ser la directiva Vue, menos conocida inline-template. Sus grandes y maravillosos poderes nos permiten definir un componente Vue usando el marcado que ya tenemos. Es el punto medio perfecto entre obtener HTML estático del servidor y montar elementos DOM dinámicos en el cliente.

Primero, el navegador obtiene el HTML y luego Vue lo obliga a hacer cosas. Dado que el marcado lo crea WordPress, en lugar de Vue en el navegador, los componentes pueden usar fácilmente cualquier información que los administradores del sitio puedan editar. Y, a diferencia de los archivos .vue (que son excelentes para crear más aplicaciones), podemos mantener la misma separación de preocupaciones que usamos para todo el sitio: estructura y contenido en PHP, estilo en CSS y funcionalidad en JavaScript. .

Para mostrar cómo encaja todo esto, crearemos algunas funciones para un blog de recetas. Primero, agregaremos una forma para que los usuarios califiquen recetas. Luego crearemos un formulario de comentarios basado en esa calificación. Finalmente, permitiremos a los usuarios filtrar recetas según etiquetas y calificación.

Construiremos algunos componentes que comparten estado y viven en la misma página. Para que funcionen bien juntos y para que sea más fácil agregar componentes adicionales en el futuro, convertiremos toda la página en nuestra aplicación Vue y registraremos los componentes dentro de ella.

Cada componente vivirá en su propio archivo PHP y se incluirá en el tema usando get_template_part.

Sentando las bases

Hay algunas consideraciones especiales a tener en cuenta al aplicar Vue a páginas existentes. La primera es que Vue no quiere que cargues scripts dentro de él; si lo haces, envía errores siniestros a la consola. La forma más sencilla de evitar esto es agregar un elemento contenedor alrededor del contenido de cada página y luego cargar scripts fuera de ella (lo cual ya es un patrón común por todo tipo de razones). Algo como esto:

?php /* header.php */ ?body ?php body_class(); ?div
?php /* footer.php */ ? /div !-- #site-wrapper --?php wp_footer(); ?

La segunda consideración es que se debe llamar a Vue al final del elemento del cuerpo para que se cargue después de que el resto del DOM esté disponible para analizar. Pasaremos verdadero como quinto argumento ( in_footer) de la wp_enqueue_scriptfunción. Además, para asegurarnos de que Vue se cargue primero, lo registraremos como una dependencia del script principal.

?php // functions.phpadd_action( 'wp_enqueue_scripts', function() {  wp_enqueue_script('vue', get_template_directory_uri() . '/assets/js/lib/vue.js', null, null, true); // change to vue.min.js for production  wp_enqueue_script('main', get_template_directory_uri() . '/assets/js/main.js', 'vue', null, true);

Finalmente, en el guión principal, inicializaremos Vue en el site-wrapperelemento.

// main.jsnew Vue({  el: document.getElementById('site-wrapper')})

El componente de calificación de estrellas

Nuestra plantilla de publicación única actualmente se ve así:

?php /* single-post.php */ ?article  ?php /* ... post content */ ?  !-- star rating component goes here --/article

Registraremos el componente de calificación de estrellas y agregaremos algo de lógica para administrarlo:

// main.jsVue.component('star-rating', {  data () {    return {      rating: 0    }  },  methods: {    rate (i) { this.rating = i }  },  watch: {    rating (val) {      // prevent rating from going out of bounds by checking it to on every change      if (val  0)         this.rating = 0      else if (val  5)         this.rating = 5      // ... some logic to save to localStorage or somewhere else    }  }})// make sure to initialize Vue after registering all componentsnew Vue({  el: document.getElementById('site-wrapper')})

Escribiremos la plantilla del componente en un archivo PHP separado. El componente constará de seis botones (uno para no clasificados y cinco con estrellas). Cada botón contendrá un SVG con un relleno negro o transparente.

?php /* components/star-rating.php */ ?star-rating inline-template  div    pRate recipe:/p    button @click="rate(0)"      svgpath d="..." :fill="rating === 0 ? 'black' : 'transparent'"/svg    /button    button v-for="(i in 5)" @click="rate(i)"      svgpath d="..." :fill="rating = i ? 'black' : 'transparent'"/svg    /button  /div/star-rating

Como regla general, me gusta darle al elemento superior de un componente un nombre de clase que sea idéntico al del mismo componente. Esto facilita el razonamiento entre marcado y CSS (por ejemplo, star-ratingpuede considerarse como .star-rating).

Y ahora lo incluiremos en nuestra plantilla de página.

?php /* single-post.php */ ?article  ?php /* post content */ ?  ?php get_template_part('components/star-rating'); ?/article

Todo el HTML dentro de la plantilla es válido y el navegador lo entiende, excepto star-rating. Podemos hacer un esfuerzo adicional para solucionar este problema utilizando isla directiva de Vue:

div is="star-rating" inline-template.../div

Ahora decimos que la calificación máxima no es necesariamente 5, pero es controlable por el editor del sitio web usando Advanced Custom Fields , un popular complemento de WordPress que agrega campos personalizados para páginas, publicaciones y otro contenido de WordPress. Todo lo que necesitamos hacer es inyectar ese valor como accesorio del componente que llamaremos maxRating:

?php // components/star-rating.php// max_rating is the name of the ACF field$max_rating = get_field('max_rating');?div is="star-rating" inline-template :max-rating="?= $max_rating ?"  div    pRate recipe:/p    button @click="rate(0)"      svgpath d="..." :fill="rating === 0 ? 'black' : 'transparent'"/svg    /button    button v-for="(i in maxRating) @click="rate(i)"      svgpath d="..." :fill="rating = i ? 'black' : 'transparent'"/svg    /button  /div/div  

Y en nuestro script, registramos el accesorio y reemplazamos el número mágico 5:

// main.jsVue.component('star-rating', {  props: {    maxRating: {      type: Number,      default: 5 // highlight    }  },  data () {    return {      rating: 0    }  },  methods: {    rate (i) { this.rating = i }  },  watch: {    rating (val) {      // prevent rating from going out of bounds by checking it to on every change      if (val  0)         this.rating = 0      else if (val  maxRating)         this.rating = maxRating      // ... some logic to save to localStorage or somewhere else    }  }})        

Para guardar la calificación de la receta específica, necesitaremos pasar el ID de la publicación. De nuevo, la misma idea:

?php // components/star-rating.php$max_rating = get_field('max_rating');$recipe_id = get_the_ID();?div is="star-rating" inline-template :max-rating="?= $max_rating ?" recipe-id="?= $recipe_id ?"  div    pRate recipe:/p    button @click="rate(0)"      svgpath d="..." :fill="rating === 0 ? 'black' : 'transparent'"/svg    /button    button v-for="(i in maxRating) @click="rate(i)"      svgpath d="..." :fill="rating = i ? 'black' : 'transparent'"/svg    /button  /div/div 
// main.jsVue.component('star-rating', {  props: {    maxRating: {       // Same as before    },    recipeId: {      type: String,      required: true    }  },  // ...  watch: {    rating (val) {      // Same as before      // on every change, save to some storage      // e.g. localStorage or posting to a WP comments endpoint      someKindOfStorageDefinedElsewhere.save(this.recipeId, this.rating)    }  },  mounted () {    this.rating = someKindOfStorageDefinedElsewhere.load(this.recipeId)      }})

Ahora podemos incluir el mismo componente de archivo en la página de archivo (un bucle de publicaciones), sin ninguna configuración adicional:

?php // archive.phpif (have_posts()): while ( have_posts()): the_post(); ?article  ?php // Excerpt, featured image, etc. then:  get_template_part('components/star-rating'); ?/article?php endwhile; endif; ?

El formulario de comentarios

El momento en que un usuario califica una receta es una gran oportunidad para solicitar más comentarios, así que agregamos un pequeño formulario que aparece justo después de establecer la calificación.

// main.jsVue.component('feedback-form', {  props: {    recipeId: {      type: String,      required: true    },    show: { type: Boolean, default: false }  },  data () {    return {      name: '',      subject: ''      // ... other form fields    }  }})
?php // components/feedback-form.php$recipe_id = get_the_ID();?div is="feedback-form" inline-template recipe-id="?= $recipe_id ?" v-if="showForm(recipe-id)"  form    input type="text" :id="first-name-?= $recipe_id ?" v-model="name"    label for="first-name-?= $recipe_id ?"Your name/label    ?php /* ... */ ?  /form/div

Observe que estamos agregando una cadena única (en este caso recipe-id) al ID de cada elemento del formulario. Esto es para garantizar que todos tengan identificaciones únicas, incluso si hay varias copias del formulario en la página.

Entonces, ¿dónde queremos que viva esta forma? Necesita conocer la calificación de la receta para saber que necesita abrirse. Solo estamos construyendo buenos componentes, así que usamos la composición para colocar el formulario dentro de star-rating:

?php // components/star-rating.php$max_rating = get_field('max_rating');$recipe_id = get_the_ID();?div is="star-rating" inline-template :max-rating="?= $max_rating ?" recipe-id="?= $recipe_id ?"  div    pRate recipe:/p    button @click="rate(0)"      svgpath d="..." :fill="rating === 0 ? 'black' : 'transparent'"/svg    /button    button v-for="(i in maxRating) @click="rate(i)"      svgpath d="..." :fill="rating = i ? 'black' : 'transparent'"/svg    /button    ?php get_template_part('components/feedback-form'); ?  /div/div 

Si en este punto está pensando: “Realmente deberíamos componer ambos componentes en un solo componente principal que maneje el estado de calificación”, entonces concédase 10 puntos y espere pacientemente.

Una pequeña mejora progresiva que podemos agregar para que el formulario sea utilizable sin JavaScript es darle la acción tradicional de PHP y luego anularla en Vue. Lo usaremos @submit.preventpara evitar la acción original y luego ejecutaremos un submitmétodo para enviar los datos del formulario en JavaScript.

?php // components/feedback-form.php$recipe_id = get_the_ID();?div is="feedback-form" inline-template recipe-id="?= $recipe_id ?"  form action="path/to/feedback-form-handler.php"       @submit.prevent="submit"               input type="text" :id="first-name-?= $recipe_id ?" v-model="name"    label for="first-name-?= $recipe_id ?"Your name/label   !-- ... --  /form/div  

Entonces, asumiendo que queremos usar fetch, nuestro submitmétodo puede ser algo como esto:

// main.jsVue.component('feedback-form', {  // Same as before  methods: {    submit () {      const form = this.$el.querySelector('form')      const URL = form.action      const formData = new FormData(form)      fetch(URL, {method: 'POST', body: formData})        .then(result = { ... })        .catch(error = { ... })    }  }})

Bien, entonces, ¿qué queremos hacer en .theny .catch? Agreguemos un componente que mostrará comentarios en tiempo real sobre el estado de envío del formulario. Primero agreguemos el estado para rastrear el envío, el éxito y el fracaso, y una propiedad calculada que nos indica si estamos pendientes de resultados.

// main.jsVue.component('feedback-form', {  // Same as before  data () {    return {      name: '',      subject: ''      // ... other form fields      sent: false,      success: false,​​      error: null    }  },  methods: {    submit () {      const form = this.$el.querySelector('form')      const URL = form.action      const formData = new FormData(form)      fetch(URL, {method: 'POST', body: formData})        .then(result = {           this.success = true         })        .catch(error = {           this.error = error         })      this.sent = true    }  }})      

Para agregar el marcado para cada tipo de mensaje (éxito, fracaso, pendiente), podríamos crear otro componente como los demás que hemos creado hasta ahora. Pero dado que estos mensajes no tienen sentido cuando el servidor muestra la página, es mejor que los mostremos sólo cuando sea necesario. Para hacer esto, colocaremos nuestro marcado en una templateetiqueta HTML nativa, que no muestra nada en el navegador. Luego lo referenciaremos por id como plantilla de nuestro componente.

?php /* components/form-status.php */ ?template v-if="false"  div    div v-if="pending"      img src="?= get_template_directory_uri() ?/spinner.gif"      pPatience, young one./p    /div    div v-else-if="success"      img src="?= get_template_directory_uri() ?/beer.gif"      pHuzzah!/p    /div    div v-else-if="error"      img src="?= get_template_directory_uri() ?/broken.gif"      pOoh, boy. It would appear that: {{ error.text }}/p    /div  /div/template

¿Por qué agregar v-if="false"en la parte superior? Es una pequeña cosa complicada. Una vez que Vue recoge el HTML template, inmediatamente lo considerará como Vue templatey lo renderizará. A menos que, lo hayas adivinado, le digamos a Vuenotto que lo renderice. Un pequeño truco, pero ahí lo tienes.

Como solo necesitamos este marcado una vez en la página, incluiremos el componente PHP en el pie de página.

?php /* footer.php */ ?/div !-- #site-wrapper --?php get_template_part('components/form-status'); ??php wp_footer(); ? 

Ahora registraremos el componente con Vue…

// main.jsVue.component('form-status', {  template: '#form-status-component'  props: {    pending: { type: Boolean, required: true },    success: { type: Boolean, required: true },    error: { type: [Object, null], required: true },  }})

…y llámalo dentro de nuestro componente de formulario:

?php // components/feedback-form.php$recipe_id = get_the_ID();?div is="feedback-form" inline-template recipe-id="?= $recipe_id ?"  form action="path/to/feedback-form-handler.php"         @submit.prevent="submit"                   input type="text" :id="first-name-?= $recipe_id ?" v-model="name"    label for="first-name-?= $recipe_id ?"Your name/label    ?php // ... ?  /form  form-status v-if="sent" :pending="pending" :success="success" :error="error" //div 

Dado que nos registramos form-statususando Vue.component, está disponible globalmente, sin incluirlo específicamente en el directorio principal components: { }.

Filtrar recetas

Ahora que los usuarios pueden personalizar algunas partes de su experiencia en nuestro blog, podemos agregar todo tipo de funciones útiles. Específicamente, permitamos a los usuarios establecer una calificación mínima que quieran ver, usando una entrada en la parte superior de la página.
Lo primero que necesitamos es algún estado global para rastrear la calificación mínima establecida por el usuario. Dado que comenzamos inicializando una aplicación Vue en toda la página, el estado global serán solo datos en la instancia de Vue:

// main.js// Same as beforenew Vue({  el: document.getElementById('site-wrapper'),  data: {    minimumRating: 0  }})   

¿Y dónde podemos poner los controles para cambiar esto? Dado que toda la página es la aplicación, la respuesta es casi cualquier lugar. Por ejemplo, en la parte superior de la página de archivo:

?php /* archive.php */ ?label for="minimum-rating-input"Only show me recipes I've rated at or above:/labelinput type="number" v-model="minimumRating"?php if (have_posts()): while ( have_posts()): the_post(); ?article  ?php /* Post excerpt, featured image, etc. */ ?  ?php get_template_part('components/star-rating'); ?/article?php endwhile; endif; ?  

Mientras esté dentro de nuestro site-wrappercomponente y no dentro de otro componente, simplemente funcionará. Si queremos, también podríamos construir un componente de filtrado que cambiaría el estado global. Y si quisiéramos ser más sofisticados, incluso podríamos agregar Vuex a la mezcla (dado que Vuex no puede persistir el estado entre páginas de forma predeterminada, podríamos agregar algo como vuex-persist para usar localStorage).

Entonces, ahora necesitamos ocultar o mostrar una receta según el filtro. Para hacer esto, necesitaremos envolver el contenido de la receta en su propio componente, con una v-showdirectiva. Probablemente sea mejor usar el mismo componente tanto para la página única como para la página de archivo. Desafortunadamente, ninguno de los requiredos get_template_partpuede pasar parámetros al archivo llamado, pero podemos usar globalvariables:

?php /* archive.php */ ?label for="minimum-rating-input"Only show me recipes I've rated at or above:/labelinput type="number" v-model="minimumRating"?php $is_archive_item = true;if (have_posts()): while ( have_posts()): the_post();  get_template_part('components/recipe-content');endwhile; endif; ?  

Luego podemos usarlo $is_archive_itemcomo globalvariable dentro del archivo del componente PHP para verificar si está configurado y true. Como no necesitaremos ocultar el contenido en la página de publicación única, agregaremos la v-showdirectiva de manera condicional.

?php  // components/recipe-content.phpglobal $is_archive_item; ?div is="recipe-content"  article     ?php if ($is_archive_item): ?       v-show="show"    ?php endif; ?      ?php    if ($is_archive_item):      the_excerpt();    else      the_content();    endif;        get_template_part('components/star-rating');    ?  /article/div

En este ejemplo específico, también podríamos haber probado is_archive()dentro del componente, pero en la mayoría de los casos necesitaremos configurar accesorios explícitos.

Necesitaremos mover el ratingestado y la lógica al recipe-contentcomponente para que pueda saber si necesita ocultarse. En el interior star-rating, haremos una costumbre v-modelreemplazando calificación valuecon y this.rating = icon $emit('input', i)también. Entonces nuestro registro de componentes ahora se verá así:

// main.jsVue.component('recipe-content', {  data () {    rating: 0  },  watch: {    rating (val) {      // ...    }  },  mounted () {    this.rating = someKindOfStorageDefinedElsewhere.load(this.recipeId)      }})Vue.component('star-rating', {  props: {    maxRating: { /* ... */ },    recipeId: { /* ... */ },    value: { type: Number, required: true }  },  methods: {    rate (i) { this.$emit('input', i) }  },})

Agregaremos y v-modelcambiaremos a . Además, ahora podemos subir a :star-rating.phpratingvaluefeedback-formrecipe-content

?php // components/star-rating.php$max_rating = get_field('max_rating');$recipe_id = get_the_ID();?div is="star-rating"   inline-template   :max-rating="?= $ max_rating ?"   recipe-id="?= $recipe_id ?"   v-model="value"  div    pRate recipe:/p    button @click="rate(0)"      svgpath d="..." :fill="value === 0 ? 'black' : 'transparent'"/svg    /button    button v-for="(i in maxRating) @click="rate(i)"      svgpath d="..." :fill="value = i ? 'black' : 'transparent'"/svg    /button  /div/div 
?php // components/recipe-content.phpglobal $is_archive_item; ?div is="recipe-content"  article     ?php if ($is_archive_item): ?       v-show="show"    ?php endif; ?          ?php    if ($is_archive_item):      the_excerpt();    else      the_content();    endif;        get_template_part('components/star-rating');    get_template_part('components/feedback-form');    ?  /article/div 

Ahora todo está configurado para que el renderizado inicial muestre todas las recetas y luego el usuario pueda filtrarlas según su calificación. En el futuro, podríamos agregar todo tipo de parámetros para filtrar contenido. Y no tiene que basarse en la entrada del usuario: podemos permitir el filtrado según el contenido en sí (por ejemplo, número de ingredientes o tiempo de cocción) pasando los datos de PHP a Vue.

Conclusión

Bueno, fue un camino un poco largo, pero mira lo que hemos creado: componentes independientes, componibles, mantenibles, interactivos y mejorados progresivamente en nuestro tema de WordPress . ¡Reunimos lo mejor de todos los mundos!

He estado usando este enfoque en producción desde hace un tiempo y me encanta la forma en que me permite razonar sobre las diferentes partes de mis temas. Espero haberte inspirado a probarlo también.


  1. Por supuesto, dos días antes del lanzamiento, el departamento legal del cliente decidió que no quería recopilar toda esa información. Actualmente, la forma viva no es más que una sombra de su yo en desarrollo.
  2. Dato curioso: Rasmus Lerdorf dijo que su intención original era que PHP fuera solo plantillas, con toda la lógica de negocios manejada en C. Dejemos que esto se asimile por un momento. Luego, libera una hora de tu agenda y mira la charla completa.
  3. Existen motores de plantillas de WordPress de terceros que pueden compilarse en PHP optimizado. Me viene a la mente Twig , por ejemplo. Estamos intentando ir por la ruta inversa y enviar PHP básico para que sea manejado por JavaScript.
SUSCRÍBETE A NUESTRO BOLETÍN 
No te pierdas de nuestro contenido ni de ninguna de nuestras guías para que puedas avanzar en los juegos que más te gustan.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir

Este sitio web utiliza cookies para mejorar tu experiencia mientras navegas por él. Este sitio web utiliza cookies para mejorar tu experiencia de usuario. Al continuar navegando, aceptas su uso. Mas informacion