Tales from the Evil Empire

Bertrand Le Roy's blog

News


Bertrand Le Roy

BoudinFatal's Gamercard

Tales from the Evil Empire - Blogged

Blogs I read

My other stuff

Archives

May 2011 - Posts

Orchard list customization: first item template

I got this question more than once: "how can you use a different template for the first blog post?" The scenario is illustrated by this example:How the New-York Times displays headlines

If you look at the default rendering for the list of posts in a blog, you'll see this:

<ul class="blog-posts content-items">
  <li class="first">
    <article class="content-item blog-post">
      <header>...

As you can see, there is a "first" class added to the first list item tag, which allows for some styling customization out of the box. Unfortunately CSS is only half of the story, and it is insufficient if you are aiming for something like the NYT list pictured above.

What we really want is an alternate template for the first item, so that we can profoundly modify the layout, and display more things, like a large photo or more text.

Luckily, Orchard has just the right feature, aptly called Shape Alternates. If you enable Shape Tracing (which can be found in the Designer Tools module) and select a post in the list, you'll see something like this:Shape Tracing the blog post summary

As you can see, we already have some possible alternates in here. All we need to do is add a new one for the first item in the list and we will then be able to override it in our theme.

In order to do that, we need to somehow get into the list rendering and modify the item summary shapes from there.

The default rendering for lists is defined in code, not in a template (Orchard considers methods with a Shape attribute and cshtml templates to be equivalent ways of rendering a shape). Here is the code for it:

[Shape]
public void List(
    dynamic Display,
    TextWriter Output,
    IEnumerable<dynamic> Items,
    string Tag,
    string Id,
    IEnumerable<string> Classes,
    IDictionary<string, string> Attributes,
    IEnumerable<string> ItemClasses,
    IDictionary<string, string> ItemAttributes) {

    if (Items == null)
        return;

    var count = Items.Count();
    if (count < 1)
        return;

    var listTagName = string.IsNullOrEmpty(Tag) ? "ul" : Tag;
    const string itemTagName = "li";

    var listTag =
GetTagBuilder(listTagName, Id, Classes, Attributes); Output.Write(listTag.ToString(TagRenderMode.StartTag)); var index = 0; foreach (var item in Items) { var itemTag =
GetTagBuilder(itemTagName, null,
ItemClasses, ItemAttributes); if (index == 0) itemTag.AddCssClass("first"); if (index == count - 1) itemTag.AddCssClass("last"); Output.Write(itemTag.ToString(TagRenderMode.StartTag)); Output.Write(Display(item)); Output.Write(itemTag.ToString(TagRenderMode.EndTag)); ++index; } Output.Write(listTag.ToString(TagRenderMode.EndTag)); }

The logic of that method can be overridden by a new template in our theme. As Shape Tracing can show, we can override the list rendering for a blog by creating a Parts.Blogs.BlogPost.List.cshtml template in our theme's Views folder:

@using Orchard.DisplayManagement.Shapes;
@{
    var list = Model.ContentItems;
    var items = list.Items;
    var count = items.Count;
    var listTag = Tag(list, "ul");
    listTag.AddCssClass("content-items");
    listTag.AddCssClass("blog-posts");
    var index = 0;
}
@listTag.StartElement
    @foreach (var item in items) {
        var itemTag = Tag(item, "li");
        if (index == 0) {
            itemTag.AddCssClass("first");
        }
        else if (index == count - 1) {
            itemTag.AddCssClass("last");
        }
        @itemTag.StartElement
        @Display(item)
        @itemTag.EndElement
        ++index;
    }
@listTag.EndElement

Like the shape method above, this template is rendering the UL and LI tags with the appropriate classes and then loops over the items in the list, delegating the rendering of each to the proper template.

So far so good, we have effectively taken over the rendering of the list, but the actual HTML that this generates should be exactly identical to what we had before. The only difference is the implementation detail that it is our theme that did the rendering.

Alternates are a collection of strings that describe additional shape names for the current shape. That list of strings lives in the Metadata.Alternates property of any shape:Alternates in the debugger

All we need to do is add to this list and the system will be able to see a specialized template for our first item. It looks like all you would have to do is to add to the collection from within that "if (index == 0)" block above.

Not so fast.

That will not work because at the time this code is running, the rest of the system has not had a chance to chime in and propose its own alternates (the ones we saw in the first screenshot). The list at this point is empty. The problem is that alternate templates are being matched starting from the end of the list of alternates. If we added our alternate from here, it would be first in the list, so it would have the lowest priority.

The way out of this is fairly simple, we just need to respect the lifecycle and add our own event handler to the right point (OnDisplaying):

ShapeMetadata metadata = item.Metadata;
string alternate = metadata.Type + "_" +
        metadata.DisplayType + "__" +
        item.ContentItem.ContentType +
        "_First";
metadata.OnDisplaying(ctx => {
    metadata.Alternates.Add(alternate);
});

This will work because the theme code will be run last: the theme is considered more specialized than any module.

Here is the complete code for Parts.Blogs.BlogPost.List.cshtml:

@using Orchard.DisplayManagement.Shapes;
@{
    var list = Model.ContentItems;
    var items = list.Items;
    var count = items.Count;
    var listTag = Tag(list, "ul");
    listTag.AddCssClass("content-items");
    listTag.AddCssClass("blog-posts");
    var index = 0;
}
@listTag.StartElement
    @foreach (var item in items) {
        var itemTag = Tag(item, "li");
        if (index == 0) {
            ShapeMetadata metadata = item.Metadata;
            string alternate = metadata.Type + "_" +
                    metadata.DisplayType + "__" +
                    item.ContentItem.ContentType +
                    "_First";
            metadata.OnDisplaying(ctx => {
                metadata.Alternates.Add(alternate);
            });
            itemTag.AddCssClass("first");
        }
        else if (index == count - 1) {
            itemTag.AddCssClass("last");
        }
        @itemTag.StartElement
        @Display(item)
        @itemTag.EndElement
        ++index;
    }
@listTag.EndElement

The list of alternates from Shape Tracing now contains a new item:The new alternate template

We can now create a Content-BlogPost.First.Summary.cshtml template:

@using Orchard.Utility.Extensions;
@{
  var contentTypeClassName =
((string)Model.ContentItem.ContentType).HtmlClassify(); var item = Model.ContentItem; } <article class="content-item @contentTypeClassName"> <header> <h1>
<
a href="@item.RoutePart.Path">@item.RoutePart.Title</a>
</
h1> </header> @item.BodyPart.Text </article>

The results can be seen in the following screenshot:image

Using Taxonomies in Orchard

In this screencast, I will demonstrate how to use the Taxonomies module in Orchard to create a simple news site.

Even better customizability in Orchard

(c) Bertrand Le Roy 2011One of our goals in Orchard is to make it possible and simple to change and customize the markup and style for everything that gets rendered by the application and its modules. Of course, this is made a lot trickier by our other big requirement of making everything a composition of atomic parts.

Yesterday, we brought on site a web developer who is a fan of Drupal and is occasionally using Joomla! and WordPress, in order to get some good feedback after her using Orchard on a project. And that we got.

One of the many interesting things she told us had one essential quality though: it was immediately actionable. Here is the idea. This is the generated markup for an HTML widget in Orchard:

<article class="widget-html-widget widget">
    <header>
        <h1>Title goes here</h1>
        
    </header>
    <p>Text goes here</p>
</article>

And here is the same thing in Drupal:

<div class="block block-block unstyled-block" id="block-block-2">
  <h2 class="title">Title goes here</h2>
  <div class="content"><p>Text goes here</p></div>
</div>

As you can see, Drupal is more generous and precise in the information it gives to designers that they can use to give precise styling to the widget. In particular it gives an id to the widget's outer tag, which is useful if you want to target that particular widget instance. I think we should give an id as well in Orchard, but my thinking was that I could immediately build a module to make this better.

Introducing the Vandelay.Classy module. This new module adds a part that can be added to any content type and that adds an id as well as custom CSS classes and scripts. It should be most useful with widget types but nothing prevents you from using it with regular content types.

Once you've installed the module and enabled the feature, you can go to Content / Content Types and edit the type that you want to enhance, for example the Html Widget:Edit a widget type

You can now add the part:Click "Add Part"Add the "Custom Css" part

Now if you go to the widgets page and edit a specific widget, you'll see additional properties:You may now add an id, CSS classes and scripts from the admin UI

With the values above, here is the new markup that we're getting from the front-end:

<article class="widget-html-widget widget my-custom-class and-another"
id="myCustomId"> <header> <h1>First Leader Aside</h1> </header> <p>[...]</p> </article>

This way, it is now trivial to style this widget individually using a #myCustomId rule, or this widget and similarly modified others through the custom classes that we were able to add.

If you want to know how this was implemented, just look at the source code, and in particular in CustomCssHandler. Adding the classes and id is almost trivial from the BuildDisplayShape event:

context.Shape.Classes.Add(classes);
// ...
context.Shape.Attributes.Add("id", id);

The script property deserves a separate explanation as the requirement for it is a little different. Several users have expressed needs to add script to a specific widget or page, because they wanted to use an existing JavaScript component but didn't want to go through the creation of a full module, preferring to use the HTML editor.

One of the problems of that approach is that JavaScript files are shared resources: if more than one widget for example includes a given file, you don't want that file to be included more than once. Reasons for that are that it is wasteful and that there might be side-effects.

With the new scripts list that this module adds, the scripts are added through Orchard's resource manager, that will take care of removing duplicates.

You can include your own scripts (be sure to use a .js extension) or you can use registered names for libraries that the system knows about (such as "jQuery" or "jQueryUI").

The module can be found in Orchard through Modules / Gallery, or downloaded from here:
http://orchardproject.net/gallery/List/Modules/Orchard.Module.Vandelay.Classy

How to get a source code enlistment set-up for Orchard

This screencast shows the basic set-up for an Orchard dev environment.

More Posts