Our powerful JS Calendar component


Post by kumarbv »

Is there angular code for the tbar implementation for filtering events based on different fields.

I am looking for angular code for the following demo.
https://bryntum.com/products/calendar/examples/frameworks/angular/filtering/dist/filtering/

trying to filter events by text based left side selct box
trying to filter events by text based left side selct box
Screenshot_20230201_020103.png (24.75 KiB) Viewed 404 times

How can we implement above in angular? Filtering events based on select box field.

My config file.

export const config = {
  features: {
    eventTooltip: {
      // Configuration options are passed on to the tooltip instance.
      // Override the default which is to show on click.
      showOn: 'hover',
      // Create content for Tooltip header
      titleRenderer: (eventRecord) => eventRecord.name,
      tools: {
        delete: false,
      },
    },
    eventEdit: false,
  },
  tbar: {
    items: {
      filterBy: {
        items: ['Chair', 'Title', 'Subject'],
        value: 'Chair',
        label: 'Filter by',
        weight: 600,
        type: 'combo',
        triggers: {
          filter: {
            align: 'start',
          },
        },
        onChange({ value }) {
          console.log('select value change', value);
        },
      },
      filterText: {
        type: 'textfield',
        icon: 'b-fa b-fa-filter',
        weight: 600,
        placeholder: 'Enter text',
        clearable: true,
        keyStrokeChangeDelay: 100,
        triggers: {
          filter: {
            align: 'start',
            cls: 'b-fa b-fa-filter',
          },
        },
        style: { width: '40em' },
      // "up." means look in ownership tree. Will be found on the Calendar
        onChange : 'onNameSearchChange'
      },
    },
  },
  date: new Date(),
  // Modes are the views available in the Calendar.
  // An object is used to configure the view.
  modes: {
    year: false,
    agenda: false,
    list: {
      columns: [
        { text: 'Date', field: 'date', flex: 1 },
        { text: 'Start', field: 'startDate', hidden: true, flex: 1 },
        { text: 'Finish', field: 'endDate', hidden: true, flex: 1 },
        { text: 'starts', field: 'startTime', flex: 1 },
        { text: 'ends', field: 'endTime', flex: 1 },
        { text: 'Subject', field: 'name', flex: 3 },
        { text: 'Chair', field: 'pm', flex: 1 },
        { text: 'Location', field: 'location', flex: 1 },
        { text: 'Company', field: 'company', flex: 1 },
        { text: 'Sector', field: 'sector', flex: 1 },
      ],
    },
    day: {
      // These two settings decide what time span is rendered.
      dayStartTime: 6,
      dayEndTime: 22,
      fitHours: true,
    },
    week: {
      dayStartTime: 6,
      dayEndTime: 22,
      fitHours: true,
    },
  },
};

Post by tasnim »

Hi,

You could get it from here Calendar\examples\frameworks\angular\filtering


Post by kumarbv »

look at the implementaion for angular, can we conclude that we can not place filters inside the calendar config?

unlike this

<div class = "demo-toolbar align-right">
    <bryntum-text-field
        label = "Filter tasks by name"
        clearable = true
        keyStrokeChangeDelay = 100
        [triggers] = "filterTriggers"
        (onChange) = "onFindChange($event)"
    ></bryntum-text-field>
    <bryntum-text-field
        label = "Highlight tasks"
        clearable = true
        keyStrokeChangeDelay = 100
        [triggers] = "highlightTriggers"
        (onChange) = "onHighlightChange($event)"
    ></bryntum-text-field>
</div>

Post by alex.l »

Hi kumarbv,

What do you mean by "place filters"?
If you are talking about textfields to manage filters, it is possible to put it into config. As example as items for https://bryntum.com/products/calendar/docs/api/Calendar/view/Calendar#config-tbar
Here is docs for a textfield itself https://bryntum.com/products/calendar/docs/api/Core/widget/TextField

    tbar : {
        items : {
             myFilterField : {
                 type : 'textfield',
                 label : 'FIlter by something',
                 listeners : {
                    change : myMethodName
                 }
             }
        }
    }

All the best,
Alex


Post by kumarbv »

Hi alex,

how do you manage to bind myMethodName in config with the onFindChange function below.

import { AfterViewInit, Component, ViewChild, ViewEncapsulation } from '@angular/core';
import { BryntumCalendarComponent } from '@bryntum/calendar-angular';
import { Calendar, EventModel, EventStore } from '@bryntum/calendar';
import { calendarConfig } from './app.config';

@Component({
    selector      : 'app-root',
    templateUrl   : './app.component.html',
    styleUrls     : ['./app.component.scss'],
    encapsulation : ViewEncapsulation.None
})
export class AppComponent implements AfterViewInit {
    @ViewChild(BryntumCalendarComponent) calendarComponent: BryntumCalendarComponent;

private calendar: Calendar;
private eventStore: EventStore;

// calendar configuration
calendarConfig = calendarConfig;

filterTriggers = {
    filter : {
        align : 'start',
        cls   : 'b-fa b-fa-filter'
    }
};

highlightTriggers = {
    filter : {
        align : 'start',
        cls   : 'b-fa b-fa-highlighter'
    }
};

/**
 * Find by name text field change handler
 */
onFindChange({ value }: any): void {
    // We filter using a RegExp, so quote significant characters
    const val = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

    // A filter with an id replaces any previous filter with that id.
    // Leave any other filters which may be in use in place.
    this.calendar.eventStore.filter({
        id       : 'eventNameFilter',
        filterBy : (event: EventModel) => event.name.match(new RegExp(val, 'i'))
    });
}

/**
 * Highlight text field change handler
 */
onHighlightChange({ value }: any): void {
    const
        val = value.toLowerCase(),
        { eventStore, calendar } = this;

    // Calendars refresh on any data change so suspend that.
    // We will trigger the store's change event when we're done.
    eventStore.suspendEvents();

    // Loop through all events in the store
    eventStore.forEach((task) => {
        // The cls field is a DomClassList with add and remove methods
        if (val !== '' && task.name.toLowerCase().includes(val)) {
            task.cls.add('b-match');
        }
        else {
            task.cls.remove('b-match');
        }
    });
    eventStore.resumeEvents();

    // Announce that data has changed which will refresh UIs.
    eventStore.trigger('change');

    calendar.element.classList[value.length > 0 ? 'add' : 'remove']('b-highlighting');
}

/**
 * Called after View is initialized
 */
ngAfterViewInit(): void {
    this.calendar = this.calendarComponent.instance;
    this.eventStore = this.calendar.eventStore;

    // Uncomment the code below in this method to start "logging" eventStore events
    // const eventStore = this.calendar.calendarInstance.eventStore;
    // eventStore.on('change', ({ action, record, records }) => {
    //
    //     const eventNames = records && records.map(eventRecord => eventRecord.name).join(',');
    //
    //     switch (action) {
    //         case 'update':
    //             console.log(`Event updated: ${record.name}`);
    //             break;
    //
    //         case 'add':
    //             console.log(`Events added: ${eventNames}`);
    //             break;
    //
    //         case 'remove':
    //             console.log(`Events removed: ${eventNames}`);
    //             break;
    //     }
    // });
}

onCalendarEvents(event: any): void {
    // Uncomment the code in this method to start "logging" events
    // switch (event.type) {
    //     case 'aftereventsave':
    //         console.log(`New event saved: ${event.eventRecord.name}`);
    //         break;
    //
    //     case 'beforeeventdelete':
    //         console.log(`Events removed: ${event.eventRecords.map(eventRecord => eventRecord.name).join(',')}`);
    //         break;
    // }
}
}


Post by tasnim »

Hello,

You could use ref and widgetMap to listen for change and add your listenerFn to that change listener

Here are the steps

In your config file add the tbar item like this

    tbar : {
        items : [{
            type : 'textfield',
            placeholder : 'Filter',
            ref : 'filterFieldRef'
        }]
    }

And then in your app.component.ts file
get the searchField and listen to the change listener

this.calendar.widgetMap['searchFieldRef'].on('change', this.yourMethodName)

Docs
https://bryntum.com/products/calendar/docs/api/Core/widget/Widget#config-ref
https://bryntum.com/products/calendar/docs/api/Calendar/view/Calendar#property-widgetMap

All the best :),
Tasnim


Post by kumarbv »

Hi Tasnim,

thanks for the help, I bind the function with widget map, but is it dynamic or static binding.

when I am trying to access any of the varibales inside yourMethodName of the ts file, then it is showing undefined.

like

yourMethodName(event){
	//console output undefined, while this.data already defined.
	console.log(this.data);
}

consoling any of the variables inside yourMethodName of app.ts file gives undefined.

my partial ts code

export class CalendarComponent extends BaseComponent implements AfterViewInit {
  @ViewChild(BryntumCalendarComponent)
  bryntumCalendarComponent: BryntumCalendarComponent;

  private calendar: Calendar;
  private eventStore: EventStore;

  calendarConfig = config;

  events: Array<CalendarEventModel>;
  allEvents : Array<CalendarEventModel>;
  assignments: Array<AssignmentModel>;
  resources: Array<CalendarResouce>;
  authUserEmail: string;
  meetingsData: Array<MeetingsDto> = [];
  attachmentList: any = [];
  routeQueryParamsSubscription: Subscription;
  dialogRef: MatDialogRef<DialogCalendarEvent>;
  newMeetingDialogRef: MatDialogRef<any>;
  filterByOptions ={
    'Subject':'name',
    'Chair':'pm',
  }
  filterBy = 'Subject';
  filterText;

  updateLoading = this._updateLoading.bind(this);

  meetingType: Array<CalendarResouce> = [
    {
      id: ResourceIds.ALL_CALENDAR_ENTRIES_RESOURCE_ID,
      name: 'All Calendar entries',
      eventColor: '#7498BF',
    },
    {
      id: 6,
      name: 'Analyst/Consultant Meeting',
      eventColor: '#8A8E2B',
    },
    {
      id: 1,
      name: 'Company Visit',
      eventColor: '#958BBF',
    },
    {
      id: 2,
      name: 'Conference',
      eventColor: '#C67A9E',
    },
    {
      id: 3,
      name: 'Group',
      eventColor: '#7C9D7C',
    },
    {
      id: 4,
      name: 'One on One',
      eventColor: '#DBAA5A',
    },
    {
      id: 5,
      name: 'Other',
      eventColor: '#AA5051',
    },
    {
      id: ResourceIds.UNDEFINED_RESOURCE_ID,
      name: 'Undefined',
      eventColor: '#AAAAAA',
    },
    {
      id: ResourceIds.MY_MEETINGS_ID,
      name: 'My Meetings',
      eventColor: '#6871C1',
    },
  ];
  isLoading = true;


  constructor(
    public router: Router,
    private route: ActivatedRoute,
    private ngStore: Store,
    private authService: AuthService,
    private meetingsService: MeetingsService,
    public dialog: MatDialog,
    public zone: NgZone
  ) {
    super();
  }

  _updateLoading(loading: boolean): void {
    this.zone.run(() => {
      this.isLoading = loading;
    });
  }

  onFilterByChange(event){
    this.filterBy = event.value;
    this.filterText = '';
  }

  onFilterTextChange(event){
    let sub = event.value.toLowerCase();
    //all these 
    console.log(this.allEvents);
    console.log(this.filterByOptions);
    console.log(this.filterBy);
    this.events =  this.allEvents.filter(item => item[this.filterByOptions[this.filterBy]]
      .toLowerCase()
      .startsWith(sub.slice(0, Math.max(item[this.filterByOptions[this.filterBy]].length - 1, 1)))
    );
  }

  ngAfterViewInit(): void {
    this.route.queryParams.subscribe((params) => {
      if (params.eventId) {
        this.dialog.open(DialogCalendarEvent, {
          minWidth: '50vw',
          maxHeight: '85vh',
          position: {
            top: '5.5rem',
          },
          data: {
            event: params.eventId,
          },
        });
      } else if (params.createNewMeeting) {
        this.addNewMeeting();
      }
    });

this.authUserEmail = this.authService.getEmail();
const params = {
  starts: dateFormat(moment().startOf('week').toDate()),
  ends: dateFormat(moment().endOf('week').toDate()),
  limit: undefined,
  offset: 0,
};

this.updateLoading(true);
this.meetingsService.getMeetings(params).subscribe((data) => {
  this.meetingsData = data;
  this.parseData();

  setTimeout(() => {
    this.updateLoading(false);
  });
});
  }


  parseData(): void {
    this.allEvents = this.parseEvents();
    this.events = [...this.allEvents];
    this.resources = this.meetingType;
    this.assignments = this.parseAssignments();
    this.calendar = this.bryntumCalendarComponent.instance;
    this.calendar.widgetMap['filterBy'].on('change', this.onFilterByChange);
    this.calendar.widgetMap['filterText'].on('change', this.onFilterTextChange);
    this.eventStore = this.calendar.eventStore;
    this.calendar.project.assignmentStore.data = this.assignments;
    this.calendar.features.drag.disabled = true;
  }

  parseEvents(): Array<CalendarEventModel> {
    const events = [];
    this.meetingsData.forEach((item) => {
      const obj = {
        id: item.id,
        startDate: item.starts,
        date: getShortMonthFormattedDate(new Date(item.starts)),
        startTime: moment(item.starts).format('hh:mm a'),
        endTime: moment(item.ends).format('hh:mm a'),
        endDate: item.ends,
        name: item.subject,
        meetingTypeId: item.meetingTypeId !== 0 ? item.meetingTypeId : 7,
        pm: item.pm,
        analyst: item.analyst,
        createdBy: item.createdBy,
        location: item.location,
        company: item.company,
        sector: item.sector,
      };
      events.push(obj);
    });
    return events;
  }

  parseAssignments(): Array<AssignmentModel> {
    const events = [...this.events];
    const existedMeetingIds = new Set<number>();
    this.meetingsData.forEach((item) => {
      const obj = {
        meetingTypeId:
          item.meetingTypeId !== 0
            ? item.meetingTypeId
            : ResourceIds.UNDEFINED_RESOURCE_ID,
      };
      existedMeetingIds.add(obj.meetingTypeId);
    });
    const assignments = [];
    let idCounter = 1;
    [...existedMeetingIds].forEach((eventId) => {
      events.forEach((data) => {
        if (data.meetingTypeId === eventId) {
          const assign = {
            id: idCounter,
            eventId: data.id,
            resourceId: data.meetingTypeId,
          };
          assignments.push(assign);
          idCounter++;
          //adding assignment for All calendar entries
          const allRecord = {
            id: idCounter,
            eventId: data.id,
            resourceId: ResourceIds.ALL_CALENDAR_ENTRIES_RESOURCE_ID,
          };
          assignments.push(allRecord);
          idCounter++;
          //adding my meetings
          const includedEmails = new Set<string>();
          includedEmails.add(data.pm);
          includedEmails.add(data.createdBy);
          data.analyst?.split(',').forEach((element) => {
            element.trim() !== '' ? includedEmails.add(element.trim()) : null;
          });
          if (includedEmails.has(this.authUserEmail)) {
            const myMeetingsRecord = {
              id: idCounter,
              eventId: data.id,
              resourceId: ResourceIds.MY_MEETINGS_ID,
            };
            assignments.push(myMeetingsRecord);
            idCounter++;
          }
        }
      });
    });

return assignments;
  }

  getMeetingsData(params: GetMeetingsDto) {
    this.updateLoading(true);
    this.meetingsService.getMeetings(params).subscribe((data) => {
      this.meetingsData = data;
      this.parseData();

  setTimeout(() => {
    this.updateLoading(false);
  });
});
  }


Post by alex.l »

Check the context of the method, seems it's not that you expected.
You can use .bind(this) to apply required context or pass thisObj config to on method, see docs https://bryntum.com/products/gantt/docs/api/Core/mixin/Events#function-on

All the best,
Alex


Post by kumarbv »

Hi Alex,

Thanks a lot. This has helped, looks resolved.


Post Reply