Search code examples
javascriptangular-ui-routerstatecomposite-view

Ui Router: composite view render view once


What I want to achieve is have 2 sibling views, where 1 is run-once and the other will get updated.

I have a template like so:

<body>
    <ui-view="header />
    <ui-view="body" />
</body>

And I have 1 catch-all state:

.state('root', {
    params: { … },
    resolve: {
        data: fn() { … }
    },
    url: '/{p1:string}/{p2:string}/{p3:string}',
    views: {
        header: {
            controller: 'HeaderCtrl as Header',
            templateUrl: '…'
        },
        body: {
            controllerProvider: fn() { … },
            templateUrl: fn() { … }
        }
    }
});

The catch-all state dynamically choses the appropriate controller, data, and template. Everything is hunky-dory except that the HeaderCtrl gets re-executed on every state change. I want it to get executed only once.

I tried creating 2 states, root and base, where the above becomes root.base. I moved the header view to the new root state like so:

.state('root', {
    abstract: true,
    url: '/',
    views: {
        header: {
            controller: 'HeaderCtrl as Header',
            templateUrl: '…'
        }
    }
})
.state('root.base', {
    params: { … },
    resolve: {
        data: fn() { … }
    },
    url: '/{p1:string}/{p2:string}/{p3:string}',
    views: {
        body: {
            controllerProvider: fn() { … },
            templateUrl: fn() { … }
        }
    }
});

But then only the parent (root) state engages.


Solution

  • I created working plunker here. You are almost there. This is the updated state def:

    $stateProvider
      .state('root', {
        //abstract: true,
        //url: '/',
        views: {
            header: {
                controller: 'HeaderCtrl as Header',
                templateUrl: 'tpl.header.html',
            },
        }
    })
    .state('root.base', {
        params: { pl : null, p2: "", p3: "abc" },
        resolve: {
            data: function () { return  1 }
        },
        url: '/{p1:string}/{p2:string}/{p3:string}',
        views: {
            'body@': {
                controller: "MyCtrl",
                templateUrl: 'tpl.body.html',
            }
        }
    });
    

    Firstly, very important, we have to use absolute naming for child state view name 'body@'. See:

    View Names - Relative vs. Absolute Names

    Behind the scenes, every view gets assigned an absolute name that follows a scheme of viewname@statename, where viewname is the name used in the view directive and state name is the state's absolute name, e.g. contact.item. You can also choose to write your view names in the absolute syntax.

    For example, the previous example could also be written as:

    .state('report',{
        views: {
          'filters@': { },
          'tabledata@': { },
          'graph@': { }
        }
    })
    

    Secondly, no url at the parent is specified... that will make it not accessible by url.

    Also, abstract is not needed... could be ... but if we would like to use .when() it is better to make it non abstract. Check these:

    Working plunker with the above stuff is here