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.
npm install @gravity-ui/page-constructor
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.
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>
);
interface PageConstructorProps {
content: PageContent; //Blocks data in JSON format.
shouldRenderBlock?: ShouldRenderBlock; // A function that is invoked when rendering each block and lets you set conditions for its display.
custom?: Custom; //Custom blocks (see `Customization`).
renderMenu?: () => React.ReactNode; //A function that renders the page menu with navigation (we plan to add rendering for the default menu version).
navigation?: NavigationData; // Navigation data for using navigation component in JSON format
}
interface PageConstructorProviderProps {
isMobile?: boolean; //A flag indicating that the code is executed in mobile mode.
locale?: LocaleContextProps; //Info about the language and domain (used when generating and formatting links).
location?: Location; //API of the browser or router history, the page URL.
analytics?: AnalyticsContextProps; // function to handle analytics event
ssrConfig?: SSR; //A flag indicating that the code is run on the server size.
theme?: 'light' | 'dark'; //Theme to render the page with.
mapsContext?: MapsContextType; //Params for map: apikey, type, scriptSrc, nonce
}
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;
}
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
orcustom.subBlocks
parameter of thePageConstructor
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';
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; // data loading method
component: React.ComponentType; //blog to pass loaded data
}
type FetchLoadableData<TData = any> = (blockKey: string) => Promise<TData>;
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>
);
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} />;
Each block is an atomic top-level component. They're stored in the src/units/constructor/blocks
directory.
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
.
-
In the
src/blocks
orsrc/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 thesrc/models/constructor-items/blocks.ts
orsrc/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 thesrc/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 injson-schema
format. - Export it in the
schema/validators/blocks.ts
orschema/validators/sub-blocks.ts
file. - Add it to
enum
orselectCases
in theschema/index.ts
file.
- Add a
-
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 indata.json
at story dir. The genericStory
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
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 theThemeSupporting<T>
generic, whereT
is the type of the property. -
In the file with the block's
react
component, get the value of the property with the theme viagetThemedValue
anduseTheme
hook (see examples in theMediaBlock.tsx
block). -
Add theme support to the property validator: in the block's
schema.ts
file, wrap this property inwithTheme
.
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',
});
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
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; // only a 'string' type is supported
}>;
It is possible to configure an event to which an analytics system to sent.
type AnalyticsCounters = {
include?: string[]; // array of analytics counter ids that will be applied
exclude?: string[]; // array of analytics counter ids that will not be applied
};
Pass context
value to define place in a project where an event is fired.
Use selector below or create logic that serves project needs.
// analyticsHandler.ts
if (isCounterAllowed(counterName, counters)) {
analyticsCounter.reachGoal(counterName, name, parameters);
}
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', // default events which fire on every button click
Play = 'play', // React player event
Stop = 'stop', // React player event
}
npm ci
npm run dev
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.
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.
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 branchmain
. 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 become3.0.0
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.
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} />
);
Comprehensive documentation is available at the provided link.