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.