Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Decoupling Drupal with Vue.js
Search
Oliver Davies
June 07, 2019
Programming
0
2.7k
Decoupling Drupal with Vue.js
Presented at Blue Conf 2019.
Oliver Davies
June 07, 2019
Tweet
Share
More Decks by Oliver Davies
See All by Oliver Davies
Building Static Websites with Sculpin
opdavies
0
1.7k
Taking Flight with Tailwind CSS
opdavies
0
5.5k
TDD - Test Driven Drupal
opdavies
0
4.2k
Building "Build Configs"
opdavies
0
550
Communities and contribution
opdavies
0
270
Working without Workspace
opdavies
0
320
Things you should know about PHP
opdavies
1
870
An Introduction to Mob Programming
opdavies
0
380
Deploying PHP applications with Ansible, Ansible Vault and Ansistrano
opdavies
0
6.5k
Other Decks in Programming
See All in Programming
CSC307 Lecture 08
javiergs
PRO
0
670
ぼくの開発環境2026
yuzneri
0
230
コマンドとリード間の連携に対する脅威分析フレームワーク
pandayumi
1
450
Best-Practices-for-Cortex-Analyst-and-AI-Agent
ryotaroikeda
1
110
Vibe Coding - AI 驅動的軟體開發
mickyp100
0
180
なぜSQLはAIぽく見えるのか/why does SQL look AI like
florets1
0
460
AI Agent の開発と運用を支える Durable Execution #AgentsInProd
izumin5210
7
2.3k
AIエージェントのキホンから学ぶ「エージェンティックコーディング」実践入門
masahiro_nishimi
5
460
それ、本当に安全? ファイルアップロードで見落としがちなセキュリティリスクと対策
penpeen
7
3.9k
AgentCoreとHuman in the Loop
har1101
5
240
360° Signals in Angular: Signal Forms with SignalStore & Resources @ngLondon 01/2026
manfredsteyer
PRO
0
130
CSC307 Lecture 01
javiergs
PRO
0
690
Featured
See All Featured
The Anti-SEO Checklist Checklist. Pubcon Cyber Week
ryanjones
0
67
How to optimise 3,500 product descriptions for ecommerce in one day using ChatGPT
katarinadahlin
PRO
0
3.4k
HU Berlin: Industrial-Strength Natural Language Processing with spaCy and Prodigy
inesmontani
PRO
0
220
The State of eCommerce SEO: How to Win in Today's Products SERPs - #SEOweek
aleyda
2
9.5k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
16k
The Invisible Side of Design
smashingmag
302
51k
Stewardship and Sustainability of Urban and Community Forests
pwiseman
0
110
The B2B funnel & how to create a winning content strategy
katarinadahlin
PRO
1
270
Mozcon NYC 2025: Stop Losing SEO Traffic
samtorres
0
140
Leveraging Curiosity to Care for An Aging Population
cassininazir
1
160
We Have a Design System, Now What?
morganepeng
54
8k
How STYLIGHT went responsive
nonsquared
100
6k
Transcript
Decoupling Drupal with Vue.js
My a%empt at... Decoupling Drupal with Vue.js
• PHP and Front End Developer • System Administrator •
Senior Engineer at Inviqa • Part-!me freelancer • Open sourcer • @opdavies • oliverdavies.uk
Drupal Content management system, wri!en in PHP
Vue.js Progressive framework for building user interfaces
What do I mean by decoupling?
Drupal is a full stack CMS
Request ➡ Drupal ➡ Twig ➡ Response
Request ➡ Vue.js ➡ Response
Vue.js ➡ Drupal ➡ Vue.js
Why decouple?
• 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
Example: conference talk submission website
Configuring Drupal
drush pm:enable jsonapi
None
Displaying sessions in Vue.js
Configuring CORS in Drupal
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.
# 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
# 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
Configuring Vue.js
None
.env APP_VUE_DRUPAL_URL=http://blueconf.docksal
src/App.vue <script> import _ from 'lodash' import AcceptedSessionsList from '@/components/AcceptedSessionsList'
import SessionForm from '@/components/SessionForm' const axios = require('axios') export default { // ... } </script>
src/App.vue components: { AcceptedSessionsList, SessionForm }
src/App.vue data () { return { loaded: false, sessions: []
} }
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 }) }
src/App.vue computed: { sortedSessions: function () { return _(this.sessions) .sortBy(({
attributes }) => attributes.title) } }
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>
src/components/AcceptedSessionsList.vue props: { sessions: { type: Object, required: true }
}
src/components/AcceptedSessionsList.vue computed: { acceptedSessions: function () { return this.sessions .filter(session
=> this.isAccepted(session)) .value() } }
src/components/AcceptedSessionsList.vue methods: { isAccepted: function ({ attributes }) { return
attributes.field_session_status === 'accepted' } }
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>
Crea%ng sessions: POSTing back to Drupal
None
None
None
drush user:create api
drush user:role:add api_user api
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>
SessionForm.vue data () { return { form: { body: '',
field_session_status: 'accepted', field_session_type: 'full', title: '' } } },
SessionForm.vue methods: { submit () { const adminUuid = '11dad4c2-baa8-4fb2-97c6-12e1ce925806'
const apiUuid = '63936126-87cd-4166-9cb4-63b61a210632' // ... } }
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' } } } } } }
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' } })
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 = '' })
App.vue <session-form @submitted="addSession($event)" /> methods: { addSession: function (session) {
this.sessions.push(session) } }
SessionForm.vue // ... .catch(({ response: { data } }) =>
{ this.errors = _(data.errors).map('detail').value() })
Demo
Thanks! opdavi.es/drupal-vuejs @opdavies oliverdavies.uk