IMPORTANT: if you are a student from last year looking for the file I/O work, this is currently available here.
Today we will look at:
These points might be useful for the assignment, but we have not had time to introduce them yet, so we will do it this week.
This may help you. bindPopup()
can take a DOM node as well as plain text or HTML. So you can create a DOM structure containing your form and pass it in as an argument to bindPopup()
, e.g.:
const domDiv = document.createElement('div'); // ... other code to create a form ... marker.bindPopup(domDiv); // add the DOM-created div to a marker} Then, at the point you want to show it, change the
display
property from JavaScript to block
, e.g.:
document.getElementById('myDiv').style.display = 'block';Then, when you want to hide it again, change
display
back to none
:
document.getElementById('myDiv').style.display = 'none';
Now onto the main topic for this week. Today we will look at using a more modular, easier-to-maintain structure for our Node/Express applications by making use of Data Access Objects (DAOs) and controllers.
Note that this is a more advanced topic, aimed at those of you who are coping reasonably well with the module. You should complete previous exercises before attempting this week's - particularly login systems from last week.
class StudentDao { // db is our sqlite database connection // table is the table storing the students constructor(db, table) { this.db = db; this.table = table; } // find a student with a given ID findStudentById(id) { const stmt = this.db.prepare(`SELECT * FROM ${this.table} WHERE id=?`); const rows = stmt.all(id); if (rows.length == 0) { // return null if no results return null; } else { // only one student will be found but "results" will still be an array with one member. // To simplify code which makes use of the DAO, extract the one and only row from the array // and return that. return rows[0]; } } // find all students on a given course findStudentsByCourse(course) { const stmt = this.db.prepare(`SELECT * FROM ${this.table} WHERE course=?`); const rows = stmt.all(course); return rows; // will be an empty array if there are no results } // add a new student addStudent(name, course) { const stmt = this.db.prepare(`INSERT INTO ${this.table} (name,course) VALUES (?,?)`); const info = stmt.run(name, course); return info.lastInsertRowid; } // update a student - takes student ID, name, and course as parameters and // updates the name and the course of the record with that ID // For you to complete! updateStudent(id, name, course) { } // delete a student - takes a student ID as a parameter // and deletes the record with that ID. // For you to complete! deleteStudent(id) { } } export default StudentDao; // so that other code can use it
We can further enhance the modularity of our code through the use of controllers. Controllers are part of the model/view/controller (MVC) architecture, in which we divide the code into three components:
Hopefully, from this you may be able to figure out that the router takes the role of the controller. To improve the modularity of your code, however, you can create separate controller objects to represent the controller, and add methods to these objects to carry out the functionality of your router. Then, in your router, you create a controller object and call these methods from each route in the router.
A good way to organise your project is to have sub-folders within the main folder of your project for routes, controllers and DAOs. These folders could simply be named:
routes
containing all your route files;controllers
containing all your controllers;dao
containing all your DAOs.
So, if we imagine a student router, within the routes
folder:
import express from 'express'; const studentRouter = express.Router(); // Initialise database, not shown // assume the StudentController is inside a 'student.mjs' file within the // 'controllers' folder of the project, as described above import StudentController from '../controllers/student.mjs'; // Create the controller object, and pass in the database connection as an argument const sController = new StudentController(db); // handle get requests to route /id/:id using the controller's findStudentById() method studentRouter.get('/id/:id', sController.findStudentById.bind(sController)); // handle get requests to route /course/:course using the controller's findStudentByCourse() method studentRouter.get('/course/:course', sController.findStudentByCourse.bind(sController)); // handle post requests to /create using the controller's addStudent() method studentRouter.post('/create', sController.addStudent.bind(sController)); export default studentRouter; // so that main application can use itNote how the router creates a StudentController object, passing the database connection to it (which will be needed by the DAO) and note also how we handle each route with methods of the controller object. Remember we looked at "
bind()
" in week 4. Here we are using it slightly differently. bind()
is necessary here to preserve the context of the this
object in callbacks. By default, the context of this
(the current object) is lost in a callback function, and to preserve the context of this
, you have to bind the callback to the object you wish to use as this
(which will be the controller here). You can read about the use of bind()
to preserve the context of this
in callbacks in more detail here.
So, let's look at the Controller object, which would be in the controllers
folder:
// assume the DAO is in a 'student.mjs' file within the //'dao' folder of the project, as described above import StudentDao from '../dao/student.mjs'; class StudentController { constructor(db) { // Create a DAO for communicating with the database this.dao = new StudentDao(db, "students"); } // findStudentById() // calls the DAO's findStudentById() method, passing the ID parameter to // it, and formats the JSON returned. findStudentById(req, res) { try { const student = this.dao.findStudentById(req.params.id); // Remember from the DAO that the method returns null if there are no results if(student == null) { res.status(404).json({error: "No student with that ID"}); } else { res.json(student); } } catch(e) { res.status(500).json({error: e}); } } // findStudentByCourse() // calls the DAO's findStudentByCourse() method, passing the course parameter to // it, and formats the JSON returned. findStudentByCourse(req, res) { try { const students = this.dao.findStudentByCourse(req.params.course); res.json(students); } catch(e) { res.status(500).json({error: e}); } } // addStudent() // calls the DAO's addStudent() method, passing in the 'name' and 'course' // POST data to it, and formats the allocated ID as JSON to send back to the client. addStudent(req, res) { try { const studentId = this.dao.addStudent(req.body.name, req.body.course); res.json({id: studentId}); } catch(e) { res.status(500).json({error: e}); } } } export default StudentController; // so that the router can use itNote how the methods of the controller object can act as route handlers, as they have the
req
and res
parameters. Note also how they call the methods of the DAO and then format the data returned from the DAO as JSON, ready to be used by the client. Hopefully, by examining the code, you can see how the controller is acting as a "middle-person" between the model (the DAO) and the view (the client).
Rewrite your Express server to use appropriate DAOs and controllers as well as routes: