diff --git a/trac/htdocs/js/auto_preview.js b/trac/htdocs/js/auto_preview.js
--- a/trac/htdocs/js/auto_preview.js
+++ b/trac/htdocs/js/auto_preview.js
@@ -1,6 +1,85 @@
 // Automatic preview through XHR
 
 (function($) {
+  // Enable automatic previewing of form submissions.
+  //
+  // Arguments:
+  //  - `args`: additional form data to be passed with the XHR.
+  //  - `update`: the function that is called with the preview results. It
+  //              is called with the request data and the reply.
+  $.fn.autoFormPreview = function(args, update) {
+    if (auto_preview_timeout <= 0)
+      return this;
+    var form = this.closest('form');
+    var data = {};
+    for (var key in args)
+      data[key] = args[key];
+    var timer = null;
+    var timeout = auto_preview_timeout * 1000;
+    var updating = false;
+    var queued = false;
+    
+    // Return true iff the values have changed
+    function values_changed(new_values) {
+      for (var i in values) {
+        var value = values[i], new_value = new_values[i];
+        if ((value.name != new_value.name) || (value.value != new_value.value))
+          return true;
+      }
+      return false;
+    }
+    
+    // Request a preview through XHR
+    function request() {
+      if (!updating) {
+        var new_values = form.serializeArray();
+        if (values_changed(new_values)) {
+          values = new_values;
+          updating = true;
+          var data = {};
+          for (var i in values) {
+            var value = values[i];
+            data[value.name] = value.value.replace(/\n/g, '\r\n');
+          }
+          for (var key in args)
+            data[key] = args[key];
+          $.ajax({
+            type: "POST", url: form.attr('action'), data: data,
+            dataType: "html",
+            success: function(reply) {
+              if (queued)
+                timer = setTimeout(request, timeout);
+              updating = false;
+              queued = false;
+              update(data, reply);
+            },
+            error: function(req, err, exc) {
+              updating = false;
+              queued = false;
+            },
+          });
+        }
+      }
+    }
+    
+    // Trigger a request after the given timeout
+    function trigger() {
+      if (!updating) {
+        if (timer)
+          clearTimeout(timer);
+        timer = setTimeout(request, timeout);
+      } else {
+        queued = true;
+      }
+      return true;
+    }
+
+    var values = form.serializeArray();
+    return this.each(function() {
+      $(this).keydown(trigger).keypress(trigger).change(trigger).blur(request);
+    });
+  };
+
   // Enable automatic previewing to <textarea> elements.
   //
   // Arguments:
@@ -56,5 +135,5 @@
       
       $(this).keydown(trigger).keypress(trigger).blur(request);
     });
-  }
+  };
 })(jQuery);
diff --git a/trac/ticket/templates/ticket.html b/trac/ticket/templates/ticket.html
--- a/trac/ticket/templates/ticket.html
+++ b/trac/ticket/templates/ticket.html
@@ -18,23 +18,6 @@
         $("div.description").find("h1,h2,h3,h4,h5,h6").addAnchor(_("Link to this section"));
         $(".foldable").enableFolding(false, true);
       <py:when test="ticket.exists">
-        var args = {realm: "ticket", id: ${ticket.id}, escape_newlines: ${int(preserve_newlines)}}
-        $("#comment").autoPreview("${href.wiki_render()}", args, function(textarea, text, rendered) {
-            $("#ticketchange div.comment").html(rendered);
-            if (rendered)
-              $("#ticketchange").show();
-            else if ($("#ticketchange ul.changes").length == 0)
-              $("#ticketchange").hide();
-        });
-        $("#trac-comment-editor textarea").autoPreview("${href.wiki_render()}", args,
-                                                       function(textarea, text, rendered) {
-          var comment = $("#trac-comment-editor").next("div.comment");
-          comment.html(rendered);
-          if (rendered)
-            comment.show();
-          else
-            comment.hide();
-        });
         $("#modify").parent().toggleClass("collapsed");
         $(".trac-topnav a").click(function() { $("#modify").parent().removeClass("collapsed"); });
 
@@ -49,6 +32,25 @@
         }
         actions.click(updateActionFields);
         updateActionFields();
+        
+        $("#propertyform").find("textarea, select, :text, :checkbox, :radio")
+          .autoFormPreview({}, function(data, rendered) {
+            $("#ticketchange-content").html(rendered);
+            if (rendered)
+              $("#ticketchange").show();
+            else if ($("#ticketchange ul.changes").length == 0)
+              $("#ticketchange").hide();
+          });
+        var args = {realm: "ticket", id: ${ticket.id}, escape_newlines: ${int(preserve_newlines)}};
+        $("#trac-comment-editor textarea").autoPreview("${href.wiki_render()}", args,
+                                                       function(textarea, text, rendered) {
+          var comment = $("#trac-comment-editor").next("div.comment");
+          comment.html(rendered);
+          if (rendered)
+            comment.show();
+          else
+            comment.hide();
+        });
       </py:when>
       <py:otherwise>
         $("#field-summary").focus();
@@ -190,8 +192,7 @@
       <form py:if="has_property_editor" method="post" id="propertyform"
             action="${ticket.exists and href.ticket(ticket.id) + '#trac-add-comment' or href.newticket()}">
         <!--! Add comment -->
-        <div py:if="ticket.exists and can_append" class="field"
-             py:with="show_comment_preview = (change_preview.fields or change_preview.comment) and cnum_edit is None">
+        <div py:if="ticket.exists and can_append" class="field">
           <div class="trac-nav">
             <a href="#content" title="View ticket fields and description">View</a> &uarr;
           </div>
@@ -211,16 +212,6 @@
             Warnings are shown at the <a href="#warning">top of the page</a>. The ticket validation
             may have failed.
           </div>
-          <!--! Preview of ticket changes -->
-          <div id="ticketchange" class="ticketdraft" style="${not show_comment_preview and 'display: none' or None}">
-            <h3 class="change" id="${'cnum' in change_preview and 'comment:%d' % change_preview.cnum or None}">
-              <span class="threading" py:if="'replyto' in change_preview">
-                in reply to: ${commentref('&uarr;&nbsp;', change_preview.replyto)}
-              </span>
-              <i18n:msg params="author">Changed by ${authorinfo(change_preview.author)}</i18n:msg>
-            </h3>
-            <xi:include href="ticket_change.html" py:with="change = change_preview"/>
-          </div>
         </div>
 
         <div>
@@ -377,6 +368,19 @@
           </fieldset>
         </div>
 
+        <!--! Preview of ticket changes -->
+        <div py:if="ticket.exists and can_append" id="ticketchange" class="ticketdraft"
+             style="${(not (change_preview.fields or change_preview.comment)
+                       or cnum_edit is not None) and 'display: none' or None}">
+          <h3 class="change" id="${'cnum' in change_preview and 'comment:%d' % change_preview.cnum or None}">
+            <span class="threading" py:if="'replyto' in change_preview">
+              in reply to: ${commentref('&uarr;&nbsp;', change_preview.replyto)}
+            </span>
+            <i18n:msg params="author">Changed by ${authorinfo(change_preview.author)}</i18n:msg>
+          </h3>
+          <div id="ticketchange-content"><xi:include href="ticket_change.html" py:with="change = change_preview"/></div>
+        </div>
+
         <!--! Attachment on creation checkbox -->
         <p py:if="not ticket.exists and 'ATTACHMENT_CREATE' in perm(ticket.resource.child('attachment'))">
           <label>
diff --git a/trac/ticket/templates/ticket_change.html b/trac/ticket/templates/ticket_change.html
--- a/trac/ticket/templates/ticket_change.html
+++ b/trac/ticket/templates/ticket_change.html
@@ -13,7 +13,7 @@ Arguments:
       xmlns:i18n="http://genshi.edgewall.org/i18n"
       py:with="show_editor = value_of('show_editor', False)" py:strip="">
   <ul py:if="change.fields" class="changes">
-    <li py:for="field_name, field in change.fields.items()">
+    <li py:for="field_name, field in sorted(change.fields.iteritems(), key=lambda item: item[1].label.lower())">
       <strong>${field.label}</strong>
       <py:choose>
         <py:when test="field_name == 'attachment'"><i18n:msg params="name">
diff --git a/trac/ticket/web_ui.py b/trac/ticket/web_ui.py
--- a/trac/ticket/web_ui.py
+++ b/trac/ticket/web_ui.py
@@ -454,6 +454,7 @@ class TicketModule(Component):
                 version = int(version)
             except ValueError:
                 version = None
+        xhr = req.get_header('X-Requested-With') == 'XMLHttpRequest'
 
         req.perm('ticket', id, version).require('TICKET_VIEW')
         ticket = Ticket(self.env, id, version=version)
@@ -539,6 +540,8 @@ class TicketModule(Component):
             # validates and there were no problems with the workflow side of
             # things.
             valid = self._validate_ticket(req, ticket, not valid) and valid
+            if xhr:
+                req.args['preview'] = True
             if 'preview' not in req.args:
                 if valid:
                     # redirected if successful
@@ -572,6 +575,11 @@ class TicketModule(Component):
         self._insert_ticket_data(req, ticket, data,
                                  get_reporter_id(req, 'author'), field_changes)
 
+        if xhr:
+            # TODO: Differentiate between comment append and comment edit
+            data['change'] = data['change_preview']
+            return 'ticket_change.html', data, None
+
         mime = Mimeview(self.env)
         format = req.args.get('format')
         if format:

