In a current project, I have an Angular application inside a big Symfony2 website.

So the SPA (Single Page Application) homepage is a Symfony route which lead to a Twig template.

Why use HTML5 urls?

I used the great UI-Router in my application and initially let the # inside my routes..

But when I discovered how it’s simple to upgrade to HTML5 History API with Angular, I changed it immediatly and you should do the same! :)

Imagine these routes in a website (parts after # are SPA urls):

  • http://www.mywebsite.com/editor/projects/51
  • http://www.mywebsite.com/editor/projects/51/#/stories/16
  • http://www.mywebsite.com/editor/projects/51/#/stories/16/tasks/82

Now with HTML5 History API:

  • http://www.mywebsite.com/editor/projects/51
  • http://www.mywebsite.com/editor/projects/51/stories/16
  • http://www.mywebsite.com/editor/projects/51/stories/16/tasks/82

Users can directly access any of these but don’t care and don’t have to know when they really change “pages” or when a change is done inside the SPA (In others words when it’s Symfony2 routing or Angular routing which is involved).

They can always copy/paste urls and use next/prev browser buttons.

What on Angular side?

Not a lot of things, just tell Angular you want HTML5 urls and add base node in the header to let him know where the SPA url begin inside the full url (because # isn’t there anymore to do the separation).

appProject.config(function($locationProvider) {
    $locationProvider.html5Mode(true);
});
<head>
    <meta charset="utf-8">
    <base href="{ { path('editor_edit', {id: project_id}) } }">
</head>

What on Symfony2 side?

We create two Symfony2 routes leading to the same controller/action.

The first is the SPA homepage and the second is used for SPA direct access routes.

editor_edit:

    path: /editor/projects/{id}/

    defaults: { _controller: BibzEditorBundle:Editor:edit }

    requirements:

        id: \d+

        

editor_edit_angular:

    path: /editor/projects/{id}/{whatever}

    defaults: { _controller: BibzEditorBundle:Editor:edit }

    requirements:

        id: \d+

        whatever: .+ # Require to accept character "/" inside

The controller is really simple here:

<?php
class EditorController extends Controller
{
    /**
     * @Template()
     * @Secure(roles="ROLE_USER")
     */
    public function editAction($id)
    {
        return array(
            'project_id' => $id
        );
    }
}

What about browsers support?

Don’t worry for (few) people with old browsers not compatibles with HTML5 History API (http://caniuse.com/#feat=history), Angular manage it really well going back with # when not supported!

Like Paul said in the comments, there was a missing part in this tutorial, links inside the app.
UI-Router with uiSref directive automatically generates the good href property depending on HTML5 mode activation or not.

Here is what happens in details:

  • Imagine an angular application on this page:
http://etin.yourphototravel.com/fr/editor/etin/6
  • So the html base tag is:
<base href="/fr/editor/etin/6/">
  • We use UI-Router directive to directly point to a state:
<a ui-sref="app.tab2.step({ idStep: step.id })">link</a>
  • The directive generates this common SPA url in classic mode:
<a href="#/chapter/7">link</a>
  • And it builds this href for HTML5 urls mode :
<a href="/fr/editor/etin/6/chapter/7">link</a>

Note that the base tag href property is added to have a full route from the domain.

[This post follow and upgrade this older: Recursivity exercice with AngularJS]

Drag & drop:

I used the native html5 drag & drop api.
So it doesn’t work on mobile/tablet devices: http://caniuse.com/#feat=dragndrop

Animations:

Animations are done with the nice css library Animate.css
It is very easy to use it with angular and ngAnimate, see that (SASS syntax):

.li-condition {
    &.ng-enter {
        @include animation(pulse 0.4s);
    }
    &.ng-leave {
        @include animation(zoomOut 0.4s);
    }
    &.ng-move {
        @include animation(zoomIn 0.4s);
    }
}

.li-group {
    &.ng-enter {
        @include animation(flipInX 0.4s);
    }
    &.ng-leave {
        @include animation(zoomOut 0.4s);
    }
    &.ng-move {
        @include animation(zoomIn 0.4s);
    }
}

Angularjs 1.3:

Note that the new 1.3 ngRepeat as syntax is used there:

<li ng-repeat="element in data.elements | orderBy:'position' as filteredData track by element.id">...</li>

Like this, I can use the filteredData variable inside this scope node without recalculating it.

Try it:

There is a graphical bug/weird behavior linked to ngMove:

You can reproduce it by move an element down on same level, animations occurs not on the moved element but on all others between old and new position..

I found this issue on github with this example who explains it very well:
http://plnkr.co/edit/4yRkLWbsU57YxrYOrWUQ?p=preview
But there is no solution yet.