Frontend
Layouts

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;