To-Do list app using React

Introduction

This is a To-Do List app extended example which was created with DataKernel and React. It shows how to integrate React in a DataKernel project and how to simply manage routing using the HTTP module. You can find full example sources on GitHub.

Here we will consider only the main app class - ApplicationLauncher. It uses DataKernel’s HttpServerLauncher and AsyncServlet classes for setting up an embedded application server. With this approach, you can create servers with no XML configurations or third-party dependencies. Moreover, HttpServerLauncher will automatically take care of launching, running and stopping the application. You’ll only need to provide launcher with servlets.

Creating Launcher

ApplicationLauncher is the main class of the program. Besides launching the application, it also handles routing and most of the corresponding logic. We will use DataKernel HttpServerLauncher and extend it:

public final class ApplicationLauncher extends HttpServerLauncher {

	private static final StructuredCodec<Plan> PLAN_CODEC = object(Plan::new,
			"text", Plan::getText, STRING_CODEC,
			"isComplete", Plan::isComplete, BOOLEAN_CODEC);

	private static final StructuredCodec<Record> RECORD_CODEC = object(Record::new,
			"title", Record::getTitle, STRING_CODEC,
			"plans", Record::getPlans, ofList(PLAN_CODEC));

	@Provides
	RecordDAO recordRepo() {
		return new RecordImplDAO();
	}

	@Provides
	Executor executor() {
		return newSingleThreadExecutor();
	}

}

So, first, we define codecs for our two entities: Plan and Record. These codecs will help us to encode/decode Plan and Record from/to JSONs to communicate with TodoService.js.

Method object returns a new StructuredCodec and, in the case of Plan and Record entities, that requires the following parameters:

  • TupleParser2 constructor - basically a constructor of your class with 2 parameters. There are several predefined TupleParsers for up to 6 parameters.
  • String field1 - the first field of the encoded/decoded class
  • Function getter1 - getter of field1
  • StructuredCodec codec1 - codec for field1 (depends on the type of the field, for example, STRING_CODEC, BOOLEAN_CODEC)
  • String field2 - another field of the class
  • Function getter2 - getter of field2
  • StructuredCodec codec1 - codec for field2

Next, we provided RecordDAO with the application business logic and Executor which is needed for our AsyncServlet. AsyncServlet loads static content from /build directory and takes care of the routing:

@Provides
AsyncServlet servlet(Executor executor, RecordDAO recordDAO) {
	return RoutingServlet.create()
			.map("/*", StaticServlet.ofClassPath(executor, "build/")
					.withIndexHtml())

}

Routing in DataKernel HTTP module resembles Express approach. Method map(@Nullable HttpMethod method, String path, AsyncServlet servlet) adds routes to the RoutingServlet:

  • method (optional) is one of the HTTP methods (GET, POST etc)
  • path is the path on the server
  • servlet defines the logic of request processing. If you need to get some data from the request while processing you can use:
    • request.getPathParameter(String key)/request.getQueryParameter(String key) (see example of query parameter usage) to provide the key of the needed parameter and receive back a corresponding String
    • request.getPostParameters() to get a Map of all request parameters

Pay attention to the * in the provided request. It states that whichever path until the next / is received, it will be processed by our static servlet, which uploads static content from /build directory.

Servlet should be able to add a new record, get all available records, delete record by its id and also mark plans of particular record as completed or not, so we provide corresponding routing:

.map(POST, "/add", loadBody()
		.serve(request -> {
			ByteBuf body = request.getBody();
			try {
				Record record = JsonUtils.fromJson(RECORD_CODEC, body.getString(UTF_8));
				recordDAO.add(record);
				return HttpResponse.ok200();
			} catch (ParseException e) {
				return HttpResponse.ofCode(400);
			}
		}))
.map(GET, "/get/all", request -> {
	Map<Integer, Record> records = recordDAO.findAll();
	return HttpResponse.ok200()
			.withJson(ofMap(INT_CODEC, RECORD_CODEC), records);
})
//[START REGION_4]
.map(GET, "/delete/:recordId", request -> {
	int id = parseInt(request.getPathParameter("recordId"));
	recordDAO.delete(id);
	return HttpResponse.ok200();
})
//[END REGION_4]
.map(GET, "/toggle/:recordId/:planId", request -> {
	int id = parseInt(request.getPathParameter("recordId"));
	int planId = parseInt(request.getPathParameter("planId"));

	Record record = recordDAO.find(id);
	Plan plan = record.getPlans().get(planId);
	plan.toggle();
	return HttpResponse.ok200();
});

Pay attention to the requests with :, for example:

.map(GET, "/delete/:recordId", request -> {
	int id = parseInt(request.getPathParameter("recordId"));
	recordDAO.delete(id);
	return HttpResponse.ok200();
})

: states that the following characters until the next / is a variable whose keyword, in this case, is recordId.

Running the application

To run the example clone DataKernel and import it as a Maven project. Check out branch v3.0. Before running the example, build the project (Ctrl + F9 for IntelliJ IDEA).

Then, run the following command in datakernel -> examples -> tutorials -> advanced-react-integration -> front directory in terminal:

$ npm i
$ npm run-script build

Open ApplicationLauncher and run its main method. Then open your favourite browser and go to localhost:8080. Try to add and delete some tasks or mark them as completed.