@gravity-ui/page-constructor ·
![storybook](https://img.shields.io/badge/Storybook-deployed-ff4685)
Page constructor
Page-constructor
is a library for rendering web pages or their parts based on JSON
data (support for YAML
format is to be added later).
When creating pages, component-based approach is used: a page is built using a set of ready-made blocks that can be placed in any order. Each block has a certain type and set of input data parameters.
For the format of input data and list of available blocks, see the documentation.
Install
npm install @gravity-ui/page-constructor
Required dependencies
Please note that to start using the package, your project must also have the following installed: @diplodoc/transform
, @gravity-ui/uikit
, react
. Check out the peerDependencies
section of package.json
for accurate information.
Getting started
The page constructor is imported as a React component. To make sure it runs properly, wrap it in PageConstructorProvider
:
import {PageConstructor, PageConstructorProvider} from '@gravity-ui/page-constructor';
const Page: React.PropsWithChildren<PageProps> = ({content}) => (
<PageConstructorProvider>
<PageConstructor content={content} />
</PageConstructorProvider>
);
Parameters
interface PageConstructorProps {
content: PageContent;
shouldRenderBlock?: ShouldRenderBlock;
custom?: Custom;
renderMenu?: () => React.ReactNode;
navigation?: NavigationData;
isBranded?: boolean;
}
interface PageConstructorProviderProps {
isMobile?: boolean;
locale?: LocaleContextProps;
location?: Location;
analytics?: AnalyticsContextProps;
ssrConfig?: SSR;
theme?: 'light' | 'dark';
mapsContext?: MapsContextType;
}
export interface PageContent extends Animatable {
blocks: Block[];
menu?: Menu;
background?: MediaProps;
}
interface Custom {
blocks?: CustomItems;
subBlocks?: CustomItems;
headers?: CustomItems;
loadable?: LoadableConfig;
}
type ShouldRenderBlock = (block: Block, blockKey: string) => Boolean;
interface Location {
history?: History;
search?: string;
hash?: string;
pathname?: string;
hostname?: string;
}
interface Locale {
lang?: Lang;
tld?: string;
}
interface SSR {
isServer?: boolean;
}
interface NavigationData {
logo: NavigationLogo;
header: HeaderData;
}
interface NavigationLogo {
icon: ImageProps;
text?: string;
url?: string;
}
interface HeaderData {
leftItems: NavigationItem[];
rightItems?: NavigationItem[];
}
interface NavigationLogo {
icon: ImageProps;
text?: string;
url?: string;
}
Server utils
The package provides a set of server utilities for transforming your content.
const {fullTransform} = require('@gravity-ui/page-constructor/server');
const {html} = fullTransform(content, {
lang,
extractTitle: true,
allowHTML: true,
path: __dirname,
plugins,
});
Under the hood, a package is used to transform Yandex Flavored Markdown into HTML - diplodoc/transfrom
, so it is also in peer dependencies
You can also use useful utilities in the places you need, for example in your custom components
const {
typografToText,
typografToHTML,
yfmTransformer,
} = require('@gravity-ui/page-constructor/server');
const post = {
title: typografToText(title, lang),
content: typografToHTML(content, lang),
description: yfmTransformer(lang, description, {plugins}),
};
You can find more utilities in this section
Custom blocks
The page constructor lets you use blocks that are user-defined in their app. Blocks are regular React components.
To pass custom blocks to the constructor:
-
Create a block in your app.
-
In your code, create an object with the block type (string) as a key and an imported block component as a value.
-
Pass the object you created to the custom.blocks
, custom.headers
or custom.subBlocks
parameter of the PageConstructor
component (custom.headers
specifies the block headers to be rendered separately above general content).
-
Now you can use the created block in input data (the content
parameter) by specifying its type and data.
To use mixins and constructor style variables when creating custom blocks, add import in your file:
@import '~@gravity-ui/page-constructor/styles/styles.scss';
Loadable blocks
It's sometimes necessary that a block renders itself based on data to be loaded. In this case, loadable blocks are used.
To add custom loadable
blocks, pass to the PageConstructor
the custom.loadable
property with data source names (string) for the component as a key and an object as a value.
export interface LoadableConfigItem {
fetch: FetchLoadableData;
component: React.ComponentType;
}
type FetchLoadableData<TData = any> = (blockKey: string) => Promise<TData>;
Grid
The page constructor uses the bootstrap
grid and its implementation based on React components that you can use in your own project (including separately from the constructor).
Usage example:
import {Grid, Row, Col} from '@gravity-ui/page-constructor';
const Page: React.FC<PageProps> = ({children}) => (
<Grid>
<Row>
<Col sizes={{lg: 4, sm: 6, all: 12}}>{children}</Col>
</Row>
</Grid>
);
Navigation
Page navigation can also be used separately from the constructor:
import {Navigation} from '@gravity-ui/page-constructor';
const Page: React.FC<PageProps> = ({data, logo}) => <Navigation data={data} logo={logo} />;
Blocks
Each block is an atomic top-level component. They're stored in the src/units/constructor/blocks
directory.
Sub-blocks
Sub-blocks are components that can be used in the block children
property. In a config, a list of child components from sub-blocks is specified. Once rendered, these sub-blocks are passed to the block as children
.
How to add a new block to the page-constructor
-
In the src/blocks
or src/sub-blocks
directory, create a folder with the block or sub-block code.
-
Add the block or sub-block name to enum BlockType
orSubBlockType
and describe its properties in the src/models/constructor-items/blocks.ts
or src/models/constructor-items/sub-blocks.ts
file in a similar way to the existing ones.
-
Add export for the block in the src/blocks/index.ts
file and for the sub-block in the src/sub-blocks/index.ts
file.
-
Add a new component or block to mapping in src/constructor-items.ts
.
-
Add a validator for the new block:
- Add a
schema.ts
file to the block or sub-block directory. In this file, describe a parameter validator for the component in json-schema
format. - Export it in the
schema/validators/blocks.ts
or schema/validators/sub-blocks.ts
file. - Add it to
enum
or selectCases
in the schema/index.ts
file.
-
In the block directory, add the README.md
file with a description of input parameters.
-
In the block directory add storybook demo in __stories__
folder. All demo content for story should be placed in data.json
at story dir. The generic Story
must accept the type of block props, otherwise incorrect block props will be displayed in Storybook.
-
Add block data template to src/editor/data/templates/
folder, file name should match block type
-
(optional) Add block preview icon to src/editor/data/previews/
folder, file name should match block type
Themes
The PageConstructor
lets you use themes: you can set different values for individual block properties depending on the theme selected in the app.
To add a theme to a block property:
-
In the models/blocks.ts
file, define the type of the respective block property using the ThemeSupporting<T>
generic, where T
is the type of the property.
-
In the file with the block's react
component, get the value of the property with the theme via getThemedValue
and useTheme
hook (see examples in the MediaBlock.tsx
block).
-
Add theme support to the property validator: in the block's schema.ts
file, wrap this property in withTheme
.
i18n
The page-constructor
is a uikit-based
library, and we use an instance of i18n
from uikit. To set up internationalization, you just need to use the configure
from uikit:
import {configure} from '@gravity-ui/uikit';
configure({
lang: 'ru',
});
Maps
To use maps, put the map type, scriptSrc and apiKey in field mapContext
in PageConstructorProvider
.
You can define environment variables for dev-mode in .env.development file within project root.
STORYBOOK_GMAP_API_KEY
- apiKey for google maps
Analytics
Init
To start using any analytics, pass a handler to the constructor. The handler must be created on a project side. The handler will receive the default
and custom
event objects. The passed handler will be fired on a button, link, navigation, and control clicks. As one handler is used for all events treatment, pay attention to how to treat different events while creating the handler. There are predefined fields that serve to help you to build complex logic.
Pass autoEvents: true
to constructor to fire automatically configured events.
function sendEvents(events: MyEventType []) {
...
}
<PageConstructorProvider
...
analytics={{sendEvents, autoEvents: true}}
...
/>
An event object has only one required field - name
. It also has predefined fields, which serve to help manage complex logic. For example, counter.include
can help to send event in a particular counter if several analytics systems are used in a project.
type AnalyticsEvent<T = {}> = T & {
name: string;
type?: string;
counters?: AnalyticsCounters;
context?: string;
};
It is possible to configure an event type needed for a project.
type MyEventType = AnalyticsEvent<{
[key: string]?: string;
}>;
Counter selector
It is possible to configure an event to which an analytics system to sent.
type AnalyticsCounters = {
include?: string[];
exclude?: string[];
};
context parameter
Pass context
value to define place in a project where an event is fired.
Use selector below or create logic that serves project needs.
if (isCounterAllowed(counterName, counters)) {
analyticsCounter.reachGoal(counterName, name, parameters);
}
Reserved event types
Several predefined event types are used to mark automatically configured events. Use the types to filter default events, for example.
enum PredefinedEventTypes {
Default = 'default-event',
Play = 'play',
Stop = 'stop',
}
Development
npm ci
npm run dev
Note about Vite
import react from '@vitejs/plugin-react-swc';
import dynamicImport from 'vite-plugin-dynamic-import';
export default defineConfig({
plugins: [
react(),
dynamicImport({
filter: (id) => id.includes('/node_modules/@gravity-ui/page-constructor'),
}),
],
});
For Vite, you need to install the vite-plugin-dynamic-import
plugin and configure the config so that dynamic imports work
Release flow
In usual cases we use two types of commits:
- fix: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in Semantic Versioning).
- feat: a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in Semantic Versioning).
- BREAKING CHANGE: a commit that has a footer BREAKING CHANGE:, or appends a ! after the type/scope, introduces a breaking API change (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be part of commits of any type.
- To set release package version manually you need to add
Release-As: <version>
to your commit message e.g.
git commit -m 'chore: bump release
Release-As: 1.2.3'
You can see all information here.
When you receive the approval of your pull-request from the code owners and pass all the checks, please do the following:
- You should check if there is a release pull-request from robot with changes from another contributor (it looks like
chore(main): release 0.0.0
). If it exists, you should check why it is not merged. If the contributor agrees to release a shared version, follow the next step. If not, ask him to release his version, then follow the next step. - Squash and merge your PR (It is important to release a new version with Github-Actions)
- Wait until robot creates a PR with a new version of the package and information about your changes in CHANGELOG.md. You can see the process on the Actions tab.
- Check your changes in CHANGELOG.md and approve robot's PR.
- Squash and merge PR. You can see release process on the Actions tab.
Alpha versions release
If you want to release alpha version of the package from your branch you can do it manually:
- Go to tab Actions
- Select workflow "Release alpha version" on the left page's side
- You can see on the right side the button "Run workflow". Here you can choose the branch.
- You can also see field with manually version. If you release alpha in your branch for the first time, do not set anything here. After first release you have to set the new version manually because we don't change package.json in case that the branch can expire very soon. Use the prefix
alpha
in you manual version otherwise you will get error. - Push "Run workflow" and wait until the action will finish. You can release versions as many as you want but do not abuse it and release versions if you really need it. In other cases use npm pack.
Beta-major versions release
If you want to release a new major version, you will probably need for a beta versions before a stable one, please do the following:
- Create or update the branch
beta
. - Add there your changes.
- When you ready for a new beta version, release it manually with an empty commit (or you can add this commit message with footer to the last commit):
git commit -m 'fix: last commit
Release-As: 3.0.0-beta.0' --allow-empty
- Release please robot will create a new PR to the branch
beta
with updated CHANGELOG.md and bump version of the package - You can repeat it as many as you want. When you ready to release the latest major version without beta tag, you have to create PR from branch
beta
to branch main
. Notice that it is normal that your package version will be with beta tag. Robot knows that and change it properly. 3.0.0-beta.0
will become 3.0.0
Release flow for previous major-versions
If you want to release a new version in previous major after commit it to the main, please do the following:
- Update necessary branch, the previous major release branch names are:
version-1.x.x/fixes
- for major 1.x.xversion-2.x.x
- for major 2.x.x
- Checkout a new branch from the previous major release branch
- Cherry-pick your commit from the branch
main
- Create PR, get an approval and merge into the previous major release branch
- Squash and merge your PR (It is important to release a new version with Github-Actions)
- Wait until robot creates a PR with a new version of the package and information about your changes in CHANGELOG.md. You can see the process on the Actions tab.
- Check your changes in CHANGELOG.md and approve robot's PR.
- Squash and merge PR. You can see release process on the Actions tab.
Page constructor editor
Editor provides user interface for page content management with realtime preview.
How to use:
import {Editor} from '@gravity-ui/page-constructor/editor';
interface MyAppEditorProps {
initialContent: PageContent;
transformContent: ContentTransformer;
onChange: (content: PageContent) => void;
}
export const MyAppEditor = ({initialContent, onChange, transformContent}: MyAppEditorProps) => (
<Editor content={initialContent} onChange={onChange} transformContent={transformContent} />
);
Tests
Comprehensive documentation is available at the provided link.