Problems with overlay controller/model binding in v13

I’m struggling to overcome issues in my custom view because my custom controller only seems to partially be working. I am implementing a custom back office search feature and intercepting clicks on the search button in the UI and opening my own overlay. The overlay opens my custom view as intended. using the following code:

const overlay = {
    title: "Global Search",
    view: "/App_Plugins/Example/backoffice/GlobalSearch/GlobalSearchOverlay.html",
    size: "medium",
    submit: function(model) {
        console.log("Example Global Search Override: Submit called", model);
    },
    close: function() {
        console.log("Example Global Search Override: Close called");
    }
};

editorService.open(overlay);

A simplified version of my view looks like this:

<div class="global-search" ng-controller="Example.GlobalSearch.controller as vm">

	<!-- Debug Info  -->
	<div class="debug-panel">
		<p><strong>Debug Info:</strong></p>
		<p>Controller Initialized: {{ vm.isInitialized }}</p>
		<p>Debug: {{ vm.isDebug }}</p>
		<p>Filter Types Count: {{ vm.typeFilters.length }}</p>
		<p>Selected Filters: {{ vm.getSelectedFilterCount() }}</p>
		<p>Current Term: "{{ vm.term }}"</p>
		<p>Results Count: {{ vm.results.length }}</p>
		<p>Is Loading: {{ vm.isLoading }}</p>
	</div>

	<div class="search-container">
		<div class="search-input-wrapper">
			<input type="text"
				   ng-model="vm.term"
				   placeholder="Search content or media..."
				   ng-change="vm.onSearch()"
				   class="umb-textstring"
				   autofocus/>
			<button
					class="btn btn-primary"
					ng-click="vm.triggerSearch()"
					ng-disabled="!vm.term && vm.getSelectedFilterCount() === 0">
				Search
			</button>
		</div>
	</div>
</div>

And a simplified version of my controller in typescript before compilation looks like this:

angular.module("umbraco").controller("Example.GlobalSearch.controller", [
    "$http",
    "$scope",
    "$timeout",
    "editorService",
    function ($http: any, $scope: any, $timeout: any, editorService: any) {
        console.log('Global Search Controller: Initializing...');

        console.log($scope);

        const vm = this;

        console.log("vm at startup:", vm);

        // Initialize properties
        vm.isDebug = true;
        vm.term = "";
        vm.results = [];
        vm.typeFilters = [];
        vm.isLoading = false;
        vm.hasSearched = false;
        vm.isInitialized = false;

        /**
         * Get selected filter count
         */
        vm.getSelectedFilterCount = function(): number {
            try {
                if (!vm.typeFilters || !Array.isArray(vm.typeFilters)) {
                    return 0;
                }
                return vm.typeFilters.filter((t: any) => t && t.selected === true).length;
            } catch (e) {
                console.error('Error in getSelectedFilterCount:', e);
                return 0;
            }
        };


        vm.onSearch = function (): void {
            console.log('Global Search: onSearch called with term:', vm.term);
        };


        vm.onFilterChange = function(): void {
            console.log('Global Search: Filter changed');
            if (searchTimeout) {
                $timeout.cancel(searchTimeout);
            }
            vm.performSearch();
        };

        vm.clearAllFilters = function(): void {   };

        vm.performSearch = function (): void {
            console.log('Global Search: performSearch called');
        };

        vm.triggerSearch = function(): void {
            console.log('Global Search: Manual search triggered');
        };


        vm.close = function (): void {
            console.log('Global Search: Closing overlay');
        };

        vm.openItem = function (item: any): void {
            console.log('Global Search: Opening item:', item);
        };

        vm.loadFilterTypes = function (): void {
            console.log('Global Search: Loading filter types...');
        };

        // Initialize immediately
        console.log('Global Search: Starting initialization...');
        
        vm.loadFilterTypes();

        console.log('Global Search Controller: Setup complete');
    }
]);

None of my console logging occurs in my controller, only a few select properties such as the following

  1. getEditUrl: ƒ (s)
  2. isLoading: false
  3. onSearch: ƒ ()
  4. performSearch: ƒ ()
  5. results:
  6. term: “”
  7. typeFilters:

…but the rest are missing. Something seems to be fundamentally wrong with the controller binding and I think I have been looking at it for too long now to try and spot any obvious mistakes.

Can anyone suggest what I am missing or doing wrong please?

Thanks

This is driving me nuts - I can’t find any specifics on how the scoping of this sort of controller might work and I’m seeing really weird behaviour.

None of my console logging appears from my controller but my typing a search query produces search results, clicking a filter does nothing but clicking a filter then amending the search query runs the search using the checked filter.

You said: this what is looks like before compiling. Doesn’t your typescript compiler just strip the console.logs?

@LuukPeters No, all of my other console logging works fine. There is some weirdness around the scoping of overlay views.

Ok I just put this post into ChatGPT because I have no idea and it says the following you can try:

It looks like your overlay view is loading correctly, but the controller isn’t being instantiated — that’s why you only see a few inherited properties (like onSearch, term, etc.) but none of your custom console logs or variables.

The core issue is that Umbraco’s editorService.open() expects you to explicitly provide a controller when you open an overlay. In earlier versions, the system would automatically bind an ng-controller inside your view, but in v13, this behavior has changed: overlay controllers are now attached via the configuration object, not by adding ng-controller in the HTML.

Might be worth a try. Although I didn’t find any official breaking change documentation, so it might be hallucinating.

I’ve tried both ChatGPT and Claude and both end up sending me around in circles with neither coming up with a working solution. As mentioned, it partially works but in an odd way. Using developer tools the model is incomplete but some of it is reactive so it has found and loaded my controller.

Also, if you look at the Umbraco source code they still seem to have the ng-controller binding in the view. I’ve tried without and it definitely doesn’t work.