In this part of our per-paragraph commenting annotations app, we emulate the loading of annotations like they would with Ajax calls, and then associate each annotation with its respective content-block.
So far, the annotations were just there when the page loaded as our page samples were fairly static. This means that the page was pieced together by the server before it was sent to the client browser where Javascript did what it did. While this could very well be the case, it seems like the content rendering and the annotation rendering engines are the same, or at least closely coupled. But loosely coupled, app-based designs are proliferating nowadays for they offer much more flexibility in terms of design, and are more easy to scale. Also, it makes a lot more sense.
There are a few more considerations too.
- What the user primarily wants is to read the main content. I think that is the major motive why he/she is visiting the page, right? Thus, that part of the page must load with minimum hitch and clutter. If the user has a slow internet connection, there will be two things that would happen. First, if all data is collated and served in a single go, he'll only see the page once everything has arrived from the server. Until then he might see the text, but in a poorly formatted fashion (since CSS might not have loaded, or scripts are still waiting for the page to load completely). And second, the Javascripts usually work after the page is ready. Thus if Annotations are interspersed in between the main article, they are a nuisance to look at, big time. So, it would be much better if the loading of annotations can be delegated to a time after the main content has loaded.
- Annotations can also be made on the fly. That is why we are making them as close to the content, on the margin of the leaf. We haven't yet designed the form, but we'll do so in due time. We had wished that the user must not have to reload the page after posting each annotation he makes. This means, that we'd be using Ajax calls to post annotations in the future and updating the DOM through Javascript. In that event, a single annotation will be coming and going on the wire (when we are making the annotation). So, if it can come once, it can also come twice, or any number of time using the same Ajax methods. Then, why do we have to load the first instance through a different path of backend handling?
So what we would like to do now, is to start from a state where there is no annotations belonging to any container. Also, you will remember that the boxed-up approach for clubbing the content and its annotations was done later. The base application (which renders the original content) may not want to change its template. Thus, we'll take care of manipulating the DOM to create those containers when annotations are loaded. Thus, here is our agenda:
- Load the page with as little as possible markup.
- Load annotations through Javascript (using fixtures in this case).
- Transform the page so that we may reach the state we demonstrated in iteration 4.
There are still a few pre-requisites for the markup though. It is loosely coupled, not completely decoupled design.
- The outermost block on which annotations would work must have the class
- Content blocks must have unique IDs
Here, one may raise a doubt that when we are making an effort to have unique IDs for each content block and also having an identifier wrapping them all up, what is the problem with loading more markup so that the coupling of content-block and its annotations is rendered by the server itself. It is true, and I think this middle ground between everything on frontend and everything at backend is quite amicable. So, what we can do is load the skeleton layout from the backend rather than javascript manipulating the DOM to such extensive degree, while the Javascript may later fetch the annotations and put them in their respective containers. The only constraint placed in the backend template must be that it must not in any manner mess with the overall layout of content in case the Javascript fails to load.
But for the current tutorial, we'll continue with our original idea, that the backend provides minimal markup while Javascript does the heavy-lifting.
Other than that, there are a few bug-fixes we'd introduce.
- On mobile resolutions, when we click the annotations button, the sliding out is too abrupt. In fact, there is no sliding out. We'll smoothen that.
- The rolling out of content from the viewport on mid-size screens is smooth, but rolling back in isn't.
- The annotations must disappear if we click anywhere else on the screen.
On with it!
First, the markup looks like it was in iteration 1, except that the paragraphs now have an ID, and the overall block has an ID. That is as much markup we want to start with. The stylesheets would stay the same as we left in iteration 4.
See the Pen Annotations Tutorial (Part 1): Final by Anshul Thakur (@anshulthakur) on CodePen.
Now, all is left for the Javascript to fill in data. We'll break it down into finite steps.
- When the page loads, we need to find the container we must work on.
- For each immediate child of that block, we wrap the child in the annotation grouping
div
. - In each annotation grouping container, we append the annotations container, and the button.
- Load annotations from fixtures.
- Update the annotation count in the annotation speech bubble.
Okay, in the current implementation, we could shift the 4th and 5th steps a little bit up to save an iteration. But let it stay that way for a while (for reasons that will become apparent, if they aren't yet).
Find the container we want to work on:
var annotationCopy = $('#commentable-container').clone(true,true);
We have two options, either we could remove the element from flow, and reinsert it later after we had worked on it; or we can work on each child in the flow itself. But the second approach would be taxing (I'm not exactly sure though). In the article 'Speeding up JavaScript: Working with the DOM', it is recommended that we "create multiple elements and insert them into the DOM triggering a single reflow. It uses something called a DocumentFragment. We create a DocumentFragment outside of the DOM (so it is out-of-the-flow). We then create and add multiple elements to this. Finally, we move all elements in the DocumentFragment to the DOM but trigger a single reflow.". We're not exactly using a DOM Fragment, but yes, we're making elements out-of-the-flow and only inserting them once we are done, so that there is only one reflow.
Now, why did we clone it? If we just removed the entire container out of the flow, how would we know where we have to re-insert it later? For that, we'd either need to leave a placeholder there, which means creating a placeholder and inserting it there. Also if, God forbid, something went wrong with the Javascript in between (though that shouldn't really happen in production version) the reader would be left searching for the content in the want of what, annotations? Clearly content is more important to the reader.
Also note that we've not just cloned the element, but also retained all the event handler and data attributes. That is just because we don't want to assume that our's will be the only or the first script to run on that content. We clearly don't want to annul anyone else's script effects.
For each immediate child of the block, wrap the block in an annotation grouping:
annotationCopy.children().each(wrapContent);
Where wrapContent
is:
var wrapContent = function(){ index = parseInt($(this).attr('id')); $(this).wrap('<div class="annotation--container clearfix" data-section-id="'+index+'"></div>');}
Append annotation container:
In the wrapContent
method, append the following lines:
$('<div class="comments clearfix">'+ '<h3 class="comments--toggle rectangular-speech"> </h3><p>+ </p>'+ '<div class="comments-container hidden">'+ '</div></div>').insertAfter($(this));
At this time, we've covered fair ground. So, let us reload the page to see how it looks. How does it look? Mine looks fine. I can now see annotation bubbles when I hover on their respective content blocks. Check the markup too (in the inspector to make sure the insertion and appending worked well.
Load annotations:
Now that we've done the necessary groundwork, let us just load the annotations from fixed fixture for now. Later, they'll be served from the server via Ajax calls.
But it would be good time to see what information we would want the annotations to contain so that they can be rendered and associated with:
- Annotation content.
- The content on which they were created.
- The user who created the annotation.
{ content_id: "1", paragraph_id: "1", annotation_body: "Lorem Ipsum", user : { user_id: "1", user_name: "John Doe", user_gravatar: "images/default_large.png", user_url: "#", }, }
I guess this is self-explanatory? What we will receive from the server will be a list of such objects encoded as JSON, and we'll parse them into our markup.
Now, let's see how the call will take place later. We'll make an Ajax call to load annotations, and it will return asynchronously. For a successful response, we will have registered a callback function to which, control will be passed when the server replies. This function must then take that JSON formatted data, and render it into our markup. When we already know this much, let us write placeholder functions which can later be expanded for more complex tasks, like making the ajax query.
First, we declare a few hand-code fixtures:
fixtures = [ { content_id: "1", paragraph_id: "1", annotation_body: "Lorem Ipsum", user : { user_id: "1", user_name: "John Doe", user_gravatar: "images/default_large.png", user_url: "#", }, }, { content_id: "1", paragraph_id: "1", annotation_body: "Your documentation source should be written as regular Markdown files, and placed in a directory somewhere in your project.", user : { user_id: "2", user_name: "Someone Else", user_gravatar: "images/female.png", user_url: "#", }, }, { content_id: "1", paragraph_id: "6", annotation_body: "But we have to make tradeoffs to make something work", user : { user_id: "1", user_name: "John Doe", user_gravatar: "images/default_large.png", user_url: "#", }, } ];
As you can see, it is an array of objects with each object having a similar structure.
var loadAnnotations = function(){ renderAnnotations(fixtures); };
This function currently makes a direct call into renderAnnotations()
method as if the data was available. By doing this, we are trying to emulate how our Ajax call would have called that method.
var renderAnnotations = function(data){ console.log('Data received: '); for(i=0; i< data.length;i++){ /* Find the parent container */ currentObject = annotationCopy.children('[data-section-id="'+data[i]['paragraph_id']+'"]'); /* Find the annotations container inside that and append the comment */ currentComment = $(''); currentComment.text(data[i]['annotation_body']); currentObject.find('.comments-container').append(currentComment); /* Update the annotation count on the button */ temp = currentObject.find('.comments--toggle p').text(); commentCount = ((temp = currentObject.find('.comments--toggle p').text()) ==='+') ? 1 : (parseInt(temp)+1); currentObject.find('.comments--toggle p').text(commentCount); } };
Here, we iterate through all the data objects passed into the method, and try to find their parent containers (which we had created in the previous step). We are not doing any sorting here, just pick and drop, assuming that the backend server has taken care of ordering them for us according to some rule (sort by creation date, maybe?).
For each annotation, we create its markup, and append it to the end of the comment-container
, and also update its comment count. Note that this function does not assume how many comments are there before, or how many have come. This implies that we can reuse this method as it is at a later stage when we post new comments, and rest assured that the comment count will be updated, and the new annotation will be visible as soon as it is successful without disturbing the rest of the annotations.
Update page:
Now that we've done all our processing, we can finally reattach our DOM subtree we've been morphing, into the DOM which will cause 1 reflow.
$('#commentable-container').replaceWith(annotationCopy);
Then, we bind to the annotation bubbles to toggle the viewing and hiding of annotations.
bindAnnotations();
Okay, so far so good. We've squeezed the elephant, now the only thing left is its tail. Let us attend to the minor bugs we mentioned at the beginning of the post.
-
Transitions aren't smooth.
Add the following to #annotations-container:
@media(min-width: $screen-md) and (max-width: $screen-lg){ transition: all 1s; }
The reason why it wasn't working on folding is because it was being applied to the class '
annotations-active
' and when we clicked on the bubble, we removed that class. So, rather than changing the property, we were removing it altogether, and hence, it won't transition.For the drop down effect, we are in a fix. I just discovered that CSS transitions will not work on
display
property. The reason is again the same as above. When we remove that class (hidden), it just ceases to exist, the browser doesn't make it fade away, but whoosh, it's gone. A few people recommended not touching thedisplay: block
property, but playing with the height and opacity properties. I did, but they too aren't producing the desirable effects. Another person suggested using thekeyframes
to control how animations occur. I tried that, and the results are just as good as fiddling with the height and opacity properties.@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @mixin animation($function: fadeIn, $duration: .3s){ -webkit-animation: $function $duration; animation: $function $duration; }
And using it in
.comments-container
:@include animation(fadeIn, .15s);
This isn't the most elegant solution, but certainly better than the abrupt nature of the previous versions. For a more smooth effect, I think we'll resort to jQuery, but later and only if it is too important.
-
Annotations must fold back on clicking anywhere other than the annotation.
We'll use the '
closest()
' method from jQuery, to find out if we are anywhere in the vicinity of the annotation we were working on. If we aren't, we can toggle it off. If we are, then we probably don't want to toggle them off yet.Thus, we'll bind a click event to the entire document if any annotation is active. This event must be unbound when there are no annotations selected.
So, inside the '
toggleAnnotations()
' method, we'd add the following lines for binding. Note that we add it before we've updated our elements.if(annotations.currentAnnotation === 0){ $(document).on('click', function(e) { console.log('Trigger'); //console.log($(event.target)); //console.log($(event.target).closest('.side-comment').length); /* * If the element we clicked on is not close to the comments, * close the annotations then. */ if (!($(e.target).closest('.comments').length)){ /* Hide all other annotations */ $('.comments').children('.comments-container').addClass('hidden'); $('*[data-section-id="'+annotations.currentAnnotation+'"]').find('.comments--toggle').removeClass('annotation-highlight'); $('#commentable-container').removeClass('annotations-active'); /* Reset the currently selected Annotation state */ annotations.currentAnnotation = 0; } }); } /* OR the other method */ $('*[data-section-id="'+annotations.currentAnnotation+'"]').find('.comments--toggle').removeClass('annotation-highlight');...
And then, we add the unbind method, inside the if condition of collapsing:
$(document).unbind();
Simple?
Here is the completed pen
See the Pen Annotations tutorial: Dynamically loading annotations by Anshul Thakur (@anshulthakur) on CodePen.
With this, we have fairly completed our frontend design. Except one minor thing. We have no form to create annotations. Nor can we delete any. Also, we wouldn't want anonymous annotations, because privacy belongs to the ones with names, the anonymous have nothing to lose. We will complete the form and posting part in our next tutorial, and leave the deletion part for later parts, though it isn't much of a hassle. (It's because I forgot when I was actually doing it, and only later did it occur to me that I was missing a very basic thing in CRUD.) So long!