---
title: "Angular Event calendar Recurring Events Editor Example | Mobiscroll"
component: "Event calendar"
framework: "Angular"
library_version: "6.2.0"
url: "https://demo.mobiscroll.com/angular/eventcalendar/recurring-event-add-edit-dialog"
---

# Recurring Events Editor - Angular Event calendar demo

Add recurring event management to an Angular event calendar — use preset daily, weekly, monthly, and yearly rules or a custom recurrence editor.

- [Live demo](https://demo.mobiscroll.com/angular/eventcalendar/recurring-event-add-edit-dialog)
- [View code on GitHub](https://github.com/acidb/mobiscroll-demos-angular/tree/main/src/demos/eventcalendar/calendar-view/recurring-event-add-edit-dialog)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/eventcalendar/recurring-event-add-edit-dialog.md), [jQuery](https://demo.mobiscroll.com/jquery/eventcalendar/recurring-event-add-edit-dialog.md), [React](https://demo.mobiscroll.com/react/eventcalendar/recurring-event-add-edit-dialog.md), [Vue](https://demo.mobiscroll.com/vue/eventcalendar/recurring-event-add-edit-dialog.md)

## Description

As seen in the [previous example](https://demo.mobiscroll.com/angular/eventcalendar/create-read-update-delete-CRUD), an add/edit form can be created with custom and base event fields. While the CRUD example features delete with undo, this example shows off how to build a fully customizable recurrence editor right in the add/edit screen.

Above some common presets, like **Daily**, **Weekly on X day**, **Monthly on Xth**, **Annually on the same day**, **Every Weekday** a fully custom recurrence editor is implemented.

Copy & paste or delve into the code and understand how to generate recurrence objects or RRULES as seen in this [live rule builder](https://demo.mobiscroll.com/angular/eventcalendar/recurring-events).

## What this demo shows

- A desktop month view event calendar with recurring events already rendered across the month.
- **Calendar header** The header shows the current month and year on the left, and blue month navigation arrows with a `Today` button (for jumping back to the current date) between them on the right. 
- **Event creation** Users can create a new event by clicking and dragging across calendar cells or by double-clicking a day cell.
- **Create dialog** Creating an event opens an event editor with fields for title, description, all-day, start date, end date, and recurrence.
- **Date selection** The start and end fields open a date picker with range selection.
- **Recurring presets** The Repeats field opens a dropdown with preset options including Does not repeat, Daily, Weekly, Monthly, Yearly, and Custom.
- **Custom recurrence** Choosing Custom expands the editor with additional controls for defining a custom recurring rule.
- **Event interaction** Hovering over an event label highlights it. This action shows resize handles on both sides, indicating that the event can be resized by clicking and dragging.
- **Event editing** Clicking an existing event opens the same dialog in edit mode with the current event values prefilled.
- **Save and cancel flow** Clicking outside the dialog or pressing Cancel closes the editor without applying changes, while Save applies the changes to the event.
- **Delete flow** In edit mode, the dialog includes a Delete event action for removing the selected event.
- **Recurring delete options** When deleting a recurring event, a confirmation popup appears with options for This event only, This and following events, and All events, plus Cancel and Ok actions.
- **Responsive editor** The event editor adapts to screen size and device type, using touch-friendly controls such as scrollers on smaller or touch devices.
- **Cell hover and selection** Hovering a day cell highlights the day number with a gray background, while clicking an empty part of a day cell selects that day and highlights the day number with a blue background.

## Best for

- **Recurring event management** Apps that need users to create, edit, and delete repeating events directly from a calendar view.
- **Exception handling** Scenarios where users may need to delete a single occurrence, all following occurrences, or the entire recurring series.
- **Business and operational scheduling** Internal tools or customer-facing products for planning appointments, classes, shifts, programs, or other repeating activities.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-themeVariant) - option
- [`touchUi`](https://mobiscroll.com/docs/angular/select/api#opt-touchUi) - option
- [`clickToCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToResize) - option
- [`view`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-view) - option
- [`labels`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-labels) - option
- [`onEventClick`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventClick) - event
- [`onEventUpdate`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventUpdate) - event
- [`onEventCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventCreate) - event
- [`onEventCreated`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventCreated) - event
- [`onEventDeleted`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventDeleted) - event
- [`onEventUpdated`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventUpdated) - event
- [`cssClass`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-cssClass) - option
- [`display`](https://mobiscroll.com/docs/angular/popup/api#opt-display) - option
- [`fullScreen`](https://mobiscroll.com/docs/angular/popup/api#opt-fullScreen) - option
- [`scrollLock`](https://mobiscroll.com/docs/angular/popup/api#opt-scrollLock) - option
- [`height`](https://mobiscroll.com/docs/angular/popup/api#opt-height) - option
- [`onClose`](https://mobiscroll.com/docs/angular/popup/api#event-onClose) - event
- [`responsive`](https://mobiscroll.com/docs/angular/popup/api#opt-responsive) - option
- [`width`](https://mobiscroll.com/docs/angular/popup/api#opt-width) - option
- [`touchUi`](https://mobiscroll.com/docs/angular/popup/api#opt-touchUi) - option
- [`cssClass`](https://mobiscroll.com/docs/angular/popup/api#opt-cssClass) - option
- [`buttons`](https://mobiscroll.com/docs/angular/popup/api#opt-buttons) - option
- [`select`](https://mobiscroll.com/docs/angular/datepicker/api#opt-select) - option
- [`showRangeLabels`](https://mobiscroll.com/docs/angular/datepicker/api#opt-showRangeLabels) - option
- [`touchUi`](https://mobiscroll.com/docs/angular/datepicker/api#opt-touchUi) - option
- [`data`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-data) - option
- [`selectedDate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-selectedDate) - option
- [`anchor`](https://mobiscroll.com/docs/angular/popup/api#opt-anchor) - option
- [`headerText`](https://mobiscroll.com/docs/angular/popup/api#opt-headerText) - option
- [`controls`](https://mobiscroll.com/docs/angular/datepicker/api#opt-controls) - option
- [`responsive`](https://mobiscroll.com/docs/angular/datepicker/api#opt-responsive) - option
- [`startInput`](https://mobiscroll.com/docs/angular/datepicker/api#opt-startInput) - option
- [`endInput`](https://mobiscroll.com/docs/angular/datepicker/api#opt-endInput) - option
- [`data`](https://mobiscroll.com/docs/angular/select/api#opt-data) - option
- [`label`](https://mobiscroll.com/docs/angular/select/api#opt-label) - option
- [`responsive`](https://mobiscroll.com/docs/angular/select/api#opt-responsive) - option
- [`inputStyle`](https://mobiscroll.com/docs/angular/select/api#opt-inputStyle) - option
- [`onChange`](https://mobiscroll.com/docs/angular/select/api#event-onChange) - event
- [`position`](https://mobiscroll.com/docs/angular/forms/radio#opt-position) - option
- [`display`](https://mobiscroll.com/docs/angular/datepicker/api#opt-display) - option
- [`dateFormat`](https://mobiscroll.com/docs/angular/datepicker/api#localization-dateFormat) - option
- [`returnFormat`](https://mobiscroll.com/docs/angular/datepicker/api#opt-returnFormat) - option
- [`inputStyle`](https://mobiscroll.com/docs/angular/datepicker/api#opt-inputStyle) - option

## Code

### Component

```ts
import { CommonModule } from '@angular/common';
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
  formatDate,
  MbscCalendarEvent,
  MbscDatepickerOptions,
  MbscEventcalendarOptions,
  MbscModule,
  MbscPopup,
  MbscPopupOptions,
  setOptions,
  updateRecurringEvent,
} from '@mobiscroll/angular';

setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

const days = [
  {
    name: 'Sunday',
    short: 'SU',
    checked: true,
  },
  {
    name: 'Monday',
    short: 'MO',
    checked: false,
  },
  {
    name: 'Tuesday',
    short: 'TU',
    checked: false,
  },
  {
    name: 'Wednesday',
    short: 'WE',
    checked: false,
  },
  {
    name: 'Thursday',
    short: 'TH',
    checked: false,
  },
  {
    name: 'Friday',
    short: 'FR',
    checked: false,
  },
  {
    name: 'Saturday',
    short: 'SA',
    checked: false,
  },
];

const ordinalList: any = { 1: 'first', 2: 'second', 3: 'third', 4: 'fourth' };

function getWeekDayNum(date: Date): number {
  const year = date.getFullYear();
  const month = date.getMonth();
  const firstDayOfMonth = new Date(year, month, 1);
  const lastDayOfMonth = new Date(year, month + 1, 0);
  let count = 0;

  for (let d = firstDayOfMonth; d < lastDayOfMonth; d.setDate(d.getDate() + 7)) {
    if (date >= d) {
      count++;
    } else {
      break;
    }
  }

  return Math.max(1, count);
}

@Component({
  selector: 'app-root', 
  templateUrl: './app.component.html',
  standalone: true,
  imports: [CommonModule, FormsModule, MbscModule],
})
export class AppComponent implements OnInit {
  @ViewChild('popup', { static: false })
  popup!: MbscPopup;
  popupEventTitle: string | undefined;
  popupEventDescription = '';
  popupEventAllDay = true;
  popupEventDates: any;
  calendarSelectedDate: any = new Date();
  myEvents: MbscCalendarEvent[] = [
    {
      id: 1,
      start: '2026-11-21T00:00',
      end: '2026-11-24T00:00',
      title: 'Alice OFF',
      allDay: true,
      color: '#67ab0d',
    },
    {
      id: 2,
      start: '18:00',
      end: '20:00',
      recurring: {
        repeat: 'weekly',
        weekDays: 'MO',
      },
      title: 'Football training',
      allDay: false,
      color: '#fd966a',
    },
    {
      id: 3,
      start: '17:00',
      end: '17:30',
      recurring: {
        repeat: 'monthly',
        day: 1,
      },
      title: 'Rent payment',
      allDay: false,
      color: '#3ea39e',
    },
    {
      id: 4,
      recurring: {
        repeat: 'yearly',
        day: 6,
        month: 5,
      },
      title: 'Dexter BD',
      allDay: true,
      color: '#d00f0f',
    },
  ];
  repeatData = [
    {
      value: 'norepeat',
      text: 'Does not repeat',
    },
    {
      value: 'daily',
      text: 'Daily',
    },
    {
      value: 'weekly',
      text: 'Weekly',
    },
    {
      value: 'monthly',
      text: 'Monthly',
    },
    {
      value: 'monthly-pos',
      text: 'Monthly',
    },
    {
      value: 'yearly',
      text: 'Yearly',
    },
    {
      value: 'yearly-pos',
      text: 'Yearly',
    },
    {
      value: 'weekday',
      text: 'Every weekday (Monday to Friday)',
    },
    {
      value: 'custom',
      text: 'Custom',
    },
  ];
  selectedRepeat = 'norepeat';
  selectResponsive = {
    xsmall: {
      touchUi: true,
    },
    small: {
      touchUi: false,
    },
  };
  weekDays = ['SU'];
  showCustomRepeat = false;
  repeatType = 'daily';
  repeatNr = 1;
  monthlyDays = [1];
  monthlyDay = 1;
  yearlyDays = [1];
  yearlyDay = 1;
  months = [
    {
      value: 1,
      text: 'January',
    },
    {
      value: 2,
      text: 'February',
    },
    {
      value: 3,
      text: 'March',
    },
    {
      value: 4,
      text: 'April',
    },
    {
      value: 5,
      text: 'May',
    },
    {
      value: 6,
      text: 'June',
    },
    {
      value: 7,
      text: 'July',
    },
    {
      value: 8,
      text: 'August',
    },
    {
      value: 9,
      text: 'September',
    },
    {
      value: 10,
      text: 'October',
    },
    {
      value: 11,
      text: 'November',
    },
    {
      value: 12,
      text: 'December',
    },
  ];
  selectedMonth = 1;
  untilDate: any;
  occurrences = 10;
  condition = 'never';
  tempEvent: any;

  @ViewChild('recurringEditModePopup', { static: false })
  recurringEditModePopup!: MbscPopup;

  recurringEditMode: any = 'current';
  newEvent: MbscCalendarEvent = {};
  originalRecurringEvent: any;
  eventOccurrence: MbscCalendarEvent = {};
  editFromPopup = false;
  recurringDelete = false;
  recurringActionText = 'Edit';

  calendarOptions: MbscEventcalendarOptions = {
    clickToCreate: 'double',
    dragToCreate: true,
    dragToMove: true,
    dragToResize: true,
    view: {
      calendar: { labels: true },
    },
    onEventClick: (args: any) => {
      const event = args.event;
      this.isEdit = true;
      this.tempEvent = args.event;

      // Recurring event
      if (event.recurring) {
        this.originalRecurringEvent = event['original'];
        this.eventOccurrence = { ...event };

        // Fill popup form with event data
        this.loadPopupForm(event);
      } else {
        this.originalRecurringEvent = {};
        // Fill popup form with event data
        this.loadPopupForm(event);
      }

      // Set popup options
      this.popupHeaderText = 'Edit event';
      this.popupButtons = this.popupEditButtons;
      this.popupAnchor = args.domEvent.currentTarget;
      // Open the popup
      this.popup.open();
    },
    onEventUpdate: (args: any) => {
      const event = args.event;
      if (event.recurring) {
        this.originalRecurringEvent = args.oldEvent;
        // We need this on delete
        this.tempEvent = event;
        this.eventOccurrence = args.oldEventOccurrence;
        if (args.domEvent.keyCode === 46) {
          this.recurringActionText = 'Delete';
          this.recurringDelete = true;
          this.recurringEditModePopup.open();
        } else {
          this.recurringActionText = 'Edit';
          this.recurringDelete = false;
          this.recurringEditModePopup.open();
        }
        return false;
      } else {
        return true;
      }
    },
    onEventCreate: (args: any) => {
      const originEvent = args.originEvent;
      if (originEvent && originEvent.recurring) {
        this.newEvent = args.event;
        return false;
      } else {
        return true;
      }
    },
    onEventCreated: (args: any) => {
      setTimeout(() => {
        this.isEdit = false;
        this.tempEvent = args.event;
        // Fill popup form with event data
        this.loadPopupForm(args.event);
        // Set popup options
        this.popupHeaderText = 'New Event';
        this.popupButtons = this.popupAddButtons;
        this.popupAnchor = args.target;
        this.resetCustomValues();
        // Open the popup
        this.popup.open();
      });
    },
    onEventDeleted: (args: any) => {
      setTimeout(() => {
        this.deleteEvent(args.event);
      });
    },
    onEventUpdated: () => {
      // Here you can update the event in your storage as well, after drag & drop or resize
      // ...
    },
  };
  popupHeaderText!: string;
  popupAnchor: HTMLElement | undefined;
  popupAddButtons = [
    'cancel',
    {
      handler: () => {
        this.saveEvent();
      },
      keyCode: 'enter',
      text: 'Add',
      cssClass: 'mbsc-popup-button-primary',
    },
  ];
  popupEditButtons = [
    'cancel',
    {
      handler: () => {
        if (Object.keys(this.originalRecurringEvent).length !== 0) {
          this.editFromPopup = true;
          this.recurringActionText = 'Edit';
          this.recurringDelete = false;
          this.recurringEditModePopup.open();
        } else {
          this.saveEvent();
        }
      },
      keyCode: 'enter',
      text: 'Save',
      cssClass: 'mbsc-popup-button-primary',
    },
  ];
  popupButtons: any = [];
  popupOptions: MbscPopupOptions = {
    display: 'bottom',
    contentPadding: false,
    fullScreen: true,
    scrollLock: false,
    height: 500,
    onClose: () => {
      this.repeatData = this.repeatData.filter((item) => item.value !== 'custom-value');
      if (!this.isEdit) {
        // Refresh the list, if add popup was canceled, to remove the temporary event
        this.myEvents = [...this.myEvents];
      }
      this.editFromPopup = false;
    },
    responsive: {
      medium: {
        display: 'anchored',
        width: 510,
        fullScreen: false,
        touchUi: false,
      },
    },
    cssClass: 'md-recurring-event-editor-popup',
  };
  editModePopupOptions: MbscPopupOptions = {
    display: 'bottom',
    contentPadding: false,
    buttons: [
      'cancel',
      {
        text: 'Ok',
        keyCode: 'enter',
        handler: () => {
          if (this.recurringDelete) {
            this.deleteRecurringEvent();
          } else {
            if (this.editFromPopup) {
              this.setTempEvent();
            }

            if (this.recurringEditMode === 'current') {
              delete this.tempEvent.id;
              delete this.tempEvent.recurring;
              delete this.tempEvent.recurringException;
            }

            const events = updateRecurringEvent(
              this.originalRecurringEvent,
              this.eventOccurrence,
              this.editFromPopup ? null : this.newEvent,
              this.editFromPopup ? this.tempEvent : null,
              this.recurringEditMode,
            );

            // Update event
            let newEventList = [...this.myEvents];
            const index = newEventList.findIndex((x) => x.id === events.updatedEvent.id);
            newEventList[index] = events.updatedEvent;

            // Add new event
            if (events.newEvent) {
              newEventList = [...newEventList, events.newEvent];
            }

            this.myEvents = newEventList;

            this.popup.close();
            this.recurringEditModePopup.close();
          }
        },
        cssClass: 'mbsc-popup-button-primary',
      },
    ],
    onClose: () => {
      this.recurringEditMode = 'current';
    },
  };
  datePickerControls = ['date'];
  datePickerResponsive: any = {
    medium: {
      controls: ['calendar'],
      touchUi: false,
    },
  };
  datetimePickerControls = ['datetime'];
  datetimePickerResponsive = {
    medium: {
      controls: ['calendar', 'time'],
      touchUi: false,
    },
  };
  datePickerOptions: MbscDatepickerOptions = {
    select: 'range',
    showRangeLabels: false,
    touchUi: true,
  };
  isEdit = false;
  deleteRecurringEvent(): void {
    switch (this.recurringEditMode) {
      case 'following': {
        let followingExceptions = this.tempEvent.recurringException || [];
        followingExceptions = [...followingExceptions, this.tempEvent.start];
        this.originalRecurringEvent.recurringException = followingExceptions;
        this.originalRecurringEvent.recurring.until = this.tempEvent.start;
        this.myEvents = [...this.myEvents];
        break;
      }
      case 'all':
        this.myEvents = this.myEvents.filter((item) => item.id !== this.tempEvent.id);
        break;
      case 'current':
      default: {
        let currentExceptions = this.tempEvent.recurringException || [];
        currentExceptions = [...currentExceptions, this.tempEvent.start];
        this.originalRecurringEvent.recurringException = currentExceptions;
        this.myEvents = [...this.myEvents];
        break;
      }
    }
    this.popup.close();
    this.recurringEditModePopup.close();
  }
  loadPopupForm(event: MbscCalendarEvent): void {
    const startDate = new Date(event.start as any);
    this.popupEventTitle = event.title;
    this.popupEventDescription = event['description'];
    this.popupEventDates = [startDate, event.end];
    this.untilDate = formatDate('YYYY-MM-DD', new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() + 1));
    this.popupEventAllDay = event.allDay || false;
    this.updateCustomForm();
  }
  getCustomRule(): any {
    let recurringRule: any;
    const d = this.editFromPopup ? this.popupEventDates[0] : new Date(this.tempEvent.start);
    const weekday = d.getDay();
    const monthday = d.getDate();
    const weekNr = getWeekDayNum(d);

    if (this.editFromPopup && this.tempEvent.start && this.tempEvent.recurring) {
      switch (this.selectedRepeat) {
        default:
        case 'daily':
          recurringRule = {
            repeat: 'daily',
          };
          break;
        case 'weekly':
          recurringRule = {
            repeat: 'weekly',
            weekDays: days[weekday].short,
          };
          break;
        case 'monthly':
          recurringRule = {
            repeat: 'monthly',
            day: monthday,
          };
          break;
        case 'monthly-pos':
          recurringRule = {
            repeat: 'monthly',
            weekDays: days[weekday].short,
            pos: weekNr,
          };
          break;
        case 'yearly':
          recurringRule = {
            repeat: 'yearly',
            day: monthday,
            month: d.getMonth() + 1,
          };
          break;
        case 'yearly-pos':
          recurringRule = {
            repeat: 'yearly',
            month: d.getMonth() + 1,
            weekDays: days[weekday].short,
            pos: weekNr,
          };
          break;
      }
    } else {
      switch (this.selectedRepeat) {
        case 'daily':
          recurringRule = { repeat: 'daily' };
          break;
        case 'weekly':
          recurringRule = { repeat: 'weekly', weekDays: days[d.getDay()].short };
          break;
        case 'monthly':
          recurringRule = { repeat: 'monthly', day: d.getDate() };
          break;
        case 'monthly-pos':
          recurringRule = { repeat: 'monthly', weekDays: days[weekday].short, pos: weekNr };
          break;
        case 'yearly':
          recurringRule = { repeat: 'yearly', month: d.getMonth() + 1 };
          break;
        case 'yearly-pos':
          recurringRule = { repeat: 'yearly', month: d.getMonth() + 1, weekDays: days[weekday].short, pos: weekNr };
          break;
        case 'weekday':
          recurringRule = { repeat: 'weekly', weekDays: 'MO,TU,WE,TH,FR' };
          break;
        case 'custom':
        case 'custom-value':
          recurringRule = {
            repeat: this.repeatType,
            interval: this.repeatNr,
          };
          switch (this.repeatType) {
            case 'weekly':
              recurringRule.weekDays = this.weekDays.join(',');
              break;
            case 'monthly':
              recurringRule.day = this.monthlyDay;
              break;
            case 'yearly':
              recurringRule.day = this.yearlyDay;
              recurringRule.month = this.selectedMonth;
              break;
          }

          switch (this.condition) {
            case 'until':
              recurringRule.until = this.untilDate;
              break;
            case 'count':
              recurringRule.count = this.occurrences;
              break;
          }
          break;
      }
    }
    return recurringRule;
  }
  setTempEvent(): void {
    this.tempEvent.title = this.popupEventTitle;
    this.tempEvent.description = this.popupEventDescription;
    this.tempEvent.start = this.popupEventDates[0];
    this.tempEvent.end = this.popupEventDates[1];
    this.tempEvent.allDay = this.popupEventAllDay;
    this.tempEvent.recurring = this.getCustomRule();
  }
  saveEvent(): void {
    this.setTempEvent();

    if (this.isEdit) {
      // Update the event in the list
      this.myEvents = [...this.myEvents];
      // Here you can update the event in your storage as well
      // ...
    } else {
      // Add the new event to the list
      this.myEvents = [...this.myEvents, this.tempEvent];
      // Here you can add the event to your storage as well
      // ...
    }
    // Navigate the calendar
    this.navigateTo();
    // Close the popup
    this.popup.close();
  }
  deleteEvent(event: MbscCalendarEvent): void {
    this.myEvents = this.myEvents.filter((item) => item.id !== event.id);
  }
  onDeleteClick(): void {
    if (this.tempEvent.recurring) {
      this.recurringActionText = 'Delete';
      this.recurringDelete = true;
      this.recurringEditModePopup.open();
    } else {
      this.deleteEvent(this.tempEvent);
      this.popup.close();
    }
  }

  // Set custom values to default
  resetCustomValues(): void {
    this.repeatType = 'daily';
    this.repeatNr = 1;
    this.condition = 'never';
    this.occurrences = 10;
    this.selectedMonth = 1;
    this.monthlyDay = 1;
    this.yearlyDay = 1;
    this.weekDays = ['SU'];
    this.selectedRepeat = 'norepeat';
    this.showCustomRepeat = false;
    this.repeatData = this.repeatData.filter((item) => item.value !== 'custom-value');
  }

  updateCustomForm(): void {
    const d = new Date(this.tempEvent.start);
    const weekday = d.getDay();
    const monthday = d.getDate();
    const newData = [...this.repeatData];
    const weekNr = getWeekDayNum(d);

    // Update select texts by selected date
    for (const item of newData) {
      switch (item.value) {
        case 'weekly':
          item.text = 'Weekly on ' + days[weekday].name;
          break;
        case 'monthly':
          item.text = 'Monthly on day ' + monthday;
          break;
        case 'yearly':
          item.text = 'Annually on ' + this.months[d.getMonth()].text + ' ' + monthday;
          break;
        case 'monthly-pos':
          item.text = 'Monthly on the ' + ordinalList[weekNr] + ' ' + days[weekday].name;
          break;
        case 'yearly-pos':
          item.text = 'Annually on the ' + ordinalList[weekNr] + ' ' + days[weekday].name + ' of ' + this.months[d.getMonth()].text;
          break;
        default:
      }
    }

    this.repeatData = newData;

    const rec = this.tempEvent.recurring;

    if (rec) {
      this.repeatType = rec.repeat;
      this.weekDays = rec.repeat === 'weekly' ? rec.weekDays.split(',') : ['SU'];
      if (rec.interval) {
        // Set custom text
        let customText = '';

        this.repeatNr = rec.interval;

        switch (rec.repeat) {
          case 'daily':
            customText = this.repeatNr > 1 ? 'Every ' + this.repeatNr + ' days' : 'Daily';
            break;
          case 'weekly':
            customText = this.repeatNr > 1 ? 'Every ' + this.repeatNr + ' weeks' : 'Weekly';
            customText += ' on ' + rec.weekDays;
            break;
          case 'monthly':
            this.monthlyDay = rec.day;
            customText = this.repeatNr > 1 ? 'Every ' + this.repeatNr + ' months' : 'Monthly';
            customText += ' on day ' + this.monthlyDay;
            break;
          case 'yearly':
            this.yearlyDay = rec.day;
            this.selectedMonth = rec.month;
            customText = this.repeatNr > 1 ? 'Every ' + this.repeatNr + ' years' : 'Annualy';
            customText += ' on ' + this.months[this.selectedMonth - 1].text + ' ' + this.yearlyDay;
            break;
        }

        if (rec.until) {
          this.condition = 'until';
          this.untilDate = rec.until;
          customText += ' until ' + formatDate('MMMM D, YYYY', new Date(this.untilDate));
        } else if (rec.count) {
          this.condition = 'count';
          this.occurrences = rec.count;
          customText += ', ' + this.occurrences + ' times';
        } else {
          this.condition = 'never';
        }

        // Add custom value
        this.repeatData = [...this.repeatData, { value: 'custom-value', text: customText }];
        // Set custom value
        this.selectedRepeat = 'custom-value';
      } else if (rec.weekDays === 'MO,TU,WE,TH,FR') {
        this.selectedRepeat = 'weekday';
      } else {
        this.selectedRepeat = rec.repeat;
      }
    } else {
      this.resetCustomValues();
    }
    this.showCustomRepeat = this.selectedRepeat === 'custom' || this.selectedRepeat === 'custom-value';
  }

  // Popuplate data for months
  populateMonthDays(month: number, type: string): void {
    const day30 = [2, 4, 6, 9, 11];
    const newValues = [];

    for (let i = 1; i <= 31; i++) {
      if (!(i === 31 && day30.includes(month)) && !(i === 30 && month === 2)) {
        newValues.push(i);
      }
    }

    if (type === 'monthly') {
      this.monthlyDays = newValues;
      this.monthlyDay = 1;
    } else {
      this.yearlyDays = newValues;
      this.yearlyDay = 1;
    }
  }

  monthChange(ev: any): void {
    this.selectedMonth = ev.value;
    this.yearlyDay = 1;
    this.populateMonthDays(ev.value, 'yearly');
  }

  navigateTo(): void {
    const rec = this.tempEvent.recurring;
    const d = new Date(this.tempEvent.start);
    let nextYear = 0;

    // Navigate the calendar to the correct view
    if (rec && rec.repeat === 'yearly') {
      if (d.getMonth() + 1 > +rec.month && d.getDay() > +rec.day) {
        nextYear = 1;
      }
      this.calendarSelectedDate = new Date(d.getFullYear() + nextYear, rec.month - 1, rec.day);
    } else {
      this.calendarSelectedDate = d;
    }
  }

  ngOnInit(): void {
    this.populateMonthDays(1, 'monthly');
    this.populateMonthDays(1, 'yearly');
  }
}
```

### Template

```html
<mbsc-eventcalendar [data]="myEvents" [options]="calendarOptions" [(selectedDate)]="calendarSelectedDate"></mbsc-eventcalendar>
<mbsc-popup [options]="popupOptions" [anchor]="popupAnchor" [buttons]="popupButtons" [headerText]="popupHeaderText" #popup>
  <div class="mbsc-form-group">
    <mbsc-input label="Title" [(ngModel)]="popupEventTitle"></mbsc-input>
    <mbsc-textarea label="Description" [(ngModel)]="popupEventDescription"></mbsc-textarea>
  </div>

  <div class="mbsc-form-group">
    <mbsc-switch label="All-day" [(ngModel)]="popupEventAllDay"></mbsc-switch>
    <mbsc-datepicker
      [(ngModel)]="popupEventDates"
      [options]="datePickerOptions"
      [controls]="popupEventAllDay ? datePickerControls : datetimePickerControls"
      [responsive]="popupEventAllDay ? datePickerResponsive : datetimePickerResponsive"
      [startInput]="startInput"
      [endInput]="endInput"
    ></mbsc-datepicker>
    <mbsc-input #startInput label="Starts"></mbsc-input>
    <mbsc-input #endInput label="Ends"></mbsc-input>
    <mbsc-select [(ngModel)]="selectedRepeat" [data]="repeatData" label="Repeats" [responsive]="selectResponsive"></mbsc-select>
  </div>

  <div *ngIf="selectedRepeat === 'custom' || selectedRepeat === 'custom-value'">
    <mbsc-segmented-group [(ngModel)]="repeatType">
      <mbsc-segmented value="daily">Daily</mbsc-segmented>
      <mbsc-segmented value="weekly">Weekly</mbsc-segmented>
      <mbsc-segmented value="monthly">Monthly</mbsc-segmented>
      <mbsc-segmented value="yearly">Yearly</mbsc-segmented>
    </mbsc-segmented-group>

    <div class="md-recurrence-options">
      <span>Repeat every</span>
      <span class="md-recurrence-input md-recurrence-input-nr">
        <mbsc-input [(ngModel)]="repeatNr" inputStyle="outline"></mbsc-input>
      </span>
      <span *ngIf="repeatType === 'daily'">day(s)</span>
      <span *ngIf="repeatType === 'monthly'">
        month(s) on day
        <span class="md-recurrence-input md-recurrence-input-nr">
          <mbsc-select [(ngModel)]="monthlyDay" [data]="monthlyDays" inputStyle="outline"></mbsc-select>
        </span>
      </span>
      <span *ngIf="repeatType === 'yearly'">
        year(s) <br />on day
        <span class="md-recurrence-input md-recurrence-input-nr">
          <mbsc-select [(ngModel)]="yearlyDay" [data]="yearlyDays" inputStyle="outline"></mbsc-select>
        </span>
        <span>of</span>
        <span class="md-recurrence-input">
          <mbsc-select [(ngModel)]="selectedMonth" [data]="months" inputStyle="outline" (onChange)="monthChange($event)"></mbsc-select>
        </span>
      </span>

      <p *ngIf="repeatType === 'daily'" class="md-recurrence-desc">
        The event will be repeated every day or every x days, depending on the value
      </p>
      <p *ngIf="repeatType === 'weekly'" class="md-recurrence-desc">The event will be repeated every x weeks on specific weekdays</p>
      <p *ngIf="repeatType === 'monthly'" class="md-recurrence-desc">
        The event will be repeated every x month on specific day of the month
      </p>
      <p *ngIf="repeatType === 'yearly'" class="md-recurrence-desc">
        The event will be repeated every x years on specific day of a specific month
      </p>
    </div>

    <mbsc-segmented-group *ngIf="repeatType === 'weekly'" [(ngModel)]="weekDays" select="multiple">
      <mbsc-segmented value="SU">Sun</mbsc-segmented>
      <mbsc-segmented value="MO">Mon</mbsc-segmented>
      <mbsc-segmented value="TU">Tue</mbsc-segmented>
      <mbsc-segmented value="WE">Wen</mbsc-segmented>
      <mbsc-segmented value="TH">Thu</mbsc-segmented>
      <mbsc-segmented value="FR">Fri</mbsc-segmented>
      <mbsc-segmented value="SA">Sat</mbsc-segmented>
    </mbsc-segmented-group>

    <div class="md-recurrence-ends">Ends</div>

    <div class="mbsc-form-group">
      <mbsc-radio-group [(ngModel)]="condition">
        <mbsc-radio label="Never" position="start" description="The event will repeat indefinitely" value="never"> </mbsc-radio>
        <mbsc-radio value="until" position="start">
          Until
          <span class="md-recurrence-input">
            <mbsc-datepicker
              [controls]="['calendar']"
              display="anchored"
              [touchUi]="false"
              dateFormat="YYYY-MM-DD"
              returnFormat="iso8601"
              [(ngModel)]="untilDate"
              (click)="condition = 'until'"
              inputStyle="outline"
            ></mbsc-datepicker>
          </span>
          <span class="mbsc-description">The event will run until it reaches a specific date</span>
        </mbsc-radio>
        <mbsc-radio value="count" position="start">
          After
          <span class="md-recurrence-input md-recurrence-input-nr">
            <mbsc-input inputStyle="outline" [(ngModel)]="occurrences" (click)="condition = 'count'"></mbsc-input>
            occurrences
            <span class="mbsc-description">The event will repeat until it reaches a certain amount of occurrences</span>
          </span>
        </mbsc-radio>
      </mbsc-radio-group>
    </div>
  </div>

  <div *ngIf="isEdit" class="mbsc-button-group">
    <mbsc-button class="mbsc-button-block" color="danger" variant="outline" (click)="onDeleteClick()">Delete event</mbsc-button>
  </div>
</mbsc-popup>

<mbsc-popup [options]="editModePopupOptions" #recurringEditModePopup>
  <div class="mbsc-form-group">
    <div class="mbsc-form-group-title">{{recurringActionText}} recurring event</div>
    <mbsc-radio-group [(ngModel)]="recurringEditMode">
      <mbsc-radio label="This event only" value="current"></mbsc-radio>
      <mbsc-radio label="This and following events" value="following"></mbsc-radio>
      <mbsc-radio label="All events" value="all"></mbsc-radio>
    </mbsc-radio-group>
  </div>
</mbsc-popup>
```

### CSS

```css
.md-recurring-event-editor-popup .md-recurrence-input .mbsc-textfield-wrapper {
  display: inline-block;
  margin: 3px;
  z-index: 3;
  line-height: normal;
}

.md-recurring-event-editor-popup .md-recurrence-input .mbsc-textfield {
  width: 140px;
  height: 32px;
}

.md-recurring-event-editor-popup .md-recurrence-input-nr .mbsc-textfield {
  width: 90px;
}

.md-recurring-event-editor-popup .md-recurrence-input .mbsc-select-icon {
  top: 50%;
  height: 1.5em;
  margin-top: -0.75em;
}

.md-recurrence-options {
  padding: 0 1em;
  margin: 1em 0;
}

.md-recurrence-ends {
  padding: 0 1em;
  margin: 1.25em 0 -1.25em 0;
}

.md-recurrence-desc {
  font-size: 12px;
  font-weight: normal;
  color: #555;
  margin: 1em 0;
}

.md-hide-elm {
  display: none;
}
```

## Implementation notes

- Use `view: { calendar: { labels: true } }` with `clickToCreate: 'double'`, `dragToCreate: true`, `dragToMove: true`, `dragToResize: true`. Pre-load 4 default recurring events: "Take vitamins" (daily every day, 10:00-12:00, `#67ab0d`), "Football training" (weekly Monday, 18:00-20:00, `#fd966a`), "Rent payment" (monthly on day 10, 17:00-18:00, `#3ea39e`), "Dexter BD" (yearly June 6, all-day, `#d00f0f`).
- The **add/edit popup** (`Popup`, responsive: anchored 510px on medium, bottom full-screen on mobile) contains: **Title** (`Input`), **Description** (`Textarea`), **All-day** (`Switch`), **Start/End** (`Datepicker` range with `startInput`/`endInput`; controls: `['calendar', 'time']` normally, `['calendar']` all-day; mobile: `['datetime']` / `['date']`), **Repeats** (`Select`), the **custom recurrence editor** section (shown only when repeat is `'custom'` or `'custom-value'`), and a **Delete event** button (edit mode only).
- The **Repeats `Select`** data has these preset values whose display texts update dynamically based on the event's start date: `norepeat` ("Does not repeat"), `daily` ("Daily"), `weekly` ("Weekly on [day name]"), `monthly` ("Monthly on day [N]"), `monthly-pos` ("Monthly on the [ordinal] [day name]"), `yearly` ("Annually on [Month] [day]"), `yearly-pos` ("Annually on the [ordinal] [day name] of [Month]"), `weekday` ("Every weekday (Monday to Friday)"), `custom` ("Custom"). When an existing event has a custom rule with `interval`/`count`/`until`, a `custom-value` entry with generated descriptive text is inserted dynamically.
- **`loadPopupForm(event)`**: populates all popup fields from the event object; sets the default `untilDate` to the day after `event.start`; rebuilds the Repeats data with date-specific labels. For recurring events: if the rule has `interval`/`count`/`until` set `selectedRepeat = 'custom-value'` and add the generated text entry; if `weekDays === 'MO,TU,WE,TH,FR'` → `'weekday'`; if positional (`pos`) → `repeat + '-pos'`; otherwise → `repeat`. Calls `resetCustomValues()` for non-recurring events.
- **Custom recurrence editor**: a `SegmentedGroup` (Daily/Weekly/Monthly/Yearly) picks `repeatType`. Below it: "Repeat every [interval `Input`] [days/weeks/months/years]". For Weekly: a multi-select weekday `SegmentedGroup` (Sun-Sat). For Monthly: day `Select` (1-31). For Yearly: day `Select` + month `Select`. An **Ends** section with a `RadioGroup`: "Never" / "Until [date `Datepicker`]" / "After [count `Input`] occurrences".
- **`getCustomRule()`**: builds the `recurring` object to save on the event. Presets map directly using the event's start date for weekday/day/month values. `weekday` → `{ repeat: 'weekly', weekDays: 'MO,TU,WE,TH,FR' }`. `custom`/`custom-value` → `{ repeat: repeatType, interval: repeatNr, ...weekDays/day/month as needed, ...until/count from condition }`. Returns `null`/`undefined` for `norepeat`.
- `onEventClick` → open popup in **edit mode**: store `originalRecurringEvent = event.original` if recurring, call `loadPopupForm(event)`, set anchor to clicked element, open with "Edit event" header and "Save" button.
- `onEventCreated` → open popup in **add mode**: call `resetCustomValues()`, `loadPopupForm(event)`, open with "New Event" header and "Add" button. **On popup close without confirming** (add mode): refresh the events array to strip the temporary event; for the imperative API, call `inst.removeEvent(event)` in the popup's `onClose` callback.
- `onEventCreate` → if `args.originEvent` is a recurring event: store `args.event` as `newEvent` and `return false` (prevents duplicate creation; the recurrence mode popup will complete the operation).
- `onEventUpdate` → if the updated event is recurring: store `args.oldEvent` as `originalRecurringEvent` and `args.oldEventOccurrence` as `eventOccurrence`; open the **recurrence mode popup** in "Edit" or "Delete" mode (based on `args.isDelete`); `return false`.
- Handle `onEventDeleted` to delete the event by filtering the events array; for the imperative API, handle deletion in the Delete button's click handler with `inst.removeEvent(editedEvent)`.
- **Recurrence mode popup** (separate `Popup`): header "[Edit/Delete] recurring event"; `RadioGroup`: "This event only" / "This and following events" / "All events" (default: `current`); buttons: Cancel / Ok.
  - **Ok → delete** (`deleteRecurringEvent`): `current` - push `event.start` to `recurringException`, update original; `following` - set `original.recurring.until = event.start`, update original; `all` - remove original. Apply changes to the events array; for the imperative API, call `inst.updateEvent(original)` or `inst.removeEvent(original)`.
  - **Ok → edit**: call `updateRecurringEvent(originalRecurringEvent, eventOccurrence, newEvent, updatedEventOrNull, recurringEditMode)`. React imports it from `@mobiscroll/react`, Vue from `@mobiscroll/vue`, Angular from `@mobiscroll/angular`, JS/jQuery use `mobiscroll.updateRecurringEvent`. Returns `{ updatedEvent, newEvent? }` - replace the original in the list and, if `newEvent` exists, append it; for the imperative API, call `inst.updateEvent(result.updatedEvent)` and `inst.addEvent(result.newEvent)`.
  - When triggered from the **popup Save button** (edit path): pass the form-built event as the 4th argument and `null` as the 3rd. When triggered from **drag/drop** (`onEventUpdate` path): pass `null` as 4th and the drag-created `newEvent` as 3rd.
- **Add button**: build event from form state with `getCustomRule()` as `recurring`, append to the events list; JS/jQuery: call `inst.updateEvent(newEvent)` to finalize the temp event. Then call `navigateTo()` and close popup.
- **Save button (non-recurring)**: build updated event from form state, replace in list, call `navigateTo()`, close popup.
- **`navigateTo()`**: for yearly events, compute the correct year and navigate to `recurring.month`/`recurring.day`; for all others, navigate to `event.start`.

## Run it in your project

1. If not signed up yet, or don't own a license, you can [start a free trial](https://mobiscroll.com/starttrial).

2. Clone the repository:

   ```
   git clone https://github.com/acidb/mobiscroll-demos-angular.git
   ```

3. If not installed yet, install the Mobiscroll CLI from NPM:

   ```
   npm install -g @mobiscroll/cli
   ```

4. Config Mobiscroll - open a terminal window in the root directory and run:

   ```
   mobiscroll config angular
   ```

5. Install dependencies - open a terminal window in the root directory and run:

   ```
   npm install
   ```

6. Run the project:

   ```
   ng serve
   ```

On a trial, add `--trial` to the config command: `mobiscroll config angular --trial`.

Running a demo locally requires a Mobiscroll trial or a license; setup is per framework at https://mobiscroll.com/docs/angular/getting-started/installation

## Related demos

- Previous: [Add, Edit and Delete Events](https://demo.mobiscroll.com/angular/eventcalendar/create-read-update-delete-CRUD.md) - Add full event management to an Angular event calendar — create events by drag or click, edit them in a custom dialog, and delete with undo.
- Next: [Block Past Event Creation](https://demo.mobiscroll.com/angular/eventcalendar/disallow-past-event-creation.md) - Keep past dates read-only in an Angular event calendar — block creation and editing on past days and show toast feedback on failed attempts.
