Skip to content

Instantly share code, notes, and snippets.

@inkless
Created July 20, 2016 18:24
Show Gist options
  • Save inkless/5d6e0ad0130b4564b96cc20c442f4aab to your computer and use it in GitHub Desktop.
Save inkless/5d6e0ad0130b4564b96cc20c442f4aab to your computer and use it in GitHub Desktop.

Tile Engine

Old Engine

1. Create page

When creating a page, server side will generate a new config for this page. Some data will be read from database too. This includes:

  • layout
  • page name

The class used to generate new config is:

PageConfig

-- PageMeta

-- LayoutConfig

-- List<LegoConfig>

PageMeta will be generated with empty data, and page name will be saved to layout params.

{
  layout: {
    name: "kiwi.layout",
    params: { layout: { title: "New Tile" } },
    meta: null,
    data: { }
  },
  legos: [ ],
  meta: {
    pageMeta: {
      pageMeta: [
        [
          { name: "name", value: "description" },
          { name: "content", value: "" }
        ],
        [
          { name: "name", value: "keywords" },
          { name: "content", value: "" }
        ]
      ]
    }
  }
}

Server will render this config and send to client (will be discussed later), and client can then start to modify the page config, and finally send it back to server.

2. Compile Page Config

Client will send the whole page config back to server to compile. Server will save the page and then rerender the page.

    composePageDraftDao.saveContentForDefaultPage(hostSite, target, path, config);
    draftWithoutContentCache.evict(hostSite, target, path);
    pageRenderingService.notifyPageChanged(hostSite, target, path, ConfigState.DRAFT);

In notifyPageChanged, it will actually render the whole whole page again. renderTemplate will be called. And in renderTemplate, we'll fetch pageConfig from database.

LegoMeta

Backend will parse all legos specified as valid lego, and parse the meta data inside it, then it will send these lego meta data to front end as lego list.

{{#meta "generic.button"}}
{
  "name": "generic.button",
  "version": "1.0.0",
  "display_name": "Button",
  "category": "Generic",
  "priority": 10,
  "controls": [{
    "id": "button-content",
    "name": "Content"
  }, {
    "id": "button-layout",
    "name": "Layout"
  }, {
    "id": "button-style",
    "name": "Style"
  }, {
    "id": "padding",
    "name": "Padding"
  }]
}
{{/meta}}

PageConfig

PageConfig has a method toString which will convert the JSON config to a handlebars string.

{
  "layout": {
    "name": "kiwi.layout",
    "params": {
      "layout": {
        "title": "New Tile",
        ...
      },
      "additionalStyle": {
        "_style": ".wrapper .compose-column,.lego-columns-content>div, .generic-column>div {padding-left: 6px;padding-right: 6px;}"
      },
      "styleConf": { "fontFamilies": "{}" }
    },
    "meta": null,
    "data": {
      "additionalStyle": {
        ...
      }
    }
  },
  "legos": [
    {
      "id": "T3JepX",
      "name": "generic.textblock",
      "params": {
        "style": { ... },
        "template": { ... },
        "additionalStyle": {
          "_style": " a{font-weight:inherit;font-style:inherit;text-decoration:underline;color:#3399cc;} a:hover{font-weight:inherit;font-style:inherit;text-decoration:underline;color:#3399cc;}"
        }
      },
      "block": "<p>Write something...</p>",
      "legos": null,
      "meta": null,
      "data": { ... },
      "source": null,
      "global": false
    },
    {
      "id": "6Z15JZ",
      "name": "generic.textblock",
      "params": {
        "style": { ... },
        "template": { ... },
        "additionalStyle": {
          "_style": " a{font-weight:inherit;font-style:inherit;text-decoration:underline;color:#3399cc;} a:hover{font-weight:inherit;font-style:inherit;text-decoration:underline;color:#3399cc;}"
        }
      },
      "block": "<h1 style=\"text-align: center;\"><span style=\"font-size: 120px;\">Hello World !</span></h1>",
      "legos": null,
      "meta": null,
      "data": { ... },
      "source": "hello-world",
      "global": true
    }
  ],
  "meta": {
    "pageMeta": {
      "pageMeta": [
        [{ "name": "name", "value": "description" }, { "name": "content", "value": "" }],
        [{ "name": "name", "value": "keywords" }, { "name": "content", "value": "" }]
      ]
    }
  }
}
{{#partial 'pagemeta'}}<meta name="description" content="">
<meta name="keywords" content="">{{/partial}}
{{#layout "kiwi.layout" _style='.wrapper .compose-column,.lego-columns-content>div, .generic-column>div {padding-left: 6px;padding-right: 6px;}' showHeader='true' paddingRight='6px' fontFamilies='{}' title='New Tile' paddingLeft='6px' headerSnippetDisabled='false' showFooter='true'}}
{{#lego "generic.textblock" _id="T3JepX" visibilityLabel='Show on all devices' backgroundColor='transparent' hideMobile='false' hideTablet='false' paddingRight='0px' visibilityClass='' paddingBottom='0px' _style=' a{font-weight:inherit;font-style:inherit;text-decoration:underline;color:#3399cc;} a:hover{font-weight:inherit;font-style:inherit;text-decoration:underline;color:#3399cc;}' tabletData='{"legoStyle":{},"legoParams":{},"legoControl":{},"legoBlock":""}' borderWidth='0px' customAttrs='' hideDesktop='false' phoneData='{"legoStyle":{},"legoParams":{},"legoControl":{},"legoBlock":""}' paddingTop='0px' paddingLeft='0px'}}
<p>Write something...</p>
{{/lego}}
{{global "hello-world" _global_id="6Z15JZ"}}
{{/layout}}

This mechanism allows us to transfer pageConfig between front end and back end and when rendering, it can make use of handlebars to render it.

Several things to be noticed:

  • LegoConfig.toExternalString will be called in PageConfig.toString _ id will be changed to _id as attribute of lego helper
  • params.template and params.style will be merged to parameters and put to the attributes of lego helper
  • params.additionalStyle._style will be put to _style in helper
  • block will be directly put into the handlebars template
  • global lego will be transformed to a global helper

Known bug Since parameters are injected to lego helper through attributes, it cannot differentiate string and boolean, so all boolean are converted to string.

3. Render Page

Rendering happens in these situations:

  • visit site directly
  • preview
  • open compose (renderPageConfig in ComposeConfigController)
  • email preview
  • send email
  • send test email

And all of them will actually use the handlebars template shown in 2. Compile Page to render the page.

{{#partial "pageMeta"}} -> rendered in page.base.hbs by partial helper
{{#layout}} -> kiwi.layout.hbs -> page.base.hbs
{{#lego "lego.name"}} -> lego.name.hbs
{{#global}} -> handlebar global helper

Notice, template inside lego's definition will be rendered into the templates, And style and javascript will be covered in the later section.

In template, style, javascript, they have access to the hash value defined in the lego attributes.

{{#template "generic.button"}}
<div class="button-container">
  {{#editMode}}
  <a class="button-box"
    ng-href="[[legoParams.href]]" target="[[legoParams.windowTarget]]"
    compose-style="{
      width: legoParams.buttonWidth,
      height: legoParams.buttonHeight,
      float: legoParams.alignment
    }">
    <span class="button-text" ng-bind="legoParams.buttonName"></span>
  </a>
  {{/editMode}}

  {{#viewMode}}
  <a class="button-box" href="{{param $.href}}" target="{{param $.windowTarget}}">
    <span class="button-text">{{param $.buttonName ''}}</span>
  </a>
  {{/viewMode}}
</div>
{{/template}}

Hash value can be accessed by:

  • {{param $.hash.key}}
  • {{css $.hash.key}}
  • {{$.hash.key}}

They are considered handlebar variables and will be replaced by real value during the rendering.

The drawback would be, it's very expensive to do this variable replacement everytime, especially if it's static data.

viewMode and editMode are designed to separate editor and live mode.

4. Styles/Javascript Rendering

Scripts

In each lego def, it allows a script definition. During handlebar compilation, these script will be collected by the handlebar helper, and stored in a collection, they will be merged together and put into a scripts helper which is actually in page.base.hbs.

Styles

Currently inlined styles are not used. During handlebar compilation, it will collect all styles from each lego, merge them and compile(less) them. The rendered result will be store in cache, which is accessible through StaticFileController - home.compose.css. For production, a composeResourceVersion is used to handle the CDN updates.

else if (path.contains(COMPOSE_EXT)) {
  ComposeHostSite composeHostSite = site.asComposeHost();
  LegoTarget legoTarget = LegoTarget.fromString(target);
  ConfigState pageConfigState = ConfigState.valueOf(configState.toUpperCase());
  String pagePath = path.substring(path.lastIndexOf("/") + 1).replace(COMPOSE_EXT, "");

  css =
      composeStylesManager.getBundleStyles(
          composeHostSite, legoTarget, pagePath, pageConfigState, version);
  if (pageConfigState == ConfigState.LIVE) {
    cache = true;
  }
}

5. Saved Tiles

Each saved tile has its own cache, it's rendered by global helper. Everytime one saved tile is updated, it will flush the cache for styles, generate new resourceVersion, and recompile all tiles and generate new styles.

When draging new saved tiles, it will generate new id, and compile the whole tile.

{
  "id": "__ID__",
  "name": "generic.textblock",
  "params": {
    "template": {
      "hideMobile": false,
      "hideTablet": false,
      "hideDesktop": false,
      "visibilityLabel": "Show on all devices",
      "visibilityClass": "",
      "tabletData": { "legoStyle": { }, "legoParams": { }, "legoControl": { }, "legoBlock": "" },
      "phoneData": { "legoStyle": { }, "legoParams": { }, "legoControl": { }, "legoBlock": "" },
      "customAttrs": ""
    },
    "additionalStyle": {
      "_style": " a{font-weight:inherit;font-style:inherit;text-decoration:underline;color:#3399cc;} a:hover{font-weight:inherit;font-style:inherit;text-decoration:underline;color:#3399cc;}"
    },
    "style": {
      "borderWidth": "0px",
      "paddingTop": "0px",
      "paddingLeft": "0px",
      "paddingRight": "0px",
      "paddingBottom": "0px",
      "backgroundColor": "transparent"
    }
  },
  "block": "<p>sad</p>",
  "legos": null,
  "meta": null,
  "data": {
    "linkStyle": {
      "normal": { "fontWeight": "", "fontStyle": "", "textDecoration": "underline", "color": "#3399cc" },
      "hover": { "fontWeight": "", "fontStyle": "", "textDecoration": "underline", "color": "#3399cc" }
    },
    "additionalStyle": {
      "styles": {
        "main": {
          "linkStyle": ".generic-textblock-__ID__ a{font-weight:inherit;font-style:inherit;text-decoration:underline;color:#3399cc;}.generic-textblock-__ID__ a:hover{font-weight:inherit;font-style:inherit;text-decoration:underline;color:#3399cc;}"
        },
        "raw": {
          "linkStyle": " a{font-weight:inherit;font-style:inherit;text-decoration:underline;color:#3399cc;} a:hover{font-weight:inherit;font-style:inherit;text-decoration:underline;color:#3399cc;}"
        }
      }
    }
  },
  "source": "sad",
  "global": true
}

id is always marked as 'ID' as a placeholder. In both front end and back end we are doing some string replacement to make sure the global tile is using the correct id.

6. Email Variables

Currently email triggers handlebars compile multiple times and replace those html tags to handelbar vars and then compile. It does not affect the currect tile engine. So for new tile engine we can actually leave it there for now. Though it does need a complte rewrite...

So I'll ignore this part for now..

New Engine

1. Tile Def

Rewrite in separate files

html, js, css, meta

Storage of tile def

TBD:

  • database
  • file

Should still support:

  • get tile list
  • render single tile

2. Template

Move current layout/template to front end, and need to make sure it does not break handlebar rendering. This might be very related to Rendering part

3. Rendering

Tile rendering

When working in edit mode, the tile should be changed instantly. The way to handle it might be:

  • Rerender the whole tile and replace html/style
  • Keep separate viewMode and editMode and continue using angular
  • Using virtual dom

This require some performance benchmark measurement

Whole page rendering

Compile stuff to handlebar templates which will still support all our old handlebar statement

Storage of rendered template

The rendered template (hbs template) should be stored in database, when visiting directly to sites, it should be able to fetched from database and rendered correctly.

4. Styles

Save rendered styles to backend

The styles has placeholder with saved tiles styles

Storage of styles

rewrite xxx.compose.css logic to support new styles

The caching logic should remain the same, but with new tile engine on, it should fetch styles from database, and replace the saved tiles' placeholder, then send the styles as css file. CDN logic is also the same.

5. Saved Tile

Handlebar helper for saved tile

For saved tile, we have to use handlebar helper to render, cuz it's actally dynamic data. We'll reuse the old caching logic, but slightly modified it to adapt to new tile engine.

Styles for saved tile

The styles will no longer be less file, but will be a handlebar file, mostly is css, but might have some placeholder for saved tiles.

Storage and caching of saved tile render result

Saved tile require new persistent layer of rendered result, including both html and css

6. Gradual roll out

Site plugin to turn on/off the new tile engine

Backend support for new engine and old engine at the same time

Frontend support for both engine at the same time

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment