Angular - Create (POST)


In this video we will add the functionality to our Angular front end that allows the creation of new Blog Posts via our Symfony 3 REST API.

The API does the real hard work here, we just need a way to allow a user to enter a Blog Post title and body, and when they click 'submit', that data should be sent off to the server via a POST request.

As we've already created a (somewhat) nicely styled form for our Twig create implementation, we are going to do a little copy / pasting to save ourselves time on re-implementing that. But we will need to 'Angularize' it.

The first step is to create the new controller and template files:

app/blogPost/create/create.html

and

app/blogPost/create/createController.js

Once these two files have been created, it would be a good time to add the controller file to our index.js file:

<!-- /app/index.html -->

  <!-- * snip * --> 

  <script src="app.js"></script>
  <script src="blogPost/index.js"></script>
  <script src="blogPost/Api.js"></script>
  <script src="blogPost/list/listController.js"></script>
  <script src="blogPost/create/createController.js"></script>
</body>
</html>

And we also need to setup the /create route:

// /app/blogPost/index.js

'use strict';

angular.module('myApp.blogPost', ['ngRoute'])

    .config(['$routeProvider', function($routeProvider) {

        $routeProvider
            .when('/list', {
                templateUrl: 'blogPost/list/list.html',
                controller: 'listController'
            })
            .when('/create', {
                templateUrl: 'blogPost/create/create.html',
                controller: 'createController'
            })
        ;

    }]);

One thing to note here - you may need to stop / restart your local development server to get this new route to come into play. You could also try a hard refresh of your browser. This process was very temperamental for me.

With all these parts in place, we can now get on with the guts of the implementation.

New Blog Post Template

As mentioned, there is no point reinventing the wheel. Instead, I am going to copy the 'create' HTML template from the Twig CRUD implementation to our Angular project, and update where necessary:

<!-- /app/blogPost/create/create.html -->

<form name="blog_post" method="post" class="form-horizontal">
    <div id="blog_post">
        <div class="form-group">
            <label class="col-sm-2 control-label required" for="blog_post_title">Title</label>
            <div class="col-sm-10">
                <input type="text"
                       id="blog_post_title"
                       required="required"
                       class="form-control"/>
            </div>
        </div>
        <div class="form-group">
            <label class="col-sm-2 control-label required" for="blog_post_body">Body</label>
            <div class="col-sm-10">
                <input type="text"
                       id="blog_post_body"
                       required="required"
                       class="form-control"/>
            </div>
        </div>
        <div class="form-group">
            <div class="col-sm-2"></div>
            <div class="col-sm-10">
                <button type="submit"
                        id="blog_post_submit"
                        class="btn-default btn">
                    Submit
                </button>
            </div>
        </div>
    </div>
</form>

This is the raw HTML, with no Angular specifics yet added.

Next, we need a way of hooking up Angular and our template, so that when data is tracked as it is entered into the title or body inputs. To do this we can add the ngModel directive to each of our inputs which will bind our view data with the model data - which we haven't yet created. So let's quickly do that:

// /app/blogPost/create/createController.js

'use strict';

angular.module('myApp.blogPost')

    .controller('createController', ['$scope', 'Api', function($scope, Api) {

        $scope.blogPost = {};

    }]);

Simple enough, we just need an object on our $scope to store each of the field's data.

Now we can hook up the ng-model in our view:

<!-- /app/blogPost/create/create.html -->

<form name="blog_post" method="post" class="form-horizontal">
    <div id="blog_post">
        <div class="form-group">
            <label class="col-sm-2 control-label required" for="blog_post_title">Title</label>
            <div class="col-sm-10">
                <input type="text"
                       id="blog_post_title"
                       ng-model="blogPost.title"
                       required="required"
                       class="form-control"/>
            </div>
        </div>
        <div class="form-group">
            <label class="col-sm-2 control-label required" for="blog_post_body">Body</label>
            <div class="col-sm-10">
                <input type="text"
                       id="blog_post_body"
                       ng-model="blogPost.body"
                       required="required"
                       class="form-control"/>
            </div>
        </div>
        <div class="form-group">
            <div class="col-sm-2"></div>
            <div class="col-sm-10">
                <button type="submit"
                        id="blog_post_submit"
                        ng-click="create(blogPost)"
                        class="btn-default btn">
                    Submit
                </button>
            </div>
        </div>
    </div>
</form>

And I've also gone ahead and added the ngClick directive to our submit button, to define what will happen when the button is clicked. In this case, we are saying that on click, we want to run the create function, and pass in the contents of the blogPost object.

Note that both the blogPost object and the create function need to be available on the $scope, but we don't reference the $scope inside the template. Which is to say our ng-click function is:

ng-click="create(blogPost)"

not

ng-click="$scope.create($scope.blogPost)"

Handling ngClick

The important 'stuff' needs to happen when a user clicks the submit button.

We've already declared that clicking our submit button should call the $scope.create function, and that it should pass in the contents of the blogPost.

Let's write out that function now:

// /app/blogPost/create/createController.js

'use strict';

angular.module('myApp.blogPost')

    .controller('createController', ['$scope', 'Api', function($scope, Api) {

        $scope.blogPost = {};

        $scope.create = function (blogPost) {

            Api.post(blogPost)
                .then(function (result) {
                    console.log('result', result);
                }, function (error) {
                    console.log('error', error);
                })
        };

    }]);

Remember, after the refactoring exercise we undertook in the previous video, we have extracted all our API interaction to a factory method. Whilst we can immediately use this Api factory inside our Controller, we have had to inject it, but we haven't actually created the post function as of yet. So let's do that:

// /app/blogPost/Api.js
'use strict';

angular.module('myApp.blogPost')

    .factory('Api', [
        '$http',
        function ($http) {

            var ROOT_URL = 'http://api.symfony-3.dev/app_dev.php/posts';

            function getAll() {
                return $http.get(ROOT_URL);
            }

            function post(blogPost) {
                return $http.post(ROOT_URL, blogPost);
            }

            return {
                getAll: getAll,
                post: post
            }

        }
    ]);

There's really not a lot to it, but if you are unsure on this then watch the previous video.

One nice thing about Angular's $http is that we can just pass in the JavaScript object (blogPost) without needing to explicitly JSON.stringify the object. This is something that we will need to do in the React implementation.

Simple Redirect On Create

There are many things we could do when a new BlogPost has been created. I'm quite partial to a slice of Toast.

However, in our simple little app, the only thing I want to happen is that the user get redirect to the /list view on the successfuly POST'ing of a new BlogPost.

To do this, as best as I am aware, we need to tell the $window where we would like to redirect too.

I am going to add this to the then in our Api.post call:

// /app/blogPost/create/createController.js

'use strict';

angular.module('myApp.blogPost')

    .controller('createController', ['$scope', 'Api', '$window', function($scope, Api, $window) {

        $scope.blogPost = {};

        $scope.create = function (blogPost) {

            Api.post(blogPost)
                .then(function (result) {
                    console.log('result', result);
                    $window.location.href = '#!list';
                }, function (error) {
                    console.log('error', error);
                })
        };
    }]);

Note here the inclusion of $window, and then the use of the hashbang (#!) in the href. This is how ngRoute likes our routes to be named.

Knowing this about hashbang's in the URL, we can also add a quick 'Create' button to the bottom of our List view, allowing our users to easily create new blog posts:

<!-- /app/blogPost/list/list.html -->

    <!-- * snip * -->
</table>

<a href="#!create" class="btn btn-lg btn-success">Create</a>

And with that, we are done with our Create functionality.

Code For This Video

Get the code for this video.

Episodes