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 islandsmeta
: A JavaScript object, equivalent to the meta.json
 in CMS modulesfields
: 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
// 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:
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:
{
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:
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:
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.
// 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
:
//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.