“NoT” badges
There are two card-like distinct sections on the UI now. For simplicity’s sake, let’s call them “cards” until the UI changes. The left one (top in mobile view) is the “task card”, and the right one is the “note card”. As their names suggest, the task card shows a list of tasks, and the note card shows a list of notes.
On page load, the note card shows a list of available note names. (It currently shows all notes. Will need some logic for showing some specific notes only, for example, the last-modified 10 notes, or 5 “pinned” ones and the last-modified 5. But I haven’t decided on that yet.) The task card shows nothing except a button for creating new tasks on page load. If a note name on the note card is clicked, the tasks that belong to that note are shown on the task card. Pretty straight-forward, intuitive UI.
Beside each note name on the note card, a number may be shown in the form of a badge. This is, currently, the number of “pending” tasks for that note. I’m calling these badges “Not” badge. “NoT” here is a dumb acronym for “number of tasks”.
It might have been a lot better to call them “pending badges”, but I’m planning to show the number of “done” tasks too there. This will be due to personal preferences, since I don’t think many people would want both numbers there. Seems like Mantle will need a “preferences” feature to show/hide “done” badges.
Updating “NoT” badges
These badges need to update in real-time as the user marks tasks as “done” or “pending”. There are a few ways to do that:
-
Updating data directly in the Vue instance after the request to the API (for updating a task’s status) has succeeded.
-
Sending a “GET” request for fetching notes after the request to the API has succeeded.
-
Using WebSockets and Laravel’s event broadcasting.
The choice might seem obvious, seeing how we’re using Echo already. But only because we’re using one thing doesn’t mean we can’t use anything else. The best way to do something is to use whatever tools do it best.
#1 Directly updating the relevant note in Vue
Way #1 depends on whether the request to the API succeeded or not. We’d need to check the response status and do the update in Vue data only if a success status was returned.
axios.put(url, data)
.then(response => {
if (response.status === 200) {
// Somehow directly update in Vue
}
})
Updating Vue data would need some lines of code. Because, if we only checked the status to make sure the request succeeded, we’d need to loop through Vue’s notes array to find out which note’s task status changed. (Remember, to update a “NoT” badge, we need to modify Vue’s notes array which is in the NoteList Vue component, and the clicked task’s ID would come from the TaskListcomponent.)
if (response.status === 200) {
this.$emit('status-changed', {
id: response.data.noteId,
numOfPendingTasks: response.data.numOfPendingTasks
});
}
updateNotes(updatedNoteData) {
for (let i = this.notes.length - 1; i >= 0; --i) {
const note = this.notes[i];
if (note.id === updatedNoteData.id) {
note.numOfPendingTasks = updatedNoteData.numOfPendingTasks;
}
}
}
#2 Replacing/updating the entire notes array in Vue
Way #2 makes the second part of way #1 easier. Instead of finding which note should be updated, we get data for the entire note list and just replace it (like rebuilding a house instead of repairing a single room). This is easier than the first but the payload is much larger.
By the way, this can be done without sending a separate “GET” request. The response would just have to return the array.
if (response.status === 200) {
this.$emit('status-changed', response.data.notes);
}
updateNotes(notes) {
this.notes = notes;
}
#3 Using WebSockets and event broadcasting
Way #3 is easier on the front-end side. The two things are independent here:
-
Sending a request to the API for updating a task’s status.
-
Listening for any update in notes.
Once we set up an Echo listener, it will update the notes array whenever there’s an appropriate change on the back-end.
Echo.channel('notes')
.listen('NoteListNumsOfTasksUpdated', (data) => {
this.notes = data.notes;
});
And we can just send requests to the API whenever we need.
axios.put(url, data);
This way, the code for updating a task status can be separated from the code for updating “NoT” badges.
If that doesn’t seem very impressive, well, WebSockets payloads are quite smaller.
But there’s still work to do. We need to create events and listeners on the Laravel end. And that might make way #3 seem like a lot more work than the earlier ways, yet I chose it because WebSockets seemed better to me than the site getting bogged down due to many unfinished HTTP requests.
And don’t forget, the payloads are smaller.
Event listeners for updating “NoT” badges
We already have a TaskUpdated event. But it broadcasts data to update the task list. We need to update the note list. Can an event broadcast multiple sets of data on different channels? Maybe. broadcastWith() returns an array, and broadcastOn can return an array. Maybe the sequence of channel names and data matches up? Or maybe the entire data array is broadcast on all the specified channels.
I didn’t dig deeper that way. Instead, (after reading up on listeners) it seemed it’d be cleaner if there was a listener just for updating “NoT” badges. But Echo listens to events, not listeners. So, the listener would have to emit another event.
This made sense. Rather than listening to a catch-all “TaskUpdated” event for both updating the task list and updating the note list, each should get a separate event.
So, here’s how things turned out:
Laravel Laravel Front-end
controller listener Echo |
| | | |
| | | |
|<--------------------- Task status changes -----------------------|
| | | |
| "TaskUpdated" | | Updates |
|---------------->|---- Data for task list ----->|---------------->|
| event | | task list |
| | | |
| | | Updates |
| |---- Data for note list ----->|---------------->|
| | | "NoT" badges |
| | | |
This is what I did at first. But the payload was too large since NoteListNumsOfTasksUpdated was sending data for the entire note list. Plus, the event and the controller both had to be sending data in the same format, which made it difficult to maintain properly later.
This was definitely not cleaner.
Event listeners for updating “NoT” badges, but better
Behold the TaskStatusChanged event that broadcasts whatever data it is passed! And the listener passes it an object with only two properties: a note ID and the number of pending tasks for that note. So, the payload become much smaller.
public function __construct($data, $channelName, $channelType = 'public') {
$this->data = !is_array($data) ? [] : $data;
$this->channelName = !$channelName ? '' : $channelName;
$this->channelType = !$channelType ? '' : $channelType;
}
public function broadcastOn() {
if ($this->channelName !== '') {
if ($this->channelType === 'public') {
return new Channel( $this->channelName );
}
}
}
public function broadcastWith() {
return $this->data;
}
The structure of TaskStatusChanged has to be this way. Since the name is a generic one, it might get used for other purposes in future. So, instead of restricting the class now and change again later, it’s better to make it flexible from the start.
Also, all the case-specific code is in the listener now. UpdateNotBadge - a listener made for a specific purpose - now listens to TaskUpdated - an event that may be fired by various causes - and broadcasts some specific data via an event. Echo will be able to identify different cases based on channel names.
So, now, for any future change we need to make that’s related to updating “NoT” badges, we’ll only have to modify the UpdateNotBadge listener which is not at all likely to be modified for some other purpose. And if we ever change the “NoT” badges system entirely, we’ll just remove the listener from EventServiceProvider.php and then delete the UpdateNotBadge.php file. TaskStatusChanged will never even know about it.
The problem of returning data in the same format from multiple places is solved here, though not because of anything WebSockets does. The only argument against returning a single note’s data could be that a loop is needed in Vue. But that’s a lot better than broadcasting data for all notes (a large payload) every time.
Echo.channel('notes')
.listen('TaskStatusChanged', (note) => {
this.updateNotBadgeNumber(note);
});
updateNotBadgeNumber(updatedNote) {
for (let i = this.notes.length - 1; i >= 0; --i) {
const note = this.notes[i];
if (note.id === updatedNote.id) { // Matching note's badge is updated
note.numOfPendingTasks = updatedNote.numOfPendingTasks;
break;
}
}
}
Logic is separate inside a specific listener, payload is smaller, everyone is happy.