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

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;