Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: add custom element page #840

Merged
merged 10 commits into from
May 6, 2024
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
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";
};
};
```
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
2 changes: 2 additions & 0 deletions packages/composables/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,15 @@ export function resolveCmsComponent(content: CmsSection | CmsBlock | CmsSlot) {

return {
componentName,
componentNameToResolve,
isResolved: resolvedComponent !== componentName,
resolvedComponent:
typeof resolvedComponent !== "string" ? resolvedComponent : undefined,
};
} catch (e) {
return {
componentName,
componentNameToResolve,
resolvedComponent: undefined,
resolved: false,
error: (e as Error).message,
Expand Down