What Formulize expects from a theme

A Formulize theme is an ordinary ImpressCMS theme, and most of it is yours to design however you like. There are a few things Formulize looks for. Get these right and forms, lists and maps behave correctly in your theme; miss them and those things quietly stop working rather than raising an error.

1. Fire formulize_pageShown when the page is revealed

Most themes hide the body while the page assembles and reveal it once everything has loaded. When you reveal it, dispatch this event on window:

window.dispatchEvent(new CustomEvent('formulize_pageShown'));

Formulize waits for it before putting a reader back where they were on a form, a list or a map. If your theme never fires it, a reader who saves a long form is returned to the top of it every time.

If you don’t hide and reveal the body, fire it once the page has loaded anyway.

2. Say what scrolls, if it isn’t obvious

Formulize saves a reader’s scroll position when they save a form, and puts it back afterwards. To do that it has to know which element scrolls. And when a theme names the element, the mouse wheel or trackpad scrolls it from anywhere on the page, not only with the pointer over it.

Usually you don’t need to do anything. Formulize looks for the nearest ancestor of the form that is actually scrollable — an element taller than its own box with overflow-y set to auto or scroll — and falls back to the window. A theme that scrolls the window, and a theme that scrolls a main content pane, both work without being told.

Declare it when that guess would be wrong — several nested scrollable elements, say. Put the selector on your <body> tag:

<body data-formulize-scroll-container=".my-main-pane">

The selector can name several elements, for a theme that scrolls a different one at different widths, or on different screens. Formulize uses the first of them that is scrolling at the time, and if none is, looks for one as if nothing were named. Lyris scrolls a list’s entries or a form’s card on wide screens, and the page on phones:

<body data-formulize-scroll-container=".lyris-list__body, .lyris-form-screen, .lyris-main">

Naming it also means a reader can scroll it from anywhere on the page: over the margins beside a narrow column, a title bar, a bar of buttons. The wheel scrolls the named element unless the pointer is already over it, or over something else that scrolls on its own, such as an open menu or a wide table. It is also left alone over a modal and its backdrop (anything with aria-modal="true" or role="dialog", such as the entry drawer), so the page behind a modal stays where it is, and over a menu or list of options opened outside the element (role="menu", role="listbox", or jQuery UI’s .ui-menu, as an autocomplete’s suggestions are). This is include/js/scroll_container.js, which Formulize adds to every page; a theme that names nothing isn’t affected.

Use none when nothing in the page scrolls, because something outside it does:

<body data-formulize-scroll-container="none">

Then Formulize will not try to save or restore a position at all.

3. Embed themes need a marker file

A theme meant for screens embedded in another website is chosen from a separate list in the Formulize preferences, and is kept out of the normal theme pickers. Put a file named formulize-embed-theme.marker in the theme folder to mark it as one. A theme without the marker is not used for embedded screens, even if it is chosen in the preferences.

The simplest way to make your own is to copy the themes/formulize_embed folder. Keep these in your copy:

  • the marker file
  • the short script just after the <body> tag in theme.html, which turns off the page’s own scrolling when it is inside a frame
  • the script at the bottom of theme.html, which is how an embedded screen reports its height to the page hosting it
  • session-timeout-warning.html, including its session-timeout-warning id, which the script at the bottom uses to bring the warning into view

4. Style screens so they still work without your header and menus

An embedded screen borrows your theme’s styling without its page layout. It loads your css/reset.css, if you have one, then your css/style.css, and then any colours, font and logo set in the Appearance editor. It does not load your theme.html or your script. The <body> has the class formulize-inline.

Two optional files let you adjust how your theme looks when embedded:

  • embed-content.html: the elements your theme puts around the page content, with <{$icms_contents}> inside them and your header and menus left out. Add it if your stylesheet styles screens through those elements. Without it, the screen is drawn inside a plain <main>.

    <div class="my-layout">
      <main class="my-main-pane">
        <{$icms_contents}>
      </main>
    </div>
    
  • css/embed.css: loaded after your other stylesheets, only on embedded screens. Use it for anything that assumes your page fills the window. An embedded screen is exactly as tall as its content, so a content area that fills the window and scrolls on its own should just be as tall as its content:

    .my-main-pane {
      height: auto;
      overflow: visible;
    }
    

The Anari theme has both files, if you want an example.

5. Give every page the same space at its edges

Every page should have the same space between its content and the edges of the window, your header and your menu, whatever is on the page. Formulize has several types of screen (list of entries, form, template, calendar and map), and more will be added. None of them should look different from the others in this, and neither should the pages that are not screens, such as an application’s menu page.

Formulize doesn’t supply that space, and no type of screen brings its own. Your theme does, in one place: put a wrapper around all the content in your theme.html, and give it padding.

<main class="my-main">
  <div class="my-page">
    <{$icms_contents}>
  </div>
</main>
.my-page {
  padding: var(--fz-page-padding);
}

--fz-page-padding is the Appearance editor’s Page padding, so the space follows that setting. Then there is nothing more to do: a new type of screen gets the space without any change to your theme, and a screen inside another (a template screen’s code can call displayForm or displayEntries) is not inset twice, because neither has space of its own.

Make the wrapper its own element, inside the part of your page that scrolls, and leave the scrolling part without padding. Then the wrapper is also the place to keep pages to a maximum width (section 7), and anything your theme pins to the top or bottom of the scrolling part is not pushed in by it.

Don’t put the space on the parts of a screen instead, in your screen templates. A screen drawn with other templates would have none, and a screen inside another would have it twice.

On a phone you may want less space, or want your list and form screens to use the whole width of the screen. Make the padding smaller there, and take it off only for the pages you mean. Lyris does both: see “The page frame” and “Mobile” in themes/Lyris/css/style.css. Content should never touch the edge of the screen, so anything that reaches the edge needs padding of its own inside it.

If your embed stylesheet or your embed-content.html (above) is used, leave the space out there: the page your screen is embedded in supplies it.

6. Work with the Appearance editor (optional)

The Appearance editor edits a theme’s appearance on a preview of sample screens the theme provides (below): a theme without them can’t be edited there. In simple mode it changes the logo, colours, fonts and page width, which every theme that calls formulize_renderAppearanceHead() follows.

Its advanced mode, and the Compact and Comfortable looks, change Formulize UI’s component tokens: sizes (--fz-field-height, --fz-row-height, --fz-title-text), fonts (--fz-label-font), colours (--fz-button-bg, --fz-header-bg) and corners (--fz-field-radius), and the rest. They only do anything in a theme whose own CSS styles things with those tokens, so a theme says when it does, and only then does the editor offer them. Each colour and font token defaults to the palette colour or font it stands for, so a theme that uses them looks the same until one is changed.

Opt in by declaring --formulize-size-tokens on :root in your css/tokens.css:

:root {
  --formulize-size-tokens: 1;
}

If your theme’s own font comes from Google Fonts, say which weights of it to download, with your other tokens:

:root {
  --fz-font-sans: Poppins, Helvetica, sans-serif;
  --formulize-font-weights: 400 500 600;
}

--formulize-font-weights is a list of numbers with spaces between them. Each number is a font-weight that your stylesheet uses: 400 is regular, 700 is bold, and the others are the steps around them. A webfont is a separate file for each weight, and only the weights you list are downloaded.

  • List every weight your CSS uses. Text set in a weight you left out is shown in the nearest weight you did list: with 400 500 600, bold (700) text is shown at 600.
  • List only weights the font has. Its page on Google Fonts shows them. If you list one it doesn’t have, the font isn’t loaded at all.
  • Leave out the weights you don’t use. Each one is another file for the browser to fetch.

The font that is loaded is the first one named in --fz-font-sans, so that name has to be its name on Google Fonts. It is loaded on every page that has your theme’s appearance: the pages your theme.html draws, the popups of the list screens, and screens embedded in another site. When another font is chosen in the Appearance editor, yours is no longer loaded.

Don’t load the font in theme.html as well. A theme that leaves --formulize-font-weights out gets Geist, the font Lyris uses.

Provide sample screens for the editor’s preview, in an appearance_preview folder in your theme. Each is an HTML file named after a screen: form.html, list.html, drawer.html and cards.html. Provide the ones that suit your theme; the editor shows the ones it finds, and without any, the theme can’t be edited there. A sample is the markup your theme puts in <body> for that kind of page, written out with sample content, and it is shown with your css/reset.css, your css/style.css and your generated appearance stylesheet, in a <body> with the id formulize and the class formulize-screen. No scripts run in it.

Mark each part of the sample that can be selected with data-fz-part, naming the part:

<input type="button" class="formulize-form-submit-button" value="Save" data-fz-part="button">

The parts, and the tokens of each one, are the components in modules/formulize/include/appearance_tokens.json: logo (the link around your logo image), page, tabs, title, form, label, field, value (a read-only value), options (radio buttons and checkboxes), help, button, toolbar, menu, header (column headings), row, card and drawer. The editor shows a new logo by changing the src of the image inside the logo part.

Pieces shared between samples go in files starting with an underscore, and are included by name in double braces: is the contents of `_list.html`. and `` are your logo and the site’s name. Clicking an element with data-fz-toggle="some-id" in the preview toggles the class open on the element with that id, for showing a menu.

Lyris’s samples, in themes/Lyris/appearance_preview/, are a complete example. A theme edited in simple mode only, such as Anari (themes/Anari/appearance_preview/), only needs to mark its logo, as the logo part.

7. Offer the Page width setting (optional)

The Page width setting, in the Appearance editor’s site-wide settings, keeps pages to a maximum width on a wide screen, or lets them use the full width of the window. It sets --formulize-content-max-width: a width in pixels, or 100% for full width. Laying the page out to that width is up to the theme, so a theme says when it does, and only then is the setting offered.

Opt in by declaring --formulize-content-max-width on :root in your css/tokens.css. What you declare is your theme’s own width, which is what the setting starts at and what a reset goes back to: a width in pixels, or 100% to start at full width. Lyris declares 1200 pixels:

:root {
  --formulize-content-max-width: 1200px;
}

Then use it as the maximum width of your content, on the wrapper that has the space at the page’s edges (section 5), so that every type of screen is kept to it and none needs anything of its own. Lyris makes its pages a column of that width, centred in the window. In it, each part of a list or a form is a card: a list’s title bar, its entries, a form, and a floating bar at the bottom for a list’s pagination or a form’s buttons. The column is centred in the window rather than beside the sidebar, so opening the sidebar doesn’t move it unless it has to; the header’s links stay at the window’s edge; and phones are left full width. See the “Content width” section at the end of themes/Lyris/css/style.css.

If your theme has a menu beside the content and you want the column centred in the window, not in the space beside the menu, the wrapper is what makes that possible. The room to leave either side depends on how wide the content area is, and in CSS a percentage in an element’s padding is measured against its parent, not itself. So the content area can’t work it out for itself, but the wrapper inside it can: on the wrapper, 100% is the content area’s width.

A page that fills the window and scrolls inside itself, as Lyris’s lists and forms do, needs the wrapper to be exactly as tall as the scrolling part. Give the wrapper min-height: 100% for every page, and height: 100% only on the pages that fill the window. Every other page is then as tall as its content, and the page scrolls.

On paper, take the wrapper’s padding and height off again, with the rest of your theme’s layout (see Printing, under For theme authors, in Formulize UI).

A screen can be set to use the full width whatever the site’s setting is, on the screen’s Appearance tab. Formulize sets --formulize-content-max-width to 100% on that screen’s page, so a theme that uses the property as above has nothing more to do.

Checking your theme

Open a long form in your theme, scroll down, and save it. You should be returned to where you were rather than to the top of the page. If you are not, the theme is either not firing formulize_pageShown or scrolling an element Formulize could not find.

Open a screen of each type you have, and an application’s menu page. On every one, the content should have the same space around it, not sit against the edge of the window or your header. If you have a template screen with a form or a list inside it, check that the form or list lines up with the rest of that page, not further in.