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.