- Explain the purpose of Factories in Angular.
- Use
ngResource
to pull information from an API. - Use $stateParams to access query parameters and update the URL.
- Create separate views and routes for each CRUD action.
In the last couple of classes, we've been using hardcoded values in our controller to act as our "backend". We probably won't ever do that again. Instead we'll be connecting to an external API using resources and providing an interface to models using factories.
With the person next to you, take 15 minutes to walk through the following part of the lesson plan, up to the
Factories
header. Read our descriptions of the different components.
We'll then take the next 5 minutes for questions.
Run the below commands to clone the starter code. You will not be using the code you created in the ui-router
class.
$ git clone https://github.com/ga-wdi-exercises/grumblr_angular.git
$ cd grumblr_angular
$ git checkout factory-resource-starter
Where we're picking up the app, it has...
- A functioning index route that uses grumbles hardcoded into the index controller
- The makings of a show route. We'll need to build this out.
<!DOCTYPE html>
<html data-ng-app="grumblr">
<head>
<title>Angular</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.4.2/angular-ui-router.min.js"></script>
<script src="js/app.js"></script>
</head>
<body>
<h1><a data-ui-sref="grumbleIndex">Grumblr</a></h1>
<main data-ui-view></main>
</body>
</html>
data-ng-app: Establishes the domain of our Angular application.
data-ui-sref: This creates a link that, when clicked, directs the user to
#/grumbles
without reloading the page.We use this instead of the usual
href
becausesref
refers to a state and automatically grabs the URL for that state. It's like link helpers in rails.data-ui-view: Whichever view is triggered by the user will be displayed in the DOM element with this attribute.
<h2>These are all the Grumbles</h2>
<div data-ng-repeat="grumble in vm.grumbles">
<p>{{grumble.title}}</p>
</div>
data-ng-repeat: Allows us to iterate through each item in the array passed in as an argument. In this case, the controller's
grumbles
property.vm: Represents the current instance of our index controller.
angular
.module("grumblr", [
"ui.router"
])
.config([
"$stateProvider",
RouterFunction
]);
function RouterFunction($stateProvider){
$stateProvider
.state("grumbleIndex", {
url: "/grumbles",
templateUrl: "js/ng-views/index.html",
controller: "GrumbleIndexController",
controllerAs: "vm"
})
.state("grumbleShow", {
url: "/grumbles/:id",
templateUrl: "js/ng-views/show.html"
});
}
.module: A module is a container for controllers, directives, services -- all parts of our application. A module can have sub-modules .
ui.router: A 3rd party module that functions as a router. Allows our application to have multiple states.
$stateProvider: A ui-router service that allows us to define states in our application.
.state: Used to define an individual state in our application. Arguments include (1) state name and (2) an object that contains information about route, template and controller used.
.controller("GrumbleIndexController", [
GrumbleIndexControllerFunction
]);
function GrumbleIndexControllerFunction(){
this.grumbles = [
{title: "These"},
{title: "Are"},
{title: "Hardcoded"},
{title: "Grumbles"}
]
}
GrumbleIndexController: The name of this controller.
GrumbleIndexControllerFunction: A function that contains this controller's behavior. This is a stylistic decision - we could have passed in an anonymous function to
.controller
if we wanted to.
You'll notice that, at the moment, we have hardcoded models into the Grumbles controller. Today we'll be learning about ngResource
, a module that allows us to make calls to that Rails API we'll set up now.
Let's start by cloning and running a Grumblr Rails API in the background. Our front-end Grumblr application will make AJAX calls to this API.
$ git clone https://github.com/ga-wdi-exercises/grumblr_rails_api.git
$ cd grumblr_rails_api
$ bundle install
$ rails db:drop
$ rails db:create
$ rails db:migrate
$ rails db:seed
$ rails s
First up, we'll convert the hardcoded data to read from an external API using a factory.
A factory is an Angular component that adds functionality to an Angular application. It does this by generating new instances of something. In this case, Grumbles.
Factories allow us to separate concerns and extract functionality that would otherwise be defined in our controller. We do this by creating an object, attaching properties and methods to it and then returning that object.
Let's start building out a factory in Grumblr.
.factory( "GrumbleFactory", [
GrumbleFactoryFunction
]);
function GrumbleFactoryFunction(){
return {
helloWorld: function(){
console.log( "Hello world!" );
}
}
}
Factories can also take dependencies. In that case, the arguments passed into a factory will look a little different. We'll see that in play when we learn about
ng-resource
later today.
Now we can call it in a controller...
.controller( "GrumbleIndexController", [
// The factory is passed in as a dependency to our controller.
"GrumbleFactory",
GrumbleIndexControllerFunction
]);
function GrumbleIndexControllerFunction( GrumbleFactory ){
// When `helloWorld` is called, it runs the function we defined in our factory file.Factory
GrumbleFactory.helloWorld();
}
This is nice because it keeps our controller clean. We leave the function declaration(s) to our factory.
Do not code along. You will have the chance to write all this code in the following exercise.
Let's make a factory that's actually useful. It's purpose: enable us to perform CRUD actions on our Rails Grumblr API.
By default, Angular does not include a way to interact with APIs. For that, there is a separate module, called ngResource. The ngResource module provides interaction support with RESTful services.
Let's include it in our application using a CDN.
<!-- index.html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.0-beta.2/angular-resource.min.js"></script>
Add ngResource
as a dependency to our application.
angular
.module("grumblr", [
"ui.router",
"ngResource"
])
And $resource
as a dependency to our GrumbleFactory
.factory( "GrumbleFactory", [
"$resource",
GrumbleFactoryFunction
]);
function GrumbleFactoryFunction( $resource ){
return $resource( "http://localhost:3000/grumbles/:id" );
}
Out of the box, this gives us several methods for our newly defined Grumble
factory...
GrumbleFactory.get
GrumbleFactory.save
GrumbleFactory.query
GrumbleFactory.remove
GrumbleFactory.delete
Where's
update
, you ask? We're going to define that ourselves later on.
When the data is returned from the server, the response object is an instance of the resource class. The actions save
, remove
and delete
are available on it as methods with the $
prefix. This allows you to easily perform CRUD operations on server-side data like this...
var Grumble = $resource('/grumbles/:id');
var grumble = Grumble.get( { id:123 }, function(grumble) {
grumble.abc = true;
grumble.$save();
});
Let's update our index controller so that, instead of using hardcoded grumbles, this.grumbles
is set to the result of making a GET
request to http://localhost:3000/grumbles
.
.controller( "GrumbleIndexController", [
"GrumbleFactory",
GrumbleIndexControllerFunction
]);
// Whenever `.grumbles` is called on our ViewModel, it returns the response from `.query()`
function GrumbleIndexControllerFunction( GrumbleFactory ){
this.grumbles = GrumbleFactory.query();
}
- Include values for
controller
andcontrollerAs
.
.state("grumbleShow", {
url: "/grumbles/:id",
templateUrl: "js/ng-views/show.html",
controller: "GrumbleShowController",
controllerAs: "vm"
});
Each grumble listed here should link to its corresponding show page.
<div data-ng-repeat="grumble in vm.grumbles | orderBy:'-created_at'">
<p><a data-ui-sref="grumbleShow({id: grumble.id})">{{grumble.title}}</a></p>
</div>
NOTE: The
data-ui-sref
attribute is not only set to route namegrumbleShow
, butgrumbleShow
also takes anid
as an argument so that it knows which show page it should direct the user to.
This will look very similar to the index controller, which a couple exceptions.
The controller requires access to ui-router's $stateParams
service. We pass it in the same way we do GrumbleFactory
.
$stateParams
returns an object containing the information passed into, in this case,grumbleShow
. If we print it to the console, it looks like this...
Object {id: "6"}
The controller will have a grumble
property. It should be set to the return value of ngResource
's get
method.
ngResource
'sget
method requires an object as an argument, which contains a key-value pair for the grumble's id.
.controller("GrumbleShowController", [
"GrumbleFactory",
"$stateParams",
GrumbleShowControllerFunction
]);
function GrumbleShowControllerFunction(GrumbleFactory, $stateParams){
this.grumble = GrumbleFactory.get({id: $stateParams.id});
}
Use what you learned on your first day of Angular to create a show view for a Grumble. It should display the Grumble's title, author name, created at date, content and photo.
Do not code along. You will have the chance to write all this code in the following exercise.
This link will trigger the grumbleNew
state when clicked.
<a data-ui-sref="grumbleNew">New Grumble</a>
<h2>These are all the Grumbles</h2>
<div data-ng-repeat="grumble in vm.grumbles">
<p><a data-ui-sref="grumbleShow({id: grumble.id})">{{grumble.title}}</a></p>
</div>
// app.js
$stateProvider
.state("grumbleIndex", {
url: "/grumbles",
templateUrl: "js/ng-views/index.html",
controller: "GrumbleIndexController",
controllerAs: "vm"
})
.state("grumbleNew", {
url: "/grumbles/new",
templateUrl: "js/ng-views/new.html",
controller: "GrumbleNewController",
controllerAs: "vm"
})
.state("grumbleShow", {
url: "/grumbles/:id",
templateUrl: "js/ng-views/show.html",
controller: "GrumbleShowController",
controllerAs: "vm"
});
NOTE:
grumbleNew
is placed beforegrumbleShow
. This is important - why? SwitchinggrumbleNew
andgrumbleShow
may shed some light on this...
In the GrumbleNewControllerFunction
, let's bring in GrumbleFactory
, instantiate a new
instance of it that can be modified by form inputs, and add a create
method that allows it to be send to our API.
.controller( "GrumbleNewController", [
"GrumbleFactory",
GrumbleNewControllerFunction
]);
function GrumbleNewControllerFunction( GrumbleFactory ){
this.grumble = new GrumbleFactory();
this.create = function(){
this.grumble.$save()
}
}
Let's create a form view for creating Grumbles. Notice how data-ng-model
references what we defined as this.grumble
in our controller in the previous step.
<!-- js/ng-views/new.html -->
<h2>Create Grumble</h2>
<form>
<input placeholder="Title" data-ng-model="vm.grumble.title" />
<input placeholder="Author name" data-ng-model="vm.grumble.authorName" />
<input placeholder="Photo URL" data-ng-model="vm.grumble.photoUrl" />
<textarea placeholder="Grumble content" data-ng-model="vm.grumble.content"></textarea>
<button type="button" data-ng-click="vm.create()">Create</button>
</form>
Fields are matched to grumble properties using the
data-ng-model
directive.
The steps here are pretty similar to those of the last "I Do," with a few exceptions. The biggest one is...
ngResource
does not come with a native update
method. We need to define it in the FactoryFunction
return statement, indicating that update
corresponds to a PUT
request.
.factory( "GrumbleFactory", [
"$resource",
FactoryFunction
])
function FactoryFunction( $resource ){
return $resource( "http://localhost:3000/grumbles/:id", {}, {
update: { method: "PUT" }
});
}
The rest of the steps are a bit more straightforward...
Let's update our ng-repeat
div so that it also displays a link with each Grumble that will direct us to an edit page.
<!-- js/ng-views/index.html -->
<div data-ng-repeat="grumble in vm.grumbles">
<p><a data-ui-sref="grumbleShow({id: grumble.id})">{{grumble.title}}</a></p>
<a data-ui-sref="grumbleEdit({id: grumble.id})">Edit</a>
</div>
Follow the same process we did for grumbleNew
, making sure to use the word edit
wherever necessary.
Not sure what URL to use? Think about what the path would look like for an edit form in a Rails app...
The big addition here is our controller's update
method. You'll notice that it makes use of $update
. THIS is the method we defined in the grumble factory. It is preceded by a $
because this is how ngResource
indicates it's an instance method.
.controller( "GrumbleEditController", [
"GrumbleFactory",
"$stateParams",
GrumbleEditControllerFunction
]);
function GrumbleEditControllerFunction( GrumbleFactory, $stateParams ){
this.grumble = GrumbleFactory.get({id: $stateParams.id});
this.update = function(){
this.grumble.$update({id: $stateParams.id})
}
}
The form on this page will look a lot like the one in new.html
, but you'll need to make some changes to it...
- Reference the proper controller instance. You probably called it
vm
. - Replace your inputs'
placeholder
attribute withvalue
so we have some content to work with in our input fields upon page load. - Set these value attributes to the contents of the Grumble like so...
<input value="vm.grumble.title" ... >
- In the button's
ng-click
directive, reference the controller's.update
method instead of.create
.
We may not get to this in-class.
Contrary to how we've done things for every other RESTful route, we will not be creating a separate controller for delete
. This is because we want to be able to delete a Grumble simply by clicking a button on each grumble's show page.
When clicked, the delete button will trigger a destroy
method that we have yet to define in edit.controller.js
.
<form>
<input value="vm.grumble.title" data-ng-model="vm.grumble.title" />
<input value="vm.grumble.authorName" data-ng-model="vm.grumble.authorName" />
<input value="vm.grumble.photoUrl" data-ng-model="vm.grumble.photoUrl" />
<textarea value="vm.grumble.content" data-ng-model="vm.grumble.content"></textarea>
<button data-ng-click="vm.update()">Update</button>
<button data-ng-click="vm.destroy()">Delete</button>
</form>
.controller( "GrumbleEditController", [
"GrumbleFactory",
"$stateParams",
GrumbleEditControllerFunction
]);
function GrumbleEditControllerFunction( GrumbleFactory, $stateParams ){
this.grumble = GrumbleFactory.get({id: $stateParams.id});
this.update = function(){
this.grumble.$update({id: $stateParams.id})
}
this.destroy = function(){
this.grumble.$delete({id: $stateParams.id});
}
}
- Why do we use factories?
- What do factories return?
- What are
ngResource
and$resource
? What methods do they provide us with? - How do we use
$resource
and a factory to create and save something to an API/database?
Only attempt the following once you have set up full CRUD functionality for Grumblr.
Use state.go
so that when a user creates, edits or deletes something, they are directed to a page that is not the same form.
We're not covering this in today's class. Learn more in the
ui-router
documentation.
Only use a single view and controller (i.e., you should be to execute full CRUD functionality from the index). The user should only be able to see forms for creating and updating after clicking buttons for those respective actions.
This will require making use of directives we didn't use in today's class, like
ng-show
andng-hide
.
Finish implementing full CRUD functionality for Grumblr using ngResource
. In other words, finish going through all the code in this lesson plan.
Links to the starter and solution code can be found in the grumblr_angular
repo.
- Angular documentation for ngResource and $resource.
- Angular: What Goes Where?
- Factory vs. Service vs. Provider