Copied to clipboard

Flag this post as spam?

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


  • Dan 1288 posts 3921 karma points c-trib
    Sep 29, 2022 @ 20:15
    Dan
    0

    Append settings data to block list preview model generated via custom API

    Hi,

    I'm implementing razor previews for block list editor back-office previews, using this article and accompanying repository as a reference.

    Part of the solution is to use a custom API to return the populated HTML of each block by constructing a typed object from its JSON data. The pertinent code which does this is this method:

        private async Task<string> GetMarkupForBlock(BlockItemData blockData)
        {
    
            // convert the json data to a IPublishedElement (using the built-in conversion)
            var element = this._blockEditorConverter.ConvertToElement(blockData, PropertyCacheLevel.None, true);
    
            // get the models builder type based on content type alias
            var blockType = _typeFinder.FindClassesWithAttribute<PublishedModelAttribute>().FirstOrDefault(x =>
                x.GetCustomAttribute<PublishedModelAttribute>(false).ContentTypeAlias == element.ContentType.Alias);
    
            // create instance of the models builder type based from the element
            var blockInstance = Activator.CreateInstance(blockType, element, _publishedValueFallback);
    
            // get a generic block list item type based on the models builder type
            var blockListItemType = typeof(BlockListItem<>).MakeGenericType(blockType);
    
            // create instance of the block list item
            // if you want to use settings this will need to be changed.
            var blockListItem = (BlockListItem)Activator.CreateInstance(blockListItemType, blockData.Udi, blockInstance, null, null);
    
            // render the partial view for the block.
            var partialName = $"/Views/Partials/blocklist/Components/{element.ContentType.Alias}.cshtml";
    
            var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
            viewData.Model = blockListItem;
    
            var actionContext = new ActionContext(this.HttpContext, new RouteData(), new ActionDescriptor());
    
            await using var sw = new StringWriter();
            var viewResult = _razorViewEngine.GetView(partialName, partialName, false);
    
            if (viewResult?.View != null)
            {
                var viewContext = new ViewContext(actionContext, viewResult.View, viewData, new TempDataDictionary(actionContext.HttpContext, _tempDataProvider), sw, new HtmlHelperOptions());
                await viewResult.View.RenderAsync(viewContext);
            }
    
            return sw.ToString();
        }
    

    This all works great. However, the model is returned without any Settings data. I wondered if anyone had any pointers as to how to append the settings data to the model too as I use that data to do some important stuff in the view. A lot of this stuff is over my head!

    Many thanks for any help.

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 01, 2022 @ 14:16
    Huw Reddick
    0

    what exactly do you need to do?

    The settings will be available in your view that this code renders, so it will display what your block looks like in the UI, clicking the view in the backoffice should still give you access to the settings.

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 01, 2022 @ 14:22
    Huw Reddick
    0

    Ah, I see what you mean, the preview gets a null settings :( I will take a look see if I can figure it out :)

  • Dan 1288 posts 3921 karma points c-trib
    Oct 01, 2022 @ 16:06
    Dan
    0

    Thanks Huw. I'm making progress, but hit a bit of a stopper.

    To me, it doesn't seem possible to pull all of the settings data together from only the 'data' object that's posted from Angular to the API. A BlockListItem is obviously composed of two doc-types, one for content and one for settings, so it would seem that both need to be passed to the API in order to reconstruct a 'fully hydrated' instance of a block (could be wrong but could not see how to get settings from only the content part). So what I've done is modified the Angular controller and resource such that they're now posting an object to the API which contains both content and settings data for the block - it just posts them both as part of a wrapped json object:

    $http.post(apiUrl + '?pageId=' + pageId, { content:data, settings:settings })
    

    Then in the API this new 'dual' object is split out into content and settings parts, each cast to a BlockItemData type:

    public async Task<IActionResult> PreviewMarkup([FromBody] JObject data, [FromQuery] int pageId = 0, [FromQuery] string culture = "")
    {
      BlockItemData content = data["content"].ToObject<BlockItemData>();
      BlockItemData settings = data["settings"].ToObject<BlockItemData>();
      etc...
    }
    

    Both content and settings objects are then passed as parameters into the GetMarkupForBlock method in the API controller. This works. Stepping through it I can see both parts populated correctly.

    It's then possible in the GetMarkupForBlock method to effectively duplicate all of the existing code to construct a settings version of all of the objects that are constructed. My code is slightly modified from the original but the updated version so far is:

    private async Task<string> GetMarkupForBlock(BlockItemData blockContent, BlockItemData blockSettings)
    {
    
      // convert the json data to a IPublishedElement (using the built-in conversion)
      var element = this._blockEditorConverter.ConvertToElement(blockContent, PropertyCacheLevel.None, true);
      var settingsElement = this._blockEditorConverter.ConvertToElement(blockSettings, PropertyCacheLevel.None, true);
    
      // get the models builder type based on content type alias
      var blockType = _typeFinder.FindClassesWithAttribute<PublishedModelAttribute>().FirstOrDefault(x =>
          x.GetCustomAttribute<PublishedModelAttribute>(false).ContentTypeAlias == element.ContentType.Alias);
    
      var settingsBlockType = _typeFinder.FindClassesWithAttribute<PublishedModelAttribute>().FirstOrDefault(x =>
          x.GetCustomAttribute<PublishedModelAttribute>(false).ContentTypeAlias == settingsElement.ContentType.Alias);
    
      // create instance of the models builder type based from the element
      var blockInstance = Activator.CreateInstance(blockType, element, _publishedValueFallback);
      var settingsBlockInstance = Activator.CreateInstance(settingsBlockType, settingsElement, _publishedValueFallback);
    
      // get a generic block list item type based on the models builder type
      var blockListItemType = typeof(BlockListItem<>).MakeGenericType(blockType);
      var settingsBlockListItemType = typeof(BlockListItem<>).MakeGenericType(settingsBlockType);
    
      // create instance of the block list item
      // if you want to use settings this will need to be changed.
      var blockListItem = (BlockListItem)Activator.CreateInstance(blockListItemType, blockContent.Udi, blockInstance, null, null);
    
      // render the partial view for the block.
      var partialName = $"/Views/Partials/blocklist/Modules/{element.ContentType.Alias}.cshtml";
    
      ...etc
    }
    

    So I've now got both content and settings versions of all of the different parts.

    The bit I'm struggling with is how then to bundle the content and settings things together into the Activator.CreateInstance method in order to construct the full BlockListItem which includes the settings. The signature on that method seems a little spurious to me - I'm not understanding how it works in order to know what to pass to it.

    Any ideas on this last part would be amazing!

  • Dan 1288 posts 3921 karma points c-trib
    Oct 01, 2022 @ 16:44
    Dan
    0

    Actually I can see what Activator.CreateInstance is doing now. Passing blockSettings.Udi and settingsBlockInstance to it as the last 2 parameters (rather than null and null) seems to work, but something is still awry as the settings object is still null once it hits the view. Will try not to make this a running commentary but will keep going!

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 01, 2022 @ 16:49
    Huw Reddick
    0

    Yes, that is the part I was looking at too :)

  • Dan 1288 posts 3921 karma points c-trib
    Oct 01, 2022 @ 17:54
    Dan
    101

    Figured it out. In the angular controller I was posting block.settings rather than block.settingsData. Sorry this is a bit piecemeal but as I say I've made quite a few modifications to the code along the way (added web components and a scaling directive to ensure desktop breakpoints are observed). The pertinent change in the block preview Angular controller is this:

            $scope.$watchGroup(['block.data', 'block.settingsData'], function (newValues, oldValues) {
                $timeout.cancel(timeoutPromise);
                timeoutPromise = $timeout(function () {
                    loadPreview(newValues[0], newValues[1]);
                }, 500);
            }, true);
    

    ...where $scope.watch has been modified to $scope.watchGroup to watch both data and settingsData. The loadPreview function has then been updated to post both things to the getPreview method of the preview resource:

            function loadPreview(blockData, blockSettings) {
                $scope.markup = $sce.trustAsHtml('Loading preview');
                $scope.loading = true;
    
                previewResource.getPreview(blockData, blockSettings, $scope.id, $scope.language).then(function (data) {
                    $scope.markup = $sce.trustAsHtml(data);
                    $scope.loading = false;
                });
            }
    
  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 02, 2022 @ 18:41
    Huw Reddick
    0

    Worked great :)

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 01, 2022 @ 18:06
    Huw Reddick
    0

    Well done, I will try this out myself tomorrow :)

  • Claushingebjerg 939 posts 2574 karma points
    Oct 11, 2022 @ 18:01
    Claushingebjerg
    0

    @Dan

    Could you possibly share your files?

    Im not really a .net developer, but would love to have preview of block list items. But i'm having a bit of trouble getting Paul Seals tutorial to run. So i would like to try your approach, but i'm getting errors, so the full files would help a bunch.

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 12, 2022 @ 07:40
    Huw Reddick
    0
  • Claushingebjerg 939 posts 2574 karma points
    Oct 12, 2022 @ 07:55
    Claushingebjerg
    0

    THANKS, but the c# controller link returns a 404 ;)

  • Dan 1288 posts 3921 karma points c-trib
    Oct 12, 2022 @ 08:14
    Dan
    0

    Hey Claus. I've modified the files quite a lot for my implementation, and my version is really only a proof-of-concept so there are a few hacky things in there that I wouldn't deem 'production ready' but I can try to pull something generic out this evening if Huw's links don't land. Is it the settings part you're getting stuck on specifically or just getting the initial previews working?

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 12, 2022 @ 08:23
    Huw Reddick
    1

    ah, sorry most likely because I forgot to change the file extension from .cs :)

    Try this one

  • Martin Griffiths 826 posts 1269 karma points c-trib
    Oct 12, 2022 @ 13:24
    Martin Griffiths
    0

    Uggh sooooo close!

    I've implemented all of the code as recommended in this thread. But now a new blocklist item will only appear in the list if I save or publish. I set the code back to not include settings the item will render immediately.

    Any ideas?

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 12, 2022 @ 14:27
    Huw Reddick
    0

    sorry no, will need to investigate

  • Martin Griffiths 826 posts 1269 karma points c-trib
    Oct 12, 2022 @ 14:45
    Martin Griffiths
    0

    Thanks Huw

    Would you like me to paste my code? To be fair it's just a merge of your examples and the original 24days demo.

    Martin.

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 12, 2022 @ 14:53
    Huw Reddick
    0

    I will check how mine behaves first and if it is different then yes we can do a compare, but will check mine first

  • Martin Griffiths 826 posts 1269 karma points c-trib
    Oct 12, 2022 @ 14:53
    Martin Griffiths
    0

    Huw you are a star!

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 12, 2022 @ 15:53
    Huw Reddick
    0

    Ok, I have worked out what the cause is, just need to figure out how to fix it :)

    basically the original code uses $watch which whenever you add a new control it fires the preview api function. The new code is using watchgroup so that it can also get the settings data, however this does not seem to call the preview api when you add a new item, trying to figure out why that is.

  • Dan 1288 posts 3921 karma points c-trib
    Oct 12, 2022 @ 16:12
    Dan
    0

    I've not been able to check, but assume that the version I have is also susceptible to the same issue. Not sure if you saw this: https://github.com/dawoe/24-days-block-list-article/issues/2 but it applies the settings a slightly different way (using separate Watches rather than a watch group). Might be worth updating to see if that resolves the issue.

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 12, 2022 @ 16:27
    Huw Reddick
    2

    I will take a look, however I have managed to get mine working using watchCollection on the block, that now fires the preview controller, so definitely looks like watchGroup is broken.

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 12, 2022 @ 16:30
    Huw Reddick
    1

    I will post my code when I'm done testing it

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 12, 2022 @ 18:37
    Huw Reddick
    0

    ok, these are my js files, the apicontroller cs file is still the same as the one in the link I posted earlier.

    preview.resource.js

        (function () {
      'use strict';
    
      function previewResource($http, umbRequestHelper) {
    
        var apiUrl = Umbraco.Sys.ServerVariables.TwentyFourDays.PreviewApi;
    
        var resource = {
          getPreview: getPreview
        };
    
        return resource;
    
        function getPreview(data, settings, pageId, culture) {
            var dataSet = {
                data : data,
                settings : settings
            };
          return umbRequestHelper.resourcePromise(
            $http.post(apiUrl + '?pageId=' + pageId + '&culture=' + culture, dataSet),
            'Failed getting preview markup'
          );
        };
      }
    
        angular.module('umbraco.resources').factory('TwentyFourDays.Resources.PreviewResource', previewResource);
    
    })();
    

    block-preview.controller.js

    angular.module('umbraco').controller('TwentyFourDays.Controllers.BlockPreviewController',
        ['$scope', '$sce', '$timeout', 'editorState', 'TwentyFourDays.Resources.PreviewResource',
        function ($scope,$sce, $timeout, editorState, previewResource) {
          $scope.language = editorState.getCurrent().variants.find(function (v) {
            return v.active;
          }).language.culture;
    
          $scope.id = editorState.getCurrent().id;
            $scope.loading = true;
            $scope.markup = $sce.trustAsHtml('Loading preview');
    
    
          function loadPreview(blockData) {
              $scope.markup = $sce.trustAsHtml('Loading preview');
              $scope.loading = true;
              previewResource.getPreview(blockData.data, blockData.settingsData, $scope.id, $scope.language).then(function (data) {
                  $scope.markup = $sce.trustAsHtml(data);
                  $scope.loading = false;
              });
          }
          var timeoutPromise;
    
          $scope.$watchCollection('block', function (newValue, oldValue) {
              $timeout.cancel(timeoutPromise);
              timeoutPromise = $timeout(function () {
                  loadPreview(newValue);
              }, 500);
          }, true);
    
        }
    ]);
    
  • Martin Griffiths 826 posts 1269 karma points c-trib
    Oct 13, 2022 @ 08:30
    Martin Griffiths
    0

    Hi Huw

    Awwwwwesssome! That did the trick! Thank you so much! :-)

    A quick question, I might be a bit of numpty on this one. I cannot get the angular controller to work without allowing language to pass as a null ?

    enter image description here

    enter image description here

    I will also use this when I get around to trying out the new block grid editor in 10.3. Quite why this isn't baked into the codebase, I find a little crazy! I mean who really wants to have to write extra angular.js views when you can re-use what you already have!

    Can I buy you a coffee?

    Thanks again Martin

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 13, 2022 @ 08:37
    Huw Reddick
    0

    A quick question, I might be a bit of numpty on this one. I cannot get the angular controller to work without allowing language to pass as a null ?

    Not sure TBH, seems OK for me, do you have multiple languages defined or just a single language?

  • Martin Griffiths 826 posts 1269 karma points c-trib
    Oct 13, 2022 @ 09:06
    Martin Griffiths
    0

    Yes just en-gb so it may be that. It's no biggie.

    Hey last question and i'll stop bugging you! Paul Seals demos on YouTube offer the ability to remove blocks from flow using a Hide toggle switch. The twenty four days code has a filter to set the item with less opacity to visually show its "disabled". This was working in the backoffice without the view taking the item completely out of the flow, but now it's removing it! One step forward.....lol

    The code in the view is simply...

    if (blockSettings?.Hide ?? false) { return; }
    

    And in the directive...

    function setStyle(scope, element) {
      if (scope.settings.hide === '1') {
        element[0].style.opacity = 0.25;
        return;
      }
    

    It's pretty clear the return is preventing the view from rendering, so i'm a little ensure as to how it could've work before...but it did!

    (EDIT: It worked before because Pauls code omitted settings - which I now have! lol. My first idea is I could use CSS class names and then use the CleanMarkUp method to strip the "hide" classname from the backoffice version. The downside is i'm passing HTML to the front I don't need.

    I suppose I may need to come up with something that differentiates between the front and back, but that kinda defeats the point of re-use.

    I may just take the "hide" option out altogether. Incidentally, the nested contently package by Nathan Wolfe does the same thing by providing a UI toggle and keying on umbracoNaviHide and IsVisible().

    Any thoughts?

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 13, 2022 @ 09:44
    Huw Reddick
    0

    I never actually implemented hide stuff so not sure. having a nightmare today, windows update has killed IISExpress :( so trying to sort that out currently :D

  • Martin Griffiths 826 posts 1269 karma points c-trib
    Oct 13, 2022 @ 09:49
    Martin Griffiths
    0

    Uggh! Sorry to hear!

    Yea i'm probably overthinking the use case for content editors. We didn't have the function in nested content on our Umbraco 7 project and nobody ever complained!

    Thanks again for your help.

    M.

  • Martin Griffiths 826 posts 1269 karma points c-trib
    Oct 13, 2022 @ 10:32
    Martin Griffiths
    0

    Hey Huw.

    I hope you've solved your IISExpress issue!

    I solved the problem pretty quickly by removing "hide" from the block model in the API controller.

    if (blockSettings != null)
                {
                    blockSettings.RawPropertyValues.Remove("hide");
    ...
    

    I've really struggled getting a good dev. env. working in Umbraco 10. Previously I used a taskrunner in Umbraco 7 which combined:

    1. Browsersync for CSS/JS code injection
    2. Bundling/Minifying
    3. SASS/BEM compiling

    This made writing front end code a doddle.

    Morroring this in Umbraco 10 while still being able to use dotnet watch run for all the CS/CSTML side of things took me flipping ages to iron out. The big issue for me was getting a local certificate working in Browsersync to proxy the kestrel server. I got there in the end and can now inject my SASS/CSS changes as well as getting recompilation of my views/cs.

    This final step of getting a decent preview of my block modules will completely transform the way content editors use Umbraco. I've done a couple of demos and have had great feedback.

    All the best Martin

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Oct 13, 2022 @ 10:57
    Huw Reddick
    0

    seems it isn't IISExpress, it has broken dotnet 6 too :( currently even dotnet run crashes, thanks Microsoft!! older .net frameworks run in IISExpress no problem. Guess I won't be doing much coding today :D :D

Please Sign in or register to post replies

Write your reply to:

Draft