orchardcmsorchardcms-1.9

How to create a custom container element for Orchard Layout?


I'm using Orchard 1.9.3 and followed some tutorials on how to create a custom normal Element in Orchard. I couldn't find any specifically on creating container elements so I dug around a bit in the source and this is what I have so far:

Elements/Procedure.cs

public class Procedure : Container
{
    public override string Category
    {
        get { return "Content"; }
    }

    public override string ToolboxIcon
    {
        get { return "\uf0cb"; }
    }

    public override LocalizedString Description
    {
        get { return T("A collection of steps."); }
    }

    public override bool HasEditor
    {
        get { return false; }
    }
}

Drivers/ProcedureElementDriver.cs

public class ProcedureElementDriver : ElementDriver<Procedure> {}

Services/ProcedureModelMap

public class ProcedureModelMap : LayoutModelMapBase<Procedure> {}

Views/LayoutEditor.Template.Procedure

@using Orchard.Layouts.ViewModels;
<div class="layout-element-wrapper" ng-class="{'layout-container-empty': getShowChildrenPlaceholder()}">
<ul class="layout-panel layout-panel-main">
    <li class="layout-panel-item layout-panel-label">Procedure</li>
    @Display()
    @Display(New.LayoutEditor_Template_Properties(ElementTypeName: "procedure"))
    <li class="layout-panel-item layout-panel-action" title="@T("Delete {{element.contentTypeLabel.toLowerCase()}} (Del)")" ng-click="delete(element)"><i class="fa fa-remove"></i></li>
    <li class="layout-panel-item layout-panel-action" title="@T("Move {{element.contentTypeLabel.toLowerCase()}} up (Ctrl+Up)")" ng-click="element.moveUp()" ng-class="{disabled: !element.canMoveUp()}"><i class="fa fa-chevron-up"></i></li>
    <li class="layout-panel-item layout-panel-action" title="@T("Move {{element.contentTypeLabel.toLowerCase()}} down (Ctrl+Down)")" ng-click="element.moveDown()" ng-class="{disabled: !element.canMoveDown()}"><i class="fa fa-chevron-down"></i></li>
</ul>
<div class="layout-container-children-placeholder">
    @T("Drag a steps here.")
</div>
@Display(New.LayoutEditor_Template_Children())

All of this is more or less copied from the Row element. I now have a Procedure element that I can drag from the Toolbox onto my Layout but it is not being rendered with my template, even though I can override the templates for the other layout elements this way, and I still can't drag any children into it. I had hoped that simply inheriting from Container would have made that possible.

I essentially just want to make a more restrictive Row and Column pair to apply some custom styling to a list of arbitrary content. How can I tell Orchard that a Procedure can only be contained in a Column and that it should accept Steps (or some other element) as children?


Solution

  • I figured out how to make container and containable elements from looking at Mainbit's layout module. The container elements require some additional Angular code to make them work. I still need help figuring out how to limit which elements can be contained!

    Scripts/LayoutEditor.js

    I had to extend the LayoutEditor module with a directive to hold all of the Angular stuff pertaining to my element:

    angular
    .module("LayoutEditor")
    .directive("orcLayoutProcedure", ["$compile", "scopeConfigurator", "environment",
        function ($compile, scopeConfigurator, environment) {
            return {
                restrict: "E",
                scope: { element: "=" },
                controller: ["$scope", "$element",
                    function ($scope, $element) {
                        scopeConfigurator.configureForElement($scope, $element);
                        scopeConfigurator.configureForContainer($scope, $element);
                        $scope.sortableOptions["axis"] = "y";
                    }
                ],
                templateUrl: environment.templateUrl("Procedure"),
                replace: true
            };
        }
    ]);
    

    Scripts/Models.js

    And a Provider for Orchard's LayoutEditor to use:

    var LayoutEditor;
    (function (LayoutEditor) {
    
    LayoutEditor.Procedure = function (data, htmlId, htmlClass, htmlStyle, isTemplated, children) {
        LayoutEditor.Element.call(this, "Procedure", data, htmlId, htmlClass, htmlStyle, isTemplated);
        LayoutEditor.Container.call(this, ["Grid", "Content"], children);
    
        //this.isContainable = true;
        this.dropTargetClass = "layout-common-holder";
    
        this.toObject = function () {
            var result = this.elementToObject();
            result.children = this.childrenToObject();
            return result;
        };
    };
    
    LayoutEditor.Procedure.from = function (value) {
        var result = new LayoutEditor.Procedure(
            value.data,
            value.htmlId,
            value.htmlClass,
            value.htmlStyle,
            value.isTemplated,
            LayoutEditor.childrenFrom(value.children));
        result.toolboxIcon = value.toolboxIcon;
        result.toolboxLabel = value.toolboxLabel;
        result.toolboxDescription = value.toolboxDescription;
        return result;
    };
    
    LayoutEditor.registerFactory("Procedure", function (value) {
        return LayoutEditor.Procedure.from(value);
    });
    
    })(LayoutEditor || (LayoutEditor = {}));
    

    This specifically is the line that tells the element what it can contain:

    LayoutEditor.Container.call(this, ["Grid", "Content"], children);
    

    ResourceManifest.cs

    Then I made a resource manifest to easily make these available in Orchard's module.

    public class ResourceManifest : IResourceManifestProvider
    {
        public void BuildManifests(ResourceManifestBuilder builder)
        {
            var manifest = builder.Add();
            manifest.DefineScript("MyModule.Models").SetUrl("Models.js").SetDependencies("Layouts.LayoutEditor");
            manifest.DefineScript("MyModule.LayoutEditors").SetUrl("LayoutEditor.js").SetDependencies("Layouts.LayoutEditor", "MyModule.Models");
        }
    }
    

    By default, .SetUrl() points to the /Scripts folder in your module/theme.

    Handlers/LayoutEditorShapeEventHandler.cs

    Finally, I added this handler to load my scripts on the admin pages that use the Layout Editor.

    public class LayoutEditorShapeEventHandler : IShapeTableProvider
    {
        private readonly Work<IResourceManager> _resourceManager;
        public LayoutEditorShapeEventHandler(Work<IResourceManager> resourceManager)
        {
            _resourceManager = resourceManager;
        }
    
        public void Discover(ShapeTableBuilder builder)
        {
            builder.Describe("EditorTemplate").OnDisplaying(context =>
            {
                if (context.Shape.TemplateName != "Parts.Layout")
                    return;
    
                _resourceManager.Value.Require("script", "MyModule.LayoutEditors");
            });
        }
    }
    

    Hopefully, this will help someone out in the future. However, I still don't know how to make it so that my Container will only contain my Containable or that my Containable will only allow itself to be contained by my Container. It seems like adjusting LayoutEditor.Container.call(this, ["Grid", "Content"], children); would have been enough to achieve this, but it's not. More help is still welcome.