Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Sam Boswell 21 posts 106 karma points
    Oct 02, 2015 @ 15:56
    Sam Boswell
    0

    Render pages from multinode tree picker in grid view

    Hia,

    I have a custom grid editor which allows a user to pick nodes using a multinodetreepicker.

    Within Views\Partials\Grid\Editors\Card.cshtml I can output the id's of the selected nodes using

    @model dynamic
    @using Umbraco.Web.Templates
    
    @Html.Raw(Model.value)
    

    I'd like to do something along the lines of:

    foreach (var pageid in Model.value.ToString().Split(',')) {
    var page = Umbraco.TypedContent(pageid);
        <p>page.Name</p>
        }
    

    But I'm getting the error: 'TypedContent' does not exist in the namespace 'Umbraco'

    What do I need to include to allow me the tools I'm used to in this partial view?

  • Lars-Erik Aabech 349 posts 1100 karma points MVP 7x c-trib
    Oct 05, 2015 @ 05:33
    Lars-Erik Aabech
    100

    You have to inherit the view from UmbracoViewPage to use the Umbraco helper.

    Add this to the top of your view:

    @inherits Umbraco.Web.Mvc.UmbracoViewPage<dynamic>
    

    Umbraco.TypedContent should work after that. :)

  • Sam Boswell 21 posts 106 karma points
    Oct 05, 2015 @ 08:26
    Sam Boswell
    0

    Thank you, that was the missing bit for me!

  • Daniel Gillett 72 posts 149 karma points
    Oct 25, 2015 @ 13:08
    Daniel Gillett
    0

    Hi Sam,

    Would you be able to post the files you made to use the MultinodeTreePicker in my data grid?

    Many thanks, Daniel

  • Sam Boswell 21 posts 106 karma points
    Oct 26, 2015 @ 12:15
    Sam Boswell
    0

    Sure - I set mine up to be a "card picker" allowing people to select a number of other pages to be displayed as cards.

    I created a new folder in App_Plugins called "CardContentPicker" which contained:

    package.manifest

    {
    //you can define multiple editors
    propertyEditors: [
                        {
                    /*this must be a unique alias*/
                    alias: "RHP.MultiCardContentPicker",
                    /*the name*/
                    name: "Multi Card Content Picker",
                    isParameterEditor: true,
                    /*the html file we will load for the editor*/
                    editor: {
                                view: "~/App_Plugins/CardContentPicker/cardeditor.html"
                            }
                    },
    ]
    ,
    //array of files we want to inject into the application on app_start
    javascript: [
    '~/App_Plugins/CardContentPicker/cardeditor.controller.js'
    ]
    }
    

    cardeditor.html

    <div ng-controller="My.CardEditorController" class="umb-editor umb-contentpicker">
    <ng-form name="contentPickerForm">
    
        <ul class="unstyled list-icons"
            ui-sortable
            ng-model="renderModel">
    
            <li ng-repeat="node in renderModel">
    
                <i class="icon icon-navigation handle"></i>
    
                <a href="#" prevent-default ng-click="remove($index)">
                    <i class="icon icon-delete red hover-show"></i>
                    <i class="{{node.icon}} hover-hide"></i>
                    {{node.name}}
                </a>
                <div ng-if="!dialogEditor && model.config.showEditButton">
                    <small><a href umb-launch-mini-editor="node">Edit</a></small>
                </div>
            </li>
        </ul>
    
        <ul class="unstyled list-icons" ng-show="true === true || renderModel.length === 0">
            <li>
                <i class="icon icon-add blue"></i>
                <a href="#" ng-click="openContentPicker()" prevent-default>
                    <localize key="general_add">Add</localize>
                </a>
            </li>
        </ul>
    
        <!--These are here because we need ng-form fields to validate against-->
        <input type="hidden" name="minCount" ng-model="renderModel" />
        <input type="hidden" name="maxCount" ng-model="renderModel" />
    
        <div class="help-inline" val-msg-for="minCount" val-toggle-msg="minCount">
            You need to add at least {{model.config.minNumber}} items
        </div>
    
        <div class="help-inline" val-msg-for="maxCount" val-toggle-msg="maxCount">
            You can only have {{model.config.maxNumber}} items selected
        </div>
        </ng-form>
    </div>
    

    and finally cardeditor.controller.js

    angular.module("umbraco").controller("My.CardEditorController", function ($scope, dialogService, entityResource, editorState, $log, iconHelper, $routeParams, fileManager, contentEditingHelper) {
    
    function trim(str, chr) {
        var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g');
        return str.replace(rgxtrim, '');
    }
    
    function startWatch() {
        //We need to watch our renderModel so that we can update the underlying $scope.model.value properly, this is required
        // because the ui-sortable doesn't dispatch an event after the digest of the sort operation. Any of the events for UI sortable
        // occur after the DOM has updated but BEFORE the digest has occured so the model has NOT changed yet - it even states so in the docs.
        // In their source code there is no event so we need to just subscribe to our model changes here.
        //This also makes it easier to manage models, we update one and the rest will just work.
        $scope.$watch(function() {
            //return the joined Ids as a string to watch
            return _.map($scope.renderModel, function(i) {
                return i.id;
            }).join();
        }, function(newVal) {
            var currIds = _.map($scope.renderModel, function(i) {
                return i.id;
            });
            $scope.model.value = trim(currIds.join(), ",");
    
            //Validate!
            if ($scope.model.config && $scope.model.config.minNumber && parseInt($scope.model.config.minNumber) > $scope.renderModel.length) {
                $scope.contentPickerForm.minCount.$setValidity("minCount", false);
            } else {
                $scope.contentPickerForm.minCount.$setValidity("minCount", true);
            }
    
            if ($scope.model.config && $scope.model.config.maxNumber && parseInt($scope.model.config.maxNumber) < $scope.renderModel.length) {
                $scope.contentPickerForm.maxCount.$setValidity("maxCount", false);
            } else {
                $scope.contentPickerForm.maxCount.$setValidity("maxCount", true);
            }
        });
    }
    
    $scope.renderModel = [];
    
    $scope.dialogEditor = editorState && editorState.current && editorState.current.isDialogEditor === true;
    
    //the default pre-values
    var defaultConfig = {
        multiPicker: "1",
        showEditButton: false,
        startNode: {
            query: "",
            type: "content",
            id: 2131
        },
        minNumber: 1,
        maxNumber:null
    };
    
    if ($scope.model.config) {
        //merge the server config on top of the default config, then set the server config to use the result
        $scope.model.config = angular.extend(defaultConfig, $scope.model.config);
    }
    
    
    //Umbraco persists boolean for prevalues as "0" or "1" so we need to convert that!
    //$scope.model.config.multiPicker = ($scope.model.config.multiPicker === "1" ? true : false);
    //$scope.model.config.showEditButton = ($scope.model.config.showEditButton === "1" ? true : false);
    
    //var entityType = $scope.model.config.startNode.type === "member"
    //    ? "Member"
    //    : $scope.model.config.startNode.type === "media"
    //    ? "Media"
    //    : "Document";
    
    var entityType =  "Document";
    
    //the dialog options for the picker
    var dialogOptions = {
        multiPicker: true,
        entityType: entityType,
        filterCssClass: "not-allowed not-published",
        startNodeId: null,
        callback: function(data) {
            if (angular.isArray(data)) {
                _.each(data, function(item, i) {
                    $scope.add(item);
                });
            } else {
                $scope.clear();
                $scope.add(data);
            }
        },
        treeAlias: "content",//$scope.model.config.startNode.type,
        section: "content"//$scope.model.config.startNode.type
    };
    
    //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the
    // pre-value config on to the dialog options
    angular.extend(dialogOptions, $scope.model.config);
    
    //We need to manually handle the filter for members here since the tree displayed is different and only contains
    // searchable list views
    if (entityType === "Member") {
        //first change the not allowed filter css class
        dialogOptions.filterCssClass = "not-allowed";
        var currFilter = dialogOptions.filter;
        //now change the filter to be a method
        dialogOptions.filter = function(i) {
            //filter out the list view nodes
            if (i.metaData.isContainer) {
                return true;
            }
            if (!currFilter) {
                return false;
            }
            //now we need to filter based on what is stored in the pre-vals, this logic duplicates what is in the treepicker.controller,
            // but not much we can do about that since members require special filtering.
            var filterItem = currFilter.toLowerCase().split(',');
            var found = filterItem.indexOf(i.metaData.contentType.toLowerCase()) >= 0;
            if (!currFilter.startsWith("!") && !found || currFilter.startsWith("!") && found) {
                return true;
            }
    
            return false;
        }
    }
    
    
    //if we have a query for the startnode, we will use that.
    /*/if ($scope.model.config.startNode.query) {
        var rootId = $routeParams.id;
        entityResource.getByQuery($scope.model.config.startNode.query, rootId, "Document").then(function(ent) {
            dialogOptions.startNodeId = ent.id;
        });
    } else {
        dialogOptions.startNodeId = $scope.model.config.startNode.id;
    }*/
    var rootId = $routeParams.id;
    entityResource.getByQuery("$site/descendant::CardListing[@isDoc]", rootId, "Document").then(function (ent) {
        dialogOptions.startNodeId = ent.id;
    });
    //dialogOptions.startNodeId = 2131;
    
    //dialog
    $scope.openContentPicker = function() {
        var d = dialogService.treePicker(dialogOptions);
    };
    
    $scope.remove = function(index) {
        $scope.renderModel.splice(index, 1);
    };
    
    $scope.add = function(item) {
        var currIds = _.map($scope.renderModel, function(i) {
            return i.id;
        });
    
        if (currIds.indexOf(item.id) < 0) {
            item.icon = iconHelper.convertFromLegacyIcon(item.icon);
            $scope.renderModel.push({ name: item.name, id: item.id, icon: item.icon });
        }
    };
    
    $scope.clear = function() {
        $scope.renderModel = [];
    };
    
    $scope.$on("formSubmitting", function(ev, args) {
        var currIds = _.map($scope.renderModel, function(i) {
            return i.id;
        });
        $scope.model.value = trim(currIds.join(), ",");
    });
    
    //load current data
    var modelIds = $scope.model.value ? $scope.model.value.split(',') : [];
    entityResource.getByIds(modelIds, entityType).then(function(data) {
    
        //Ensure we populate the render model in the same order that the ids were stored!
        _.each(modelIds, function(id, i) {
            var entity = _.find(data, function(d) {
                return d.id == id;
            });
    
            entity.icon = iconHelper.convertFromLegacyIcon(entity.icon);
            $scope.renderModel.push({ name: entity.name, id: entity.id, icon: entity.icon });
    
        });
    
        //everything is loaded, start the watch on the model
        startWatch();
    
    });
    
    });
    

    Inside Grid / Editors I then made a partial view called Card.cshtml where I can get the pages selected using

    foreach (var pageid in Model.value.ToString().Split(',')) {
        var cardItemNode = new Node(int.Parse(pageid));
    

    I hope that helps!

  • Daniel Gillett 72 posts 149 karma points
    Oct 26, 2015 @ 15:39
    Daniel Gillett
    0

    Thanks very much, Sam!

    I've gone through and created the plugins folder and the files the way you described and then pasted in your code accordingly.

    I couldn't get it to work. lol

    The only thing I can think is the issue has to do with the Card.cshtml that you mentioned at the end. I placed it in the Views/Partials/Grid/Editors directory ...also, looking at that file, it looks incomplete. so I tried adding the braces and some markup with didn't help either.

    I am DESPARATE to have a multi node picker in my Grid. I've gone so far with it that if I can't figure this out I'm going to have to revert back to not using the grid. (waste of time)

    hahaha my poor mother's site has been taking ages! lol

    I learned a lot from reading your code though!

    Many thanks!

    Daniel

    PS - I'll spend some time and try to figure out what I did wrong.

    package.manifest Solution Explorer Card.cshtml location Solution Explorer Plugins files

  • Sam Boswell 21 posts 106 karma points
    Oct 26, 2015 @ 15:44
    Sam Boswell
    0

    Ooh yes - You'll need to add in some new settings in \Config\grid.editors.config.js

    {
    "name": "Multi Page Picker",
    "alias": "multipicker",
    "view": "/app_plugins/GridContentPicker/gridcardeditor.html",
    "icon": "icon-thumbnail-list",
    "render": "/Views/Partials/Grid/Editors/Card.cshtml"
    }
    

    This tells the grid picker where to look for the editor, and how to render the control.

  • Sam Boswell 21 posts 106 karma points
    Oct 26, 2015 @ 15:56
    Sam Boswell
    0

    And a full Card.cshtml might look something like:

    @model dynamic
    @using umbraco.NodeFactory
    @using Umbraco.Web.Templates
    
    @{
    foreach (var pageid in Model.value.ToString().Split(',')) {
        var cardItemNode = new Node(int.Parse(pageid));
        <div class="row card-item">
            <div class="col-md-12">
                <h3>@cardItemNode.Name</h3>
            </div>
        </div>
        }
    }
    
  • Daniel Gillett 72 posts 149 karma points
    Oct 26, 2015 @ 17:19
    Daniel Gillett
    0

    Hi Sam,

    that last bit was really helpful.

    The js config file was the missing bit. thank you!

    I updated the filepaths to match your original file names which worked.

    when I choose the picker in the grid's cell. It loads the html file, but when I click the +Add, the page just refreshes. Sorry if I'm taking up your time.

    Add Link Doesn't Appear to be Handled

    I've also uploaded a video so you can see what I'm doing: My Video Demo

  • Daniel Gillett 72 posts 149 karma points
    Oct 26, 2015 @ 17:36
    Daniel Gillett
    0

    I'm soo close I just can't give up yet!

    Could it be that the click event that calls the openContentPicker isn't being picked up?

    Also, there are no settings to set the min and max, so is there another file or should I add the config information for these? Or could it be the "localize key"?

                <a href="#" ng-click="openContentPicker()" prevent-default>
                    <localize key="general_add">Add</localize>
                </a>
    

    Kind regards, Daniel

Please Sign in or register to post replies

Write your reply to:

Draft