views:

244

answers:

2

I am a novice to intermediate JavaScript programmer. This is some of by best code.

I'm not sure if this community is looking for stuff like this, but I'd be very interested to get some critiques on this JavaScript comment widget I wrote.

I have a couple of mentors, but I can never get enough code review. It seems to improve my code by light years whenever I get feedback from an expert.

So, if you have any thoughts on this code, criticism or commentary, I'd very much appreciate it.

I open soured the code with a demo here: http://www.trailbehind.com/comment_widget/

Here is the complete module pasted as well:

//stores and displays comments, and lets users post comments
function CommentWidget(id, getCommentsURL, commentURL, replyURL, commentEditURL, commentText) {
  //the div that contains the comment form and comments
  if (id) this.div = document.getElementById(id); 
  //a url to post comments to
  this.commentURL = commentURL?commentURL:'/comments/post_comment/'; 
  //a url to post replies to
  this.replyURL = replyURL?replyURL:'/comments/post_reply/';
  //a url that returns a JSON list of comments  
  this.getCommentsURL = getCommentsURL?getCommentsURL:'/comments/get_comments/'; 
  //a url to post comment edits to  
  this.commentEditURL = commentEditURL?commentEditURL:'/comments/post_edit/';
  //the text to display in the comment text box
  this.commentText = commentText?commentText:'Your comment goes here.';
  //the object being commented on: {'type':string, 'id':integer}
  this.target = null;                       
  //a list of comments 
  this.comments = [];                     
}


//displays the comment button and comments
CommentWidget.prototype.refresh = function() {
  if (typeof this.div == 'undefined') return;
  clearDiv(this.div)
  this.div.appendChild(this.commentButtonHTML());
  if (this.comments.length > 0) { 
    this.div.appendChild(this.commentsHTML());  
  }
}


//displays the comment form when the comment button is pressed
CommentWidget.prototype.displayCommentForm = function() {  
  if (typeof this.div == 'undefined') return;
  clearDiv(this.div)
  this.div.appendChild(this.commentFormHTML());
  if (this.comments.length > 0) { 
    this.div.appendChild(this.commentsHTML());   
  }
}


//returns the HTML for the button that reveals the comment form
CommentWidget.prototype.commentButtonHTML = function() {
  var input = document.createElement('input');
  input.type = "button";
  input.value = "Comment";
  var self = this;
  input.onclick = function () { self.displayCommentForm() };
  var p = document.createElement('p');
  p.appendChild(input);
  return p;
}  


//returns the HTML for the commenting form
CommentWidget.prototype.commentFormHTML = function() {
  var form = document.createElement('form');
  var h1 = document.createElement('h1');
  h1.appendChild(dct('Comment on \"' + map.target.name + '\"'));
  form.appendChild(h1);
  var textarea = document.createElement('textarea');
  textarea.onclick = expandCommentBox;
  textarea.name = "comment";
  textarea.value = this.commentText;
  textarea.className = 'commentText'
  form.appendChild(textarea);
  var input = document.createElement('input');
  input.type = 'button';
  var self = this
  input.onclick = function() { self.postComment(form) };
  input.value = "Submit";
  form.appendChild(input);
  input = document.createElement('input');
  input.type = 'button';
  input.onclick = function() { self.refresh() };
  input.value = "Cancel";
  form.appendChild(input);
  return form;
}


//returns the HTML for the tree of comments
CommentWidget.prototype.commentsHTML = function() {
  //children[0] is a list of comments with no parent
  //chidlren[i] is a list of comments whose parent comment id is i
  var children =  [];
  children[0] = [];
  for (var i = 0; i < this.comments.length; i++) {
    comment = this.comments[i];
    //if the comment has no parent, append it to the no parents list
    if (comment.parent == null){
      children[0].push(comment);
    }
    //if a list already exists for the parent, append the comment to that list
    else if (children[comment.parent]){       
      children[comment.parent].push(comment);
    }
    //otherwise, create a list of comments for that parent and append the comment
    else{  
      children[comment.parent] = new Array();
      children[comment.parent].push(comment); 
    }          
  }
  var commentHTML = document.createElement('div');
  commentHTML.appendChild(this.createCommentHTML(children[0], children));
  return commentHTML
}


//recursive function to create the HTML for the comments
CommentWidget.prototype.createCommentHTML = function(comments, children) {
  var ul = document.createElement('ul'); 
  for (var i = 0; i < comments.length; i++){
    var comment = comments[i];   
    var li = document.createElement('li');
    li.appendChild(document.createTextNode(comment.comment));
    var div = document.createElement('div');
    div.style.color = '#696969';
    li.appendChild(div);  
    if (comment.comment != 'comment withdrawn' && comment['author']) {
      div.innerHTML = "by ";
      var a = document.createElement('a');
      a.href = '/user/' + comment.username;
      a.innerHTML = comment['author'];
      div.appendChild(a);
    } else { 
      div.innerHTML += "by Anonymous";
    }
    div.innerHTML += " " + comment.time_created + " ago "
    var a = document.createElement('a');
    a.replyID = comment.id;
    a.className = 'jLink';
    var self = this;
    if (comment.username == user) {
      a.onclick = function() { self.revealEditForm(this) };
      a.innerHTML = "(edit)";
    } else {
      a.onclick = function() { self.revealReplyBox(this) };
      a.innerHTML = "(reply)";
    }
    div.appendChild(a);
    div.appendChild(document.createElement('br')); 
    div.appendChild(document.createElement('br')); 
    ul.appendChild(li);
    //if the comment has children, then recurse
    if (children[comment.id] != null){
      var subul = this.createCommentHTML(children[comment.id], children);
      ul.appendChild(subul);
    }
  } 
  return ul;
} 


CommentWidget.prototype.revealEditForm = function(replyA) {
  replyA.onclick = null;
  var c = null; 
  for (var i = 0; i < this.comments.length; i++) {
    console.log(this.comments[i].id)
    console.log(replyA.replyID)
    if (this.comments[i].id == replyA.replyID) 
      c = this.comments[i];
  }
  var p = document.createElement('p');
  var form = document.createElement('form');
  p.appendChild(form);
  form.commentId = replyA.replyID;
  var textarea = document.createElement('textarea');
  textarea.className = 'commentText'
  textarea.onclick = function () {
    this.className += ' clickedTextBox';
  }
  textarea.name = 'commentDiv' + replyA.replyID;
  textarea.value = c.comment;  
  form.appendChild(textarea);
  var input = document.createElement('input');
  input.type = 'button'; 
  var self = this;
  input.onclick = function () { self.postEdit(this.parentNode) } ;
  input.value = 'Save';
  form.appendChild(input);
  input = document.createElement('input');
  input.type = 'button';
  input.onclick = function() { self.refresh() };
  input.value = "Cancel";
  form.appendChild(input);
  var li = replyA.parentNode.parentNode;
  li.style.listStyle = 'none';
  clearDiv(li)
  li.appendChild(p);
}


//reveals a reply form when a reply link is clicked
CommentWidget.prototype.revealReplyBox = function(replyA) {
  replyA.onclick = null;
  var p = document.createElement('p');
  var form = document.createElement('form');
  p.appendChild(form);
  form.replyId = replyA.replyID;
  var textarea = document.createElement('textarea');
  textarea.className = 'replyText'
  textarea.onclick = expandCommentBox;
  textarea.name = 'replyDiv' + replyA.replyID;
  textarea.value = "Your reply goes here.";  
  form.appendChild(textarea);
  var input = document.createElement('input');
  input.type = 'button'; 
  var self = this;
  input.onclick = function () { self.postReply(this.parentNode) } ;
  input.value = 'Reply';
  form.appendChild(input);
  input = document.createElement('input');
  input.type = 'button';
  input.onclick = function() { self.refresh() };
  input.value = "Cancel";
  form.appendChild(input);
  clearDiv(replyA)
  replyA.appendChild(p);
}


//initiates an XHR request
CommentWidget.prototype.asyncRequest = function(method, uri, form) {
  var o = createXhrObject()
  if(!o) { return null; } 
  o.open(method, uri, true);
  o.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  var self = this;
  o.onreadystatechange =  function () {self.callback(o)};
  if (form) { 
    o.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
    o.send(makePostData(form)); 
  } else {
    o.send(''); 
  }  
}


//after a comment is posted, this rewrites the comments on the page
CommentWidget.prototype.callback = function(o) {                  
  if (o.readyState != 4) { return }
  //turns the JSON string into a JavaScript object.
  var response_obj = eval('(' + o.responseText + ')');
  console.log(response_obj);
  this.comments = response_obj.comments;
  this.displayCommentForm();
  this.updateCommentLink();
}


//creates a POST strng from a form
function makePostData (form) {
  var data = []
    for (var i = 0; i < form.elements.length; i++) {
    oElement  = form.elements[i];
    oName = oElement.name;
    oName = encodeURIComponent(oName) + '=';
    oValue = encodeURIComponent(oElement.value);
    data[i] = oName + oValue;
  }
  return data.join('&');
}


//a cross-browser-safe way to make an XHR request, cribbed from YUI
function createXhrObject() {
  var msxml_progid = ['Microsoft.XMLHTTP',
               'MSXML2.XMLHTTP.3.0',
           'MSXML2.XMLHTTP']
  var http;
  try {
    // Instantiates XMLHttpRequest in non-IE browsers and assigns to http.
    http = new XMLHttpRequest();
  } catch(e) {
    for(var i=0; i<msxml_progid.length; ++i){
      try {
        // Instantiates XMLHttpRequest for IE and assign to http
        http = new ActiveXObject(msxml_progid[i]);
    break;
      } catch(e2) {}
    }
  } finally {
    return http;
  }
}


//expands and clears the intro message from a comment or reply box when it's selected
function expandCommentBox() {
  this.className += ' clickedTextBox';
  this.value = "";
  this.onclick = null;
}


//a function to abbreviate the common task of clearing a div on the page
function clearDiv(div) {
  while(div.hasChildNodes()) {
    div.removeChild(div.firstChild);
  }
}


//update the widget with the new node target and report list
CommentWidget.prototype.updateFromFetch = function(responseObj) {
  console.log(responseObj);
  this.target = responseObj.target;
  this.comments = responseObj.comments;
  this.updateCommentLink();
}


//set the clicked node as the target of the widget and fetch comments
CommentWidget.prototype.updateFromNodeClick = function(node) {
  this.target = node;
  var commentURL = this.getCommentsURL + 'node/' + this.target.id; 
  this.asyncRequest('GET', commentURL, null);
}


//set the clicked trail as the target of the widget and fetch comments
CommentWidget.prototype.updateFromTrailClick = function(trail) {
  this.target = trail;
  var commentURL = this.getCommentsURL + 'trail/' + this.target.id; 
  this.asyncRequest('GET', commentURL, null);
}


CommentWidget.prototype.updateCommentLink = function() {
  var a = gel('commentLinkId');
  if (!a) return;
  rac(a);
  if (this.comments.length == 0) {
    a.appendChild(dct('Comment'));
  } else {
    a.appendChild(dct('Comments (' + this.comments.length + ')'));
  }
}


//posts the new comment, and then updates the page with the new comment
CommentWidget.prototype.postComment = function(form) {
  if (this.target.gps) {
    var commentURL = this.commentURL + 'trip/' + this.target.id;
  } else if (this.target.attr == 'trail') {
    var commentURL = this.commentURL + 'trail/' + this.target.id;
    //} else if (this.target.attr == 'user') {
    //var commentURL = this.commentURL + 'user/' + this.target.id;
  } else {
    var commentURL = this.commentURL + 'node/' + this.target.id;
  }   
  var self = this;
  var action = function () { self.asyncRequest('POST', commentURL, form) };
  checkForLoginThenPost(action);
}


//posts the new reply comment, and then updates the page with the new comment list
CommentWidget.prototype.postReply = function(form) {
  var replyURL = this.replyURL + form.replyId; 
  var self = this;
  var action = function () { self.asyncRequest('POST', replyURL, form) };
  checkForLoginThenPost(action);
}


//posts the new reply comment, and then updates the page with the new comment list
CommentWidget.prototype.postEdit = function(form) {
  var editURL = this.commentEditURL + form.commentId; 
  this.asyncRequest('POST', editURL, form);
}
+2  A: 
  //a list of comments
  this.comments = [];

I think your comments are occasionally not needed. IE: it's pretty obvious that that is a list, and probably holds comments.

This is one of those times where the code (variable names, specifically) is clear enough where a comment is unnecessary.

nilamo
Honestly, I don't think 90% of his comments are needed.
musicfreak
That's a fair criticism. A guy who taught me to program really drove it into me to write at least one comment per function, and I'm fairly religious about that, but my comments aren't always that useful. I've gotten that critique from colleagues before too.
Andrew Johnson
Comments are only useful if you can read the code below without going cross-eyed
Aiden Bell
Or accurate? Where the comment says, "returns the HTML for the button that reveals the comment form", I'd be expecting the method to return a string. It actually returns a DOM element. (Or am I the only one who thinks HTML ==> String?)
Dan Breslau
+2  A: 

After looking over some of your code, I have a few comments:

Div ID:

I notice that in the constructor for your widget, you use the line:

if (id) this.div = document.getElementById(id);

to have a DIV ID passed in to be used in your class. However, you then go on to check whether this property exists several times after this in your code with the line:

if (typeof this.div == 'undefined') return;

Perhaps you want to check whether a valid DIV is passed in in the first place, which will save you from checking later on. Also, you might want to implement this so that a DIV ID can be passed in, or the actual DIV element itself.

DOM Helpers:

I also notice that you have several occurrences of createElement() and appendChild(). DOM manipulation through Javascript isn't among the most concise of tasks. If you don't want to use libraries to help you with this, that's fine, but you may want to consider using some kind of helper functions, such as:

function create(tag, parent)
{
    var el = document.createElement(tag);   
    append(el, parent);

    return el;
}

function append(el, parent)
{
    if(typeof(el)!="undefined"&&typeof(parent)!="undefined")
        parent.appendChild(el);
}

You could also add these to your class's prototype. This would decrease the amount of code that you're writing, so would help to keep the size of your widget to as minimal as possible. It would also make it easier to read.

Property helpers:

I also noticed that you have lots of property assignments going on, such as:

textarea.className = 'replyText'

and

input.type = 'button';

Since that you are assigning one property per line, and keep having to write:

element.property = value;

over and over again, this increases the size of your code, and also, again, makes it harder to read. I would suggest that you add some kind of method to add properties from an object literal - I often do this when setting multiple styles on elements. Something like:

function setProperties(el, properties)
{
    for(var p in properties)
    {
        el[p] = properties[p];
    }
}

Code such as this:

var textarea = document.createElement('textarea');
textarea.className = 'replyText'
textarea.onclick = expandCommentBox;
textarea.name = 'replyDiv' + replyA.replyID;
textarea.value = "Your reply goes here.";

Would then be rewritten as something like:

var textarea = document.createElement('textarea');
setProperties(textarea, {className: 'replyText',
                         onClick: expandCommentBox,
                         name: 'replyDiv' + replyA.replyID,
                         value: "Your reply goes here."});

As you can see, it drastically reduces the number of lines used (I placed each property in the object literal on separate lines to make it more readable) and also reduces the amount of code you are writing, which makes it more lightweight. You could also add it as an extra optional parameter to a DOM create element method as I discussed above, if you implemented one.

Perspx
I think I have to disagree with the property helpers there. It's not *that* much less code and I don't really think it makes it more readable. It only saves you from writing the word textarea, something that can already be done using a with statement.
Andy E
I guess it depends on the context - in some places I think that it would be worth it, when he sets several properties which can take up five lines, and are repetitive. If the object literal was an optional third argument for a scenario where he creates the element, appends it to another element, AND sets properties on it, for example, it would be very useful, and would combine 7 lines of code into 1 or 2.
Perspx