Bicker Progress - (Almost) Inventors' Day
As previously discussed, I am currently in the process of remaking Bicker with modern technologies and architecture, hopefully along with lessons learned over the last decade about what makes a good community system. In the interest of transparency, I’m using the blog as a kind of developer journal while I work, posting occasional updates as to where things stand.
If you want to read the other posts in the series, you can get a full list of posts under the “bicker” tag.
Unfortunately, this has been another busy week, so I didn’t accomplish as much as I’d like, but there’s still some progress.
Testing, Preliminaries
As mentioned last time, it’s time to add tests to see if adding them after the code has already been written is significantly more painful than starting development with them.
I settled on RSpec, which was trivial to get running. Add the rspec
gem to the Gemfile
, install, and run…
rspec --init
That creates a configuration file (.rspec
) and helper object (spec/spec_helper.rb
). Running rspec
now shows exactly what we’d expect: Nothing to do, so no failures.
That’ll have to do, for now…
Inserting Replies
We could get away with posting the reply form to the server and retrieving the updated page. That would follow the same scheme as posting messages already do. But that’s a little bit out of date, in terms of user experience, and is also slower and clunkier than necessary.
Instead, what makes more sense is to just send the reply information to the server and change the DOM for the target paragraph.
Added Instrumentation
Either way, something we definitely need is more tracking. We need the ID of the paragraph—something we already have, but aren’t yet passing into the React components—and we need the distance into the string where each punctuation mark sits.
The latter sounds easy enough, of course. The paragraph controller already splits the string into an array of substrings—alternating text with punctuation, in most cases—and each of those substrings obviously has a length. However, that’s a bit more naïve than we want, because the length of our punctuation in Markdown (which is how the paragraphs are stored, remember) isn’t always the same as the length of our punctuation in HTML.
We can fix that by adding a mapping in the Paragraph component.
const lengths = {
'.': 1,
',': 1,
'!': 1,
'?': 1,
';': 1,
':': 1,
'(': 1,
')': 1,
'&': 1,
'/': 1,
'–': 2,
'—': 2,
'—': 3,
'–': 3,
'…': 3,
'…': 3,
};
If our fragment is a key in the object, we have a new length. Otherwise, we can use c.length
as we normally would.
Mark the Replies
Now, we get to the meat of things. Our various React components need paragraph-id and paragraph-offset properties, now, and these trickle down to the reply form, itself. There, we can add an offset
and pid
attribute to the button.
While we’re here, we also need to change the button type to simply button
and give it an onClick
handler. This solves our unnecessary problem from last time of how to prevent the submission from refreshing the page. We can start with something simple, like this.
function submitReply(e) {
e.persist();
var target = e.target;
var offset = Number(target.attributes.offset.value);
var paraId = Number(target.attributes.pid.value);
console.log(`${paraId} - ${offset}`);
}
Now, the reply form should have enough information to pack up the reply to send it back to the controller.
Sending the Reply
We now need to expand our submitReply()
function to also retrieve and escape the reply message, then send it off. It’s not a great move, but for now, I’m going to exploit the fact that the textarea
control is the first child of the reply form. There’s a better way to find it, but it’s not something I have time to test and debug, right now.
function submitReply(e) {
e.persist();
var target = e.target;
var offset = Number(target.attributes.offset.value);
var paraId = Number(target.attributes.pid.value);
var parent = target.parentNode;
var textarea = parent.childNodes[0];
var message = encodeURIComponent(textarea.value);
var request = new XMLHttpRequest();
var url = `/api/v1/messages/reply?paraId=${paraId}&offset=${offset}&message=${message}`;
request.open(
'GET',
url,
true
);
request.onload = function() {
if (this.status >= 200 && this.status < 400) {
var resp = this.response;
console.log(this.response);
} else {
console.error(this);
}
};
request.onerror = function() {
// There was a connection error of some sort
};
request.send();
}
Note that we:
- Pull the offset and paragraph ID off of the button attributes,
- Assume the
textarea
is the first child of the button’s parent, - URL-encode the reply text so that it can be inserted into the request path, and
- Send the AJAX request.
We also need to adjust the routes.
namespace :api do
namespace :v1 do
resources :messages, only: [
:index,
:create,
:destroy,
:update,
:reply
] do
collection do
get 'reply'
end
end
end
end
We allow the new action and create a path for it. And then restart the server, since we need to reload the routes.
Obviously, the responses need to be filled in, once we start getting useful responses.
Receiving the Reply Information
Over in app/controllers/api/v1/messages_controller.rb
, we need to create our reply
action. We’ll just go with a stub, for now, to confirm that something happens.
def reply
args = {
paraId: params[:paraId],
offset: params[:offset],
message: params[:message],
}
respond_with args
end
So, now when we try to post a reply, the server sends back something like the following.
{
"paraId": "4",
"offset": "189",
"message": "This is a reply..."
}
That’s clearly only a dumb echoing of the parameters we submitted, but it validates that we now have enough information to get the relevant paragraph, replace it with two pieces, and add the reply as a child to the first part.
Splitting the Paragraph
Now that we have the above working, we can get to messing around with the data. After we get rid of that args
object, we need the paragraph object and the two halves of the text.
paraId = params[:paraId].to_i
offset = params[:offset].to_i
message = params[:message].strip
paragraph = Paragraph.find(paraId)
firstPart = paragraph.content[0..offset].strip
lastPart = paragraph.content[offset + 1..-1].strip
The parameters are passed in as text, of course, meaning that we want them converted to integers for IDs and indices. Since we know that all of these divisions are happening at a punctuation mark, we also want to strip
the resulting fragments, so that we aren’t left with any dangling whitespace.
From here, we already know how to handle the replacement, because we already did something very much like this to insert the message paragraphs in the first place!
Specifically, we want to work backwards:
- Create a new paragraph with
lastPart
as thecontent
and itsnext_id
pointing to whatever our existing paragraph currently does. - Update the existing paragraph so that its
content
isfirstPart
and itsnext_id
to point to our new paragraph. We don’t want to create a new one, because there’s (usually) another paragraph out there already pointing to this one. - Note that, if
lastPart
is empty, we can skip both steps, since there isn’t a second paragraph. - Create any paragraphs needed for the reply, with their
parent_id
set pointing to the original paragraph.
We already have code to handle that last step, so we only really need to copy it over from the original messages controler, for now, and come back to refactor it later.
Next
That’s it for this time. From here, we need to show the replies, both when adding the replies to finish the circuit we’ve started here and on displaying the message in its entirety…probably in the reverse order. And now that we have RSpec in place and we have most of the base functionality running, it’s a good time to start writing some tests.
Oh, and it looks like there’s a small bug in escaping the message text. The punctuation in the escaped text gets transformed into buttons and we also miscount the length of the strings due to the extra characters in an escaped string. Small issue, but I’ll still want to deal with it.
Credits: The header image is Invention of the Compass from New Inventions of Modern Times, circa 1600, courtesy of The Metropolitan Museum of Art.
By commenting, you agree to follow the blog's Code of Conduct and that your comment is released under the same license as the rest of the blog.
Tags: ruby rails programming project devjournal bicker