Our state of the art Gantt chart


Post by aleix »

Good morning,
I'm creating a custom widget that renders a React component (from Reactstrap) inside Bryntum Gantt.
The widget works fine visually and fires the change event correctly, but I have an issue with initializing the value — it always shows the default color (#c2202b) instead of the value passed in the editor config.

Here’s my code:

import { Widget } from '@bryntum/gantt';
import React, { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { Input } from 'reactstrap';

const ColorPickerInput = ({ initialColor, onChange }) => {
  const [color, setColor] = useState(initialColor);
  useEffect(() => setColor(initialColor), [initialColor]);

  return (
    <Input
      type="color"
      value={color}
      onChange={(e) => {
        const newColor = e.target.value;
        setColor(newColor);
        onChange(newColor);
      }}
    />
  );
};

export default class MyColorPicker extends Widget {
  static type = 'customColorPicker';
  static get configurable() {
    return {
      color: '#c2202b',
      label: 'Color'
    };
  }

  construct(config) {
    super.construct(config);
    this.color = config.color || config.value || '#c2202b';
  }

  onPaint({ firstPaint }) {
    if (firstPaint) {
      this.root = createRoot(this.element);
      this.root.render(
        <ColorPickerInput
          initialColor={this.color}
          onChange={(newColor) => {
            this.color = newColor;
            this.trigger('change', { value: newColor });
          }}
        />
      );
    }
  }

  doDestroy() {
    this.root?.unmount();
    super.doDestroy();
  }
}

MyColorPicker.initClass();

And this is how I use it inside my editor configuration:

// workOrderEditor.ts
export const workOrderEditor: any = {

  items: [
    { 
      type: 'panel', 
      items: [
        {
          type: 'container',
          layout: "hbox",
          width: "100%",
          style: 'display: flex; gap: 12px;',
          items: [
            { 
              type: 'text', 
              label: strings.code, 
              name: 'code', 
              disabled: true,
              style: 'flex-direction: column',
              labelPosition: 'top',
              flex: '1'
            },
            { 
              type: 'text', 
              label: strings.name, 
              name: 'name', 
              disabled: true, 
              style: 'flex-direction: column',
              labelPosition: 'top',
              flex: '1'
            }
          ]
        },
        { 
          type: 'text', 
          label: strings.description, 
          name: 'description', 
          style: 'flex-direction: column',
          labelPosition: 'top' 
        },
        // DIVIDER
        {
          html: '',
          dataset: { text: strings.temporalFields },
          cls: 'b-divider',
          flex: '1 0 100%'
        },
        // DIVIDER
        { 
          type: 'number', 
          label: strings.duration, 
          name: 'duration', 
          disabled: true, 
          style: 'flex-direction: column',
          labelPosition: 'top',
          flex: '1'
        },
        { 
          type: 'dateTime', 
          label: strings.startDate, 
          name: 'startDate', 
          style: 'flex-direction: column',
          flex: '1'
        },
        { 
          type: 'dateTime', 
          label: strings.endDate, 
          name: 'endDate', 
          style: 'flex-direction: column',
        
flex: '1' }, { type: 'date', label: 'Fecha límite', name: 'deadline', labelPosition: 'top' }, { type: 'number', label: 'Prioridad', name: 'priority', min: 0, max: 10, step: 1, labelPosition: 'top' }, { type: 'customColorPicker', label: 'Color', name: 'color', labelPosition: 'top' } ] } ] };

The widget always renders with the default #c2202b, even though the record’s field color or the config’s color value is #ff9900.

I’ve logged the config object in construct(), but it seems the value property is not passed.
Could you please clarify the correct way to receive the bound field’s value (like the name binding in regular Bryntum form fields) inside a custom widget class?
Or if there’s another lifecycle hook I should use to read the record value before rendering?

Thanks a lot!


Post by tasnim »

It's hard to say why it's not working properly without testing it on our end! Could you please share a runnable test case so we can run it on our end and see what's wrong?

Best regards,
Tasnim

How to ask for help? Please read our Support Policy


Post by aleix »

Hello,

I tried to create a new minimal project but couldn’t, so I’m sharing the full code via Google Drive so you can check it directly:
https://drive.google.com/drive/folders/1IaH18bJatj-b72chvVn9wsW3rKnPGrUt?usp=drive_link

I have some suspicions that the issue is related to using a custom Popup instead of the default Task Editor.
Because even when I try to modify any field from my custom popup, it doesn’t affect the task — I can change values, but nothing happens or gets applied to the record.

Could you please check the code and confirm if the problem is indeed because of using a plain Popup?

Thanks a lot for your help!


Post by tasnim »

Thanks for sharing the code. I don't see there is a package.json file. How can we run it?
Could you please share a test case with package.json and removing the node_modules and wrap it in a zip, upload here so that we can run it on our machine with npm i && npm start and see what's wrong?

Best regards,
Tasnim

How to ask for help? Please read our Support Policy


Post by aleix »

bryntum-colorpicker-test.zip
npm run dev
(37.6 KiB) Downloaded 7 times

There it is.


Post by aleix »

Good night,
I’ve already fixed it with the following changes.

I now pass the taskRecord to each item so that I can access the values I need, like this.
I’d love to get some feedback to know if this is the correct way to handle it.

export const injectTaskRecord = (items: any[], taskRecord: any) => {
  return items.map(item => {
    const newItem = { ...item };
    newItem.taskRecord = taskRecord;

if (Array.isArray(newItem.items)) {
  newItem.items = injectTaskRecord(newItem.items, taskRecord);
}

return newItem;
  });
}
import { workOrderEditor } from './tasksEditors/workOrderEditor';
import { articleEditor } from './tasksEditors/articleEditor';
import { operationEditor } from './tasksEditors/operationEditor';
import { DomClassList, EventModel, Popup, SchedulerPro, SchedulerProTaskEdit } from '@bryntum/gantt';
import { injectTaskRecord } from './utils/injectTaskRecord';

/**
 * This handler is triggered right before Bryntum opens the Task Edit dialog.
 * It dynamically switches the editor layout depending on the task "type".
 *
 * Each type (WorkOrder, Article, Operation) has its own editor configuration,
 * defined in separate files under /editors.
 */
export const beforeTaskEdit = ({
  source,
  taskRecord,
  taskEdit,
  taskElement
}: {
  source: SchedulerPro;
  taskEdit: SchedulerProTaskEdit;
  taskRecord: EventModel;
  taskElement: HTMLElement
}) => {
  const cls = taskRecord.cls as DomClassList
  // Check task "cls" (custom CSS class) to determine which editor to load.
  if (cls.contains('gantt-workOrder')) {
    const cfg = workOrderEditor;

    const popup = new Popup({
      forElement: taskElement,
      record: taskRecord,
      closeAction: 'destroy',
      width: '80%',
      height: 'fit-content',
      modal: true,
      closable: true,
      items: injectTaskRecord(workOrderEditor.items, taskRecord),
      bbar: [
        {
          type: 'button',
          text: 'Cancel',
          cls: 'btn btn-secondary',
          onClick() {
            popup.close();
          }
        },
        {
          type: 'button',
          text: 'Save',
          color: 'b-red',
          cls: 'btn btn-primary',
          onClick() {
            popup.eachWidget((widget: any) => {
              if (widget.name && 'value' in widget) {
                taskRecord.set(widget.name, widget.value);
              }
            });
            popup.close();
          }
        }
      ]
    });

    return false;
  } else if (cls.contains('gantt-article')) {
    // Article
    console.log('article')
    // editor.items = articleEditor.items;
  } else if (cls.contains('gantt-operation')) {
    // Operation
    console.log('operation')
    // editor.items = operationEditor.items;
  }
};

and then i have acces to the value

import { Widget } from '@bryntum/gantt';
import React, { useState, useEffect } from 'react';
import { createRoot, Root } from 'react-dom/client';
import { Input } from 'reactstrap';

// --- Componente React controlado ---
const ColorPickerInput = ({
  initialColor,
  onChange
}: {
  initialColor: string;
  onChange: (val: string) => void;
}) => {
  const [color, setColor] = useState(initialColor);

  useEffect(() => {
    setColor(initialColor);
  }, [initialColor]);

  return (
    <Input
      type="color"
      value={color}
      style={{ cursor: 'pointer' }}
      onMouseDown={(e) => e.stopPropagation()}
      onClick={(e) => e.stopPropagation()}
      onChange={(e) => {
        const newColor = e.target.value;
        setColor(newColor);
        onChange(newColor);
      }}
    />
  );
};

export default class MyColorPicker extends Widget {
  private root?: Root;
  private color = '#c2202b';

  static $name = 'MyColorPicker';
  static type = 'customColorPicker';

  static get configurable() {
    return {
      color: '#c2202b',
      label: 'Color',
      style: 'display: flex; align-items: center; gap: 8px; pointer-events: auto;'
    };
  }

  construct(config: any): void {
    super.construct(config);

const initialValue = config.taskRecord?.get(config.name);
if (initialValue) {
  this.color = initialValue.toString();
}
  }

  onPaint = ({ firstPaint }: { firstPaint: boolean }) => {
    if (!firstPaint) return;

const recordColor = (this.config as any)?.taskRecord?.get((this.config as any)?.name);
if (recordColor) {
  this.color = recordColor.toString();
}

this.root = createRoot(this.element);
this.root.render(
  <ColorPickerInput
    initialColor={this.color}
    onChange={(newColor) => {
      this.color = newColor;
      this.trigger('change', { value: this.color });
    }}
  />
);
  };

  get value(): string {
    return this.color;
  }

  set value(val: string) {
    if (val !== this.color) {
      this.color = val;
      if (this.root) {
        this.root.render(
          <ColorPickerInput
            initialColor={val}
            onChange={(newColor) => {
              this.color = newColor;
              this.trigger('change', { value: this.color });
            }}
          />
        );
      }
    }
  }

  doDestroy(): void {
    this.root?.unmount();
    this.root = undefined;
    super.doDestroy();
  }
}

MyColorPicker.initClass();

Post by tasnim »

Hi there,

Nice work. Glad to know you've got it working, your fix looks fine to me.

If you have any other questions or concerns, feel free to reach out!

Best regards,
Tasnim

How to ask for help? Please read our Support Policy


Post Reply