Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular-UI Router: Nested Views Not Working

Building a multi-step form (“wizard”). Was originally following this tutorial, which worked great, but am now trying to adapt it so step one is embedded on the homepage rather than being a separate state. No matter what I try, I can not create a ui-sref path that will work. I always get:

Could not resolve '.where' from state 'home'

or

Could not resolve 'wizard.where' from state 'home'

or

Could not resolve 'wizard.where@' from state 'home'

…even though wizard.where@ works fine in <div ui-view="wizard.where@"></div>. What is the correct syntax?

Here are the relevant files:

home.js (left comments intact so you can see various methods I’m trying):

var wizard = {
  url: '/home/wizard',
  controller: 'VendorsCtrl',
  templateUrl: 'vendors/wizard.tpl.html'
};

angular.module( 'myApp.home', [
  'ui.router',
  'ui.bootstrap',
  'myApp.modal',
  'angularMoment'
])

.config(function config( $stateProvider, $urlRouterProvider ) {
  $stateProvider
    .state( 'home', {
      url: '/home',
      views: {
        "main": {
          controller: 'HomeCtrl',
          templateUrl: 'home/home.tpl.html'
        },
        "jumbotron": {
          controller: 'HomeCtrl',
          templateUrl: 'home/welcome.tpl.html'
        },
        "wizard": wizard,
        "wizard.where": {
          url: '/home/wizard/where',
          controller: 'VendorsCtrl',
          templateUrl: 'vendors/wizard-where.tpl.html',
          parent: wizard
        },
        "wizard.what": {
          url: '/home/wizard/what',
          controller: 'VendorsCtrl',
          templateUrl: 'vendors/wizard-what.tpl.html',
          parent: wizard
        },
        "wizard.when": {
          url: '/home/wizard/when',
          controller: 'VendorsCtrl',
          templateUrl: 'vendors/wizard-when.tpl.html',
          parent: wizard
        },
      },
      data: { pageTitle: 'Home' }
    })

    // route to show our basic form (/wizard)
    // .state('wizard', {
    //   url: '/wizard',
    //   views: {
    //     "main": {
    //       controller: 'VendorsCtrl',
    //       templateUrl: 'vendors/wizard.tpl.html'
    //     }
    //   },
    //   abstract: true,
    //   //data: { pageTitle: 'Vendor Search' }
    // })

    // nested states 
    // each of these sections will have their own view
    // url will be nested (/wizard/where)
    // .state('wizard.where', {
    //   url: '/where',
    //   templateUrl: 'vendors/wizard-where.tpl.html'
    // })

    // url will be /wizard/when
    // .state('wizard.when', {
    //   url: '/when',
    //   templateUrl: 'vendors/wizard-when.tpl.html'
    // })

    // url will be /wizard/vendor-types
    // .state('wizard.what', {
    //   url: '/what',
    //   templateUrl: 'vendors/wizard-what.tpl.html'
    // })
    ;

    // catch all route
    // send users to the form page 
    $urlRouterProvider.otherwise('/home/wizard/where');
})

wizard.tpl.html:

<div class="jumbotron vendate-wizard" ng-controller="VendorsCtrl as vendorsCtrl">
  <header class="page-title">
    <h1>{{ pageTitle }}</h1>
    <p>Answer the following three questions to search available vendors. All answers can be changed later.</p>

    <!-- the links to our nested states using relative paths -->
    <!-- add the active class if the state matches our ui-sref -->
    <div id="status-buttons" class="text-center">
      <a ui-sref-active="active" ui-sref="wizard.where@"><span>1</span> Where</a>
      <a ui-sref-active="active" ui-sref="wizard.what@"><span>2</span> What</a>
      <a ui-sref-active="active" ui-sref="wizard.when@"><span>3</span> When</a>
    </div>
  </header>

  <!-- use ng-submit to catch the form submission and use our Angular function -->
  <form id="signup-form" ng-submit="processForm()">

    <!-- our nested state views will be injected here -->
    <div id="form-views" ui-view="wizard.where@"></div>
  </form>
</div>

wizard.where.tpl.html:

<div class="form-group">
  <label class="h2" for="where">Where Is Your Wedding?</label>
  <p id="vendor-where-description">If left blank, vendors in all available locations will be shown.</p>
  <div class="input-group-lg">
    <input id="where" ng-model="formData.where" class="form-control" type="text" placeholder="Boston, MA" aria-describedby="vendor-where-description" />
  </div>
</div>

<ul class="list-inline">
  <li>
    <a ui-sref="wizard.what@" class="btn btn-block btn-primary">
      Next <span class="fa fa-arrow-right"></span>
    </a>
  </li>
</ul>
like image 858
Hugh Guiney Avatar asked Apr 12 '15 01:04

Hugh Guiney


2 Answers

I created working plunker here

NOTE: You should read about state nesting and named views more. Because the current state and view definition is simply wrong.

  • Nested States & Nested Views
  • Multiple Named Views

Firstly, we should not use the ONE state definition with many views: {}. But we should split them into real states. Hierarchy will have three levels

The first level - super root state

.state( 'home', {
  url: '/home',
  views: {
    "main": {
      controller: 'HomeCtrl',
      templateUrl: 'home/home.tpl.html'
    },
  }
})

The second level - wizzard, check that now we change the url. We will inherit its first part from our parent (home)

.state("wizard", {
  parent: 'home',
  //url: '/home/wizard',
  url: '/wizard',
  controller: 'VendorsCtrl',
  templateUrl: 'vendors/wizard.tpl.html'
})

The third level - all where, what, when now will also inherit url. They do not have to define parent, because it is part of their names

.state( "wizard.where",  {
      //url: '/home/wizard/where',
      url: '/where',
      controller: 'VendorsCtrl',
      templateUrl: 'vendors/wizard-where.tpl.html',
      //parent: wizard
})
.state( "wizard.what",  {
      //url: '/home/wizard/what',
      url: '/what',
      controller: 'VendorsCtrl',
      templateUrl: 'vendors/wizard-what.tpl.html',
      //parent: wizard
})
.state( "wizard.when",  {
      //url: '/home/wizard/when',
      url: '/when',
      controller: 'VendorsCtrl',
      templateUrl: 'vendors/wizard-when.tpl.html',
      //parent: wizard
})

Parent wizzard must now contain unnamed view target ui-view=""

<div ui-view=""></div>

Current wizard.tpl.html contains this:

<!-- our nested state views will be injected here -->
<div id="form-views" ui-view="wizard.where@"></div>

The sign @ should be avoided, because it could be used for absulte view naming - BUT inside of the state defintion. So, what could work is ui-view="someName

<!-- our nested state views will be injected here -->
<div id="form-views" ui-view="someName"></div>

Now, these are (in example here) view content of the home.tpl

<div>
  <h1>HOME</h1>

  <div ui-view=""></div>
</div>

And wizzard.tpl

<div>
  <h2>WIZZARD</h2>

  <div ui-view=""></div>
</div>

So, we have unnamed view target inside of home and wizard states, That is very handy, because we can use the light state definition, without views : {} object. And that is always preferred in case we do not have multi-views.

That means, that this state definition will properly be injected into above template:

// no views - search in parent for a ui-view=""
...
.state( "wizard.when",  {
      url: '/when',
      controller: 'VendorsCtrl',
      templateUrl: 'vendors/wizard-when.tpl.html',
})
...

Check the doc:

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@': { }
    }
})

Notice that the view names are now specified as absolute names, as opposed to the relative name. It is targeting the 'filters', 'tabledata', and 'graph' views located in the root unnamed template. Since it's unnamed, there is nothing following the '@'. The root unnamed template is your index.html.

Calling the state from state

Whe we want in where state navigate to when, we can use directiv ui-sref, but it must contain state name, not view naming convention

// instead of this
<a ui-sref="wizard.what@"
we need this
<a ui-sref="wizard.what"

The reason, that in this three level hierarchy we do use only parent and child names (not grand parent 'home'), is hidden in state definition. Because we used this:

.state("wizard", {
  parent: 'home',

Parent is just a parent, not part of the state name. Which is good in scenarios like this (we need the root/grand parent to establish some comon stuff, but it name is not needed for substates)

Check the doc:

ui-sref

A directive that binds a link (<a> tag) to a state. If the state has an associated URL, the directive will automatically generate & update the href attribute via the $state.href() method. Clicking the link will trigger a state transition with optional parameters.
...

You can specify options to pass to $state.go() using the ui-sref-opts attribute. Options are restricted to location, inherit, and reload.

ui-sref - string - 'stateName' can be any valid absolute or relative state

like image 174
Radim Köhler Avatar answered Oct 20 '22 00:10

Radim Köhler


[S]tep one is embedded on the homepage rather than being a separate state

You should treat each ui-view as a state, but declare wizard.where as the default/index state.

Note that the tutorial uses $urlRouterProvider to make form/profile the default state.

// catch all route
// send users to the form page 
$urlRouterProvider.otherwise('/form/profile');

In this manner, however, /form will end up as /form/profile.

You may, however, create an empty URL state with minor modification:

// route to show our basic form (/form)
.state('form', {
    url: '/form',
    templateUrl: 'form.html',
    controller: 'formController',
    abstract: true //<-- Declare parent as an abstract state. 
})

// nested states 
// each of these sections will have their own view
// url will be nested (/form)
.state('form.profile', {
    url: '', //<-- Empty string for "profile" state to override the /form abstract state
    templateUrl: 'form-profile.html'
})

// catch all route
// send users to the form page
$urlRouterProvider.otherwise('/form'); //<-- Default state is empty

@radim-köhler has also provided great insight into UI-Router and state definitions.

like image 22
SirTophamHatt Avatar answered Oct 20 '22 01:10

SirTophamHatt