Angular - Update (PUT)


In this video we are going to implement the Update functionality of our Angular CRUD application. For this, we will make use of the PUT HTTP method, and our putAction on our Symfony 3 API.

We've done quite a lot of the hard work already by implementing the Create side of things in the previous video. The implementation for Updating is going to be very similar.

Knowing this, let's do the basic setup, and then look at the differences in more detail.

Firstly, I am going to copy / paste the contents of the /app/blogPost/create directory into a new directory called /app/blogPost/update.

I need to rename the two files however:

/app/blogPost/update/update.html

and

/app/blogPost/update/updateController.js

I am also going to make sure I tell the index.html file about the updateController.js:

<!-- /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>
  <script src="blogPost/update/updateController.js"></script>
</body>
</html>

And I need to add in the new route - which is a little different to the create route, as this time we will need to know the id field of the blogPost we wish to update:

// /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'
            })
            .when('/update/:id', {
                templateUrl: 'blogPost/update/update.html',
                controller: 'updateController'
            })
        ;

    }]);

By using the :id on our route, we will be able to get access to that field via the route params a little later on.

Given that we will have this ID information, we can go ahead and update our Api factory to add in a new get function:

// /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 get(id) {
                return $http.get(ROOT_URL + '/' + id);
            }

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

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

            return {
                get: get,
                getAll: getAll,
                post: post
            }

        }
    ]);

This is just like when we were working with the Postman client to GET a Single Blog Post.

That's all the setup out of the way. Now, let's think about how we want our Update journey to play out.

Updating A Blog Post

With our Create journey, we had it a little easier. When a user visited the /create route then they should see an empty form, and be able to add some data, click submit, and create a new blog post.

With update however, we need a way to reload the existing blog post first, allow the reloaded data to be changed, and then save this data when the submit button is clicked.

We created our /update/:id route in such a way that we can accept an id, but we need to pull that id from the URL to kick start this process. We can do this with $routeParams:

// /app/blogPost/update/updateController.js

'use strict';

angular.module('myApp.blogPost')

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

        $scope.blogPost = {};

        Api.get($routeParams.id)
            .then(function (response) {
                console.log('response', response);
                $scope.blogPost = response.data;
            }, function (error) {
                console.log('error', error);
            });

    }]);

What this will do is immediately create an empty object and assign it to a variable called blogPost on the scope.

Then it will make a GET request (via the Api factory method) to try and retrieve a blog post with the id given on the URL.

If this successfully returns then the empty blogPost object will be immediately overwritten with the resulting data from the API call.

This all happens without the user having to do anything.

Because we copy / pasted the create.html template to update.html, our ng-model fields are already setup on the title and body input fields, so as soon as the user opens up the /update/{someId} page it will briefly show the form with empty fields, until the real data is pulled down, which will 'magically' appear in the respective input fields.

We do need to make a change the update.html form though:

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

<form name="blog_post" method="post" class="form-horizontal">
    <div id="blog_post">
        <!-- *snip* -->
        <div class="form-group">
            <div class="col-sm-2"></div>
            <div class="col-sm-10">
                <button type="submit"
                        id="blog_post_submit"
                        ng-click="update(blogPost)"
                        class="btn-default btn">
                    Submit
                </button>
            </div>
        </div>
    </div>
</form>

Rather than call create when the submit button is clicked, instead we want to call the update method, which is not yet defined.

At this point it is worth pointing out that a better way to do this would be to make the form re-usable. As the form has two fields, I have not done this in this case, but in a real world application it would definitely be better practice to do so.

With the update(blogPost) function added, we now need to add that function to our updateController scope:

// /app/blogPost/update/updateController.js

'use strict';

angular.module('myApp.blogPost')

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

        $scope.blogPost = {};

        Api.get($routeParams.id)
            .then(function (response) {
                console.log('response', response);
                $scope.blogPost = response.data;
            }, function (error) {
                console.log('error', error);
            });

        $scope.update = function (blogPost) {

            Api.put(blogPost.id, blogPost)
                .then(function (response) {
                    console.log('response', response);
                    $window.location.href = '#!list';
                }, function (error) {
                    console.log('error', error);
                });

        };

    }]);

This is very similar to the createController's $scope.create function. The difference being that we call Api.put rather than Api.post.

The difference between a POST and a PUT is that we need the id to be able to do a PUT, as we are expecting to update an existing record:

Api.put(blogPost.id, blogPost)

However, we haven't actually defined that function on our Api factory, so let's do so:

// /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 get(id) {
                return $http.get(ROOT_URL + '/' + id);
            }

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

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

            function put(id, data) {
                return $http.put(ROOT_URL + '/' + id, data);
            }

            return {
                get: get,
                getAll: getAll,
                post: post,
                put: put
            }

        }
    ]);

We could just have easily created a patch function and used that instead. The PATCH method works almost identically to the PUT method, but can accept only some of the fields, rather than needing all fields.

Symfony 3 API Change For PUT / PATCH

We are going to hit upon a problem.

Currently, when we send in the PUT request it's going to have something similar to the following:

{ "id": 23, "title": "an updated title", "body": "our updated body" }

And that's cool, and valid JSON.

But Symfony is a bit picky. We haven't actually said we are expecting an id field. So Symfony is going to give us a 400 error and complain about receiving extra fields:

{"code":400,"message":"Validation Failed","errors":{"errors":["This form should not contain extra fields."],"children":{"title":{},"body":{}}}}

There are a few solutions to this problem. After all, this is programming - one defacto solution would not feel right :)

If you are using Symfony 3 then you could set your form to allow_extra_fields => true, which would - unsurprisingly - allow any extra fields / not complain about extra fields. That's probably the nicer way, and only came to me a while after recording the videos that this was the nicest solution.

<?php

// /src/AppBundle/Form/Type/BlogPostType.php

namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class BlogPostType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title', TextType::class)
            ->add('body', TextType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class'         => 'AppBundle\Entity\BlogPost',
            'allow_extra_fields' => true, // this line is new
        ]);
    }

    public function getName()
    {
        return 'blog_post';
    }
}

Another way - and the way I use in the videos - is to add the id field to the form type, but set it to 'mapped' => false:

// /src/AppBundle/Controller/BlogPostsController.php

    public function putAction(Request $request, int $id)
    {
        /**
         * @var $blogPost BlogPost
         */
        $blogPost = $this->getBlogPostRepository()->find($id);

        if ($blogPost === null) {
            return new View(null, Response::HTTP_NOT_FOUND);
        }

        $form = $this->createForm(BlogPostType::class, $blogPost, [
            'csrf_protection' => false,
        ]);

        $form->add('id', IntegerType::class, [ 'mapped' => false ]); // this line is new

        $form->submit($request->request->all());

        if (!$form->isValid()) {
            return $form;
        }

        $em = $this->getDoctrine()->getManager();
        $em->flush();

        $routeOptions = [
            'id' => $blogPost->getId(),
            '_format' => $request->get('_format'),
        ];

        return $this->routeRedirectView('get_post', $routeOptions, Response::HTTP_NO_CONTENT);
    }

There are other ways too. But one of these will get you through.

We will need this same fix whether our front end is Angular, or React, or anything else.

The main thing is - don't make the front end change the shape of your JSON. If we send out id, title, and body, then we really should handle the exact same fields on update.

Code For This Video

Get the code for this video.

Episodes