Skip to content

JS Modules

A JS module is just like a traditional HubL module, in that they have fields, can be edited in the page editor, and can be dragged into Drag and Drop areas, but its HTML is generated by a React component instead of HubL and its fields are generated by JSX instead of JSON. JS Modules must be located in the components/modules/ subdirectory of the JavaScript project component. JS modules are referenced in HubL like so:

{% module "todos"
   path="@projects/hello-world-project/cms-assets/components/modules/TodoList"
%}
{% module "todos"
   path="@projects/hello-world-project/cms-assets/components/modules/TodoList"
%}

Directory Structure Requirements

A JS Module file can live at either of the following paths, using the directory or file name as the module name:

/components/modules/ExampleModule/index.js

js-package/
└── components/
    └── modules/
        └── ExampleModule/
            └── index.jsx
js-package/
└── components/
    └── modules/
        └── ExampleModule/
            └── index.jsx

/components/modules/ExampleModule.jsx

js-package/
└── components/
    └── modules/
        └── ExampleModule.jsx
js-package/
└── components/
    └── modules/
        └── ExampleModule.jsx

Regardless of the path you chose, the file (i.e. either ExampleModule/index.jsx or ExampleModule.jsx) must contain the following named exports:

  • Component: A React component to be rendered. It may contain islands
  • meta: A JavaScript object, equivalent to the meta.jsonin CMS modules
  • fields: A JSX tree using components from @hubspot/cms-components/fields to define module fields or a traditional JavaScript fields object Note that you may use re-exports, for example:
// Directory Structure
js-package/
└── components/
    └── modules/
        └── ExampleModule/
            ├── ExampleModuleFields.jsx
            ├── ExampleModuleComponent.jsx
            ├── ExampleModuleMeta.js
            └── index.js
// Directory Structure
js-package/
└── components/
    └── modules/
        └── ExampleModule/
            ├── ExampleModuleFields.jsx
            ├── ExampleModuleComponent.jsx
            ├── ExampleModuleMeta.js
            └── index.js
javascript
// index.js
/*
 Note: index.js re-exports ExampleModuleFields.jsx,
 ExampleModuleMeta.js, and ExampleModuleComponent.jsx as named
 exports
 */
export { default as Component } from './ExampleModuleComponent.js;
export { fields } from './ExampleModuleFields.js;
export { meta } from './ExampleModuleMeta.js;
// index.js
/*
 Note: index.js re-exports ExampleModuleFields.jsx,
 ExampleModuleMeta.js, and ExampleModuleComponent.jsx as named
 exports
 */
export { default as Component } from './ExampleModuleComponent.js;
export { fields } from './ExampleModuleFields.js;
export { meta } from './ExampleModuleMeta.js;

Module Fields

Fields can be expressed as a JSX tree using field components from @hubspot/cms-components/fields . These are the same module fields that a developer has access to today. They also include TypeScript definitions, so developers can benefit from autocomplete and validation when defining fields. Here are the TypeDocs of all the exported Field Types.

You may still express field definitions as an array of JavaScript objects identical to the traditional JSON structure within a HubL module, exporting in the same way as fields.

Building fields with JSX

As an alternative to JSON, JS module fields are written using JSX. We believe the JSX field syntax is easier to read than JSON fields. It also allows you to dynamically generate fields, share field logic between modules, and create custom abstractions around field definitions. For example, here is a FullNameField custom field component that abstracts out a group of 2 or 3 text fields:

javascript
import {
  ModuleFields,
  TextField,
  FieldGroup,
  BooleanField
} from '@hubspot/cms-components/fields';

const FullNameField = ({ includeMiddleName = false }) => (
  <FieldGroup
    name="full_name"
    label="Full Name"
>
    <TextField
      name="given_name"
      label="Given Name"
      default="HubSpot"
      required={true}
    />
    {includeMiddleName && (
      <TextField
        name="middle_name"
        label="Middle Name"
        default=""
      />
    )}
    <TextField
      name="family_name"
      label="Family Name"
      default="Developer"
    />
  </FieldGroup>
);


export const fields = (
  <ModuleFields>
    <TextField
      name="example_field"
      label="Example Text Field"
      default="Placeholder text" />

    <FieldGroup name="group_of_fields" label="Field Group">
      <BooleanField
        name="child_boolean_field"
        label="Child Boolean Field"
        default={true}
      />
      <TextField
        name="child_text_field"
        label="Child Text Field"
        default="Child Field"
      />
    </FieldGroup>


    {/Using the custom field component alongside other fields*/}
    <FullNameField includeMiddleName={false}>
  </ModuleFields>
);
import {
  ModuleFields,
  TextField,
  FieldGroup,
  BooleanField
} from '@hubspot/cms-components/fields';

const FullNameField = ({ includeMiddleName = false }) => (
  <FieldGroup
    name="full_name"
    label="Full Name"
>
    <TextField
      name="given_name"
      label="Given Name"
      default="HubSpot"
      required={true}
    />
    {includeMiddleName && (
      <TextField
        name="middle_name"
        label="Middle Name"
        default=""
      />
    )}
    <TextField
      name="family_name"
      label="Family Name"
      default="Developer"
    />
  </FieldGroup>
);


export const fields = (
  <ModuleFields>
    <TextField
      name="example_field"
      label="Example Text Field"
      default="Placeholder text" />

    <FieldGroup name="group_of_fields" label="Field Group">
      <BooleanField
        name="child_boolean_field"
        label="Child Boolean Field"
        default={true}
      />
      <TextField
        name="child_text_field"
        label="Child Text Field"
        default="Child Field"
      />
    </FieldGroup>


    {/Using the custom field component alongside other fields*/}
    <FullNameField includeMiddleName={false}>
  </ModuleFields>
);

It's important to note that the root component of the fields export is required to be ModuleFields. Addtionally we are making use of FieldGroup which is a component type that creates a Field Group that includes the nested fields.

In the FullNameField React component for the module fields defined above, props will have the following shape:

javascript
{
  example_field: "Placeholder text",
  group_of_fields: {
    child_boolean_field: true,
    child_text_field: "Child Field"
  },
  full_name: {
    given_name: "HubSpot",
    family_name: "Developer",
  }
}
{
  example_field: "Placeholder text",
  group_of_fields: {
    child_boolean_field: true,
    child_text_field: "Child Field"
  },
  full_name: {
    given_name: "HubSpot",
    family_name: "Developer",
  }
}

Note that the default was used to fill in the value field once it was passed. This is because module values are passed from the server, so if someone changes the value of a field in the page editor, the new value will be passed to your module. But in our current case where no page-level field value is set, the server passes the default value to your props.

RepeatedFieldGroup

In addition to ModuleFields and FieldGroup, another special component type from @hubspot/cms-components/fields is RepeatedFieldGroup. It creates a repeater and is used like so:

javascript
export const fields = (
  <ModuleFields>
    <RepeatedFieldGroup
        name="default_todos"
        label="Default Todos"
        occurrence={{
          min: 1,
          max: 500,
          default: 1,
        }}
        default={[
          {
            text: 'Todo Test 1a',
            completed: false
          },{
            text: 'Todo Test 2',
            completed: true
          },
        ]}
      >
      <TextField
        label="Todo title"
        name="text"
        default="Todo..."
        required
      />
      <BooleanField label="Todo Completed" name="completed" default={false} />
    </RepeatedFieldGroup>
  </ModuleFields>
)
export const fields = (
  <ModuleFields>
    <RepeatedFieldGroup
        name="default_todos"
        label="Default Todos"
        occurrence={{
          min: 1,
          max: 500,
          default: 1,
        }}
        default={[
          {
            text: 'Todo Test 1a',
            completed: false
          },{
            text: 'Todo Test 2',
            completed: true
          },
        ]}
      >
      <TextField
        label="Todo title"
        name="text"
        default="Todo..."
        required
      />
      <BooleanField label="Todo Completed" name="completed" default={false} />
    </RepeatedFieldGroup>
  </ModuleFields>
)

Using Field Values

Field values are passed as props to the Component export of your module. For example, to use the field structure from the previous example inside of your JS module’s component you can:

javascript
export const Component = ({ fieldValues }) => {
  return (
    <ul>
      <li>{fieldValues.example_field}</li>
      <li>{fieldValues.group_of_fields.child_text_field}</li>
    </ul>
  );
};
export const Component = ({ fieldValues }) => {
  return (
    <ul>
      <li>{fieldValues.example_field}</li>
      <li>{fieldValues.group_of_fields.child_text_field}</li>
    </ul>
  );
};

GraphQL

Like in HubL modules, you can bind a GraphQL data query to a JS module. Adding a named query export to a module will provide the query result to render in the component props as dataQueryResult. You can import and re-export a .graphql query file or a JavaScript expression that evaluates to a GraphQL query (e.g. with gql-query-builder). The result will be available via a dataQueryResult prop in the module component.

javascript
// index.js

import ModuleComponent from './ModuleComponent.js';
import ModuleFields from './ModuleFields.js';
import ModuleMeta from './ModuleMeta.js';
import myQuery from './myQuery.graphql';

// This component will receive the query result via `dataQueryResult`
export const Component = ModuleComponent;

export const meta = ModuleMeta;

export const fields = ModuleFields;

export const query = myQuery;
// index.js

import ModuleComponent from './ModuleComponent.js';
import ModuleFields from './ModuleFields.js';
import ModuleMeta from './ModuleMeta.js';
import myQuery from './myQuery.graphql';

// This component will receive the query result via `dataQueryResult`
export const Component = ModuleComponent;

export const meta = ModuleMeta;

export const fields = ModuleFields;

export const query = myQuery;

And accessing the data in ModuleComponent:

jsx
//ModuleComponent.jsx

export default function ModuleComponent(props) {
  return (
    <div>
    <span>
      {props.dataQueryResult.data.CRM.contact_collection.items[0].firstname}
    </span>
    <span>
      {props.dataQueryResult.data.CRM.contact_collection.items[0].lastname}
    </span>
    </div>
  )
}
//ModuleComponent.jsx

export default function ModuleComponent(props) {
  return (
    <div>
    <span>
      {props.dataQueryResult.data.CRM.contact_collection.items[0].firstname}
    </span>
    <span>
      {props.dataQueryResult.data.CRM.contact_collection.items[0].lastname}
    </span>
    </div>
  )
}

The GraphQL HubSpot integration currently supports querying data from HubDB and Custom Objects. To explore your portal's GraphQL data schema and for help with writing queries check out our GraphiQL implementation

Using GraphQL in this way will connect any module and subsequent down stream pages to updates to the query and upstream data. This is has implications for prerendering in that updates to data sources referenced from the query will cause the page to re-prerender.