Building components
Extending native types

Extending Existing Native Types

There are three existing native types that can be extended with additional options: time, timeRange, and granularity. These types are used in various components to provide date and time selection functionality.

  • time: Represents a specific point in time. It is typically used for selecting a single date or timestamp.
  • timeRange: Represents a range of time between two points. It is commonly used for selecting a start and end date.
  • granularity: Represents the level of detail for time-based data. It is used to specify how data should be aggregated over time (e.g., daily, weekly, monthly).

To extend these types with additional options, you can use defineOption (explained in detail in "Defining Custom Types") that build upon the existing native types.

⚠️

When extending native types, best practice is to create a new type.emb.ts file as if you were creating a custom type, but avoid using the defineType function, as this will cause build errors. The native type is already defined in the core library. You only need the defineOption function to add new options to it.

Below are examples of how to extend each type:

Extending the time Type

Create Type File

Create a new file named time.type.emb.ts in your types directory.

Extend Types

Add the following code to extend the time type with your desired new options.

import { defineOption } from '@embeddable.com/core';
 
// Example options
defineOption('time', { date: new Date(1980, 3, 28), label: 'My birthday' })
defineOption('time', { relativeTimeString: 'next week', label: 'Next week' })

Test Your New Options

Use a date picker component in your Embeddable and note that the new options are available for selection.

Extending the timeRange Type

Create Type File

Create a new file named timeRange.type.emb.ts in your types directory.

Extend Types

Add the following code to extend the timeRange type with your desired new options.

import { defineOption } from '@embeddable.com/core';
 
// Example options
defineOption('timeRange', { from: new Date(2000, 1, 1), to: new Date(), label: 'This millenium' })
defineOption('timeRange', { relativeTimeString: 'last quarter', label: 'Last quarter' })

Test Your New Options

Use a date range picker component in your Embeddable and note that the new options are available for selection.

Extending the granularity Type

Update Your Data Models

In order to use custom granularities, you will also need to add a few lines to your data model(s). This is done with a simple YAML addition to any time dimension, like so:

- name: created_at
  sql: created_at
  type: time
  description: 'The time when the order was created'
  # optional - define additional custom time intervals (granularities)
  granularities:
    - name: quarter_hour
      interval: 15 minutes
 
    - name: week_starting_on_sunday
      interval: 1 week
      offset: -1 day

To learn more about defining custom granularities in your data models, see the Cube Documentation (opens in a new tab).

Create Type File

Create a new file named granularity.type.emb.ts in your types directory.

Extend Types

Add the following code to extend the granularity type with your desired new options.

import { defineOption } from '@embeddable.com/core';
// Example options
defineOption('granularity', 'quarter_hour');
defineOption('granularity', 'week_starting_on_sunday');

Update Custom Components

Any custom components that use the granularity type must be updated to reference your new options.

By default, Embeddable's Vanilla Components will work with your custom granularities (you can also disable this behavior in the component settings). However, if you have created custom components that use the granularity type, you will need to make a couple of small changes to ensure they work with your new options.

Note: The Date Picker with Granularity component will display your new granularity options, but they will always show (the native options are filtered so that only relevant options show in the list).

See below for a complete example

Updating Custom Components to Use Extended Granularities

The following examples show how the Granularity picker was updated, and should give you the tools you need to update your own custom components.

GranularityPicker.emb.ts

Added the following input:

    {
      name: 'displayCustomGranularities',
      type: 'boolean',
      label: `Display Custom Granularities`,
      description: `If enabled, additional granularities defined by your data team will be included in the dropdown.`,
      defaultValue: true,
      category: 'Granularity options',
    },

New File: util/getCustomGranularities.ts

This function is required because depending on whether you're in the dev or production Embeddable environment, the structure of the window object is slightly different. This function handles both cases.

type NativeTypesShape = {
  granularity?: {
    options?: string[];
  };
};
 
type Embeddable = {
  nativeTypes?: NativeTypesShape;
  [key: string]: any; // bundle hash is used as a key in production
};
 
type WindowSimplified = {
  __EMBEDDABLE__?: Embeddable;
  __EMBEDDABLE_BUNDLE_HASH__?: string;
};
 
const extendedWindow = window as WindowSimplified;
 
export const getBundleHash = () => {
  return extendedWindow.__EMBEDDABLE_BUNDLE_HASH__;
};
 
const getFromWindow = (): NativeTypesShape | undefined => {
  const bundleHash = getBundleHash();
  const embeddable = extendedWindow.__EMBEDDABLE__;
 
  if (bundleHash && embeddable && embeddable[bundleHash]) {
    const bundleProp = embeddable[bundleHash];
    return bundleProp.nativeTypes as NativeTypesShape | undefined;
  } else if (embeddable && embeddable.nativeTypes) {
    return embeddable.nativeTypes;
  }
  return undefined;
};
 
export const getCustomGranularities = (): string[] => {
  if (typeof window !== 'undefined') {
    const nativeTypes = getFromWindow();
    return nativeTypes?.granularity?.options || [];
  } else {
    return [];
  }
};

index.tsx

First, we get our custom granularities. These are found under the __EMBEDDABLE__ property that's added to the window object by the Embeddable SDK.

import { getCustomGranularities } from '../../../util/getCustomGranularities';
 
const [customGranularities, setCustomGranularities] = useState<string[]>([]);
 
useEffect(() => {
  // Get granularities from the window object if available
  const windowGranularities = getCustomGranularities();
  setCustomGranularities(windowGranularities || []);
}, []);

Then we ensure that our final list of granularities includes our custom ones if the input is enabled:

const granularityOptions = (): GranularityResponse => {
  const data: { value: Granularity }[] = [];
  // display options selected by user
  options.filter((option) => props[option])?.forEach((option) => data.push({ value: option }));
 
  // Handle custom granularities from window object
  let customOptions: { value: Granularity }[] = [];
  if (props.displayCustomGranularities) {
    customOptions = customGranularities.map((title: string) => ({
      value: title as Granularity,
    }));
  }
 
  const finalData = [...data, ...customOptions];
 
  return {
    isLoading: false,
    data: finalData,
  };
};

Finally, we pass the results of that function to the Dropdown component:

<Dropdown
  unclearable
  minDropdownWidth={320}
  defaultValue={props.defaultValue}
  options={granularityOptions()}
  placeholder="Granularity"
  property={valueProp}
  onChange={(v) => handleChange(v)}
/>

One final important thing we need to adjust is to ensure visual consistency between the default granularities (lowercase with spaces) and the custom granularities (lowercase with underscores). This is just a matter of stripping any underscores and replacing them with spaces on display:

{o[props.property?.name || ''].replaceAll('_', ' ')}

In this fashion, the dropdown passes the proper value (with underscores) but displays a user-friendly version (with spaces).