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.6k
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.2k
TDD - Test Driven Drupal
opdavies
0
4.1k
Building "Build Configs"
opdavies
0
480
Communities and contribution
opdavies
0
230
Working without Workspace
opdavies
0
270
Things you should know about PHP
opdavies
1
800
An Introduction to Mob Programming
opdavies
0
320
Deploying PHP applications with Ansible, Ansible Vault and Ansistrano
opdavies
0
6.4k
Other Decks in Programming
See All in Programming
なんとなくわかった気になるブロックテーマ入門/contents.nagoya 2025 6.28
chiilog
1
270
ニーリーにおけるプロダクトエンジニア
nealle
0
830
Is Xcode slowly dying out in 2025?
uetyo
1
270
明示と暗黙 ー PHPとGoの インターフェイスの違いを知る
shimabox
2
510
PostgreSQLのRow Level SecurityをPHPのORMで扱う Eloquent vs Doctrine #phpcon #track2
77web
2
530
5つのアンチパターンから学ぶLT設計
narihara
1
170
“いい感じ“な定量評価を求めて - Four Keysとアウトカムの間の探求 -
nealle
1
9.9k
PHP 8.4の新機能「プロパティフック」から学ぶオブジェクト指向設計とリスコフの置換原則
kentaroutakeda
2
880
Deep Dive into ~/.claude/projects
hiragram
14
2.5k
テストから始めるAgentic Coding 〜Claude Codeと共に行うTDD〜 / Agentic Coding starts with testing
rkaga
12
4.3k
なぜ適用するか、移行して理解するClean Architecture 〜構造を超えて設計を継承する〜 / Why Apply, Migrate and Understand Clean Architecture - Inherit Design Beyond Structure
seike460
PRO
3
760
地方に住むエンジニアの残酷な現実とキャリア論
ichimichi
5
1.5k
Featured
See All Featured
Thoughts on Productivity
jonyablonski
69
4.7k
Bash Introduction
62gerente
613
210k
Measuring & Analyzing Core Web Vitals
bluesmoon
7
510
The World Runs on Bad Software
bkeepers
PRO
69
11k
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
eileencodes
26
2.9k
GraphQLの誤解/rethinking-graphql
sonatard
71
11k
BBQ
matthewcrist
89
9.7k
Exploring the Power of Turbo Streams & Action Cable | RailsConf2023
kevinliebholz
34
5.9k
GraphQLとの向き合い方2022年版
quramy
49
14k
Faster Mobile Websites
deanohume
307
31k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
PRO
20
1.3k
Why You Should Never Use an ORM
jnunemaker
PRO
58
9.4k
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