Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Decoupling Drupal with Vue.js

Decoupling Drupal with Vue.js

Presented at Blue Conf 2019.

71c9ebde850996d2533c5df4df2c93c6?s=128

Oliver Davies

June 07, 2019
Tweet

Transcript

  1. Decoupling Drupal with Vue.js

  2. My a%empt at... Decoupling Drupal with Vue.js

  3. • PHP and Front End Developer • System Administrator •

    Senior Engineer at Inviqa • Part-!me freelancer • Open sourcer • @opdavies • oliverdavies.uk
  4. Drupal Content management system, wri!en in PHP

  5. Vue.js Progressive framework for building user interfaces

  6. What do I mean by decoupling?

  7. Drupal is a full stack CMS

  8. Request ➡ Drupal ➡ Twig ➡ Response

  9. Request ➡ Vue.js ➡ Response

  10. Vue.js ➡ Drupal ➡ Vue.js

  11. Why decouple?

  12. • More flexibility • Different development teams • Security •

    Exposing data to mul"ple sources • Aggrega"ng data from mul"ple sources • Back-end applica"on can be swapped out
  13. Example: conference talk submission website

  14. Configuring Drupal

  15. drush pm:enable jsonapi

  16. None
  17. Displaying sessions in Vue.js

  18. Configuring CORS in Drupal

  19. Access to XMLH!pRequest at 'h!p:/ / blueconf.docksal/jsonapi/node/session' from origin 'h!p:/

    /localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
  20. # services.local.yml cors.config: enabled: false # Specify allowed headers, like

    'x-allowed-header'. allowedHeaders: [] # Specify allowed request methods, specify ['*'] to allow all possible ones. allowedMethods: [] # Configure requests allowed from specific origins. allowedOrigins: ['*'] # Sets the Access-Control-Expose-Headers header. exposedHeaders: false # Sets the Access-Control-Max-Age header. maxAge: false # Sets the Access-Control-Allow-Credentials header. supportsCredentials: false
  21. # services.local.yml cors.config: enabled: true allowedHeaders: [ 'x-csrf-token', 'authorization', 'content-type',

    'accept', 'origin', 'x-requested-with', 'access-control-allow-origin', 'x-allowed-header', '*' ] allowedMethods: ['*'] allowedOrigins: ['http://localhost:8080'] exposedHeaders: true maxAge: false supportsCredentials: true
  22. Configuring Vue.js

  23. None
  24. .env APP_VUE_DRUPAL_URL=http://blueconf.docksal

  25. src/App.vue <script> import _ from 'lodash' import AcceptedSessionsList from '@/components/AcceptedSessionsList'

    import SessionForm from '@/components/SessionForm' const axios = require('axios') export default { // ... } </script>
  26. src/App.vue components: { AcceptedSessionsList, SessionForm }

  27. src/App.vue data () { return { loaded: false, sessions: []

    } }
  28. src/App.vue mounted () { const baseUrl = process.env.VUE_APP_DRUPAL_URL axios.get(`${baseUrl}/jsonapi/node/session`) .then(({

    data }) => { this.loaded = true this.sessions = data.data }) }
  29. src/App.vue computed: { sortedSessions: function () { return _(this.sessions) .sortBy(({

    attributes }) => attributes.title) } }
  30. src/App.vue <template> <div id="app" class="antialiased min-h-screen font-sans bg-gray-100 text-black p-12">

    <div class="w-full max-w-2xl mx-auto"> <accepted-sessions-list :sessions="sortedSessions" /> <session-form /> </div> </div> </template>
  31. src/components/AcceptedSessionsList.vue props: { sessions: { type: Object, required: true }

    }
  32. src/components/AcceptedSessionsList.vue computed: { acceptedSessions: function () { return this.sessions .filter(session

    => this.isAccepted(session)) .value() } }
  33. src/components/AcceptedSessionsList.vue methods: { isAccepted: function ({ attributes }) { return

    attributes.field_session_status === 'accepted' } }
  34. src/components/AcceptedSessionsList.vue <template> <div> <h1 class="text-4xl font-semibold mb-2">Sessions</h1> <div v-if="acceptedSessions.length >

    0" class="bg-white p-6 rounded-lg border"> <ul class="-mb-3"> <li v-for="{ attributes } in acceptedSessions" :key="attributes.drupal_internal__nid" class="mb-3" > {{ attributes.title }} </li> </ul> </div> </div> </template>
  35. Crea%ng sessions: POSTing back to Drupal

  36. None
  37. None
  38. None
  39. drush user:create api

  40. drush user:role:add api_user api

  41. SessionForm.vue <template> <section class="mt-8"> <form @submit.prevent="submit"> <label class="block mb-4"> Title

    <input name="title" type="text" v-model="form.title" required /> </label> <label class="block mb-4"> Abstract <textarea name="title" rows="5" v-model="form.body" required /> </label> <input class="cursor-pointer bg-blue-500 hover:bg-blue-700 focus:bg-blue-700 text-gray-100 px-4 py-2 rounded" type="submit" value="Submit session"> </form> </section> </template>
  42. SessionForm.vue data () { return { form: { body: '',

    field_session_status: 'accepted', field_session_type: 'full', title: '' } } },
  43. SessionForm.vue methods: { submit () { const adminUuid = '11dad4c2-baa8-4fb2-97c6-12e1ce925806'

    const apiUuid = '63936126-87cd-4166-9cb4-63b61a210632' // ... } }
  44. SessionForm.vue methods: { submit () { // ... const data

    = { type: 'node--session', attributes: this.form, relationships: { 'field_speakers': { 'data': { 'id': adminUuid, 'type': 'user--user' } }, 'uid': { 'data': { 'id': apiUuid, 'type': 'user--user' } } } } } }
  45. SessionForm.vue const baseUrl = process.env.VUE_APP_DRUPAL_URL axios({ method: 'post', url: `${baseUrl}/jsonapi/node/session`,

    data: { data }, headers: { 'Accept': 'application/vnd.api+json', 'Authorization': 'Basic YXBpOmFwaQ==', 'Content-Type': 'application/vnd.api+json' } })
  46. SessionForm.vue // ... .then(({ data }) => { const title

    = data.data.attributes.title this.messages.push(`Session ${title} has been created.`) this.$emit('submitted', data.data) this.form.body = '' this.form.title = '' })
  47. App.vue <session-form @submitted="addSession($event)" /> methods: { addSession: function (session) {

    this.sessions.push(session) } }
  48. SessionForm.vue // ... .catch(({ response: { data } }) =>

    { this.errors = _(data.errors).map('detail').value() })
  49. Demo

  50. Thanks! opdavi.es/drupal-vuejs @opdavies oliverdavies.uk