Solved

Having trouble modifying an element in a custom app


<div id="trcWidget" title="TRC Widget">
  Checking
</div>
<script type="text/javascript">
    var istrc = "{{ticket.custom_fields.istrc}}";
    if (istrc = null) {alert("It's null")};
    if (istrc == "true") {
      $("#trcWidget").html("This is a TRC");
    } else {
      $("#trcWidget").html("NOT a TRC");
    }
</script>

 This gives me the error  Cannot read property 'html' of null


I'm sure it's something to do with load order but I can't get around it using the normal tricks like $(document).ready()

Maybe https://github.com/uzairfarooq/arrive can help with that. Include 'arrive.min.js' and then try something like this:
<script type="text/javascript" >

jQuery(document).arrive(".text.custom_text.field", function() {
// 'this' refers to the newly created element
var $newElem = jQuery(this);
console.log("Element created");
// Either of the next two lines will work (to hide the element)
//jQuery(".text.custom_text.field").hide();
//$newElem.hide();
// unbind all arrive events on document element which are watching for ".text.custom_text.field" selector
jQuery(document).unbindArrive(".text.custom_text.field");
});

</script>

 

 

I don't see arrivejs on any known CDNs and I'm assuming there's no way to "upload" it to my freshdesk install, so what do you recommend?

Create at least one custom ticket field of type custom. You now should see in your ticket page source one <li class="text custom_text field"> element for each text custom field.


Create a custom app with the following code. I have copied the contents of https://raw.githubusercontent.com/uzairfarooq/arrive/master/minified/arrive.min.js in the first <script> section so this should work as is.

 

<script type="text/javascript" >
  /* The contents of https://raw.githubusercontent.com/uzairfarooq/arrive/master/minified/arrive.min.js follow:  */
  /* * arrive.js * v2.3.1 * https://github.com/uzairfarooq/arrive * MIT licensed * * Copyright (c) 2014-2016 Uzair Farooq */ 
  var Arrive=function(a,b,c){"use strict";function l(a,b,c){e.addMethod(b,c,a.unbindEvent),e.addMethod(b,c,a.unbindEventWithSelectorOrCallback),e.addMethod(b,c,a.unbindEventWithSelectorAndCallback)}function m(a){a.arrive=j.bindEvent,l(j,a,"unbindArrive"),a.leave=k.bindEvent,l(k,a,"unbindLeave")}if(a.MutationObserver&&"undefined"!=typeof HTMLElement){var d=0,e=function(){var b=HTMLElement.prototype.matches||HTMLElement.prototype.webkitMatchesSelector||HTMLElement.prototype.mozMatchesSelector||HTMLElement.prototype.msMatchesSelector;return{matchesSelector:function(a,c){return a instanceof HTMLElement&&b.call(a,c)},addMethod:function(a,b,c){var d=a[b];a[b]=function(){return c.length==arguments.length?c.apply(this,arguments):"function"==typeof d?d.apply(this,arguments):void 0}},callCallbacks:function(a){for(var c,b=0;c=a[b];b++)c.callback.call(c.elem)},checkChildNodesRecursively:function(a,b,c,d){for(var g,f=0;g=a[f];f++)c(g,b,d)&&d.push({callback:b.callback,elem:g}),g.childNodes.length>0&&e.checkChildNodesRecursively(g.childNodes,b,c,d)},mergeArrays:function(a,b){var d,c={};for(d in a)c[d]=a[d];for(d in b)c[d]=b[d];return c},toElementsArray:function(b){return"undefined"==typeof b||"number"==typeof b.length&&b!==a||(b=[b]),b}}}(),f=function(){var a=function(){this._eventsBucket=[],this._beforeAdding=null,this._beforeRemoving=null};return a.prototype.addEvent=function(a,b,c,d){var e={target:a,selector:b,options:c,callback:d,firedElems:[]};return this._beforeAdding&&this._beforeAdding(e),this._eventsBucket.push(e),e},a.prototype.removeEvent=function(a){for(var c,b=this._eventsBucket.length-1;c=this._eventsBucket[b];b--)a(c)&&(this._beforeRemoving&&this._beforeRemoving(c),this._eventsBucket.splice(b,1))},a.prototype.beforeAdding=function(a){this._beforeAdding=a},a.prototype.beforeRemoving=function(a){this._beforeRemoving=a},a}(),g=function(b,d){var g=new f,h=this,i={fireOnAttributesModification:!1};return g.beforeAdding(function(c){var i,e=c.target;c.selector,c.callback;(e===a.document||e===a)&&(e=document.getElementsByTagName("html")[0]),i=new MutationObserver(function(a){d.call(this,a,c)});var j=b(c.options);i.observe(e,j),c.observer=i,c.me=h}),g.beforeRemoving(function(a){a.observer.disconnect()}),this.bindEvent=function(a,b,c){b=e.mergeArrays(i,b);for(var d=e.toElementsArray(this),f=0;f<d.length;f++)g.addEvent(d[f],a,b,c)},this.unbindEvent=function(){var a=e.toElementsArray(this);g.removeEvent(function(b){for(var d=0;d<a.length;d++)if(this===c||b.target===a[d])return!0;return!1})},this.unbindEventWithSelectorOrCallback=function(a){var f,b=e.toElementsArray(this),d=a;f="function"==typeof a?function(a){for(var e=0;e<b.length;e++)if((this===c||a.target===b[e])&&a.callback===d)return!0;return!1}:function(d){for(var e=0;e<b.length;e++)if((this===c||d.target===b[e])&&d.selector===a)return!0;return!1},g.removeEvent(f)},this.unbindEventWithSelectorAndCallback=function(a,b){var d=e.toElementsArray(this);g.removeEvent(function(e){for(var f=0;f<d.length;f++)if((this===c||e.target===d[f])&&e.selector===a&&e.callback===b)return!0;return!1})},this},h=function(){function h(a){var b={attributes:!1,childList:!0,subtree:!0};return a.fireOnAttributesModification&&(b.attributes=!0),b}function i(a,b){a.forEach(function(a){var c=a.addedNodes,d=a.target,f=[];null!==c&&c.length>0?e.checkChildNodesRecursively(c,b,k,f):"attributes"===a.type&&k(d,b,f)&&f.push({callback:b.callback,elem:node}),e.callCallbacks(f)})}function k(a,b,f){if(e.matchesSelector(a,b.selector)&&(a._id===c&&(a._id=d++),-1==b.firedElems.indexOf(a._id))){if(b.options.onceOnly){if(0!==b.firedElems.length)return;b.me.unbindEventWithSelectorAndCallback.call(b.target,b.selector,b.callback)}b.firedElems.push(a._id),f.push({callback:b.callback,elem:a})}}var f={fireOnAttributesModification:!1,onceOnly:!1,existing:!1};j=new g(h,i);var l=j.bindEvent;return j.bindEvent=function(a,b,c){"undefined"==typeof c?(c=b,b=f):b=e.mergeArrays(f,b);var d=e.toElementsArray(this);if(b.existing){for(var g=[],h=0;h<d.length;h++)for(var i=d[h].querySelectorAll(a),j=0;j<i.length;j++)g.push({callback:c,elem:i[j]});if(b.onceOnly&&g.length)return c.call(g[0].elem);setTimeout(e.callCallbacks,1,g)}l.call(this,a,b,c)},j},i=function(){function d(a){var b={childList:!0,subtree:!0};return b}function f(a,b){a.forEach(function(a){var c=a.removedNodes,f=(a.target,[]);null!==c&&c.length>0&&e.checkChildNodesRecursively(c,b,h,f),e.callCallbacks(f)})}function h(a,b){return e.matchesSelector(a,b.selector)}var c={};k=new g(d,f);var i=k.bindEvent;return k.bindEvent=function(a,b,d){"undefined"==typeof d?(d=b,b=c):b=e.mergeArrays(c,b),i.call(this,a,b,d)},k},j=new h,k=new i;b&&m(b.fn),m(HTMLElement.prototype),m(NodeList.prototype),m(HTMLCollection.prototype),m(HTMLDocument.prototype),m(Window.prototype);var n={};return l(j,n,"unbindAllArrive"),l(k,n,"unbindAllLeave"),n}}(window,"undefined"==typeof jQuery?null:jQuery,void 0);
</script>

<script type="text/javascript">
  jQuery(document).arrive(".text.custom_text.field", function() {
    // 'this' refers to the newly created element
    var $newElem = jQuery(this);
    console.log("got it");
    //jQuery(".text.custom_text.field").hide();
    //$newElem.hide();
    // unbind all arrive events on document element which are watching for ".text.custom_text.field" selector
    jQuery(document).unbindArrive(".text.custom_text.field");
  });
</script>
Now, as the ticket page is loaded and each text custom ticket field comes into being you should see in your console log a 'got it' entry ( you will get as many entries as your text custom fields ) and you can test that you can hide these elements by unommenting either of the two .hide() lines.


 

A different example, in case you are using a custom ticket field ( let's say a checkbox field named "ISTRC", created under Admin > Ticket Fields ). The following should work ( provided you change 359268 with your Freshdesk instance ID, which you can find by browsing the source of the ticket page ). Note that Freshdesk creates the element id by using the lowercase name of the field, with spaces changed to underscores, and the Freshdesk instance ID.
<div id="trcWidget" title="TRC Widget">
  Checking
</div>
<script>
  jQuery(document).on('sidebar_loaded', function() {
    var countChecked = function() {
      var n = jQuery("#helpdesk_ticket_custom_field_istrc_359268:checked").length;
      jQuery("#trcWidget").html((n > 0 ? " is" : " is NOT") + " a TRC");
    };
    countChecked();

    jQuery("#helpdesk_ticket_custom_field_istrc_359268").on("click", countChecked);
  });
</script>

 

For the record, what I was talking before was looking for an element that isn't necessarily there when the right sidebar has finished loading but may be created later on. That was a more exotic case and, somehow, I now doubt this is what you are talking about.

 

The sample app references {{requester.id}}.  Where does this come from?  Is there no way to access ticket properties without inferring them from DOM elements?

Also you completely missed the point of my post which was Cannot read property 'html' of null


Turns out $ is not defined so I switched it to jQuery and now I can actually "modify an element in a custom app"

In regards with {{requester.id}} this is an example of 'handlebars' ( text inside '{{' and '}}' ). A number of system fields plus custom ticket fields are available as handlebars. Handlebars are substituted with their respective values when the page loads ( a static replacement ).

Since handlebars are not dynamic, if you'd like to re-check the status of a checkbox, re-read the contents of a text field etc, you have to do it by reading the properties of the right DOM element.



Examples of handlebars:
<script>
    console.log("ticket.id: {{ticket.id}}");
    console.log("ticket.from_email: {{ticket.from_email}}");
    console.log("ticket.requester.id: {{ticket.requester.id}}");
    console.log("ticket.requester.name: {{ticket.requester.name}}");
    console.log("ticket.requester.email: {{ticket.requester.email}}");
    console.log("requester.id: {{requester.id}}");         // same as ticket.requester.id
    console.log("requester.name: {{requester.name}}");     // same as ticket.requester.name
    console.log("requester.email: {{requester.email}}");   // same as ticket.requester.email
    console.log("current_user.id: {{current_user.id}}");
    console.log("current_user.name: {{current_user.name}}");
    console.log("current_user.email: {{current_user.email}}");
    console.log("ticket.vessel_id: {{ticket.vessel_id}}");
    console.log("ticket.istrc: {{ticket.istrc}}");
    // Custom text ticket field named 'My Text"
    console.log("ticket.my_text: {{ticket.my_text}}");
    // Custom checkbox ticket field named 'My Checkbox"
    console.log("ticket.my_checkbox: {{ticket.my_checkbox}}");
</script>

 

Got it now, thanks.


 

<div id="trcWidget" title="TRC Widget">
  Checking
</div>
<script type="text/javascript">
  var istrc = "{{ticket.istrc}}";
  if (istrc) {
    jQuery("#trcWidget").html("This is a TRC");
  } else {
    jQuery("#trcWidget").html("NOT a TRC");
  }
</script>

 

Handlebars is not the right name - they are called 'placeholders' in Freshdesk, and a number of the standard ones for apps are listed in https://support.freshdesk.com/support/solutions/articles/32031-bringing-additional-context-into-freshdesk-using-custom-widgets. BTW, some placeholders are also usable in ticket Email Notifications and Canned Responses ( see https://support.freshdesk.com/support/solutions/articles/52630-understanding-dynamic-content-and-placeholders and https://support.freshdesk.com/support/solutions/articles/87018-why-some-placeholders-are-unavailable-in-the-placeholders-popup ) and FreshThemes ( see https://support.freshdesk.com/support/solutions/folders/117130 ). Unfortunately, there isn't much documentation about them.

 

How do I mark this solved?