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

uiBreadcrumbs: Abstract parents #11

Closed
enkodellc opened this issue May 9, 2014 · 12 comments
Closed

uiBreadcrumbs: Abstract parents #11

enkodellc opened this issue May 9, 2014 · 12 comments

Comments

@enkodellc
Copy link

I am running into an issue with the breadcrumbs when the parent states are abstract. Below is an example using the mean.io boilerplate. I updated the routes to use your breadcrumb directive. The issue I run into is that the "Articles" breadcrumb is an abstract state and therefore be used as a route. I get this error: "Cannot transition to abstract state 'article'" What do you suggest for a workaround? I would think that more devs besides me would want to use abstract states to build the breadcrumbs... Thanks.

// states for my app
        $stateProvider
            .state('articles', {
                url: '',
                abstract: true,
                template: '<ui-view/>',
                resolve: {
                    loggedin: checkLoggedin
                },
                data: {
                    displayName: 'Articles'
                }
            })
            .state('articles.all articles', {
                url: '/articles',
                templateUrl: 'articles/views/list.html',
                data: {
                    displayName: false
                }
            })
            .state('articles.create article', {
                url: '/articles/create',
                templateUrl: 'articles/views/create.html',
                data: {
                    displayName: 'New Article'
                }
            })
            .state('articles.edit article', {
                url: '/articles/:articleId/edit',
                templateUrl: 'articles/views/edit.html',
                data: {
                    displayName: '{{ article.title }}'
                }
            })
            .state('articles.article by id', {
                url: '/articles/:articleId',
                templateUrl: 'articles/views/view.html',
                data: {
                    displayName: '{{ article.title }}'
                }
            });
@enkodellc
Copy link
Author

I did come up with a workaround... but it will only work for my situation since I plan on using my abstract states for each module in a similar fashion.

                    if (displayName !== false) {                       
                        var myRoute = currentState.abstract ? currentState.name + '.all ' + currentState.name : currentState.name;

                        breadcrumbs.push({
                            displayName: displayName,
                            route: myRoute
                        });
                    }

@michaelbromley
Copy link
Owner

Hi,

Good point - I didn't account for abstract states. Thanks for posting your work around, and I'll think about how a general solution could be implemented. If you come up with anything more general, feel free to post it or even do a pull request.

@michaelbromley michaelbromley changed the title Abstract parents uiBreadcrumbs: Abstract parents May 10, 2014
@michaelbromley
Copy link
Owner

Idea: Your solution involves re-assigning any breadcrumb of an abstract state to an alternative non-abstract (concrete?) state. In your case, you have a particular convention that makes it work out for you.

More generally, we could adapt that concept but make the redirection user-definable in the $state object. Something along the lines of this:

$stateProvider
            .state('articles', {
                url: '',
                abstract: true,
                template: '<ui-view/>',
                resolve: {
                    loggedin: checkLoggedin
                },
                data: {
                    breadcrumbsProxy: 'articles.all'
                }
            })
            .state('articles.all articles', {
                url: '/articles',
                templateUrl: 'articles/views/list.html',
                data: {
                    displayName: 'Articles'
                }
            })

So we put a user-defined property called (for example, off the top of my head) breadcrumbsProxy, which tells the directive which state to substitute for the abstract one. The name of the property could be specified as an attribute, much in the same way as is currently done with the displayname-property.

When I get a bit of time I'll try to implement something along those lines.

@enkodellc
Copy link
Author

Michael that sounds like a great plan. I might have some time today to try and implement and test it with my project.

@enkodellc
Copy link
Author

Micheel, I am new to Angular and am having difficulty understanding how the code is retrieving the displayname property from the data object. I am trying to replicate that functionality for the breadcrumbProxy but am failing miserably. Can you point me in the right direction on the methodology or angularjs functionality to obtain the displayname from the data object in ui-router.

@michaelbromley
Copy link
Owner

Reading over the code myself, I even had to take a minute to figure out what was going on, so don't feel bad about not getting it!

Here's an explanation of what's going on:

https://github.com/michaelbromley/angularUtils/blob/master/src/directives/uiBreadcrumbs/uiBreadcrumbs.js#L53-L82

function getDisplayName(currentState) {
    var i;
    var propertyReference;
    var propertyArray;
    var displayName;

    if (!scope.displaynameProperty) {
        // if the displayname-property attribute was not specified, default to the state's name
        return currentState.name;
    }
    propertyArray = scope.displaynameProperty.split('.');
    propertyReference = currentState;

    for (i = 0; i < propertyArray.length; i ++) {
        if (angular.isDefined(propertyReference[propertyArray[i]])) {
            if (propertyReference[propertyArray[i]] === false) {
                return false;
            } else {
                propertyReference = propertyReference[propertyArray[i]];
            }
        } else {
            // if the specified property was not foundm default to the state's name
            return currentState.name;
        }
    }
    // use the $interpolate service to handle any bindings in the propertyReference string.
    displayName = $interpolate(propertyReference)(currentState.locals.globals);

    return displayName;
}

Let's assume the following setup:

.state('articles.article by id', {
    url: '/articles/:articleId',
    templateUrl: 'articles/views/view.html',
    data: {
        displayName: '{{ article.title }}'
        }
})
<ui-breadcrumbs displayname-property="data.displayName"></ui-breadcrumbs>
  1. So, the scope.displaynameProperty comes from our attribute on the element, and is created on the scope for us by the scope: {displaynameProperty: '@'} in our directive definition object.
  2. We then split it into an array by ".", so in out case we get an array ['data', 'displayName'].
  3. The for``loop then traverses down the current$statedefinition object, checking if firstly the "data" property is defined on it. In our example the answer is "yes", and the value of it would be: { displayName : '{{ article.title }}' }`. We then set that result as the object to check in the next iteration.
  4. In the next iteration of the loop, we check whether the next property from the array (displayName) is defined on the new object obtained from the first iteration. It is, and its value is the string `'{{ article.title }}'.
  5. Now the loop is done, and we have successfully obtained the title for the breadcrumb. It could be a simple string, boolean false, or in this case, a special string in the syntax that Angular can iterpolate to find a dynamic value.
  6. To turn the string '{{ article.title }} into the actual value of the object article.title, we need to use the Angular service $interpolate. That service takes an Angular expression which we want to interpolate ({{ article.title }}), and returns a function that then takes a context that is used to look up the value. By "context" is meant basically an object which is used to loop up the value to put into the {{ expression }}.
  7. In our case, that context is currentState.locals.globals - which is basically a pointer to resolve property on our config object. So this allows it to look in that object, find the article object, and return the value of its property title.

I hope I made that easy to follow. If you implement something similar, but for the "beadcrumbsProxy" idea, I'd suggest that a bunch of the logic from the above function could be shared by the new function to avoid repetition. Good luck!

@michaelbromley
Copy link
Owner

I implemented the fix suggested above - see e7adb82 (plus a few subsequent tweaks)

See the updated docs: https://github.com/michaelbromley/angularUtils/tree/master/src/directives/uiBreadcrumbs#working-with-abstract-states

The Plunker demo is also updated to demonstrate abstract states: http://plnkr.co/edit/bBgdxgB91Z6323HLWCzF?p=preview

@enkodellc
Copy link
Author

I got around to implementing your updates. I simplified the updateBreadcrumbsArray() a bit to get the expected output I wanted. I am not concerned about duplicate states and your demo doesn't seem to include the abstract state breadcrumb? Here is my function that I will be using. Thanks for your updates.


            /**
             * Start with the current state and traverse up the path to build the
             * array of breadcrumbs that can be used in an ng-repeat in the template.
             */
            function updateBreadcrumbsArray() {
                var breadcrumbs = [];
                var currentState = $state.$current;

                while(currentState && currentState.name !== '') {
                    var workingState = getWorkingState(currentState);
                    var displayName = getDisplayName(currentState);

                    if (displayName !== false) {
                        breadcrumbs.push({
                            displayName: displayName,
                            route: workingState.name
                        });
                    }
                    currentState = currentState.parent;
                }

                breadcrumbs.reverse();
                scope.breadcrumbs = breadcrumbs;
            }

@michaelbromley
Copy link
Owner

Hi, okay - looks good. In the Plunker demo, the 'home.users' state is abstract, and it delegates to the 'home.user.list' state. Thus, when you are in the 'home.users.detail' state, clicking on 'Users' in the breadcrumbs will take you to 'users.list'.

@enkodellc
Copy link
Author

@michaelbromley sorry, yours works great, I was mistaken. Awesome job.

@dannygoncalves
Copy link

Sorry for resurrecting this old thread, how can I do if I want to show the abstract state (done) but remove the href from it? making it plain text and non-clickable?

@enkodellc
Copy link
Author

@dannygoncalves Do do this quickly I would for the uibreadcrumb and create either a custom property or do a simple trick on the displayname.

For instance let's say you wanted to have your "Todo" breadcrumb to be only a link then instead of displayName: 'Todo' make it displayName: '[-]Todo' then update the following functions:


        function stateAlreadyInBreadcrumbs(state, breadcrumbs) {
          var i;
          var alreadyUsed = false;
          for(i = 0; i < breadcrumbs.length; i++) {
//check if route exists
             if (breadcrumbs[i].route && (breadcrumbs[i].route === state.name)) {
               alreadyUsed = true;
             }
          }
          return alreadyUsed;
        }

Inside updateBreadcrumbsArray..

             //custom logic to not add the route and so it is text only
             if (displayName.indexOf('[-]') !== -1) {
                breadcrumbs.push({
                  displayName: displayName.replace('[-]','')
                });
              } else {
                breadcrumbs.push({
                  displayName: displayName,
                  route: workingState.name
                });
              }

I tested this out and it works. You could add an additional property like textonlyProperty: '@' and check for that before you push the breadcrumb.

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

No branches or pull requests

3 participants