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, example:
{
"home.member": {
"info": {
"title": "Home Page for logged in users",
"description": "Home Page for Logged in users"
},
"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": {}
}
}
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.field.text
,form.field.upload
- 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.
Remove
TBD
Add new slot
TBD
Item View
Item View is a component that implements UI for a single item in a listing block, etc. For example: featured members, activity feeds:
// file blog/src/components/BlogItemView.tsx
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 (
<div className={classes.root}>
<div className={classes.outer}>
<Link to={to} className={classes.media}>
<span
className={classes.mediaBg}
style={{ backgroundImage: `url(${cover})` }}
/>
</Link>
<div className={classes.inner}>
<div className={classes.header}>
<Link to={to} className={classes.title}>
{item.title}
</Link>
<ItemActionMenu state={state} handleAction={control} />
</div>
<p>{item.description}</p>
<Statistic
values={item.statistic}
display={"total_like,total_view"}
/>
</div>
</div>
</div>
);
};
export default BlogItemView;
// file blog/src/views.tsx
import BlogItemView from "./components/BlogItemView";
export default {
"blog.itemView.card": BlogItemView,
};
Loading Skeleton
// file blog/src/components/BlogItemView.tsx
const LoadingSkeleton = ({ itemProps }) => {
const classes = useStyles();
return (
<div className={classes.root}>
<div className={classes.outer}>
<div className={classes.media}>
<Skeleton variant="rect" height={120} component={"div"} />
</div>
<div className={classes.inner}>
<Skeleton />
<Skeleton />
<Skeleton />
</div>
</div>
</div>
);
};
BlogItemView.LoadingSkeleton = LoadingSkeleton;
export default BlogItemView;
Listing Block
You can create new block components by extending other blocks
import createBlock from "@metafox/framework/createBlock";
import { ListViewBlockProps } from "@metafox/framework/types";
const BlogListingBlock = createBlock<ListViewBlockProps>({
extendBlock: "core.block.listview", // extend from block
name: "BlogListingBlock", // based Block compoinent
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",
blockProps: { variant: "contained" },
itemView: "blog.itemView.mainCard",
gridContainerProps: { spacing: 2 },
gridItemProps: { xs: 12, sm: 12, md: 12, lg: 12, xl: 12 },
},
});
export default BlogListingBlock;
Side Menu Block
Create a Side Menu block
import createBlock from "@metafox/framework/createBlock";
import { SideMenuBlockProps as Props } from "@metafox/framework/types";
const BlogSideMenuBlock = createBlock<Props>({
extendBlock: "core.block.sideNavigation",
name: "BlogSideMenuBlock",
displayName: "Blog Menu",
keywords: "blogs, navigation, menu",
description: "",
previewImage: "",
overrides: {
menuItems: [
{
to: "/blog",
label: "All Blogs",
active: true,
},
{
to: "/blog?view=my",
label: "My Blogs",
},
{
to: "/blog?view=friend",
label: "Friend's Blogs",
},
],
},
defaults: {
title: "Blogs",
blockProps: { variant: "plained", noHeader: true, noFooter: false },
},
});
export default BlogSideMenuBlock;
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
previewImage: "", // preview image in layout editor 200x200
overrides: {
// overrides properties will apply to derived blog automacally.
dataSource: { apiUrl: "/blog-category", apiParams: "" },
href: "/blog/category",
},
defaults: {
// properties will show in edit block modal.
title: "Categories",
blockProps: { variant: "plained" },
},
});
export default SideCategoryBlock;