Skip to content

Commit

Permalink
Merge branch 'main' into feat/api-client-new-structure
Browse files Browse the repository at this point in the history
  • Loading branch information
patzick committed May 8, 2024
2 parents e6fd9b0 + 823aa9b commit fbb97c3
Show file tree
Hide file tree
Showing 20 changed files with 489 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/eighty-islands-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"docs": minor
---

Add custom CMS element page
5 changes: 5 additions & 0 deletions .changeset/flat-eggs-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopware-pwa/composables-next": minor
---

Return `componentNameToResolve` in resolveCmsComponent function
5 changes: 5 additions & 0 deletions .changeset/smart-balloons-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopware/api-gen": minor
---

Add Blob type support
5 changes: 5 additions & 0 deletions .changeset/spotty-ties-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopware-pwa/cms-base": minor
---

Add media 3D models support
4 changes: 4 additions & 0 deletions apps/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ export const sidebar = [
text: "CMS",
link: "/getting-started/cms/",
items: [
{
text: "Custom Elements",
link: "/getting-started/cms/custom-elements",
},
{ text: "Content Pages", link: "/getting-started/cms/content-pages" },
{
text: "Customize Components",
Expand Down
137 changes: 137 additions & 0 deletions apps/docs/src/getting-started/cms/custom-elements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
---
head:
- - meta
- name: og:title
content: Custom Elements (CMS)
- - meta
- name: og:description
content: "In this chapter you will learn how to add custom elements"
- - meta
- name: og:image
content: "https://frontends-og-image.vercel.app/Custom%20Elements.png?fontSize=120px"
nav:
position: 10
---

# Custom Elements (CMS)

:::warning
This tutorial is a continuation of example from the backend part. That can be found [here](https://developer.shopware.com/docs/guides/plugins/plugins/content/cms/add-cms-element.html)
:::

All custom CMS elements created in the backend require corresponding implementations in the frontend application.

The CMS package utilizes the [resolveComponent](https://vuejs.org/api/render-function#resolvecomponent) method from Vue to identify the component returned by the backend API.
Therefore, the only requirement is to globally register the component with the appropriate name.

## Registration

### Demo store

The demo store utilizes Nuxt 3, which by default registers all components globally. For optimal application structure, we recommend adding the components to the `/components/cms` directory.

### Vue apps

[Global registration](https://vuejs.org/guide/components/registration#global-registration) in Vue apps

```ts
import CmsBlockCustomBlock from "./components/cms/CmsElementDailymotion.vue";

app.component("CmsElementDailymotion", CmsBlockCustomBlock);
```

## Naming

The component is searched in the global component register by its name.

[Resolving component in CMS package](https://github.com/shopware/frontends/blob/main/packages/composables/src/index.ts#L74)

```js
const componentNameToResolve = pascalCase(`Cms-${type}-${componentName}`);
const resolvedComponent = resolveComponent(componentNameToResolve);
```

Component name must be the same as it was registered in the backed.

```ts{3}
Shopware.Service('cmsService').registerCmsElement({
...
name: 'dailymotion',
...
});
```

Lets create new component `components/cms/element/CmsElementDailymotion.vue`

```vue
// components/cms/element/CmsElementDailymotion.vue
<script setup lang="ts">
import type { CmsSlot } from "@shopware-pwa/types";
type CmsElementDailymotion = CmsSlot & {
type: "dailymotion" | typeof String;
slot: typeof String;
config: CmsElementDailymotionConfig;
translated: {
config: CmsElementDailymotionConfig;
};
};
type CmsElementDailymotionConfig = {
dailyUrl: {
value: string;
source: "static";
};
};
const props = defineProps<{
content: CmsElementDailymotion;
}>();
</script>
<template>
<div>
<h2>Element!</h2>
<div class="sw-cms-el-dailymotion">
<div class="sw-cms-el-dailymotion-iframe-wrapper">
<iframe
frameborder="0"
type="text/html"
width="100%"
height="100%"
:src="props.content.config.dailyUrl.value"
>
</iframe>
</div>
</div>
</div>
</template>
```

### Reading config

Component settings are passed via props. The declared `defaultConfig` can be accessed through the `props.content.config` property.

The following is an example of how to convert the backend registration config to a TypeScript type.

```ts{4-9}
Shopware.Service('cmsService').registerCmsElement({
...
name: 'dailymotion',
defaultConfig: {
dailyUrl: {
source: 'static',
value: ''
}
}
...
});
```

```ts
type CmsElementDailymotionConfig = {
dailyUrl: {
value: string;
source: "static";
};
};
```
2 changes: 2 additions & 0 deletions apps/docs/src/resources/links.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Please tell us via [Community Slack](https://shopwarecommunity.slack.com/archive

Sorted by date, newest first

- [Multi-Page or Single-Page Variants Selection](https://www.brocksi.net/blog/variants-selection-multi-page-or-single-page/)
- [Create a CI/CD Pipeline for Shopware Frontends](https://kiplingi.de/create-a-ci-cd-pipeline-for-shopware-frontends/)
- [Gross and Net-Switch for B2B and B2C Shops built with Composable Frontends](https://dev.to/shopware/gross-and-net-switch-for-b2b-and-b2c-shops-built-with-composable-frontends-2b24)
- [Komponentenbasierte Entwicklung mit dem Shopware Frontends Framework](https://sitegeist.de/blog/e-commerce/komponentenbasierte-entwicklung-mit-dem-shopware-frontends-framework.html)
Expand All @@ -38,6 +39,7 @@ Sorted by date, newest first

Sorted by date, newest first

- [Developer Brunch April 2024 - "Composable frontends"](https://www.youtube.com/watch?v=Tz-86f72cDk)
- [Going Headless - One Page Shop mit Shopware & Nuxt 🚀 | shopware x synaigy Meetup](https://www.youtube.com/watch?v=RXaNWRMuea8)
- [Performance Improvements Headless Shopware Frontends - Niklas Wolf, Mothership GmbH](https://www.youtube.com/watch?v=GhniPTMtIt8)
- [Ein Microstore mit Shopware Frontends | Shopware Meetup der Mothership GmbH in München](https://www.youtube.com/watch?v=Dal-z94WLCk)
Expand Down
6 changes: 6 additions & 0 deletions packages/api-gen/src/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ export async function generate(args: { cwd: string; filename: string }) {
),
};
}
/**
* Blob type is used for binary data
*/
if (schemaObject.format === "binary") {
return "Blob";
}

/**
* We're changing "object" declarations into "GenericRecord" to allow recursive types like `associations`
Expand Down
37 changes: 37 additions & 0 deletions packages/cms-base/components/SwMedia3D.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script lang="ts" setup>
import { TresCanvas } from "@tresjs/core";
// @ts-expect-error TODO: fix missing useGLTF import
import { OrbitControls, useAnimations, useGLTF } from "@tresjs/cientos";
import { BasicShadowMap, SRGBColorSpace, NoToneMapping } from "three";
const props = defineProps<{
src: string;
}>();
const gl = {
clearColor: "#FFF",
shadows: true,
alpha: false,
shadowMapType: BasicShadowMap,
outputColorSpace: SRGBColorSpace,
toneMapping: NoToneMapping,
windowSize: false,
};
const { scene: model, animations } = await useGLTF(props.src);
const { actions } = useAnimations(animations, model);
</script>
<template>
<TresCanvas v-bind="gl">
<TresPerspectiveCamera
:args="[75, 1, 0.1, 2000]"
:position="[0, 0, 500]"
:look-at="[0, 0, 0]"
/>
<OrbitControls />
<primitive :object="model" />
<TresDirectionalLight :position="[3, 3, 3]" :intensity="1" />
<TresAmbientLight :intensity="2" />
</TresCanvas>
</template>
12 changes: 8 additions & 4 deletions packages/cms-base/components/public/cms/CmsGenericBlock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ const props = defineProps<{
}>();
const DynamicRender = () => {
const { resolvedComponent, componentName, isResolved } = resolveCmsComponent(
props.content,
);
const {
resolvedComponent,
componentName,
isResolved,
componentNameToResolve,
} = resolveCmsComponent(props.content);
if (resolvedComponent) {
if (!isResolved)
Expand Down Expand Up @@ -50,7 +53,8 @@ const DynamicRender = () => {
}),
);
}
return h("div", {}, "Loading...");
console.error("Component not resolved: " + componentNameToResolve);
return h("div", {}, "");
};
</script>

Expand Down
12 changes: 8 additions & 4 deletions packages/cms-base/components/public/cms/CmsGenericElement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ const props = defineProps<{
}>();
const DynamicRender = () => {
const { resolvedComponent, componentName, isResolved } = resolveCmsComponent(
props.content,
);
const {
resolvedComponent,
componentName,
isResolved,
componentNameToResolve,
} = resolveCmsComponent(props.content);
if (resolvedComponent) {
if (!isResolved)
return h("div", {}, "Problem resolving component: " + componentName);
Expand All @@ -25,7 +28,8 @@ const DynamicRender = () => {
class: cssClasses,
});
}
return h("div", {}, "Loading...");
console.error("Component not resolved: " + componentNameToResolve);
return h("div", {}, "");
};
</script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import type {
import { useCmsElementImage, useUrlResolver } from "#imports";
import { buildUrlPrefix } from "@shopware-pwa/helpers-next";
import { useElementSize } from "@vueuse/core";
import { computed, ref } from "vue";
import { computed, ref, defineAsyncComponent } from "vue";
import { isSpatial } from "../../../../helpers/media/isSpatial";
import { ClientOnly } from "../../../../helpers/clientOnly";
const props = defineProps<{
content: CmsElementImage | CmsElementManufacturerLogo;
Expand Down Expand Up @@ -49,6 +51,13 @@ const imageComputedContainerAttrs = computed(() => {
}
return imageAttrsCopy;
});
const SwMedia3D = computed(() => {
if (isSpatial(props.content.data.media)) {
return defineAsyncComponent(() => import("../../../SwMedia3D.vue"));
}
return "";
});
</script>
<template>
<!-- TODO: using a tag only works with externalLink, need to improve this element to deal with both internalLink & externalLink -->
Expand All @@ -74,6 +83,9 @@ const imageComputedContainerAttrs = computed(() => {
<source :src="imageAttrs.src" :type="mimeType" />
Your browser does not support the video tag.
</video>
<ClientOnly v-else-if="isSpatial(props.content.data.media)">
<component :is="SwMedia3D" :src="props.content.data.media.url" />
</ClientOnly>
<img
v-else
ref="imageElement"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { CmsElementImageGallery } from "@shopware-pwa/composables-next";
import { useCmsElementConfig } from "#imports";
import { isSpatial } from "../../../../helpers/media/isSpatial";
import SwSlider from "../../../SwSlider.vue";
import { computed, onMounted, ref } from "vue";
Expand Down Expand Up @@ -193,7 +194,20 @@ function next() {
}"
@click="() => changeCover(i)"
>
<div v-if="isSpatial(image.media)" class="h-full relative">
<img
class="w-full h-full object-center"
src="~/assets/3d.svg"
alt="3d object"
/>
<span
class="absolute bottom-0 text-sm bg-gray rounded px-1 text-white"
>
3D</span
>
</div>
<img
v-else
loading="lazy"
:src="image.media.url"
class="w-full h-full object-center object-cover"
Expand Down
11 changes: 11 additions & 0 deletions packages/cms-base/helpers/clientOnly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ref, onMounted, defineComponent } from "vue";

export const ClientOnly = defineComponent({
setup(_, { slots }) {
const init = ref(false);
onMounted(() => {
init.value = true;
});
return () => (init.value && slots.default ? slots.default() : null);
},
});
8 changes: 8 additions & 0 deletions packages/cms-base/helpers/media/isSpatial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function isSpatial<
T extends {
fileExtension: string;
url?: string;
},
>(media: T) {
return media.fileExtension === "glb" || !!media?.url?.endsWith(".glb");
}
4 changes: 4 additions & 0 deletions packages/cms-base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,20 @@
"@shopware-pwa/composables-next": "workspace:*",
"@shopware-pwa/helpers-next": "workspace:*",
"@shopware/api-client": "workspace:*",
"@tresjs/cientos": "^3.9.0",
"@tresjs/core": "^3.9.0",
"@vuelidate/core": "2.0.3",
"@vuelidate/validators": "2.0.4",
"@vueuse/core": "10.9.0",
"entities": "4.5.0",
"html-to-ast": "0.0.6",
"three": "^0.162.0",
"vue": "3.4.21"
},
"devDependencies": {
"@nuxt/schema": "3.11.2",
"@shopware-pwa/types": "workspace:*",
"@types/three": "^0.162.0",
"@vitest/coverage-v8": "1.5.0",
"@vue/eslint-config-typescript": "13.0.0",
"eslint-config-shopware": "workspace:*",
Expand Down

0 comments on commit fbb97c3

Please sign in to comment.