Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating Block List Element on Client Side #6746

Open
zharonar opened this issue Dec 12, 2024 · 4 comments
Open

Creating Block List Element on Client Side #6746

zharonar opened this issue Dec 12, 2024 · 4 comments

Comments

@zharonar
Copy link

What type of issue is it? (Choose one - delete the others)

Documentation not clear, code issue

What article/section is this about?

https://docs.umbraco.com/umbraco-cms/13.latest/extending/property-editors/build-a-block-editor

Describe the issue

We're trying to achieve creation of Block List Element programmatically, before document is even saved into the DB, because we need to call our API to assemble some data that we can pass into the Block List Element to structure it.

The issue we're occuring is with these lines of code, to try to create block entry, from the documentation sample.

// We must get a scope that exists in all the lifetime of this data. Across variants and split-view.
var scopeOfExistence = $scope;
// Setup your component to require umbVariantContentEditors and vm.umbElementEditorContent. If one of them is avaiable use the method getScope to retrieve a shared scope for multiple editors of this content.
if(vm.umbVariantContentEditors && vm.umbVariantContentEditors.getScope) {
    scopeOfExistence = vm.umbVariantContentEditors.getScope();
} else if(vm.umbElementEditorContent && vm.umbElementEditorContent.getScope) {
    scopeOfExistence = vm.umbElementEditorContent.getScope();
}
// Define variables for layout and modelObject as you will be using these throughout your property editor.
vm.layout;
var modelObject;

// When we are ready we can instantiate the Model Object and load any dependencies.
vm.$onInit = function() {
  modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence);
  modelObject.load().then(onLoaded);
}
function onLoaded() {
  // Define the default layout, this is used when there is no data stored for this property.
  var defaultLayout = [];
  // We store a reference to layout as we have to maintain this.
  // The getLayout method gives us a reference to the layout-object based on the property-editor alias. The defaultLayout will be initialized if it does not exist.
  vm.layout = modelObject.getLayout(defaultLayout);
}

It is not explained where vm comes from, where and what is umbVariantContentEditors. We managed to achieve our model by getting it from this structure that we have in our angularJS controller.

Code sample:

angular.module('umbraco').controller('aiFaqService', function ($scope, editorState, blockEditorService) {
    console.log('scope: ', $scope);
    console.log('editor state: ', editorState.current);

    if (!$scope.vm) {
        $scope.vm = {};
    }
    
    **vm.model = editorState.current.variants[0].tabs[0].properties[0];**
    // Define variables for layout and modelObject as you will be using these throughout your property editor.
    vm.layout;
    var modelObject;
    // Check for shared scope via umbVariantContentEditors
    let umbVariantContentEditors = null;
    let scopeOfExistence = $scope; // Fallback to $scope if umbVariantContentEditors is not available
    
    if ($injector.has('umbVariantContentEditors')) {
        umbVariantContentEditors = $injector.get('umbVariantContentEditors');
        if (umbVariantContentEditors && umbVariantContentEditors.getScope) {
            scopeOfExistence = umbVariantContentEditors.getScope();
        }
    }
    
    // Ensure vm.model is fully defined
    if (!vm.model || !vm.model.value || !vm.model.editor || !vm.model.config || !vm.model.config.blocks) {
        console.error('Invalid vm.model:', vm.model);
        return;
    }
    
    // When we are ready we can instantiate the Model Object and load any dependencies.
    vm.$onInit = function () {
        console.log('vm onInit');
        modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence);
        modelObject.load().then(onLoaded);
    }
    function onLoaded() {
        console.log('onLoaded');
        // Define the default layout, this is used when there is no data stored for this property.
        var defaultLayout = [];
        // We store a reference to layout as we have to maintain this.
        // The getLayout method gives us a reference to the layout-object based on the property-editor alias. The defaultLayout will be initialized if it does not exist.
        vm.layout = modelObject.getLayout(defaultLayout);
        console.log('vm.layout: ', vm.layout);
    }

//..... the rest of the code

}

One of the issues is that we cannot have scopeOfExistence the way it is described in the documentation, since we do not know in our Controller context what scope we need.

Therefore it is just injected $scope in our case...

Another issue now occurs when we try to call createModelObject, as we mentioned, the data should fit the method, except we noticed propertyEditorScope is missing in this sample, and it is defined in method that we found at the following link:

https://apidocs.umbraco.com/v13/ui/#/api/umbraco.services.blockEditorService

Calling modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence); throws exception at the following line somewhere down the code:

angular.js:15697 
 TypeError: Cannot read properties of undefined (reading '$on')
    at new BlockEditorModelObject (umbraco.services.min…0350000000:1:159325)
    at Object.createModelObject (umbraco.services.min…0350000000:1:154102)
    at vm.$onInit (aiFaqService.js?d=63…6110350000000:31:42)
    at angular.js:10653:15
    at r (angular.js:396:9)
    at p (angular.js:10642:9)
    at g (angular.js:9942:13)
    at angular.js:9807:30
    at Object.link (angular.js:29993:9)
    at angular.js:1391:15

Summary:
Could you help us reach our solution with creating the block element via Client?

@sofietoft
Copy link
Contributor

sofietoft commented Dec 12, 2024

Thanks for the detailed report @zharonar !
I'm sorry to hear that you're running into some issues when building this solution.

The article you're referencing is currently being reviewed, as we've previously received reports that the guide outlined in the article no longer fits the current version of Umbraco 13.
Also, helping you resolve your custom solution, is a bit outside of the scope of what we can help with here on the docs platform - for this, I'd suggest you get in touch on our community Forum or Discord.

I'll do what I can to get the review of this article expedited.
The code snippet you're referencing as the first bit - is that were they main issue occurs? Just want to confirm.

@zharonar
Copy link
Author

@sofietoft Thanks for the swift response. The issue occurs in the both code snippets, first is the copy paste from the code sample from the documentation, second snippet is our use of the code sample. The issue occurs in this line of code: modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence);

@zharonar
Copy link
Author

@sofietoft

We managed to get this stuff working for our use case. We fiddled with the implementation and reverse engineered the BlockListController functions, some public, some internal.

NOTE: Implementation is inside of umbraco.directives.min.js, what we could see publically in "devtools -> source" since there is no source code (we tried to find it).

NOTE: This is our constructor:

angular.module('umbraco').controller('aiFaqService', function ($scope, editorState, blockEditorService, editorService) {...

First we figured out what scopeOfExistence is. We got it initially by doing the following lines of code:

let scopeOfExistence = angular.element(document.querySelector('[data-element="your-block-list-alias"] umb-block-list-property-editor')).scope();

vm.umbVariantContentEditors && vm.umbVariantContentEditors.getScope ? scopeOfExistence = vm.umbVariantContentEditors.getScope() : vm.umbElementEditorContent && vm.umbElementEditorContent.getScope && (scopeOfExistence = vm.umbElementEditorContent.getScope());

Then we got our modelObject by calling blockEditorService.createModelObject:
(modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence, propertyFaqScope)).load().then(onLoaded)

Afterwards as stated above, we got the hand of the internal functions and copied them into our controller:

addNewBlock, getBlockObject, updateBlockObject, ensureCultureData

These functions weren't inside of the blockEditorApi, sample code:

vm.blockEditorApi = {
                    activateBlock: activateBlock,
                    editBlock: editBlock,
                    copyBlock: copyBlock,
                    requestDeleteBlock: requestDeleteBlock,
                    deleteBlock: deleteBlock,
                    openSettingsForBlock: function openSettingsForBlock(block, blockIndex, parentForm) {
                        editBlock(block, !0, blockIndex, parentForm)
                    },
                    readonly: vm.readonly,
                    singleBlockMode: vm.singleBlockMode
                },

Now that we had our PoC code, we were ready to add the values:

function onLoaded() {
    let length = vm.model.value.layout["Umbraco.BlockList"].length;
    let layoutEntry = addNewBlock(length, "acf50bbb-172c-4fc7-8772-a7324b374b00");
    let block = modelObject.getBlockObject(layoutEntry);

    block.data.question = `Question ${length}`;
    block.data.answer = `Answer ${length}`;
    block.content.variants[0].tabs[0].properties[0].value = `Question ${length}`;
    block.content.variants[0].tabs[0].properties[1].value = `Answer ${length}`;
}

We had a hard time figuring out, why we got the block elements visible, but without data even when we added it inside of
block.data.question = `Question ${length}`; and block.data.answer = `Answer ${length}`;, seems this is required only for saving it to the database. When we pressed Save or Save and Publish, the block elements refreshed and we could see the data.

To make it visible without saving first, block.content.variants[0].tabs[0].properties[0].value = `Question ${length}`; and block.content.variants[0].tabs[0].properties[1].value = `Answer ${length}`; makes the data visible in DOM.

I think the solution would be to make the rest of the internal functions inside of the blockEditorApi, because there are much of the stuff being internally set that makes the block list visible in the dom and of course making it work properly.

@sofietoft
Copy link
Contributor

So happy to hear that you managed to find a solution here @zharonar ! 👏 😄

And thanks a lot for all the details - I'll make sure these are forwarded to our developers when they take up this task 💪
Unfortunately, it won't be until next year - but I'll make sure to put some pressure on them then 😁

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants