Layouts
Layout Organization
In order to support customization without modify source base, MetaFox layout system organized in to template
, block
- Template separates a page into multiple named slot: header, aside, content, subside
- Block is a React component you can assign to a participant location in template
Example template structure
-------------------------------------------------------
header |
-------------------------------------------------------
Top |
-------------------------------------------------------
| | |
aside | content | subside |
| | |
| | |
-------------------------------------------------------
bottom |
-------------------------------------------------------
After that you can assign block and template using layouts.json
file
Path: root_module/src/assets/pages/home.member.json
Example:
Path: packages/metafox/core/src/assets/pages/home.member.json
{
"info": {
"bundle": "web",
"name": "home.member"
},
"large": {
"templateName": "three-column-fixed",
"blocks": [
{
"component": "feed.block.statusComposer",
"slotName": "main",
"title": "Status Composer",
"key": "15t1m",
"blockId": "15t1m",
"variant": "default",
"blockStyle": "Contained",
"blockLayout": "Blocker"
},
{
"component": "feed.block.homeFeeds",
"emptyPage": "core.block.no_content_with_description",
"slotName": "main",
"title": "Activity Feed",
"key": "mngh",
"blockId": "mngh",
"itemView": "feed.itemView.card",
"contentType": "feed",
"dataSource": {
"apiUrl": "/feed",
"apiParams": "view=latest",
"pagingId": "/feed"
},
"canLoadMore": true,
"canLoadSmooth": true,
"blockStyle": "Main Listings",
"gridStyle": "Feeds",
"blockLayout": "Main Listings",
"gridLayout": "Feeds"
},
{
"component": "core.block.sidebarPrimaryMenu",
"slotName": "side",
"key": "8r659",
"blockId": "8r659",
"title": "",
"variant": 8,
"displayLimit": 8,
"headerActions": {
"ml": 0,
"mr": 0
},
"blockLayout": "sidebar primary menu"
},
{
"component": "core.dividerBlock",
"slotName": "side",
"title": "",
"blockProps": {
"blockStyle": {}
},
"dividerVariant": "middle",
"key": "qlfp",
"blockId": "qlfp"
},
{
"component": "core.block.sidebarShortcutMenu",
"slotName": "side",
"key": "2c7hu",
"blockId": "2c7hu",
"title": "Shortcuts",
"itemView": "shortcut.itemView.smallCard",
"gridLayout": "Shortcut - Menu Items",
"blockLayout": "sidebar shortcut",
"canLoadMore": 0,
"canLoadSmooth": true,
"authRequired": true,
"dataSource": {
"apiUrl": "/user/shortcut"
},
"pagingId": "/user/shortcut",
"headerActions": [
{
"as": "user.ManageShortcutButton"
}
],
"emptyPageProps": {
"noBlock": 1
},
"emptyPage": "hide"
},
{
"component": "announcement.block.announcementListing",
"slotName": "subside",
"title": "Announcements",
"key": "qea3s",
"blockId": "qea3s",
"itemProps": {
"mediaPlacement": "none"
},
"blockStyle": "Profile - Side Contained",
"blockLayout": "Side Contained"
},
{
"component": "chatplus.block.contactsNewFeed",
"slotName": "subside",
"title": "Contacts",
"key": "2312ewqe",
"blockId": "2312ewqe",
"showWhen": ["truthy", "setting.chatplus.server"]
}
]
},
"small": {}
}
Notice info.name: declare name of json page that will use for pageName below
{
"info": {
"bundle": "web",
"name": "home.member"
}
}
Then you use layout named "home.member"
in to your page source to define layout
/**
* @type: route
* name: core.home
* path: /, /home
*/
import { APP_FEED } from "@metafox/feed";
import { useGlobal, useLoggedIn } from "@metafox/framework";
import { Page } from "@metafox/layout";
import * as React from "react";
export default function HomePage(props) {
const loggedIn = useLoggedIn();
const { createPageParams } = useGlobal();
const pageParams = createPageParams(props, (prev) => ({
module_name: APP_FEED,
item_type: APP_FEED,
}));
if (loggedIn) {
return <Page pageName="home.member" pageParams={pageParams} />;
}
return <Page pageName="home.visitor" pageParams={pageParams} />;
}
pageParams
Page params is a React context sharing wrapped all component in layout, its simple way to passing value without direct pass props.
Then you get props of any block, component via useParams
hooks
export default function AdminAppStoreShowDetail() {
const { useParams } = useGlobal();
const { module_name, item_type } = useParams();
}
Custom block
MetaFox will scan the comment
code for layout declaration when building. Below is the sample layout declaration
/**
* @type: itemView
* name: blog.itemView.mainCard
* keywords: blog
* description:
* previewImage:
* deps:
* priority: sort orthers.
*/
@type
itemView
: Declare grid/list item view component.embedView
: Declare embedView in feed/notification/search embed view component.block
: Declare configurable block view component.saga
: Declare redux saga effects.reducer
: Declare redux reducer function.
Filename
File types:
messages.json
: Declare translation objects.layouts.json
: Declare layout configuration objects.
Naming
Export Component Naming
- Item view: [resource_type].itemView.*, etc:
blog.itemView.mainCard
,blog.itemView.smallFlat
- Embed item view: [resource_type].embedView.*, etc:
blog.embedItem.basic
- Form Field: form.field.[name]: etc:
form.element.Text
,form.element.Select
- Block Component: [module_name].block.[name], etc:
core.block.listview
How to detect layout variants
Imagine that user is viewing the user.profile
page in medium
viewport size, but administrator hasn't defined the layout configuration for such viewport size yet.
In this case, the Layout service will check size variants and apply layout configuration in the order of up-to-down sizes: medium
, small
, xsmall
, xxsmall
, large
, xlarge
.
Check detail of selection:
- xxsmall: xxsmall, xsmall, small, medium, large, xlarge
- xsmall: xsmall, xxsmall, small, medium, large, xlarge
- small: small, xsmall, xxsmall, medium, large, xlarge
- medium: medium, small, xsmall, xxsmall, large, xlarge
- large: large, medium, small, xsmall, xxsmall, xlarge
- xlarge: xlarge, large, medium, small, xsmall, xxsmall
When to detect layout size variants
For performance reasons, layout size variants will be checked and applied when page is started rendering. It means that layout won't be updated when user resizes viewport.
Layout Elements
Block
A Block is a React component that Administrator can add, remove, configure, and drag-and-drop to layout slot
Slot:
A slot is an area where Administrators can put blocks into. Slot can contain other slots.
Actions:
Let's take a look at available actions on Slot
Resize
Resize a slot for responsive viewport size as small
, medium
, and more.
Split
Split a slot into vertical direction. It's not usually but sometimes required to build complex layouts.
Add Slot
Add other siblings slot at the start/end of the current slot.
Section
Section similar to material-ui
Container component, it defines rows in layouts.
Actions:
Let's take a look at available actions on Section
Settings
maxWidth
Values: lg
| md
| sm
| xl
| xs
| false
Determine the max-width
of the container. The container width grows relatively with the size of the screen. Set to false
to disable maxWidth
.
disableGutters
: If true
, the left and right paddings are removed.
Item View
Item View is a component that implements UI for a single item in a listing block, etc.
Example blog item main card:
/**
* @type: itemView
* name: blog.itemView.mainCard
* chunkName: blog
*/
export type BlogItemShape = {
title: string;
description: string;
} & ItemShape;
const BlogItemView = ({ item, itemProps }: ItemViewProps<BlogItemShape>) => {
const to = `/blog/view/${item.id}`;
const classes = useStyles();
const cover = /string/i.test(typeof item.image)
? item.image
: item.image["500"];
const [control, state] = useActionControl<{}>(item, {
menuOpened: false,
});
return (
<ItemView
wrapAs={wrapAs}
wrapProps={wrapProps}
testid="blog"
identity={identity}
>
<ItemMedia
asModal={itemLinkProps.asModal}
src={cover}
link={to}
alt={item.title}
backgroundImage
/>
<ItemText>
<CategoryStyled
isMobile={isMobile}
data={categories}
sx={{ mb: { sm: 1, xs: 0 } }}
/>
<ItemTitleStyled isMobile={isMobile}>
<FlagWrapper>
<FeaturedFlag variant="itemView" value={item.is_featured} />
<SponsorFlag
variant="itemView"
value={item.is_sponsor}
item={item}
/>
<PendingFlag variant="itemView" value={item.is_pending} />
</FlagWrapper>
<DraftFlag
sx={{ fontWeight: 'normal' }}
value={item.is_draft && tab !== 'draft'}
variant="h4"
component="span"
/>
<Tooltip title={item.title} arrow>
<Link to={item.link} identityTracking={identity}>
{item.title}
</Link>
</Tooltip>
</ItemTitleStyled>
{itemProps.showActionMenu ? (
<ItemAction
placement={isMobile ? 'bottom-end' : 'top-end'}
spacing="normal"
>
<ItemActionMenu
identity={identity}
icon={'ico-dottedmore-vertical-o'}
state={state}
handleAction={handleAction}
/>
</ItemAction>
) : null}
{item.description ? (
<ItemSummary sx={{ my: 1 }}>
<HtmlViewer html={item.description} simpleTransform />
</ItemSummary>
) : null}
<ItemSubInfo sx={{ color: 'text.secondary' }}>
<UserName color="inherit" to={user.link} user={user} />
<FormatDate
data-testid="creationDate"
value={creation_date}
format="ll"
/>
<Statistic
values={item.statistic}
display={'total_view'}
component={'span'}
skipZero={false}
/>
</ItemSubInfo>
</ItemText>
</ItemView>
);
};
export default BlogItemView;
Loading Skeleton
Loading Skeleton is UI for skeleton that display a placeholder preview of your content before the data gets loaded.
/**
* @type: skeleton
* name: blog.itemView.mainCard.skeleton
*/
export default function LoadingSkeleton(props) {
return (
<ItemView {...props}>
<ItemMedia>
<ImageSkeleton ratio={'169'} borderRadius={0} />
</ItemMedia>
<ItemText>
<div>
<Skeleton width={160} />
</div>
<ItemTitle>
<Skeleton width={'100%'} />
</ItemTitle>
<ItemSummary>
<Skeleton width={160} />
</ItemSummary>
<div>
<Skeleton width={160} />
</div>
</ItemText>
</ItemView>
);
}
Listing Block
You can create new block components by extending other blocks
/**
* @type: block
* name: blog.block.BrowseBlogs
* title: Browse Blogs
* keywords: blog
* description: Display listing blogs.
*/
const BlogListingBlock = createBlock<ListViewBlockProps>({
extendBlock: "core.block.listview", // extend from block
overrides: {
// override properties automacally merged to targed component
contentType: "blog", // layout editor will load view prefix by `contentType.itemView.*` to select itemView.
dataSource: { apiUrl: "/blog" },
},
defaults: {
// default properties show in layout editor,
// only property show in defaults AND NOT in overrides will be show in editor.
title: "Blogs",
itemView: 'blog.itemView.mainCard',
blockLayout: 'Main Listings',
gridLayout: 'Blog - Main Card'
},
});
export default BlogListingBlock;
Side Category
import createBlock from "@metafox/framework/createBlock";
import { CategoryBlockProps } from "@metafox/framework/types";
const SideCategoryBlock = createBlock<CategoryBlockProps>({
extendBlock: "core.categoryBlock", // based Block compoinent
name: "BlogCategoryBlock", // React component name
displayName: "Blog Categories", // display in layout editor
keywords: "blogs, category", // keyword to search on layout editor
description: "", // description in layout editor
defaults: {
// default properties of block
title: "Categories",
appName: "blog"
},
});
export default SideCategoryBlock;