3

I need some help making my solution to this problem more elegant.

Summary of app

The app is a single-page app using pushstate. The landing page (path: /) contains links to articles (path: /article/:articleId). There is also a link to create an article (path: /create). When either link is clicked, the URL is changed and the content is loaded into a modal (I'm using Bootstrap but the implementation is fairly irrelevant).

Solution

So far I have (and please excuse errors as I'm coding blind):

index.htm

<!doctype html>
<html ng-app="App">
    <head>
        <title>{{page.title}}</title>
    </head>

    <body>
        <section id="articles" ng-controller="HomepageCtrl">
            <ul>
                <li ng-repeat="article in articles">
                    <a href="/article/{{article.id}}">{{article.name}}</a>
                </li>
            </ul>
        </section>

        <!-- markup simplified for example -->
        <modal id="modal">
            <div class="content ng-view></div>
        </modal>
    </body>
</html>

app.js

angular.module('Application', [], ['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
    $routeProvider.when('/article/:articleId', {
        templateUrl : '/templates/article.htm',
        controller  : 'ArticleCtrl'
    }).when('/create', {
        templateUrl : '/templates/create.html',
        controller  : 'CreateCtrl'
    });

    $locationProvider.html5Mode(true);
}]).controller('HomepageCtrl', ['$scope', '$rootScope', function($scope, $rootScope) {
    $rootScope.page = {
        title : 'This is the homepage'
    };

    $scope.articles = [];
    $scope.articles.push({
        id   : 1,
        name : 'Interesting article'
    });
}]).controller('ArticleCtrl', ['$scope', '$rootScope', '$routeParams', function($scope, $rootScope, $routeParams) {
    $('#modal').modal();

    $rootScope.page = {
        title : 'This is article {{article.name}}' /* Not sure if this works! */
    };
}]).controller('CreateCtrl', ['$scope', '$rootScope', function($scope, $rootScope) {
    $('#modal').modal();

    $rootScope.page = {
        title : 'This is the article creation form'
    };
}]);

Templates omitted.

Problem

While this solution works, it's not great. The homepage content remains in the background and is separated from the modal content, which is good. When the route is matched, the template content is retrieved and the controller is hit, the modal opens displaying the content (ng-view). This is also good. However, I don't think controllers should be opening modals.

What is the most elegant way of doing this? The concept of routing content into the modal with unique controllers is significant to the application: there will be lots more pages added which will all need to follow this convention. There are other caveats, e.g. browser back button and closing the modal should return user to homepage, but these are less significant right now. Once I understand where to sit this logic and how it interfaces with the controllers I'll be happy.

voteforpedro
  • 187
  • 3
  • 10
  • 2
    Check angular-ui modal component. It creates a better abstraction and has a `$modal` service which can be used in controller to open and close modal. http://angular-ui.github.io/bootstrap/ – Chandermani Oct 17 '13 at 12:55
  • 1
    You shouldn't be doing DOM manipulations inside a controller with jQuery - `$('#modal').modal()`, especially since the modal markup is outside the range of the controller in the `ng-view` – m.e.conroy Oct 17 '13 at 13:18

1 Answers1

2

I'm going to give this a shot, hopefully the answer will be what you're looking for.

Any DOM manipulation (as @m.e.conroy said above) should not be done in a controller, but should instead be done in a directive. You can easily communicate between your controller and the directive by doing any number of things. I'm not going to go through an entire example, but this will hopefully get you pointed in the right direction.

To create a "modal" directive, you would do something like the following:

.directive('myModal', [ // declare any dependencies here
    function() {
        return {
            restrict: 'EA',
            templateUrl: 'path/to/the/modal/template.html',
            replace: true,
            scope: { currArticle: '='},  // create a two-way binding for "$scope.currArticle"
            link: function(scope, elem, attrs){
                elem.modal();  // initialize the modal using jQuery
                scope.$on('open-modal-' + attrs.name, function(e, article){
                    scope.currArticle = article;
                    elem.modal('open'); // or whatever call will open the modal
                });
            }
        };
    }
]);

And then your read-article template (located at path/to/the/modal/template.html for this example):

<modal my-modal name="'read-article'">
    <!-- bind to modal content here -->
    {{currArticle.id}}: {{currArticle.name}}
</modal>

And then your controller:

.controller('HomepageCtrl', ['$scope', '$rootScope', function($scope, $rootScope) {
    $rootScope.page = {
        title : 'This is the homepage'
    };
    $scope.articles = [];
    $scope.articles.push({
        id   : 1,
        name : 'Interesting article'
    });
    $scope.displayArticle = function(art){
        $scope.$emit('open-modal-read-article', art);
    };
}]);

And finally, your "home" view:

...
<section id="articles" ng-controller="HomepageCtrl">
    <ul>
        <li ng-repeat="article in articles">
            <a href="" ng-click="displayArticle(article)">{{article.name}}</a>
        </li>
    </ul>
</section>
...

And then you would repeat the above steps for the "create" modal. Hopefully that gives you a place to start from and modify as needed. Let me know if you need more clarification.

tennisgent
  • 14,165
  • 9
  • 50
  • 48
  • I'm new at AgnularJS and this example doesn't work for me. I don't know how to use it, because in "home" view it doesn't have any "" directive, only in template? This doesn't load "template.html". – ekstro Jan 06 '14 at 08:41