Blazor Gantt - Timeline Header

Gantt Chart - Custom Timeline Header

The guide below details how to implement a Custom Timeline Header for Gantt Chart in Blazor.

To implement a Custom Timeline Header, we need to be able to directly set event listeners, enable/disable buttons and perform other DOM operations on non-Blazor elements.

For this reason, we will be using JS interop - a Blazor tool, which can invoke JavaScript (JS) functions from .NET methods and .NET methods from JS functions.

  1. First, create a basic Gantt Chart with multiple tasks:
    @page "/"
    @using System.Text.Json.Serialization;
    <GanttChart @ref="gantt" DataSource="@Records"/>
    @code {
    
        public partial class GanttDataRecord
        {
    
            [JsonPropertyName("label")]
            public string Label
            {
                get;
                set;
            }
    
            [JsonPropertyName("dateStart")]
            public string DateStart
            {
                get;
                set;
            }
    
            [JsonPropertyName("dateEnd")]
            public string DateEnd
            {
                get;
                set;
            }
    
            [JsonPropertyName("type")]
            public string Type
            {
                get;
                set;
            }
    
            [JsonPropertyName("duration")]
            public int Duration
            {
                get;
                set;
            }
        }
    
        GanttChart gantt;
    
        public List<GanttDataRecord> Records;
        protected override void OnInitialized()
        {
            Records = new List<GanttDataRecord>()
            {
                new GanttDataRecord{
                Label = "Develop Website",
                DateStart = "2021-01-01",
                DateEnd = "2021-01-20",
                Type = "task"
            },
            new GanttDataRecord{
                Label = "Marketing Campaign",
                DateStart = "2021-01-05",
                 DateEnd = "2021-01-15",
                Type = "task"
            },
            new GanttDataRecord{
                Label = "Publishing",
                DateStart = "2021-01-10",
                 DateEnd = "2021-01-26",
                Type = "task"
            },
            new GanttDataRecord{
                Label = "Find clients",
                DateStart = "2021-01-12",
                 DateEnd = "2021-01-25",
                Type = "task"
            }
            };
        }
    
    }
    Basic Gantt
  2. Next, we need to create the HTML of the Custom Timeline Header and set its id to the headerTemplate property of the Gantt Chart:
    <GanttChart @ref="gantt" DataSource="@Records" HeaderTemplate="templateId" />
    
    
    
    <template id="headerTemplate">
        <div class="header-controls">
            <div class="zooming">
                <smart-button id="zoomIn"><i class="material-icons">zoom_in</i></smart-button>
                <smart-button id="zoomOut"><i class="material-icons">zoom_out</i></smart-button>
            </div>
            <div class="resource-view">
                <smart-button id="view">Show Resource View</smart-button>
            </div>
            <div class="actions">
                <smart-button id="undo" disabled><i class="material-icons">restore</i></smart-button>
                <smart-button id="redo" disabled><i class="material-icons">update</i></smart-button>
            </div>
        </div>
    </template>
    
    @code{
        string templateId = "headerTemplate";
        ....
    }
                        
  3. Finally, we need to add the CSS for the Custom Timeline Header:
    @font-face {
        font-family: 'Material Icons';
        font-style: normal;
        font-weight: 400;
        src: url(https://fonts.gstatic.com/s/materialicons/v31/2fcrYFNaTjcS6g4U3t-Y5ZjZjT5FdEJ140U2DJYC3mY.woff2) format('woff2');
    }
    
    .material-icons {
        font-family: 'Material Icons';
        font-weight: normal;
        font-style: normal;
        font-size: inherit;
        line-height: 1;
        letter-spacing: normal;
        text-transform: none;
        display: inline-block;
        white-space: nowrap;
        word-wrap: normal;
        direction: ltr;
        -webkit-font-feature-settings: 'liga';
        -webkit-font-smoothing: antialiased;
        font-size: 21px;
    }
    
    smart-gantt-chart {
        height: auto;
        --smart-gantt-chart-header-height: 40px;
    }
    
    .header-controls {
        height: 100%;
        display: grid;
        grid-template-columns: 100px 1fr 100px;
        grid-column-gap: 5px;
        overflow: hidden;
        padding: 5px;
        --smart-button-padding: 2.5px 10px;
    }
    
    .zooming,
    .resource-view,
    .actions {
        min-height: 0;
        height: 100%;
        display: flex;
        align-items: center;
    }
    
    .zooming,
    .actions {
        justify-content: space-between;
        display: grid;
        grid-template-columns: 1fr 1fr;
        grid-column-gap: 5px;
    }
    
    .resource-view {
        justify-content: center;
    }
    
    .zooming smart-button,
    .actions smart-button {
        width: auto;
        height: 100%;
    }
    
    .resource-view smart-button {
        width: auto;
        height: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
    } 

    We can see that the Timeline Header was integrated successfully, however all buttons are unresponsive.
    Custom Timeline Header

  4. To implement the actions of each button, we must use JSInterop. First inject the IJSRuntime abstraction.
    Then in the OnReady() callback, call the "setCustomHeader" JS function, which we will later create.
    @inject IJSRuntime JSInterop;
    <GanttChart @ref="gantt" DataSource="@Records" HeaderTemplate="templateId" OnReady="OnReady"/>
    @code{
      ...
      private void OnReady(GanttChart ganttChart)
      {
          JSInterop.InvokeVoidAsync("setCustomHeader");
      }
    }
  5. Now, navigate to Pages/_Host.cshtml(Blazor Server) or wwwroot/index.html(Blazor WebAssembly).
    Create a <script></script> tag and paste the code below, which will create the "setCustomHeader" function:
    <script>
        (function (global) {
            global.setCustomHeader = function(){
                const ganttChart = document.querySelector('smart-gantt-chart'), views = ['day', 'week', 'month', 'year'];
                let view, stateId = 0, states;
                function storeState() {
                    stateId++;
                    states[stateId] = ganttChart.getState();
                    document.getElementById('undo').disabled = false;
                }
                ganttChart.addEventListener('change', storeState);
                ganttChart.addEventListener('dragEnd', storeState);
                ganttChart.addEventListener('resizeEnd', storeState);
                ganttChart.addEventListener('progressChangeEnd', storeState);
                ganttChart.addEventListener('connectionEnd', storeState); 
                states = { 0: ganttChart.getState() };
                view = ganttChart.view;
                document.querySelector('.header-controls').addEventListener('click', function (event) {
                    const button = event.target.closest('smart-button');
                    if (!button) {
                        return;
                    }
                    switch (button.id) {
                        case 'view':
                            if (ganttChart.groupByResources) {
                                ganttChart.groupByResources = false;
                                button.innerHTML = 'show resource view';
                            }
                            else {
                                ganttChart.groupByResources = true;
                                button.innerHTML = 'hide resource view';
                            }
                            break;
                        case 'zoomIn':
                        case 'zoomOut': {
                            const isZoomIn = button.id === 'zoomIn', maxValue = isZoomIn ? views[views.length - 1] : views[0];
                            ganttChart.view = view = (views[views.indexOf(view) + (isZoomIn ? -1 : 1) * 1] || maxValue);
                            document.getElementById(isZoomIn ? 'zoomOut' : 'zoomIn').disabled = false;
                            if ((isZoomIn && view === 'day') || (!isZoomIn && view === 'year')) {
                                button.disabled = true;
                            }
                            break;
                        }
                        case 'undo':
                        case 'redo':
                            if (button.id === 'undo') {
                                stateId -= 1;
                                if (states[stateId]) {
                                    ganttChart.loadState(states[stateId]);
                                    document.getElementById('redo').disabled = false;
                                }
                                if (!states[stateId] || !states[stateId - 1]) {
                                    button.disabled = true;
                                }
                            }
                            else {
                                stateId += 1;
                                if (states[stateId]) {
                                    ganttChart.loadState(states[stateId]);
                                    document.getElementById('undo').disabled = false;
                                }
                                if (!states[stateId] || !states[stateId + 1]) {
                                    button.disabled = true;
                                }
                            }
                            stateId = Math.max(0, Math.min(Object.keys(states).length - 1, stateId));
                            return;
                    }
                });
                
            }
        })(window);
    </script>
  6. Now we can see that the Custom Timeline Header is complete and all buttons have their actions attached: Custom Timeline Header