SlideShare a Scribd company logo
Shopware PWA - a technical overview of
A technical overview of
Shopware PWA
Shopware PWA - a technical overview of
The Stack
Shopware 6
Store API
PWA application
Symfony
RESTful API endpoints
VueJS app
The Stack
Shopware 6
Store API
PWA application
Symfony
RESTful API endpoints
VueJS app
PWA Application
Default Theme
Shopware PWA
Nuxt JS Packages
Based on Atomic Design
State Management
Modularity
Routing
StorefrontUI
Custom Theme
PWA Application
Shopware PWA
Nuxt JS Packages
Based on Atomic Design
State Management
Modularity
Routing
Default ThemeStorefrontUI
Custom Theme
Shopware PWA
https://github.com/DivanteLtd/shopware-pwa/tree/master/packages
• ShopwarePWA is split up into different packages which are separately available
via NPM
• Easy to replace a package
• Setup follows a "typical" Nuxt project
What's in the box
CLI
$ shopware-pwa …
• build creates the build for production
• cms reads from the Shopware CMS
• dev is the hot reload server for local development
• init is the way to go to start a project
• languages fetches locales from Shopware CMS
• override quickly override components
• plugins gathers assets
• snippets fetches the text snippets from Shopware CMS
What's in the box
Shopware 6 Client
• Javascript SDK for the Store API
• Can be used standalone, no composables dependency
• Endpoints.ts provides mapping
• Services are collections of methods around a specific domain
• Use apiService.invoke.get/post/put/… for custom APIs
What's in the box
Composables
• Provides interfaces for the Shopware 6 Client
• Composables provide business logic to the template
• These should be used in everyday development
• Backwards Compatible between versions
What's in the box
Default Theme
• Components, Layouts, Pages
• CMS contains elements we recognize from Shopware CMS
• cmsMap.json connects the vuejs component with the
Shopware CMS element
• Reuse where possible
• Great source for examples
Your local Project
Setup Shopware Backend
1. Install Shopware 6.3 (latest) with sample data
2. $ cd custom/plugin && git clone https://github.com/elkmod/SwagShopwarePwa
3. $ bin/console plugin:refresh && bin/console plugin:install --activate SwagShopwarePwa
4. $ bin/console dal:refresh:index
Your local Project
Your local Project
UPDATE `sales_channel_domain` SET `url` = 'http://192.168.1.105:3000/'
WHERE `sales_channel_domain`.`id` = CAST(0x8e2582ea9f7d4fb0ac76cf9dde71b3f3 AS BINARY);
Your local Project
Setup Shopware PWA
1. $ yarn add @shopware-pwa/cli
2. $ node_modules/.bin/shopware-pwa init
3. $ yarn dev
Your local Project
Building a plugin for
Shopware PWA
Building a plugin
Building a plugin
• https://fakestoreapi.com/products
• Personalized greeting
• VS code as IDE
• Vetur installed (https://marketplace.visualstudio.com/items?itemName=octref.vetur)
• Vue.js devtools (for Chrome)
Building a plugin
• custom plugins go in sw-plugins
• or src/Resources/app/pwa in your Shopware plugin
• config.json tells ShopwarePWA about the plugin
Shopware PWA - a technical overview of
Shopware PWA - a technical overview of
config.json
{
"slots": [
{
"name": "product-page-details-after",
"file": "productListing.vue"
}
],
"settings": {}
}
Shopware PWA - a technical overview of
productListing.vue
<template>
<section>
<header class="sf-heading">
<h2 class="sf-heading__title
sf-heading__title--h2">
Have you seen these?
</h2>
<div class="sf-heading__subtitle">
#MADEFOR{{ userNameLabel.toUpperCase() }}
</div>
</header>
</section>
</template>
productListing.vue
<template>
<section>
<header class="sf-heading">
<h2 class="sf-heading__title
sf-heading__title--h2">
Have you seen these?
</h2>
<div class="sf-heading__subtitle">
#MADEFOR{{ userNameLabel.toUpperCase() }}
</div>
</header>
</section>
</template>
<script>
import { useUser } from '@shopware-pwa/composables';
import { computed } from '@vue/composition-api'
export default {
name: "ProductRecommendations",
setup() {
const { user } = useUser();
const userNameLabel = computed(() => {
return user?.value?.firstName || 'YOU'
})
return { userNameLabel }
}
}
</script>
Shopware PWA - a technical overview of
reusing components
reusing components
[...]
<template v-if="!isListView">
<SwProductCard
v-for="(product, i) in products"
:key="product.id"
class="cms-element-product-listing__product-card"
:product="product"
:style="{ '--index': i }"
/>
</template>
[...]
shopware-pwa/packages/default-theme/components/SwProductListing.vue
reusing components
<template>
<SfProductCard
:title="getName"
:image="getImageUrl"
:special-price="getSpecialPrice | price"
:regular-price="getRegularPrice | price"
:max-rating="5"
:score-rating="getProductRating"
:image-width="700"
:image-height="1000"
:link="getRouterLink"
class="sw-product-card"
:show-add-to-cart-button="true"
:is-added-to-cart="isInCart"
@click:add-to-cart="addToCart"
:wishlistIcon="false"
>
</SfProductCard>
</template>
<script>
import { SfProductCard, SfAddToCart } from "@storefront-ui/vue"
shopware-pwa/packages/default-theme/components/SwProductCard.vue
productListing.vue
<div class="cms-element-product-listing">
<SfLoader :loading="isLoading" class="cms-element-product-listing__loader" />
<div v-if="recommendations.length" class="cms-element-product-listing__wrapper">
<transition-group
[...]
:class="{ 'cms-element-product-listing__list--blur': isLoading }"
>
<SfProductCard
v-for="(item, i) in recommendations"
[...]
:show-add-to-cart-button="false"
:link="'#'"
:is-added-to-cart="false"
:wishlistIcon="false"
>
</SfProductCard>
</transition-group>
</div>
</div>
productListing.vue
<script>
[...]
import { SfProductCard, SfLoader } from "@storefront-ui/vue"
[...]
export default {
name: "ProductRecommendations",
components: {
SfProductCard,
SfLoader
},
setup() {
const isLoading = true;
[...]
https://fakestoreapi.com/products
[
{
"id": 1,
"title": "Fjallraven - Foldsack No. 1 Backpack, Fits 15
Laptops",
"price": 109.95,
"description": "Your perfect pack for everyday use and walks
in the forest. Stash your laptop (up to 15 inches) in the
padded sleeve, your everyday",
"category": "men clothing",
"image":
"https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"
}
}
useRecommendations.js
import axios from 'axios';
import { ref } from '@vue/composition-api';
const useRecommendations = (productId) => {
const recommendations = ref([])
const getRecommendations = async () => {
let { data } = await axios.get(`https://fakestoreapi.com/products?productId=${productId}`);
recommendations.value = data.slice(0, 5).map((item) => {
return {
id: item.id,
title: item.title,
image: item.image,
price: `€${item.price}`,
specialPrice: null,
rating: Math.floor(Math.random() * 3) + 2
}
})
}
getRecommendations()
return { recommendations }
}
export { useRecommendations }
productListing.vue
<script>
import { useRecommendations } from './useRecommendations'
[...]
export default {
[...]
props: {
slotContext: {
type: Object,
default: null
}
},
setup(props) {
const { user } = useUser();
const { recommendations } = useRecommendations(props.slotContext.id);
[...]
return {
[...]
recommendations
}
}
}
</script>
Shopware PWA - a technical overview of
productListing.vue
<div v-if="recommendations.length" class="cms-element-product-listing__wrapper">
<transition-group [...]>
<SfProductCard
v-for="(item, i) in recommendations"
:key="item.id"
:title="item.title"
:image="item.image"
:special-price="item.specialPrice"
:regular-price="item.price"
:max-rating="5"
:score-rating="item.rating"
:image-width="700"
:image-height="1000"
class="sw-product-card"
:style="{ '--index': i }"
:show-add-to-cart-button="false"
:link="'#'"
:is-added-to-cart="false"
:wishlistIcon="false"
>
</SfProductCard>
</transition-group>
</div>
Shopware PWA - a technical overview of
useRecommendations.js
[...]
const useRecommendations = (productId) => {
const recommendations = ref([])
const apiError = ref('')
const isLoading = ref(true)
const getRecommendations = async () => {
isLoading.value = true
try {
let { data } = await axios.get('https://fakestoreapi.com/products?productId=' + productId);
[...]
isLoading.value = false
} catch(error) {
apiError.value = error.response.data.message
}
}
getRecommendations()
return {
recommendations,
apiError,
isLoading
}
[...]
productListing.vue
<script>
[...]
setup(props) {
const { user } = useUser();
const {
recommendations,
apiError,
isLoading
} = useRecommendations(props.slotContext.id)
const userNameLabel = computed(() => {
return user?.value?.firstName || 'YOU'
})
watch (apiError, (errorMessage) => {
console.error(errorMessage)
})
return {
userNameLabel,
isLoading,
recommendations
}
}
[...]
</script>
http://bit.ly/swpwa-product-recommendations
Sander Mangel
https://twitter.com/sandermangel
https://www.linkedin.com/in/sandermangel/

More Related Content

Shopware PWA - a technical overview of

  • 2. A technical overview of Shopware PWA
  • 4. The Stack Shopware 6 Store API PWA application Symfony RESTful API endpoints VueJS app
  • 5. The Stack Shopware 6 Store API PWA application Symfony RESTful API endpoints VueJS app
  • 6. PWA Application Default Theme Shopware PWA Nuxt JS Packages Based on Atomic Design State Management Modularity Routing StorefrontUI Custom Theme
  • 7. PWA Application Shopware PWA Nuxt JS Packages Based on Atomic Design State Management Modularity Routing Default ThemeStorefrontUI Custom Theme
  • 8. Shopware PWA https://github.com/DivanteLtd/shopware-pwa/tree/master/packages • ShopwarePWA is split up into different packages which are separately available via NPM • Easy to replace a package • Setup follows a "typical" Nuxt project
  • 9. What's in the box CLI $ shopware-pwa … • build creates the build for production • cms reads from the Shopware CMS • dev is the hot reload server for local development • init is the way to go to start a project • languages fetches locales from Shopware CMS • override quickly override components • plugins gathers assets • snippets fetches the text snippets from Shopware CMS
  • 10. What's in the box Shopware 6 Client • Javascript SDK for the Store API • Can be used standalone, no composables dependency • Endpoints.ts provides mapping • Services are collections of methods around a specific domain • Use apiService.invoke.get/post/put/… for custom APIs
  • 11. What's in the box Composables • Provides interfaces for the Shopware 6 Client • Composables provide business logic to the template • These should be used in everyday development • Backwards Compatible between versions
  • 12. What's in the box Default Theme • Components, Layouts, Pages • CMS contains elements we recognize from Shopware CMS • cmsMap.json connects the vuejs component with the Shopware CMS element • Reuse where possible • Great source for examples
  • 13. Your local Project Setup Shopware Backend 1. Install Shopware 6.3 (latest) with sample data 2. $ cd custom/plugin && git clone https://github.com/elkmod/SwagShopwarePwa 3. $ bin/console plugin:refresh && bin/console plugin:install --activate SwagShopwarePwa 4. $ bin/console dal:refresh:index
  • 15. Your local Project UPDATE `sales_channel_domain` SET `url` = 'http://192.168.1.105:3000/' WHERE `sales_channel_domain`.`id` = CAST(0x8e2582ea9f7d4fb0ac76cf9dde71b3f3 AS BINARY);
  • 16. Your local Project Setup Shopware PWA 1. $ yarn add @shopware-pwa/cli 2. $ node_modules/.bin/shopware-pwa init 3. $ yarn dev
  • 18. Building a plugin for Shopware PWA
  • 20. Building a plugin • https://fakestoreapi.com/products • Personalized greeting • VS code as IDE • Vetur installed (https://marketplace.visualstudio.com/items?itemName=octref.vetur) • Vue.js devtools (for Chrome)
  • 21. Building a plugin • custom plugins go in sw-plugins • or src/Resources/app/pwa in your Shopware plugin • config.json tells ShopwarePWA about the plugin
  • 26. productListing.vue <template> <section> <header class="sf-heading"> <h2 class="sf-heading__title sf-heading__title--h2"> Have you seen these? </h2> <div class="sf-heading__subtitle"> #MADEFOR{{ userNameLabel.toUpperCase() }} </div> </header> </section> </template>
  • 27. productListing.vue <template> <section> <header class="sf-heading"> <h2 class="sf-heading__title sf-heading__title--h2"> Have you seen these? </h2> <div class="sf-heading__subtitle"> #MADEFOR{{ userNameLabel.toUpperCase() }} </div> </header> </section> </template> <script> import { useUser } from '@shopware-pwa/composables'; import { computed } from '@vue/composition-api' export default { name: "ProductRecommendations", setup() { const { user } = useUser(); const userNameLabel = computed(() => { return user?.value?.firstName || 'YOU' }) return { userNameLabel } } } </script>
  • 30. reusing components [...] <template v-if="!isListView"> <SwProductCard v-for="(product, i) in products" :key="product.id" class="cms-element-product-listing__product-card" :product="product" :style="{ '--index': i }" /> </template> [...] shopware-pwa/packages/default-theme/components/SwProductListing.vue
  • 31. reusing components <template> <SfProductCard :title="getName" :image="getImageUrl" :special-price="getSpecialPrice | price" :regular-price="getRegularPrice | price" :max-rating="5" :score-rating="getProductRating" :image-width="700" :image-height="1000" :link="getRouterLink" class="sw-product-card" :show-add-to-cart-button="true" :is-added-to-cart="isInCart" @click:add-to-cart="addToCart" :wishlistIcon="false" > </SfProductCard> </template> <script> import { SfProductCard, SfAddToCart } from "@storefront-ui/vue" shopware-pwa/packages/default-theme/components/SwProductCard.vue
  • 32. productListing.vue <div class="cms-element-product-listing"> <SfLoader :loading="isLoading" class="cms-element-product-listing__loader" /> <div v-if="recommendations.length" class="cms-element-product-listing__wrapper"> <transition-group [...] :class="{ 'cms-element-product-listing__list--blur': isLoading }" > <SfProductCard v-for="(item, i) in recommendations" [...] :show-add-to-cart-button="false" :link="'#'" :is-added-to-cart="false" :wishlistIcon="false" > </SfProductCard> </transition-group> </div> </div>
  • 33. productListing.vue <script> [...] import { SfProductCard, SfLoader } from "@storefront-ui/vue" [...] export default { name: "ProductRecommendations", components: { SfProductCard, SfLoader }, setup() { const isLoading = true; [...]
  • 34. https://fakestoreapi.com/products [ { "id": 1, "title": "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops", "price": 109.95, "description": "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday", "category": "men clothing", "image": "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg" } }
  • 35. useRecommendations.js import axios from 'axios'; import { ref } from '@vue/composition-api'; const useRecommendations = (productId) => { const recommendations = ref([]) const getRecommendations = async () => { let { data } = await axios.get(`https://fakestoreapi.com/products?productId=${productId}`); recommendations.value = data.slice(0, 5).map((item) => { return { id: item.id, title: item.title, image: item.image, price: `€${item.price}`, specialPrice: null, rating: Math.floor(Math.random() * 3) + 2 } }) } getRecommendations() return { recommendations } } export { useRecommendations }
  • 36. productListing.vue <script> import { useRecommendations } from './useRecommendations' [...] export default { [...] props: { slotContext: { type: Object, default: null } }, setup(props) { const { user } = useUser(); const { recommendations } = useRecommendations(props.slotContext.id); [...] return { [...] recommendations } } } </script>
  • 38. productListing.vue <div v-if="recommendations.length" class="cms-element-product-listing__wrapper"> <transition-group [...]> <SfProductCard v-for="(item, i) in recommendations" :key="item.id" :title="item.title" :image="item.image" :special-price="item.specialPrice" :regular-price="item.price" :max-rating="5" :score-rating="item.rating" :image-width="700" :image-height="1000" class="sw-product-card" :style="{ '--index': i }" :show-add-to-cart-button="false" :link="'#'" :is-added-to-cart="false" :wishlistIcon="false" > </SfProductCard> </transition-group> </div>
  • 40. useRecommendations.js [...] const useRecommendations = (productId) => { const recommendations = ref([]) const apiError = ref('') const isLoading = ref(true) const getRecommendations = async () => { isLoading.value = true try { let { data } = await axios.get('https://fakestoreapi.com/products?productId=' + productId); [...] isLoading.value = false } catch(error) { apiError.value = error.response.data.message } } getRecommendations() return { recommendations, apiError, isLoading } [...]
  • 41. productListing.vue <script> [...] setup(props) { const { user } = useUser(); const { recommendations, apiError, isLoading } = useRecommendations(props.slotContext.id) const userNameLabel = computed(() => { return user?.value?.firstName || 'YOU' }) watch (apiError, (errorMessage) => { console.error(errorMessage) }) return { userNameLabel, isLoading, recommendations } } [...] </script>