Skip to content

Example

Introduction

The goal of this section is to give a hands-on example on how to create a simple content element. We'll be creating a simple counter, with a bit of a twist to demonstrate the server side hooks.

Make sure you install prerequisites first!

For more details visit the Installation section.

Let's start by initializing a new project:

bash
npx @tailor-cms/tce-template
npx @tailor-cms/tce-template

name it tce-counter and follow the instructions.

bash
cd tce-counter
pnpm dev
cd tce-counter
pnpm dev

your browser should pop up and display the preview.

First run

Content Element manifest

Let's continue by editing Content Element manifest; defining basic properties like name and type. In your editor of choice, open packages/manifest/src/index.ts and modify name and type properties:

ts
// Element unique id within the target system (e.g. Tailor)
export const type = 'ACME_TCE_COUNTER';

// Display name (e.g. shown to the author within add element dialog)
export const name = 'Simple counter';
// Element unique id within the target system (e.g. Tailor)
export const type = 'ACME_TCE_COUNTER';

// Display name (e.g. shown to the author within add element dialog)
export const name = 'Simple counter';

There is also an initState function, which as name implies, initializes state of the element upon creation. Since we are bulding a simple increment element, we'll define it as:

ts
export const initState: DataInitializer = (): ElementData => ({ count: 0 });
export const initState: DataInitializer = (): ElementData => ({ count: 0 });

We also want to update the type definitions, to do so, open packages/manifest/src/interfaces.ts and update ElementData interface:

ts
export interface ElementData {
  count: number;
}
export interface ElementData {
  count: number;
}

Upon starting the runtime, the server state should now reflect these changes by displaying updated type and setting the data.count value to 0:

State after manifest update

Edit component

Now that we set the basic properties and specified how to initialize the component state, we can create Authoring component for our counter. It is not going to be the worlds most complex or usefull component; Author will have an option to click on a button, each time incrementing a counter. Navigate to packages/edit/src/components/Edit.vue and paste the following code for our simple counter:

vue
<template>
  <div class="tce-container">
    <div>Times clicked: {{ element.data.count }}</div>
    <button @click="increment">Increment</button>
  </div>
</template>

<script setup lang="ts">
import { Element } from 'tce-manifest';

const props = defineProps<{ element: Element; isFocused: boolean }>();
const emit = defineEmits(['save']);

const increment = () => {
  const { data } = props.element;
  const count = data.count + 1;
  emit('save', { ...data, count });
};
</script>

<style scoped>
.tce-container {
  background-color: transparent;
  margin-top: 1rem;
  padding: 1rem;
  border: 2px dashed #888;
  font-family: Arial, Helvetica, sans-serif;
  font-size: 1rem;
}

button {
  margin-top: 1rem;
  padding: 0.5rem 1rem;
  border: 1px solid #444;
  background-color: #fff;
}
</style>
<template>
  <div class="tce-container">
    <div>Times clicked: {{ element.data.count }}</div>
    <button @click="increment">Increment</button>
  </div>
</template>

<script setup lang="ts">
import { Element } from 'tce-manifest';

const props = defineProps<{ element: Element; isFocused: boolean }>();
const emit = defineEmits(['save']);

const increment = () => {
  const { data } = props.element;
  const count = data.count + 1;
  emit('save', { ...data, count });
};
</script>

<style scoped>
.tce-container {
  background-color: transparent;
  margin-top: 1rem;
  padding: 1rem;
  border: 2px dashed #888;
  font-family: Arial, Helvetica, sans-serif;
  font-size: 1rem;
}

button {
  margin-top: 1rem;
  padding: 0.5rem 1rem;
  border: 1px solid #444;
  background-color: #fff;
}
</style>

Note ☝️

We imported type definitions from the manifest package to enable code completion and to specify input.


You should be able to see our Edit component and click on the Increment button. As you are clicking on the Increment button, you should see both; the front-end component and the server state being modified to the same value (as upon emitting the save event, data is being passed to the server runtime).


Edit component

Edit toolbar

We can implement the same functionality from the toolbars, as they will also recieve element state and have the ability to save it.

Navigate to packages/edit/src/components/TopToolbar.vue, paste the following code and save:

vue
<template>
  <button @click="decrement">Decrement</button>
</template>

<script setup lang="ts">
import { Element } from 'tce-manifest';

const props = defineProps<{ element: Element }>();
const emit = defineEmits(['save']);

const decrement = () => {
  const { data } = props.element;
  const count = data.count - 1;
  emit('save', { ...data, count });
};
</script>

<style scoped>
button {
  margin-top: 1rem;
  padding: 0.5rem 1rem;
  border: 1px solid #444;
  background-color: #fff;
}
</style>
<template>
  <button @click="decrement">Decrement</button>
</template>

<script setup lang="ts">
import { Element } from 'tce-manifest';

const props = defineProps<{ element: Element }>();
const emit = defineEmits(['save']);

const decrement = () => {
  const { data } = props.element;
  const count = data.count - 1;
  emit('save', { ...data, count });
};
</script>

<style scoped>
button {
  margin-top: 1rem;
  padding: 0.5rem 1rem;
  border: 1px solid #444;
  background-color: #fff;
}
</style>


You sould be able to decrement the count by clicking on the Top Toolbar Decrement button.


Edit component

Display component

Now that we created an Edit component, we can create our End-user facing presentation component. For this particular example, Display component does not contain any additional logic, it simply displays our counter value. Navigate to packages/display/src/components/Display.vue and paste the following code:

vue
<template>
  <div class="tce-root">
    <div class="d-flex align-center text-h5">
      Author clicked
      <span class="counter">{{ data.count }}</span>
      times!
    </div>
  </div>
</template>

<script setup lang="ts">
import { ElementData } from 'tce-manifest';

defineProps<{ data: ElementData }>();
</script>

<style scoped>
.tce-root {
  background-color: transparent;
  margin-top: 1rem;
  padding: 1rem;
  border: 2px dashed #888;
  font-family: Arial, Helvetica, sans-serif;
  font-size: 1rem;
}

.counter {
  margin: 0 1rem;
  padding: 1rem;
  color: #23f48b;
  font-size: 2rem;
  font-weight: bold;
  background-color: #323338;
  border-radius: 0.5rem;
}
</style>
<template>
  <div class="tce-root">
    <div class="d-flex align-center text-h5">
      Author clicked
      <span class="counter">{{ data.count }}</span>
      times!
    </div>
  </div>
</template>

<script setup lang="ts">
import { ElementData } from 'tce-manifest';

defineProps<{ data: ElementData }>();
</script>

<style scoped>
.tce-root {
  background-color: transparent;
  margin-top: 1rem;
  padding: 1rem;
  border: 2px dashed #888;
  font-family: Arial, Helvetica, sans-serif;
  font-size: 1rem;
}

.counter {
  margin: 0 1rem;
  padding: 1rem;
  color: #23f48b;
  font-size: 2rem;
  font-weight: bold;
  background-color: #323338;
  border-radius: 0.5rem;
}
</style>


Note that Display component recieves spreaded element attributes as props. After applying these changes you should be able to see the Display component:


Display component

Server hooks

Finally, we are going to make one last change to our might counter and add some server-side behaviour. Server hooks are great place for interfacing with external APIs (e.g. create a video upload link), validating inputs, handling secrets and preparing the data for delivery (e.g. signing assets to enable access).

In this example, we are going to reset the counter in case it reaches 10.

Navigate to the packages/server/src/index.ts and update beforeSave function to:

ts
export function beforeSave(element: Element, services: any) {
  if (element.data.count >= 10) {
    element.data = {
      ...element.data,
      count: 0,
    };
  }
  return element;
}
export function beforeSave(element: Element, services: any) {
  if (element.data.count >= 10) {
    element.data = {
      ...element.data,
      count: 0,
    };
  }
  return element;
}


Element is a Sequelize.js instance, with data defined as a JSONB property. In order for Sequelize to detect changes, data property is entirely replaced with a new value.

Note ☝️

In case that hook code changes are not picked up automatically, please restart.

Conclusion

Congradulations! You have created your first Content Element. For more details on each of the components please visit the matching documentation section. For a fully working example, please visit https://github.com/tailor-cms/tce-counter.