Skip to main content

Event Card

This guide covers the eventPopup extension point - how to render a custom card on event clicks, how it interacts with the built-in editor, and what the injected component receives.

custom event card popup anchored to a clicked event with title, time range, and a close button

How the event card works

The event card is not a separate exported component. You write a Vue component, pass it through the eventPopup prop, and the calendar mounts it inside an anchored Popup whenever the user clicks an event.

A few rules drive how it behaves:

  • Click detection is movement-aware. The internal click directive listens for mousedown / mouseup on the section container and treats the interaction as a click only when the pointer stays within a 3 px threshold. This is what separates a click from the start of a drag.
  • Resolution is store-based. Event elements expose a data-id. The directive resolves it through api.getEvent(...) to find the stored event, then opens the popup with that event.
  • eventPopup overrides the default click path. Without eventPopup, an event click dispatches select-event (which opens the editor when one is mounted). With eventPopup, the click opens the card instead - select-event is not dispatched.
  • Empty-space clicks dismiss. Clicking outside an event closes the current card. Clicking another event swaps the card to that event.
  • The component renders inside the calendar subtree. It can read any Vue context exposed by the calendar's parent components via inject - useful for passing app-level data such as resource lists.

The card receives two props: the resolved event object and a close callback. You decide what the card looks like and when it dismisses itself.

Passing a component

Define a Vue component that accepts event and close, then hand it to eventPopup:

<!-- App.vue -->
<script setup lang="ts">
import { Calendar } from "@wx/vue-calendar";
import EventCard from "./EventCard.vue";
import { getData } from "./data";

const { data, date } = getData();
</script>

<template>
<Calendar
:events="data"
:date="date"
view="week"
:eventPopup="EventCard"
:views="['day', 'week', 'month']"
/>
</template>

The card component itself reads its props with defineProps:

<!-- EventCard.vue -->
<script setup lang="ts">
const props = defineProps<{
event: any;
close: () => void;
}>();
</script>

<template>
<div class="event-card">
<header>{{ event.text }}</header>
<p>{{ event.start.toLocaleString() }} - {{ event.end.toLocaleString() }}</p>
<button :onclick="close">Close</button>
</div>
</template>

close is the supported way to dismiss the popup from inside the card - use it after the user confirms an action, navigates away, or clicks an explicit close control.

Reading app data through context

Because the card renders inside the calendar's subtree, it inherits Vue context from any ancestor via inject - including contexts you provide in the parent that mounts <Calendar>:

<!-- parent component -->
<script setup lang="ts">
import { provide } from "vue";
import { Calendar } from "@wx/vue-calendar";

const resources = [
{ id: "alice", label: "Alice" },
{ id: "bob", label: "Bob" },
];

provide("resources", resources);
</script>

<template>
<Calendar :events="data" :date="date" :eventPopup="EventCard" />
</template>

The card pulls the same context with inject:

<script setup lang="ts">
import { inject } from "vue";

const props = defineProps<{ event: any; close: () => void }>();
const resources = inject<Array<{ id: string; label: string }>>("resources");

const assignee = resources?.find(r => r.id === props.event.unit_id)?.label;
</script>

This pattern keeps domain data out of every event and lets the card resolve it on demand.

Coexisting with the editor

The default click path opens the editor (when <Editor> is mounted) by dispatching select-event. The eventPopup prop replaces that path, so the editor will not auto-open on click while the card is active.

If you want the card and the editor side by side, route the editor explicitly from a button inside the card. Call api.exec("select-event", { id }) to open the editor for the same event the card is showing:

<!-- EventCard.vue -->
<script setup lang="ts">
import { inject } from "vue";
import type { CalendarContextApi } from "@wx/vue-calendar";

const props = defineProps<{ event: any; close: () => void }>();
const api = inject<CalendarContextApi>("calendar-api");

function openEditor() {
api.exec("select-event", { id: props.event.id });
props.close();
}
</script>

<template>
<button :onclick="openEditor">Edit</button>
<button :onclick="close">Close</button>
</template>

calendar-api is the Vue context the calendar exposes for child components via inject; see the API reference for the full surface (getState, getReactiveState, exec, fmt, getEvent).

If you prefer a card-only flow, leave the editor out of the tree - eventPopup does not require it.

When to use what

  • Editor only - users edit events through the form. No eventPopup. Click selects, editor opens.
  • Card only - users see a read-only or action-driven preview on click. eventPopup set, no <Editor>.
  • Card plus editor - card is the entry point, editor opens from a card action. eventPopup set, <Editor :api="api" /> mounted, card calls api.exec("select-event", ...).