Skip to main content

Writing Posts

In previous part we created our classes and services to handle data fetching from server. We also learned something about the vital parts of IMA.js - object container and server-side rendering. In this smaller section of the tutorial, we're going to be processing input from user and sending those data to the server.

Processing input from user

To write new posts, we need to address several issues:

  • Notifying the controller that the user submitted the new post.
  • Sending the post to our server via HTTP (remember, we don't have an actual REST API backend, so we're going to mock this).
  • Waiting for our post to be saved.
  • Showing the updated list of posts.

We want the controller to handle submitting posts to the guest book in our application instead of the postingForm component to maintain a single "source of truth" in our application. This should be the case for all information that is related to the page as a whole. Local information (for example starting music playback when the user clicks the play button of some player component) may remain stored within the component itself, as it is not necessarily important to the overall state of the page.

We'll use another IMA.js service to notify the controller that the user submitted a new post - the EventBus. In case you did not read the details about communication between the controller and the view , the EventBus is an internal event system, built on top of DOM events, used for communication like this.

Updating the form

First update the <form ... markup in the view of our PostingForm component (app/component/postingForm/PostingForm.jsx) by adding an onSubmit event listener:

<form action="" method="post" onSubmit={e => this._onSubmit(e)}>

Then we need to hook our inputs to _onChange() handler which will set the contents of those input to the state of our PostingForm component.

<input
id="postForm-name"
className="form-control"
type="text"
name="author"
value={this.state.author}
onChange={e => this._onChange(e)}
placeholder="Your name"
/>
...
<textarea
id="postForm-content"
className="form-control"
name="content"
value={this.state.content}
onChange={e => this._onChange(e)}
placeholder="What would you like to tell us?"
/>

We can't forget to define the default state for these two keys:

#containerRef;
constructor(props, context) {
super(props, context);
this.#containerRef = createRef();
this.state = {
author: '',
content: ''
};
}

Import the createRef from React to the beginning of the file:

import { createRef } from 'react';

...and add ref={this.#containerRef} to the first div in the the component:

...
render() {
return (
<div className='posting-form card' ref={this.#containerRef}>
<form action='' method='post' onSubmit={e => this._onSubmit(e)}>
...

This adds some internal state to our form component, which we'll maintain separately from the main page state maintained by the home page controller.

Now we need to define the _onChange() handler. We're going to use the name attribute of input and textarea fields so both can be handled by defining only one method. But feel free to define onChange handlers for each input separately, if that suits you better. Our _onChange() handler will look like this:

_onChange(event) {
this.setState({
[event.target.name]: event.target.value
});
}

The only thing that remains is to define the _onSubmit() in our component:

_onSubmit(event) {
event.preventDefault();

this.fire(this.#containerRef.current, 'postSubmitted', {
author: this.state.author,
content: this.state.content
});

// Reset the state after submitting
this.setState({
author: '',
content: ''
});
}

Firing EventBus events

We can fire EventBus events through this.fire() method that is available to us by extending the AbstractComponent. So in this example we fire the postSubmitted event through EventBus with the form data as the event data, clear the form, and finally we prevent the browser from submitting the form to the server.

Typescript: In functional components, the fire method is provided to us by the useComponent hook, which gives us access to the utility methods.

The this.fire() method is a short-hand for this.utils.$EventBus.fire(this, ...) call, which fires the custom DOM event using the EventBus. The this.utils property is set to the view utils - various objects, data and services that are useful for rendering the UI - and is obtained from the React context. The value returned by this.utils is configurable in the app/config/bind.js configuration file and is represented by the constant $Utils.

Capturing EventBus events

Now we need a way to capture the event in our home page controller, so open up the home controller (the app/page/home/HomeController.js file) and add the following method:

onPostSubmitted(eventData) {
// TODO
}

The IMA.js will automatically invoke this method when the postSubmitted event bus event occurs. For details on how this mechanism works, please refer to the Emitting events using the EventBus section of the third chapter of this tutorial.

Notice that our onPostSubmitted() event listener is a public method. This is because it represents the (event) interface for the view components.

Updating our post service classes

Before we fill our onPostSubmitted() event listener with content however, we need to update our post model classes first. Open the post factory class (app/model/post/PostFactory.js) and add the following method for creating a single post:

createEntity(entityData) {
return new PostEntity(entityData);
}

Since we don't like to repeat ourselves, update the return statement in the createList() method as well:

return entities.map(entityData => this.createEntity(entityData));

Now add the following method for creating new posts to the post resource (app/model/post/PostResource.js):

createPost(postData) {
return this._http
.post('http://localhost:3001/static/static/public/posts.json', postData)
.then(response => this._factory.createEntity(response.body));
}

This method accepts a plain object containing the new post data and submits them to the server using an HTTP POST request. The _http.post() method sends the HTTP POST request and returns a promise that resolves to the server's response with the response body parsed as JSON. We then use the server's response to create a post entity representing the saved post.

Next we need to create a method for creating posts in our post service (app/model/post/PostService.js):

createPost(postData) {
postData.id = null;
return this._resource.createPost(postData);
}

This method sets the id field to null as it is expected for posts that were not created yet (the post IDs should be generated by our backend) and uses the post resource to create the post. The method returns a promise that resolves to the post entity representing the created post.

Defining the onPostSubmitted method

With that in place, we can now fill in the contents of the onPostSubmitted() event listener in the home page controller (app/page/home/HomeController.js):

this._postService
.createPost(eventData)
.then(() => this._postService.getPosts())
.then(posts => this.setState({ posts }));

This snippet calls the createPost() method with our event data, waits for the post to be created, then requests the current list of posts from the post service and updates the posts field in the view's state using the setState() method. The setState() method updated only the fields of the state that are present in the provided state object without modifying the rest, and notifies the view about the new state so that the view is re-rendered.

Updating the API

Now that everything is wired up, we can start submitting new posts, right? Well, not so fast. Remember, we do not have an actual REST API backend, so the HTTP POST request will fail and no new post will be created.

Since we don't want to implement an actual backend, we will work around this issue by implementing a mock HTTP agent that fetches the posts from the server and then acts as if sending subsequent requests to the server while managing our state (the created posts) locally and creating responses on spot without any actual communication with the server. This approach is useful for both tests and our simple tutorial.

To create our HTTP mock create the app/mock directory and the app/mock/MockHttpAgent.js file with the following content:

import { HttpAgentImpl } from '@ima/core';

const GET_DELAY = 70; // milliseconds
const POST_DELAY = 90; // milliseconds

export default class MockHttpAgent extends HttpAgentImpl {
static get $dependencies() {
return ['$HttpAgentProxy', '$Cache', '$CookieStorage', '$Settings.$Http'];
}

constructor(proxy, cache, cookie, config) {
super(proxy, cache, cookie, config);

this._posts = null;
}

get(url, data, options = {}) {
if (!this._posts) {
return super.get(url, data, options).then(response => {
this._posts = response.body;

return {
body: this._posts.map(post => Object.assign({}, post))
};
});
}

return new Promise(resolve => {
setTimeout(() => {
resolve({
body: this._posts.map(post => Object.assign({}, post))
});
}, GET_DELAY);
});
}

post(url, data, options = {}) {
if (!this._posts) {
return this.get(url, {}).then(() => this.post(url, data));
}

return new Promise(resolve => {
setTimeout(() => {
let clone = Object.assign({}, data);

clone.id = this._posts[0].id + 1;
this._posts.unshift(clone);

resolve({
body: Object.assign({}, clone)
});
}, POST_DELAY);
});
}
}

Let's take this class apart and take a look at what it does. We extend the ima/http/HttpAgent class which is the HTTP agent provided by IMA.js, so we need to obtain its dependencies in our constructor (proxy, cache, cookie, config) and pass them to the super-constructor.

Next we set up the _posts field that we'll use to keep track of all posts and few REST API methods:

  • The get() method checks whether we already have the posts fetched from the server, and, if we don't, it uses the super-implementation to fetch them and store them in the _posts field. If the posts have already been fetched, the method returns a promise that resolves to a clone of the posts after the configured delay.

  • The post() method checks whether we already have the posts fetched from the server, and, if we don't, it fetches them using the get() method and then calls itself again. If we already have the posts fetched, the method clones the data passed to it in parameters, generates an ID, stores the new record as the first element of the _posts array while shifting the rest of the posts and resolves the returned promise after the configured delay to the stored post.

We included the delays in our get() and post() methods to simulate the latency imposed by a real networking. Also notice how we always clone the data we receive before storing them internally and return only clones of our internal posts storage. This is to emulate the server behavior reliably, so that new posts won't modify previously returned post arrays and later modifications of data passed to or received from our mock server won't modify the internal state or data returned by other calls to our methods.

To wire up our HTTP mock into our application, we need to update the dependencies of the app/model/post/PostResource.js:

import PostFactory from './PostFactory';
import MockHttpAgent from 'app/mock/MockHttpAgent';

export default class PostResource {
static get $dependencies() {
return [MockHttpAgent, PostFactory];
}

...
}

Go ahead and check the result in the browser, you will now be able to write new posts to our guestbook (which will disappear once you reload the page, since we keep the posts only in our HTTP mock).