Wiki

Clone wiki

Module_Onestop_Layouts / Extensions

Extending Onestop Layouts

This topic is aimed at developers who want to extend Onestop Layouts and create their own elements.

When should you (not) create your own element

Before you go ahead and create your own element, it is useful to stop and think about what an element is, as opposed to parts and fields.

Template elements are not attached to the content type like parts and fields are, but instead are attached to templates. This means that not all items of a given templated type will necessarily share the same elements.

If you have a product content type for example, and you want to attach an image to all products, you would not use an image element, but a media library field. This way, the product type has an image.

More generally, if your type "is a" something (e.g. "a blog post is made of a title, a slug, a body, and tags"), then that something should probably be a part.

If your type "has a" something (e.g. "a product has pictures"), then that something should probably be a field.

Now if you need something to properly layout an item, independently of what that item is (i.e. its type), then it should probably be a template element. For example, if you need a header picture, or a background image, then those should be implemented using image or container elements. They belong to the item when used with that specific template, but wouldn't be there otherwise.

If a part or field was the right choice based on these criteria, but you still want to control how those parts and fields get laid out, then use the part and field elements in your template. In this use-case, Onestop Layouts behaves as a more powerful and more dynamic version of placement.info.

Another example of a good use-case for a new element would be a new layout element that implements a kind of layout that rows and columns don't allow. For example, you could create a "popup" element that is implemented as a modal overlay. Such an element is pure layout and is a perfectly legitimate scenario.

Implementing a new element

If you've determined that you need to implement a new element, you will have a number of things to implement. The process is similar, but lighter-weight, to content part or field creation.

Implementing ILayoutElement

The heart of an element is implemented in a class that implements ILayoutElement. Such a class however can cover more than one element. The interface and its usage by the module are modeled after a broadcast model where the framework asks all implementations of the interface whether they want to participate in rendering or editing a particular element. This is similar to how content handlers work.

Implementing BuildEditor

#!c#
dynamic BuildEditor(
    XElement description,
    XElement data,
    string prefix,
    IDictionary<string, IEnumerator<XElement>> dataEnumerators = null)

This is the method that will build element editors to be included as part of the templated content item editor (not to be confused with the template editor, we'll come to that when we look at BuildLayoutEditors).

The method receives two XElement objects that respectively represent the settings of the template or layout element (as can be seen in the XML tab of the template and layout editors), and the content-item data for the same element.

prefix is the current prefix that will be used by the editor to differentiate the editors for each element, even when they are deeply nested. The prefix is usually passed through unchanged into a shape.

The dataEnumerators dictionary is a set of enumerators of data elements that is maintained globally for the templated part. It is used to map the data to the template definition even in the case where the template has been changed since the last time the data was saved. It is there to maintain state and should only be passed through into deeper elements as necessary (see the container element for an example). In other cases, it can be safely ignored.

An implementation of BuildEditor typically looks at description.Name.LocalName to see if it should handle the element. If it determines it wants to handle the element, it returns an edit shape. The edit shape is the editor that will be shown in the content item editor. If it should display an index for itself, it should have a UsesIndex property set to true.

Here is the code for the image element:

#!c#
switch (description.Name.LocalName.ToLowerInvariant()) {
    case "img":
        var src = XmlHelpers.Attr(data, "src");
        var defaultUrl = XmlHelpers.Attr(description, "default");
        var defaultAlt = XmlHelpers.Attr(description, "defaultalt");
        return Shape.LayoutElements_Image_Edit(
            Name: T("Image"),
            Url: src,
            DefaultUrl: defaultUrl,
            AlternateText: XmlHelpers.Attr(data, "alt"),
            DefaultAlt: defaultAlt,
            Title: XmlHelpers.Attr(description, "title"),
            Prefix: prefix,
            UsesIndex: true);

Implementing HandleEdits

#!c#
void HandleEdits(
    IDictionary<string, string> dictionary,
    XElement description,
    IUpdateModel updater,
    string prefix)

This method can be used to execute special code before elements get persisted from POST data. For most elements, this method can be left empty. It is not necessary to implement anything in this method for the element data to get persisted.

Implementing BuildDisplay

#!c#
dynamic BuildDisplay(
    XElement description,
    XElement data,
    IContent contentPart,
    string displayType,
    IDictionary<string, IEnumerator<XElement>> dataEnumerators = null)

BuildDisplay constructs the front-end shapes for elements. It works like BuildEditor, by being broadcast all elements and returning shapes when it can handle one specific element.

The method receives parameters similar to BuildEditor, except that it doesn't have a prefix parameter, and it also receives the display type, and an IContent that is the current context.

In most cases, that context will be the templated item part being rendered, but there are cases, such as a container element with context, where that root context will be swapped for something else before it's passed to child elements.

Some elements, such as part and field elements, use that context to build their shape.

Here is the BuildDisplay implementation for the image element:

#!c#
var layoutElement = description.FindParentWithAttributes("width", "height") ?? new XElement("layout");
switch (description.Name.LocalName.ToLowerInvariant()) {
    case "img":
        var src = XmlHelpers.Attr(data, "src");
        var defaultUrl = XmlHelpers.Attr(description, "default");
        var alt = XmlHelpers.Attr(data, "alt");
        var defaultAlt = XmlHelpers.Attr(description, "defaultalt");
        return Shape.LayoutElements_Image(
            Name: T("Image"),
            Url: string.IsNullOrWhiteSpace(src) ? defaultUrl : src,
            Thumbnail: src,
            AlternateText: string.IsNullOrWhiteSpace(alt) ? defaultAlt : alt,
            Width: description.AttrLength("width"),
            Height: description.AttrLength("height"),
            Top: description.AttrLength("top"),
            Left: description.AttrLength("left"),
            LayoutWidth: layoutElement.AttrInt("width"),
            LayoutHeight: layoutElement.AttrInt("height"),
            CssClass: XmlHelpers.Attr(description, "class"),
            ElementTitle: XmlHelpers.Attr(description, "title"),
            UsesIndex: true,
            ContentPart: contentPart,
            ContentItem: contentPart == null ? null : contentPart.ContentItem);

Implementing BuildLayoutEditors

#!c#
IEnumerable<dynamic> BuildLayoutEditors()

The layout/template editors for elements are mostly client-side components: they are created and managed from the client, and their data is persisted client-side into XML. The server has practically nothing to do to expose them, except declare them and serve templates.

The BuildLayoutEditors method returns a list of shapes for all the elements that it handles. The shape has typically no properties apart from its order on the toolbox.

Here is the code to declare the image element's template editor:

#!c#
return new[] {
    Shape.LayoutElements_Image_LayoutEditor(Order: "20"),

Implementing the display template for an element

The front-end, or display template for an element is the template for the shape that was returned by the BuildDisplay method. In the case of the image element, that shape was called LayoutElements_Image, so its template is a LayoutElements.Image.cshtml file under the Views folder of the module (that can of course be overwritten by another template in the theme).

The display template usually looks at the properties on the Model as it has been prepared by the BuildDisplay method, and builds HTML accordingly.

When no data or insufficient data is available, which can happen if the element has been created but not configured, some elements will want to display nothing, and some others will want to display a placeholder.

It is often useful to look at Model.Metadata.DisplayType to adapt the rendering to the circumstances. When rendering the thumbnail preview in the admin for example, the display type is "Preview". Knowing you are in preview mode is useful if you need to render images for example, as relative paths need to be made absolute because the current page is some admin page instead of the content item being previewed. For that, Onestop Layouts exposes a helper, Onestop.Layouts.Helpers.HtmlHelpers.FixedHref. That helper can be used by using Onestop.Layouts.Helpers and call it with the Href method as a parameter: Url.FixedHref(s => Href(s), url). It will do the right thing whether the URL is relative, app-relative, or absolute.

Another useful helper when building element rendering is Onestop.Layouts.Helpers.HtmlHelpers.BuildStyle. This is an extension method for IDictionary<string, string> that can be used to build the style attribute for an HTML element property by property. Here is an example of usage from the image element:

#!cshtml
var styles = new Dictionary<string, string>();
styles.AddPercentIfNonZeroPixels("width", (Length)Model.Width, (int)Model.LayoutWidth);
styles.AddIfNotEmpty("height", ((Length)Model.Height).ToString());
styles.AddPercentIfNonZeroPixels("left", (Length)Model.Left, (int)Model.LayoutWidth, "position", "absolute");
styles.AddPercentIfNonZeroPixels("top", (Length)Model.Top, (int)Model.LayoutHeight, "position", "absolute");
Model.Attributes["style"] = styles.BuildStyle();
var tag = Tag(Model, "div");
@tag.StartElement

This code builds a dictionary of style attributes, then add the resulting style string to the model's attribute collection under the "style" key. A div tag is then built and rendered from Model, which will output the correct style attribute.

Other HtmlHelper methods, some of which are used in this sample, include:

  • styles.AddIfNotEmpty(string key, string value) adds an attribute only if the value that's passed in is not null or composed of only white space.
  • styles.AddIfNotZero(string key, int value) adds an attribute as a pixel value if the integer passed in is not zero.
  • styles.AddPercentIfNonZeroPixels( string key, Length length, int reference, string extraAttributeName = null, string extraAttributeValue = null) adds an attribute only if the Length value passed in is more than zero. If the length passed in is in pixels, it is transformed into a percentage using the reference parameter as the reference 100%. Optionally, you can add an additional attribute when the length is not zero.
  • styles.MergeStyles(IDictionary<string, string> otherStyles) merges the styles dictionary with another. If an item exists in both dictionaries, the one passed as a parameter wins. This method is similar to jQuery's $.extend.
  • Percentage(Length absolute, int reference) returns a string value representing a percentage relative to the reference if the length is in pixels, and otherwise returns the value as a string without changing it.
  • ParseStyleString(string style) parses a style string and changes it into an IDictionary<string, string> that is easier to manipulate. This method is the reverse of BuildStyle.

styles in the list above stands for an IDictionary<string, string>.

Length is a useful class to represent CSS length, that have a floating point value, and a unit. Its ToString implementation returns the concatenation of the value and the unit, unless the value is zero or less, in which case it returns an empty string. The unit is a string that can in principle be anything meaningful in present and future versions of CSS, but provides the following constants for easier use:

Constant Value
Pixel px
Percent %
Em em
Point pt

Maintaining a live preview

If you want your element to be able to update its own thumbnail preview as the user edits it, you need to add a few data attributes when in preview mode. The image element does it using the following code:

#!cshtml
if (Model.Metadata.DisplayType == "Preview" && Model.UsesIndex) {
    var displayIndex = Html.GetLayoutDisplayIndex();
    var textId = "templated_" + displayIndex + "_alt";
    var urlId = "templated_" + displayIndex + "_src";
    <img class="@(urlId + "_live " + textId + "_live")"
        @Html.Raw("data-live-property-" + urlId + "=\"src\"")
        @Html.Raw("data-live-property-" + textId + "=\"alt\"")
         src="@url" alt="@Model.AlternateText"/>
}
else {
    <img src="@url" alt="@Model.AlternateText"/>
}

This code starts by acquiring a unique layout display index. This index is maintained for the whole request and is what's behind the large indexes that get rendered on top of preview elements and to the left of their editors.

The index is then used to generate identifiers that happen to be the same that the element editor shape will use to render the form fields for the properties that need live binding (see next section on creating editor shapes for elements).

Those identifiers are then used to create data-live-property-[id] attributes and [id]_live classes that tell the system what attribute needs to bound.

For example, if the index is 4, the image will be rendered as follows:

#!html
<img class="templated_4_src_live templated_4_alt_live"
     data-live-property-templated_4_src="src"
     data-live-property-templated_4_alt="alt"
     src="path/to/image.jpg" alt="Alternate description"/>

We'll see in the next section how the editor will render form fields for the source and alternate text of the image that will have "templated_4_src" and "templated_4_alt" as their ids. This convention will be enough to bind the entered values to their live preview.

Implementing the editor template for an element

The editor template for an element is the template for the shape that was returned by the BuildEditor method. In the case of the image element, that shape was called LayoutElements_Image_Edit, so its template is a LayoutElements.Image.Edit.cshtml file under the Views folder of the module (that can of course be overwritten by another template in the theme).

The display template usually looks at the properties on the Model as it has been prepared by the BuilEditor method, and builds HTML accordingly.

Edit templates for layout and template elements look in many ways like part or field editor templates, with a few differences due in particular to the live preview feature, and to the necessity to persist using generic code, without requiring the element author to write specific code. A few simple conventions enable those features at a low cost.

Identifiers and names for form fields must be built in a specific fashion. We will again use the example of the image element.

First, let's generate the identifier for the element editor, from the current prefix, and the current layout editor prefix:

#!c#
var prefix = String.IsNullOrWhiteSpace((string)Model.Prefix) ? "" : Model.Prefix + ".";
var editorIndex = Html.GetLayoutEditorIndex();
var id = prefix + "TemplatedItemData[" + editorIndex + "]";

Once we have this element editor unique id, we can generate sequential identifiers and names beneath it for each of the properties we want to maintain:

#!c#
var textId = "templated_" + editorIndex + "_alt";
var textName = id + "[0]";
var urlId = "templated_" + editorIndex + "_src";
var urlName = id + "[1]";

The editor UI can then be generated using these unique strings:

#!cshtml
<input type="hidden" name="@(urlName).Key" value="src"/>
<input type="text" id="@urlId" name="@(urlName).Value"
       value="@Model.Url" data-default-value="@Model.DefaultUrl"
       class="textMedium templated-live-field"/>
[...]
<input type="hidden" name="@(textName).Key" value="alt"/>
<input type="text" id="@textId" name="@(textName).Value"
       value="@Model.AlternateText" data-default-value="@Model.DefaultAlt"
       class="textMedium templated-live-field"/>

A first hidden field specifies through its value the name of the XML attribute to use for this property when persisting it. A second field, here a text-box, implements the value of the property. The names of the fields are in the ASP.NET MVC convention to model-bind dictionaries. The identifiers are consistent with the ones we used when setting up the live preview in the display template.

Implementing the layout or template editor for an element

The layout or template editor template for an element is the template for the shape that was returned by the BuildLayoutEditors method. In the case of the image element, that shape was called LayoutElements_Image_LayoutEditor, so its template is a LayoutElements.Image.LayoutEditor.cshtml file under the Views folder of the module (that can of course be overwritten by another template in the theme).

The template for a layout or template element is not doing much on the server, and is more a client template that uses a very simple templating language.

For example, here is the template for the image template editor:

#!cshtml
@{
    Script.Require("jQueryUI").AtFoot();
    Script.Include("layout-element-img.js").AtFoot();
}
<fieldset class="layout-editor-element-template"
          data-template="img"
          data-icon="@Url.Content("~/Modules/Onestop.Layouts/Content/Img.png")" 
          data-description="Image"
          data-for-templates="true"
          data-default-xml="&lt;img/&gt;">
    <legend>Image</legend>
    <div>
        <label for="{{title}}">@T("Title")</label>
        <input type="text" id="{{title}}" name="{{title}}" value="{{title}}" class="text-smallish" />
    </div>
    <div>
        <label for="{{default}}">@T("Default URL")</label>
        <input type="text" id="{{default}}" name="{{default}}" value="{{default}}" class="text-smallish" />
    </div>
    <div>
        <label for="{{defaultalt}}">@T("Default Alt")</label>
        <input type="text" id="{{defaultalt}}" name="{{defaultalt}}" value="{{defaultalt}}" class="text-smallish" />
    </div>
    <div>
        <label for="{{left}}">@T("X")</label>
        <input type="number" id="{{left}}" name="{{left}}" data-value="{{left}}" class="text-small left length" />
        <label for="{{top}}">@T("Y")</label>
        <input type="number" id="{{top}}" name="{{top}}" data-value="{{top}}" class="text-small top length" />
    </div>
    <div>
        <label for="{{width}}">@T("Width")</label>
        <input type="number" id="{{width}}" name="{{width}}" data-value="{{width}}" class="text-small width length" />
        <label for="{{height}}">@T("Height")</label>
        <input type="number" id="{{height}}" name="{{height}}" data-value="{{height}}" class="text-small height length" />
    </div>
    <div>
        <label for="{{class}}">@T("CSS class")</label>
        <input type="text" id="{{class}}" name="{{class}}" value="{{class}}" class="text-smallish css-class-editor" />
    </div>
</fieldset>

The template starts by requiring and including the scripts it needs. Then, the outer fieldset (that must have the layout-editor-element-template class) uses data-* elements to declare some metadata about the element.

  • data-template declares what element this is a template for.
  • data-icon declares the path to the icon file to use in the toolbox for this element.
  • data-description declares a human-readable name for the element.
  • data-for-templates, if false, declares that this element is for use in the layout editor, and otherwise that it is for use in the template editor.
  • data-default-xml declares what the empty XML markup should be for a new element of this type.

After that, there are typically a label and a form field for each of the settings for the element. In those HTML elements, attributes can be bound to XML attributes of the element definition using the "{{xmlAttributeName}}" syntax. Because some HTML attributes do not support any value (the inputs of type number in the above code, for example), it is sometimes necessary to use data-htmlAttributeName (e.g. data-value) instead of using the name of the attribute directly.

Building the preview script for a layout or template editor for an element

The layout or template editor for an element has to provide a small script that can create a new preview element for use in the preview pane of the layout and template editors. Please note that this preview is different from the thumbnail preview that is visible in the content item editor for templated content items. The preview we are talking about here is the preview in the template and layout editors.

In the case of the image element, the preview is an actual image when a default URL has been specified, and a canvas drawing of an empty, barred box otherwise.

To declare itself to the system, the script must add a function with the signature function(description, target, scale) as a property of the layoutPreviewers global object:

#!js
(window.layoutPreviewers = window.layoutPreviewers || { })
    .img = function(description, target, scale) {

The description parameter is the XML element describing the template or layout element. It is passed in in the form of a jQuery object, which enables the script to access attributes using regular jQuery methods. For example, description.attr("width") to get the value of the width attribute of the element's description.

The target parameter is the HTML element to which the function should append the preview element.

The scale parameter is providing the current scale factor of the preview. It could be used for example to make sure that the preview of an element is never smaller than a certain size, to still make selection possible no matter what the scale is. The image element uses it to draw with thicker lines in order to keep the preview readable even at small scales.

The function for an element's preview is responsible for appending its preview to target and for returning the preview as its result:

#!js
target.append(preview);
return preview;

There are a few utility functions that the layout global object exposes, that can be used in element preview scripts. In particular, layout.makeAbsolute takes a URL (relative, app-relative, or absolute), and makes it absolute so that they can be rendered in a context that is not that of the content item. For example, the image element uses it to make the source attribute of its preview img tag work from the template editor:

#!js
src: window.layout.makeAbsolute(description.attr("default"))

Updated