Umbraco Flavored Markdown (UFM) cheat sheet for v14

Umbraco v14.1 contains a partial replacement for AngularJS Filters for generating dynamic labels for blocks and list views in the backoffice: Umbraco Flavored Markdown (or UFM). Here's a quick guide.

, by Joe Glombek

⚠️ This article was written in and contains out of date information.

Foreword

This article aims to follow the same (or similar) examples to the AngularJS Filter Cheat Sheet and in a similar format.

If you'd like a detailed how-to on using UFM, I've got an article coming out as part of 24 Days in Umbraco in December.

This article currently targets Umbraco v17, syntax may differ slightly, not exist or be buggy in earlier versions. I have another article explaining the state of UFM in v14.1 and for v13 and below you should reference the AngularJS Filter Cheat Sheet.

Every example in this article has been tested against Umbraco 17.7.1 using a sample project which sets up a block list for each example, so you can see them all in one place (and check I'm not making things up). The screenshots are from that project too.

The example blocks in this article are live: they're rendered by Umbraco's own UFM code, courtesy of the UFM Playground. Click "Open in the UFM playground" under any of them to try changing the template or the values, or head to the playground to try out your own templates without an Umbraco site.

In this article, I tend to show preference for the UFM Expression syntax over UFM Components where it's a possibility.

Block labels

When you're configuring a block type in a block list or block grid, you're provided with a "Label" field. Similarly, custom columns in your list views have a "Template". You can just type some plain text in here, but using UFM block label templates is where it really shines.

The label field in a block type's configuration, containing a UFM template

For example, if you want to pull up the value of a field called "Heading" (alias heading) from the block list item into the name, you can do that by making use of the umbValue component ({umbValue: heading}, or its shorthand {=heading}) or the expression syntax (${ heading }). You can even include fixed text around it: A text block with the heading '${ heading }' (but maybe less wordy, eh?)

As for custom list views, the only additional thing you need to know is you use the value variable, e.g. ${ value } (more on these later).

For properties that aren't as simple as a textbox, we can do that too:

Content pickers

The umbContentName component is the only way to get the name of a picked content item.

e.g. Call to Action: {umbContentName: page} becomes Call to Action: Contact us.

This works for a single pick, and for a Multinode Treepicker (or a Content Picker allowing multiple items), where you'll get a comma-separated list.

e.g. Related: {umbContentName: relatedPages} becomes Related: Contact us, Services, About us.

⚠️ Note that the names aren't necessarily in the order they were picked. In my sample project, I picked "Contact us", "About us" and "Services" in that order.

Media pickers

Good news! umbContentName handles media too, so no more constructing your own UDIs.

e.g. Image: {umbContentName: image} becomes Image: Sunset

Member pickers

For a member picker, use the umbMemberName component.

e.g. Author: {umbMemberName: author} becomes Author: Jane Author

The docs suggest umbContentName supports members too, but that rendered nothing for a Member Picker in my testing, so stick to umbMemberName.

Rich text editor

The filter stripHtml strips out the HTML markup and just renders the inner text. This is useful for rendering text from a Rich text editor.

e.g. Text module: ${ bodyText | stripHtml } becomes Text module: This is some text from a rich text editor.

Or, if you prefer the component syntax, Text module: {umbValue: bodyText | stripHtml} does the same thing.

You will need the filter: the rich text value is actually an object, so without it you'll get the dreaded [object Object]. Reaching into the object with ${ bodyText.markup } doesn't help much either, you get the HTML tags written out as text.

URL Picker

The umbLink component will provide the name of a picked link. If the editor didn't give it a name, it'll fall back to the name of the picked content.

e.g. Call to Action: {umbLink: callToActionLink} becomes Call to Action: Make an enquiry

If you want something other than the name, the value is an array of link objects, so you can reach into it with an expression, much like we used to in AngularJS.

e.g. Call to Action: ${ link[0].name } (${ link[0].url }) becomes Call to Action: Umbraco (https://umbraco.com)

Other objects

That same trick works for any property editor that stores an object. For example, a colour picker (with labels enabled) stores both the colour and its label.

e.g. Colour: ${ colour.label } (#${ colour.value }) becomes Colour: Raspberry (#e91e63)

Yes/No

Toggles (true/false) give you a boolean in UFM, so we can use the value to render some text in an expression: ${ toggleAlias ? 'Yes' : 'No' }

e.g.

Text with image (${ flipped ? "Image right" : "Image left" })

becomes

Old habits from the AngularJS days die hard, so it's worth knowing that ${ flipped == 1 ? "Image right" : "Image left" } also still works (because true == 1 in JavaScript). Just using {=flipped} on its own renders true or false.

Too long?

We've got two options when it comes to truncating text:

The wordLimit filter limits to a number of words.

e.g. Text module: {umbValue: bodyText | stripHtml | wordLimit:4} becomes Text module: This is some text

While the truncate filter limits the number of characters, and adds an ellipsis if it had to cut anything.

e.g. Text module: {umbValue: bodyText | stripHtml | truncate:35} becomes Text module: This is some text from a rich text…

The truncate filter takes an optional second parameter, which replaces the ellipsis. Pass false for no tail at all: truncate:35:false. Unlike the AngularJS version, there's no "wordwise" option, so it will happily chop a word in half.

When using filters with parameters in an expression, you call them like a function instead, with the value as the first argument:

e.g. Text module: ${ truncate(stripHtml(bodyText), 35) } becomes Text module: This is some text from a rich text…

This is also the way to go for a custom tail with a leading space. The component syntax trims its parameters, so truncate:35: (more) gives you …rich text(more), whereas ${ truncate(stripHtml(bodyText), 35, " (more)") } gives you …rich text (more).

⚠️ Watch out for the | truncate: 35 syntax inside an expression (which, at the time of writing, appears in the docs). It doesn't throw an error, but the : 35 is silently ignored, so you get the full text with an ellipsis stuck on the end.

Shouty

There are also filters for changing case: uppercase, lowercase and titleCase.

e.g. {umbValue: heading | titleCase} turns how to GET in touch into How To Get In Touch

Markdown and HTML

Since UFM is Markdown, we are able to use Markdown syntax or HTML as well.

For example, **{=heading}** - _{=subheading}_ would render

Or, using HTML, **Quote:** <q>{=quote}</q> - {=attribution}. Might render you:

You can also use Umbraco's own web components, as long as their name starts with umb-, uui- or ufm- (anything else gets stripped out for security). So you can add a tag:

{=heading} <uui-tag color="positive" look="primary">New</uui-tag>

Or an icon (the inline style stops it sitting on a line of its own):

<umb-icon name="icon-phone" style="display: inline-block; vertical-align: middle"></umb-icon> {=heading}

What about settings?

Block settings are made available using the $settings property, but only in expressions.

e.g. Text with image (${ $settings.flipped ? "Image right" : "Image left" }) becomes Text with image (Image right)

Messing with the default

If you leave the label empty, the block displays the name of the block type. In AngularJS, we could use $contentTypeName to get hold of that in our own templates, but there's no equivalent in UFM (${ $contentTypeName } renders nothing). As each label belongs to one block type anyway, you'll just have to type it out.

How do I look?

Labels work just the same in the Block Grid, but the $layout property (with its columnSpan and rowSpan) didn't make the jump to UFM. Block labels only get the block's content properties, $settings and $index.

$index

The $index property can be used in block lists and block grids. This is now 0-based, where it used to be 1-based in AngularJS, so you'll probably want to add one.

e.g. ${ $index + 1 }. {=heading} becomes 1. Thing 1

Arrays

There's no need for a join filter any more: when the value is an array (tags, checkbox lists, repeatable textstrings…), the umbValue component joins them with a comma for you.

e.g. Things: {umbValue: tags} becomes Things: Thing 1, Thing 2, Cat (in hat)

If you'd rather use a different separator, an expression can call the JavaScript join method.

e.g. Things: ${ tags.join(" / ") } becomes Things: Thing 1 / Thing 2 / Cat (in hat)

We can also count them, or pick out individual items.

e.g. ${ tags.length } tags, ${ colours.length } colours becomes 3 tags, 2 colours

e.g. First thing: ${ things[0] } becomes First thing: Thing 1

Nesting block lists?

Block lists within your block can be accessed by using the alias of the block list property followed by contentData.

e.g. Tab panel (${ tabs.contentData.length } tabs) becomes Tab panel (3 tabs)

Getting at the values inside the nested blocks is a bit more involved than in AngularJS. Each block's values are now an array of { alias, value } objects, so we have to find the one we want:

Tabs: ${ tabs.contentData.map((tab) => tab.values.find((v) => v.alias == "heading").value).join(" / ") }

becomes

That's a bit of a mouthful though, which is why I'd recommend writing your own filter for this sort of thing.

Regular ol' JS

Expressions support a decent chunk of regular JavaScript: maths, comparisons, &&, ||, ??, the ternary operator, property access and methods on values (like toUpperCase(), split() and replace()).

e.g. ${ heading.toUpperCase() } (${ heading.length } characters) becomes CONTACT US (10 characters)

Unlike AngularJS, arrow functions are allowed! But (as of v17.7) only with brackets around the parameters:

  • ✅ ${ tags.map((t) => t.toUpperCase()) }
  • ❌ ${ tags.map(t => t.toUpperCase()) } renders nothing at all

It's a sandbox though, so there's no access to globals like Math, Date or window, and no template literals.

See also "Fallbacks" and "Nesting block lists?".

Fallbacks

We've got a few options for falling back to another value when a field is empty. There's a fallback filter:

e.g. {umbValue: heading | fallback:Untitled heading}

Or, using expressions, || or ??:

e.g. ${ heading || "Untitled heading" }

But they don't all behave the same! Here's how each one renders for a heading of "Contact us", a heading that's been cleared (an empty string), and a heading that's never been set:

| Template | "Contact us" | Cleared | Never set | | -------------------------------------------------- | ------------ | ---------------- | ---------------- | | {umbValue: heading \| fallback:Untitled heading} | Contact us | Untitled heading | (blank) | | ${ heading \|\| "Untitled heading" } | Contact us | Untitled heading | Untitled heading | | ${ heading ?? "Untitled heading" } | Contact us | (blank) | Untitled heading |

Try them for yourself. Each of these has the same three blocks: "Contact us", cleared, and never set.

So || is the one you want most of the time. And if your label does end up blank, you're left with an unlabelled block, which isn't very helpful to your editors!

We can also fall back to another field, like the rich text when an optional title is missing:

e.g. ${ title || bodyText | stripHtml } will be the value of the optional title field, otherwise the text of the bodyText.

And, combining this with the truncate function from earlier:

e.g. ${ truncate(title || stripHtml(bodyText), 20) } might become Some body text, beca…

Localisation

The umbLocalize component (or its shorthand, #) renders a backoffice translation in the current user's language.

e.g. {umbLocalize: general_name}: {=heading} or {#general_name}: {=heading} becomes Name: Contact us

You can register your own translation keys with a localization extension:

json { "type": "localization", "alias": "UfmSamples.Localization.En", "name": "UFM Samples English", "meta": { "culture": "en" }, "js": "/App_Plugins/UfmSamples/lang/en.js" }

js export default { ufmSamples: { callToAction: 'Call to action', }, };

e.g. {umbLocalize: ufmSamples_callToAction}: {umbContentName: page} becomes Call to action: Contact us

⚠️ This uses the backoffice UI translations, not dictionary items. A dictionary item key just renders the key itself.

Collection views

The templates for custom columns in a collection (what we used to call a "list view") use UFM too. Like in AngularJS, rather than using the property alias, you use value.

e.g. {umbValue: value | truncate:30}, ${ value ? '⭐ Featured' : '' } or ${ value.length } tags: {umbValue: value | lowercase}

A collection view with truncated summaries, "⭐ Featured" and "2 tags: contact, support" columns

Property descriptions

This one's new: property descriptions are UFM too! So you can use Markdown (including links) and localisation in them. {umbValue: label} even renders the property's own name, if you're into that sort of thing.

A property description with bold, italic and a link

Writing your own

Much like in AngularJS, it's possible to write your own filters and components. Everything here goes in an umbraco-package.json file in a folder in App_Plugins (I've used wwwroot/App_Plugins/UfmSamples/).

Filters

Here's my old blockSummary filter, brought up to date:

```js import { UmbUfmFilterBase } from '@umbraco-cms/backoffice/ufm';

class BlockSummaryFilterApi extends UmbUfmFilterBase { filter(blocks, fieldAlias, maxItems, showMore, separator) { let items = (blocks?.contentData ?? []).map( (block) => block.values.find((v) => v.alias === fieldAlias)?.value ); let more = '';

// Arguments arrive as strings when using the {umbValue: x | blockSummary:...} syntax
maxItems = Number(maxItems) || 0;
if (maxItems > 0 && items.length > maxItems) {
  if (showMore !== false && showMore !== 'false') {
    more = ` + ${items.length - maxItems} more`;
  }
  items = items.slice(0, maxItems);
}

return items.join(separator || ', ') + more;

} }

export { BlockSummaryFilterApi as api }; ```

And registered in the umbraco-package.json:

json { "name": "UFM Samples", "version": "1.0.0", "extensions": [ { "type": "ufmFilter", "alias": "UfmSamples.Filter.BlockSummary", "name": "Block Summary UFM Filter", "api": "/App_Plugins/UfmSamples/block-summary.filter.js", "meta": { "alias": "blockSummary" } } ] }

It's used as below:

Tabs: {umbValue: tabs | blockSummary:heading} becomes Tabs: Umbraco development, .NET development, Front-end builds

FAQs: {umbValue: items | blockSummary:question:1} becomes FAQs: How did I get here? + 2 more

A block labelled "FAQs: How did I get here? + 2 more"

And because custom filters are available as functions in expressions too:

Tabs: ${ blockSummary(tabs, "heading", 2) } becomes Tabs: Umbraco development, .NET development + 1 more

Components

Components are a little more involved, but let you render whatever you like. They come in two parts: the component itself, which turns {swatch: colour} into some HTML, and a web component, which looks up the value and renders it.

Here's one that renders a colour picker value as a little swatch:

```js // swatch.component.js import { UmbUfmComponentBase } from '@umbraco-cms/backoffice/ufm'; import './swatch.element.js';

class SwatchComponentApi extends UmbUfmComponentBase { render(token) { if (!token.text) return; // Note: super, not this - Marked rebinds this when calling render() const attributes = super.getAttributes(token.text); return <ufm-swatch ${attributes}></ufm-swatch>; } }

export { SwatchComponentApi as api }; ```

```js // swatch.element.js import { UmbUfmElementBase, UMBUFMRENDER_CONTEXT } from '@umbraco-cms/backoffice/ufm'; import { css, html, nothing } from '@umbraco-cms/backoffice/external/lit';

export class UfmSwatchElement extends UmbUfmElementBase { static properties = { alias: { type: String } };

constructor() { super(); this.consumeContext(UMBUFMRENDER_CONTEXT, (context) => { this.observe(context?.value, (value) => { this.value = this.alias && typeof value === 'object' ? value[this.alias] : value; }, 'observeValue'); }); }

render() { if (!this.value?.value) return nothing; return html<span class="swatch" style="background:#${this.value.value}"></span>${this.value.label}; }

static styles = css .swatch { display: inline-block; width: 1em; height: 1em; margin-right: 0.3em; vertical-align: -0.15em; border-radius: 50%; } ; }

customElements.define('ufm-swatch', UfmSwatchElement); ```

Make sure your element's name starts with ufm- (or umb-/uui-), otherwise it'll get stripped out.

Then add the component to the extensions in your umbraco-package.json. The alias in the meta is what you'll use in your templates:

json { "type": "ufmComponent", "alias": "UfmSamples.Component.Swatch", "name": "Colour Swatch UFM Component", "api": "/App_Plugins/UfmSamples/swatch.component.js", "meta": { "alias": "swatch" } }

Colour: {swatch: colour} renders:

Two blocks labelled "Colour:" followed by a raspberry and green swatch and the colour's name

Bugs and quirks

A few more things I bumped into while testing:

  • Typos fail silently. An invalid expression (${ heading +* }) renders nothing at all, as does a property alias that doesn't exist. If a label's mysteriously empty, check your spelling, then your browser's console (expressions that can't be parsed are logged there).
  • Objects don't render. {umbValue: ...} on a rich text or colour picker value renders [object Object]. Use a filter, an expression or a specific component instead.
  • Showing the syntax literally. If you want to show {=heading} or ${ heading } in a label (or, more likely, a property description), either escape it with a backslash (\{=heading}) or wrap it in backticks.
  • Kebab-case filters. You'll see strip-html, title-case and word-limit in older examples. These still work in v17 but are removed in v18, so use the camelCase versions (stripHtml, titleCase, wordLimit).

Further reading