Tutorial, part 4
We won't go into building a REST API server with an actual database storing the guestbook posts - that is beyond this tutorial and IMA.js. To give you the idea of fetching data from the server, we'll create a simpler alternative.
We'll start by creating the app/assets/static/api
directory and the
app/assets/static/api/posts.json
file with the following content (copied from
our home controller and modified):
[
{
"id": 4,
"content": "I'm lovin' this IMA.js thing!",
"author": "John Doe"
},
{
"id": 3,
"content": "JavaScript everywhere! It's just JavaScript!",
"author": "Jan Nowak"
},
{
"id": 2,
"content": "Developing applications is fun again! Thanks, IMA.js!",
"author": "Peter Q."
},
{
"id": 1,
"content": "How about a coffee?",
"author": "Daryll J."
}
]
Notice how we added the id
field - as mentioned previously, in a real world
application, you should rely on the primary keys provided to you by your
backend instead of generating them yourself.
Now that we have our data ready, we just need some way to actually fetch it from the server. To do that, we'll introduce 4 new classes into our project: an entity class, a factory class, a resource class, and a service class.
The entity class represents a typed data holder for our data (which is useful for debugging) and allows us to add various computed properties without having to modify our API backend.
Let's create the app/model
and app/model/post
directories and then a new
file app/model/post/PostEntity.js
with the following content:
export default class PostEntity {
constructor(data) {
this.id = data.id;
this.content = data.content;
this.author = data.author;
}
}
We just created a new class, exported it, and that's it, no more is currently required. Our new entity class extracts the data obtained from a data object (for example obtained from a deserialized JSON) and sets it to its fields.
So, with our entity class ready, let's take a look at the factory class. The factory class will be used to create new entities from data objects and arrays of data objects - but in our case, the latter will suffice for now.
Create a new app/model/post/PostFactory.js
file with the following content:
import PostEntity from './PostEntity';
export default class PostFactory {
createList(entities) {
return entities.map(entityData => new PostEntity(entityData));
}
}
We just created a new class with a single method named createList()
. The
method takes an array of data objects and returns an array of post entities.
We have our entity and factory, now we need a resource class. The resource
class represents our single point of access to a single REST API resource
(or entity collection, if you will). The sole purpose of a resource class is to
provide a relatively low-level API for accessing the REST API resource. Create
a new app/model/post/PostResource.js
file with the following contents:
export default class PostResource {
constructor(http, factory) {
this._http = http;
this._factory = factory;
}
getEntityList() {
return this._http
.get('http://localhost:3001/static/api/posts.json', {})
.then(response => this._factory.createList(response.body));
}
}
We defined the getEntityList()
method in our resource class which we'll use
to fetch the posts from the server. In a real-world application we would use
configuration to set the URL to the resource instead of specifying it like
this, but that is beyond the scope of this tutorial.
The _http.get()
method returns a new promise that resolves to the response
object of a GET HTTP request sent to the specified URL, with the provided query
parameters (the second parameter currently set to an empty object). The method
also automatically parses the JSON in our response body.
We then post-process the parsed response data using the Promise's then
callback which uses our factory to create an array of post entities.
You may have noticed that we have the http
and factory
parameters in our
constructor. This is how we provide the resource with the HTTP agent provided
by IMA.js, and our post entity factory. We'll take a look at how to do this
properly in a moment.
You now may be wondering what is the point of the service class. Why, the service class is not that useful in our tutorial, but it would be essential in a bigger application. The resource should handle only sending requests and processing responses without any high-level operations. The service class is there to take care of the high-level stuff. For example, should we have a REST API that provides us with paged access to posts and we would want to fetch all posts since a specific one, this would be handled by the service. The service would fetch the necessary pages from the REST API, construct the result and resolve to the constructed sequence of post entities.
In our case, however, the service will be very plain. Create a new
app/model/post/PostService.js
file with the following content:
export default class PostService {
constructor(resource) {
this._resource = resource;
}
getPosts() {
return this._resource.getEntityList();
}
}
Now that we have our entity, factory, resource and service, you may be thinking that this is a little too much code for something so simple. Well, that depends on many things. If you can expect mostly uniform data from your REST API with little modifications required, you may want to use a reflection-powered solution that requires you only to specify a single configuration item (API root URL) and to create entity classes. The solution shown here is more robust and flexible, allowing you to make slight adjustments to suit every resource you are working with as required.
So how do we actually start using our post service? First we need to wire everything up.
Open now the app/config/bind.js
file and add the following imports to the
beginning of the file:
import PostFactory from 'app/model/post/PostFactory';
import PostResource from 'app/model/post/PostResource';
import PostService from 'app/model/post/PostService';
Now add the following declaration at the first line of the exported init
callback:
oc.inject(PostFactory, []);
oc.inject(PostResource, ['$Http', PostFactory]);
oc.inject(PostService, [PostResource]);
This configures our object container (the dependency injector provided by IMA.js). The object container serves mostly the following purposes: configuring class constructor dependencies, setting default implementing classes of interfaces, creating aliases for classes, global registry of values and an instance factory and registry.
Just like an ordinary dependency injector, the Object Container is used to specify the dependencies of our classes, create and retrieve shared instances and create new instances on demand.
The object container allows us to:
- specify the dependencies of a class using the
inject()
method (the dependencies will be passed in the constructor) - create string aliases for our classes using the
bind()
method (like the$Http
alias we used to retrieve the HTTP agent provided by IMA.js) - create named object container-global constants using the
constant()
method - specify the default implementation of an interface using the
provide()
method (this allows us to specify the interface as a dependency and switch the implementation everywhere in our application by changing a single configuration item)
We can only access the object container in this configuration file. After that it works behind the scenes, providing dependencies and managing our shared instances as needed. You can find out more about its API by studying the documentation or the source code.
Let's take another look at the $Http
alias among the dependencies of our post
resource - as already mentioned, this is an instance of the HTTP agent (client)
provided by the IMA.js. All utilities and services provided by IMA.js are bound
to the object container via aliases and have their aliases prefixed with $
to
prevent accidental name collisions, but most can be used without having to use
aliases as dependency identifiers by specifying the classes and interfaces
themselves as dependencies.
Next we modify the configuration of the HomeController
alias by adding the
PostService
dependency to the dependency list. The resulting code line looks
as follows:
oc.inject(HomeController, [PostService]);
This will push an instance of our post service as the first argument to the
constructor of our home page controller. To make a use of it, open the
controller file (app/page/home/HomeController.js
) and modify the constructor
to look like this:
constructor(postService) {
super();
this._postService = postService;
}
With the post service safely in our _postService
field, we can use it to
fetch the posts from the server in our load()
method:
return {
posts: this._postService.getPosts()
};
Finally, we can make use of our new post entities in the home controller's view
(app/page/home/HomeView.jsx
). Let's modify the _renderPosts()
method to look
like this:
return this.props.posts.map((post) => {
return (
<Post key={post.id} content={post.content} author={post.author}/>
);
});
Notice how we use the post.id
as the react element key here.
Now go ahead, refresh the page and you'll see the posts still there, but this time fetched from the server! Or are they?
If you have the developer tools open, you may notice that the network log does
not show any request to http://localhost:3001/static/api/posts.json
.
You may remember that IMA.js is an isomorphic JavaScript application stack. This means that our application gets rendered at the server first, then it is sent to the client with a serialized state information, and then the application is "reanimated" at the client-side using the state information.
IMA.js caches the requests we make using the HTTP service at the server-side
and sends the serialized cache to the client. The cache is then deserialized at
the client-side, so the request to
http://localhost:3001/static/api/posts.json
we do in our post resource will
be resolved from the cache, leading to no additional HTTP request being made.
Now that we are fetching posts from the server and fully understand how that works, let's dive into writing new posts to our guestbook in the 5th part of this tutorial.
- Introduction
- Static view
- Adding some state
- Fetching the data from the server
- Writing posts
- Final polish