create model and component from javascript

  • 2
  • Question
  • Updated 3 years ago
  • Answered
Ok. My little skype click-to-call and logging app is coming along nicely. I'd like to move the model and components into the javascript so that I can easily add this functionality to any page.

Can I do this? I mean, can a page which already has models and components get additional models and components from my javascript?
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb

Posted 4 years ago

  • 2
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
oh oh .... Can javascript alter the XML of the page prior to loading? ie. can I set the field rendering to my custom snippet from javascript?

That would be slick. One reference to one static resource to automatically set all phone fields to use the snipper, model and popup component.
Photo of Zach McElrath

Zach McElrath, Employee

  • 49,056 Points 20k badge 2x thumb
Yes it would be possible to override a particular Field Renderer globally on page load, like this:

(function(skuid){

   // Get references to our original field renderers
   var fieldRenderers = skuid.ui.fieldRenderers; 
   var PHONE = skuid.ui.fieldRenderers.PHONE;
   var originalPhoneRead = PHONE.read;
   var originalPhoneReadonly = PHONE.readonly;
   var originalPhoneEdit = PHONE.edit;

   // My custom Phone field renderer
   var customPhoneFieldRenderer = function() {
       var field = arguments[0],
             value = skuid.utils.decodeHTML(arguments[1]);

      // rest of custom renderer...
   };

   // Override all of Skuid's standard PHONE field renderers to call our custom renderer
   PHONE.read = PHONE.readonly = PHONE.edit = function(){
       customPhoneFieldRenderer.apply(this,arguments);
   };

})(skuid);
Photo of Matt Sones

Matt Sones, Champion

  • 31,478 Points 20k badge 2x thumb
Ok, this is cool.

Is there a way to hijack the fieldRenderers for specific fields each time those fields occur, rather than specific field types?
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
K. One other thing. Can a popup component be generated completely from javascript?
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
Can I call this skuid.utils.createPopupFromPopupXML from a custom field renderer snippet onclick?
(Edited)
Photo of Zach McElrath

Zach McElrath, Employee

  • 49,056 Points 20k badge 2x thumb
Yep, no problem.
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
Another thing, what happens if a page include page has the same static resource referenced? Will it crash? Will it create two of everything? Can I add code to the javascript to detect whether or not the loaded page (parent and page include) has the model and component?
Photo of Zach McElrath

Zach McElrath, Employee

  • 49,056 Points 20k badge 2x thumb
It depends on what your Static Resource contains and does. Yes you can add code to the JavaScript to detect if a particular Model / Component exists, you could add something like this:

// When the page / page include is loaded...
skuid.$(document.body).one('pageload',function(){
    // If we do not have a certain Model yet, create it
    if (!skuid.$M('SomeModel')) {
         // Create the Model dynamically
    }
    // If we do not have a certain Component yet, create it dynamically
    if (!skuid.$C('SomeComponentUniqueId')) {
         // Create the Component dynamically
    }
});

Here, 'SomeModel' is the Id of a particular Model, and 'SomeComponentUniqueId' is the Unique Id of a particular Component.
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
wait for it .... wait for it .... what was me crossing the threshold into the dark side.  ( bug eyed teethy smile )

You can do so much with javascript. AWESOME!

One file to rule all phone fields to auto set the link to skype with a custom call lop popup from any page the static resource is added. Won't need to add XML, Component, inline javascript or set any of the field to custom render.

One last thing, I assume I can filter which fields will get this custom field renderer by not having the words "fax" or "facsimile" in the field label or custom label. Pointers on this would much much appreciated.
(Edited)
Photo of Zach McElrath

Zach McElrath, Employee

  • 49,056 Points 20k badge 2x thumb
Pat, note the changes I made in Bold. These changes should get you on the right track --- note the part where I check the Fields' Id to see if it contains fax or facsimile:

(function(skuid){

   // Get references to our original field renderers
   var fieldRenderers = skuid.ui.fieldRenderers; 
   var PHONE = skuid.ui.fieldRenderers.PHONE;
   var originalPhone = {
        read: PHONE.read,
        readonly: PHONE.readonly,
        edit: PHONE.edit
   };

   // My custom Phone field renderer
   var customPhoneFieldRenderer = function() {
       var field = arguments[0],
             value = skuid.utils.decodeHTML(arguments[1]);

      // rest of custom renderer...
   };

   // Override all of Skuid's standard PHONE field renderers to call our custom renderer
   PHONE.read = PHONE.readonly = PHONE.edit = function(field){
      // If our Field's API Name contains "fax" or "facsimile", then use the standard renderers
       if (skuid.utils.contains(field.id,'fax') || skuid.utils.contains(field.id,'facsimile')) {
          originalPhone[field.mode].apply(this,arguments);
       }
       // Otherwise use our custom renderer
       else {
          customPhoneFieldRenderer.apply(this,arguments);
       }
   };

})(skuid);
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
Ok. I think I'm close, but I don't understand what's not working. So close. Testing on the Contact object.

<skuidpage tabtooverride="Elite_Agent__c" showsidebar="true" showheader="true" unsavedchangeswarning="">
   <models>
      <model id="Contacts" limit="20" query="true" createrowifnonefound="false" sobject="Contact" doclone="" type="">
         <fields>
            <field id="Phone"/>
            <field id="HomePhone"/>
            <field id="MobilePhone"/>
         </fields>
         <conditions/>
         <actions/>
      </model>
   </models>
   <components>
      <skootable showconditions="true" showsavecancel="false" searchmethod="server" searchbox="true" showexportbuttons="false" pagesize="10" createrecords="false" model="Contacts" buttonposition="" mode="readonly">
         <fields>
            <field id="Phone" valuehalign="" type=""/>
            <field id="HomePhone" valuehalign="" type=""/>
            <field id="MobilePhone" valuehalign="" type=""/>
         </fields>
         <rowactions/>
         <massactions usefirstitemasdefault="true"/>
         <views>
            <view type="standard"/>
         </views>
      </skootable>
   </components>
   <resources>
      <labels/>
      <css/>
      <javascript>
         <jsitem location="inline" name="clicktocall" cachelocation="false" url="">(function(skuid){

   //Get references to our original field renderers
   var fieldRenderers = skuid.ui.fieldRenderers; 
   var PHONE = skuid.ui.fieldRenderers.PHONE;
   var originalPhone = {
        read: PHONE.read,
        readonly: PHONE.readonly,
        edit: PHONE.edit
   };

   //My custom Phone field renderer
   var customPhoneFieldRenderer = function() {
       var field = arguments[0]

        , value = arguments[1]

        , decodedValue = skuid.utils.decodeHTML(value)

        , renderer = skuid.ui.fieldRenderers[field.metadata.displaytype]

        , mode = field.mode

        , $ = skuid.$; // get a shorthand reference to jquery

        // we want to use the default skuid rendering since we are not doing
        // anything special to the information displayed in this field
        // need to call decode because the 'value' from arguments[1] is escaped
        // but the renderer needs the unescaped value as it will escape it internally

        renderer[mode](field, decodedValue);

        // find the first anchor - there should only be one but just being safe
       
        var anchor = $(field.element).find('a:first');
        
        // if we found an anchor (we won't find one when in edit mode)
        

        if (anchor &amp;&amp; anchor.length) {
        
            // set the href attribute value
        
            $(anchor).attr('href', 'skype:' + decodedValue);
        
            // add event listener for click
            // NOTE - This doesn't handle cases like right click or keyboard
            // events so if those are needed, this needs to be adjusted.  This will
            // only handle left-mouse click
        
            $(anchor).on('click', function () {
        
                var inputmodel = field.model
                , inputrow = field.row
                , whatid = inputmodel.getFieldValue(inputrow,'Id')
                , userid = skuid.utils.userInfo.userId;
            
                var popupXMLString = 
                    '&lt;popup title="New Popup" width="90%"/&gt;'
                    + '&lt;components/&gt;'
                    + '&lt;panelset type="custom" scroll="" cssclass=""/&gt;'
                    +'&lt;panels/&gt;'
                    +'&lt;panel width="100%"/&gt;'
                    +'&lt;components/&gt;'
                    +'&lt;includepanel type="skuid" querystring="eaid=' + whatid + '&amp;amp;waid=' + whatid + '&amp;amp;ownerid=' + userid + '" pagename="CallLogTopPane" module=""/&gt;';

                var popupXML = skuid.utils.makeXMLDoc(popupXMLString);
                
                var popup = skuid.utils.createPopupFromPopupXML(popupXML);
        
            });
        }

    };

    // Override all of Skuid's standard PHONE field renderers to call our custom renderer
    PHONE.read = PHONE.readonly = PHONE.edit = function(field){
        // If our Field's API Name contains "fax" or "facsimile", then use the standard renderers
        if (skuid.utils.contains(field.id,'fax') || skuid.utils.contains(field.id,'facsimile')) {
            originalPhone[field.mode].apply(this,arguments);
        } 
        // Otherwise use our custom renderer
        else {
            customPhoneFieldRenderer.apply(this,arguments);
        }
    };

})(skuid);



</jsitem>
      </javascript>
   </resources>
</skuidpage>
 
(Edited)
Photo of Zach McElrath

Zach McElrath, Employee

  • 49,056 Points 20k badge 2x thumb
Looks like it's harder than I thought to override the renderer globally, while still enabling you to use and extend the original renderers. I'll try to play around with it a bit more later, but one thing that you'll definitely need to change later is your popupXMLString, to be something like this:

 var popupXMLString =                    
'<popup title="New Popup" width="90%">'
                        + '<components>'
                            + '<panelset type="custom" scroll="" cssclass="">'
                                +'<panels>'
                                    +'<panel width="100%">'
                                        +'<components>'
                                            +'<includepanel type="skuid" querystring="eaid=' + whatid + '&amp;waid=' + whatid + '&amp;ownerid=' + userid + '" pagename="CallLogTopPane" module=""/>';
                                        +'</components>'
                                    +'</panel>'
                                +'</panels>'    
                            + '</panelset>'
                        + '</components>'
                    + '</popup>';

The way it was before would not have worked at all.
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
Cloned the page and replaced the code with yours. Doesn't crash but none of the fields are rendered with my custom renderer.
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
Zach. Need your help on this one. Just can't figure it out.
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
Added a console.log for the value and got this repeating over and over. Somehow it's looping on the first field's value.

Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
I've been testing with breakpoints and added field to the watch list.

"renderer[mode](field, decodedValue);" calls the function(field)



&

"customPhoneFieldRenderer.apply(this,arguments);" calls var customPhoneFieldRenderer = function() {



And thus creating a loop.

Please tell me there is a way to not call function(field) when using "renderer[mode](field, decodedValue);" 
(Edited)
Photo of Barry Schnell

Barry Schnell, Champion

  • 18,076 Points 10k badge 2x thumb
Pat -

The problem your having is that in your custom renderer you are calling your custom render which is causing the recursive loop and stack overflow.

The following line in customFieldRenderer:

renderer[mode](field, decodedValue);

Must be changed to:

originalPhone[field.mode].apply(this, arguments);

In short, you need to defer the actual rendering back to the original and then, after it's done, style it like you want.  It's essentially the same as the original skype renderer snippet (deferred rendering back to the original), only know you are replacing it globally for all PHONE fields.

Two other notes:
1) See my comment in the snippet itself.  You want to make sure the replacement of the render occurs directly inside of the function(skuid) not inside of another function() within that.
2) You want to make sure to do a case insensitive search on the word fax or facsimile.


Here is the snippet and sample page using a page Account to demonstrate. I didn't go in to the popup stuff, I'll leave that fun for you :)

Sample Inline Javascript Resource

(function(skuid){/*
    Note - We want the below to hook in as soon as possible when the page is being
    generated.  We do not want to put this inside of another "function()" call. Using
    a "function() call inside of this block is equivalent to a document.ready listener
    and we don't want to wait until the DOM is ready.  We want to replace the stock
    renders because skuid is going to render the elements on the page and then
    add them to the DOM.  After all that, the DOM will be ready.  At that point
    it's too late in the cycle.  See Zach's original post of the sample code and
    how it doesn't use another function().
*/
    // get a shorthand reference to jquery
var $ = skuid.$;
// get shorthand to the stock skuid field renderers
    var fieldRenderers = skuid.ui.fieldRenderers
        , PHONE = skuid.ui.fieldRenderers.PHONE
        , originalPhone = {
            read: PHONE.read
            , readonly: PHONE.readonly
            , edit: PHONE.edit
        };
    
    // we'll make fax's have a green background
    var faxRenderer = function(args) {
        var field = arguments[0]
            , value = arguments[1]
            , decodedValue = skuid.utils.decodeHTML(value)
            , renderer = skuid.ui.fieldRenderers[field.metadata.displaytype]
            , mode = field.mode;
            
        originalPhone[field.mode].apply(this, arguments);
        $(field.element).css('background-color', 'green');
    };
    
    // we'll make everything else have orange
    var skypePhoneRenderer = function(args) {
        var field = arguments[0]
            , value = arguments[1]
            , decodedValue = skuid.utils.decodeHTML(value)
            , renderer = skuid.ui.fieldRenderers[field.metadata.displaytype]
            , mode = field.mode;
            
        originalPhone[field.mode].apply(this, arguments);
        $(field.element).css('background-color', 'orange');
    };
    
    // replace the stock skuid renderer with a custom wrapper
    // we could really just replace with customFieldRenderer directly
    // but this provides us a little flexibility and keep things
    // single purposes.  This essentially becomes are "driver" and it
    // defers to a specific renderer based on the id of the field
    PHONE.read = PHONE.readonly = PHONE.edit = function(args) {
        var field = arguments[0]
            , value = arguments[1];
        // make sure to search case-insensitive
        if ((-1 != field.id.search(/fax/i)) || (-1 != field.id.search(/facsimile/i))) {
            // could just call the original but to demonstrate alternative
            // call our custom fax renderer
            //originalPhone[field.mode].apply(this, arguments);
            faxRenderer.apply(this, arguments);
        } else {
            skypePhoneRenderer.apply(this, arguments);
        }
    };
})(skuid);

Sample Account Page - Orange background for phone, green for Fax

<skuidpage unsavedchangeswarning="yes" showsidebar="true" showheader="true" tabtooverride="Account">   <models>
      <model id="Account" limit="1" query="true" createrowifnonefound="false" sobject="Account">
         <fields>
            <field id="Name"/>
            <field id="CreatedDate"/>
            <field id="Phone"/>
            <field id="BillingCity"/>
            <field id="ShippingCity"/>
            <field id="Fax"/>
         </fields>
         <conditions>
            <condition type="param" enclosevalueinquotes="true" operator="=" field="Id" value="id"/>
         </conditions>
         <actions/>
      </model>
   </models>
   <components>
      <basicfieldeditor showsavecancel="false" showheader="true" model="Account" mode="read">
         <columns>
            <column width="100%">
               <sections>
                  <section title="Basics" collapsible="no">
                     <fields>
                        <field id="Name" valuehalign="" type=""/>
                        <field id="Phone"/>
                        <field id="ShippingCity"/>
                        <field id="BillingCity"/>
                        <field id="Fax"/>
                     </fields>
                  </section>
               </sections>
            </column>
         </columns>
      </basicfieldeditor>
   </components>
   <resources>
      <labels/>
      <css/>
      <javascript>
         <jsitem location="inline" name="phoneRenderer" cachelocation="false" url="">(function(skuid){
/*
    Note - We want the below to hook in as soon as possible when the page is being
    generated.  We do not want to put this inside of another "function()" call. Using
    a "function() call inside of this block is equivalent to a document.ready listener
    and we don't want to wait until the DOM is ready.  We want to replace the stock
    renders because skuid is going to render the elements on the page and then
    add them to the DOM.  After all that, the DOM will be ready.  At that point
    it's too late in the cycle.  See Zach's original post of the sample code and
    how it doesn't use another function().
*/
    // get a shorthand reference to jquery
var $ = skuid.$;
// get shorthand to the stock skuid field renderers
    var fieldRenderers = skuid.ui.fieldRenderers
        , PHONE = skuid.ui.fieldRenderers.PHONE
        , originalPhone = {
            read: PHONE.read
            , readonly: PHONE.readonly
            , edit: PHONE.edit
        };
    
    // we'll make fax's have a green background
    var faxRenderer = function(args) {
        var field = arguments[0]
            , value = arguments[1]
            , decodedValue = skuid.utils.decodeHTML(value)
            , renderer = skuid.ui.fieldRenderers[field.metadata.displaytype]
            , mode = field.mode;
            
        originalPhone[field.mode].apply(this, arguments);
        $(field.element).css('background-color', 'green');
    };
    
    // we'll make everything else have orange
    var skypePhoneRenderer = function(args) {
        var field = arguments[0]
            , value = arguments[1]
            , decodedValue = skuid.utils.decodeHTML(value)
            , renderer = skuid.ui.fieldRenderers[field.metadata.displaytype]
            , mode = field.mode;
            
        originalPhone[field.mode].apply(this, arguments);
        $(field.element).css('background-color', 'orange');
    };
    
    // replace the stock skuid renderer with a custom wrapper
    // we could really just replace with customFieldRenderer directly
    // but this provides us a little flexibility and keep things
    // single purposes.  This essentially becomes are "driver" and it
    // defers to a specific renderer based on the id of the field
    PHONE.read = PHONE.readonly = PHONE.edit = function(args) {
        var field = arguments[0]
            , value = arguments[1];
        // make sure to search case-insensitive
        if ((-1 != field.id.search(/fax/i)) || (-1 != field.id.search(/facsimile/i))) {
            // could just call the original but to demonstrate alternative
            // call our custom fax renderer
            //originalPhone[field.mode].apply(this, arguments);
            faxRenderer.apply(this, arguments);
        } else {
            skypePhoneRenderer.apply(this, arguments);
        }
    };
})(skuid);</jsitem>
      </javascript>
   </resources>
</skuidpage>


Photo of Barry Schnell

Barry Schnell, Champion

  • 18,076 Points 10k badge 2x thumb
Pat -

After looking at your sample Contacts page above, I noticed that your table is marked ReadOnly. Based on the way the internals of skuid work, simply replacing the renderers with our own custom version isn't going to work unfortunately (don't fear, there is solution).  

In short, underneath the covers, skuid defers "READONLY" rendering to "READ" rendering but it calls the fully qualified READ function.  What would happen here is our custom renderer on a readonly field would call the readonly original but then the readonly original would call the READ custom renderer (it's trying to call the original but we've replace it) which would then call the READONLY original because, as it's written above, the custom renderer uses field.mode to determine which renderer to call.

So, what we need is a way to differentiate which mode we are actually in.  This is actually rather straightforward but the code gets bloated significantly.  The most straightforward approach would be to have a separate function for edit/read/readonly and use the appropriate version of the function to replace the original PHONE read/edit/readonly.  Assuming we only ever wanted to do one thing with phone numbers, this would be simple enough.  However, as in the example above, what if we wanted fax format one way and other phone another way.  We'd end up with a bunch of code that would have to essentially be copy/pasted if we only wanted to have one function assigned to each original read/edit/readonly.

Below is a solution that minimizes the code bloat, provides some flexibility for the future and most importantly, doesn't interrupt what skuid expects to be happening.  We are, after all, inserting ourselves in to the rendering pipeline so we need to make very certain we don't disrupt anything.

I'm providing the curry, but still leaving the popup to you :)

Here is the snippet
(function(skuid) {    
/*
    Note - We want the below to hook in as soon as possible when the page is being
    generated.  We do not want to put this inside of another "function()" call. Using
    a "function() call inside of this block is equivalent to a document.ready listener
    and we don't want to wait until the DOM is ready.  We want to replace the stock
    renders because skuid is going to render the elements on the page and then
    add them to the DOM.  After all that, the DOM will be ready.  At that point
    it's too late in the cycle.  See Zach's original post of the sample code and
    how it doesn't use another function().
*/
    // get a shorthand reference to jquery
    var $ = skuid.$;
    
    // curry tastes good and is good for you too
    var curry = function( func ) {
        // the first parameter to this function is the function we are going to call
        // so we strip it off to get the remaining parameters
        var args = Array.prototype.slice.call(arguments, 1);
        
        // curry returns a function itself
        return function() {
            // when the function that was returned is called, we merge any parameters
            // that were passed originally to curry (minus the first param which was the function
            // itself) with any parameters that are being passed to this function
            // this allows us to support skuid adding parameters in the future and it
            // not impacting our renderer wrapper - Told you curry was good :)
            return func.apply(this, args.concat(Array.prototype.slice.call(arguments)));
        }
    }    
    
    // get shorthand to the stock skuid field renderers
    var fieldRenderers = skuid.ui.fieldRenderers
        , PHONE = skuid.ui.fieldRenderers.PHONE
        , originalPhone = {
            read: PHONE.read
            , readonly: PHONE.readonly
            , edit: PHONE.edit
        };
    
    // we'll make fax's have a green background
    var faxRenderer = function(mode, field, value) {
        // mode is in arguments[0] - we're using the named parameter
        // field is in arguments[1] - we're using the named parameter
        // value is in arguments[2] - we're using the named parameter
        // in this case, we aren't following the curry principles entirely
        // if we were we would pass the full paramater chain to the calling function
        // but we're using curry to insert a first param and allow the remaining
        // parameters to by dynamic.  So, to make skuid think we didn't do anything
        // we need to strip back off our first param and pass the remaining
        // parameters to the original renderer
        var origArgs = Array.prototype.slice.call(arguments, 1);            
        
        // invoke the original renderer
        originalPhone[mode].apply(this, origArgs);
        // make it green
        $(field.element).css('background-color', 'green');
    };
    // we'll make everything else have orange
    var skypePhoneRenderer = function(mode, field, value) {
        // mode is in arguments[0] - we're using the named parameter
        // field is in arguments[1] - we're using the named parameter
        // value is in arguments[2] - we're using the named parameter
        // in this case, we aren't following the curry principles entirely
        // if we were we would pass the full paramater chain to the calling function
        // but we're using curry to insert a first param and allow the remaining
        // parameters to by dynamic.  So, to make skuid think we didn't do anything
        // we need to strip back off our first param and pass the remaining
        // parameters to the original renderer            
        var origArgs = Array.prototype.slice.call(arguments, 1);            
        
        // invoke the original renderer        
        originalPhone[mode].apply(this, origArgs);
        // make it orange        
        $(field.element).css('background-color', 'orange');
    };
    
    // our wrapper renderer that contains the logic for which renderer to actually invoke
    // this allows the specific renderers themselves to be single purposed
    var customPhoneRenderer = function(mode, field, value) {
        // make sure to search case-insensitive
        if ((-1 != field.id.search(/fax/i)) || (-1 != field.id.search(/facsimile/i))) {
            // could just call the original but to demonstrate alternative
            // call our custom fax renderer
            //originalPhone[mode].apply(this, arguments);
            faxRenderer.apply(this, arguments);
        } else {
            skypePhoneRenderer.apply(this, arguments);
        }
    };
    
    // we need way within the renderer to know which mode we should use
    // we can't rely on field.mode because underneath, skuid directly invokes
    // READ from READONLY.  However, since we have replaced the originals, we
    // end up in a recursive loop calling back in to READONLY on the original
    // because field.mode is READONLY.  Sine we made some curry we'll add
    // a little custom renderer wrapper sauce to it and mmmmm, yummy!
    PHONE.read = curry(customPhoneRenderer, 'read');
    PHONE.readonly = curry(customPhoneRenderer, 'readonly');
    PHONE.edit = curry(customPhoneRenderer, 'edit');
})(skuid);




Here is a version of your Contacts page  

<skuidpage unsavedchangeswarning="yes" showsidebar="true" showheader="true" tabtooverride="Contact">   <models>
      <model id="Contact" limit="100" query="true" createrowifnonefound="false" sobject="Contact">
         <fields>
            <field id="FirstName"/>
            <field id="LastName"/>
            <field id="CreatedDate"/>
            <field id="Phone"/>
            <field id="HomePhone"/>
            <field id="MobilePhone"/>
            <field id="Fax"/>
         </fields>
         <conditions/>
         <actions/>
      </model>
   </models>
   <components>
      <pagetitle model="Contact">
         <maintitle>
            <template>{{Model.labelPlural}}</template>
         </maintitle>
         <subtitle>
            <template>Home</template>
         </subtitle>
         <actions/>
      </pagetitle>
      <skootable showconditions="true" showsavecancel="false" searchmethod="server" searchbox="true" showexportbuttons="false" pagesize="10" createrecords="false" model="Contact" mode="readonly">
         <fields>
            <field id="FirstName" allowordering="true" valuehalign="" type=""/>
            <field id="LastName" allowordering="true"/>
            <field id="MobilePhone"/>
            <field id="HomePhone"/>
            <field id="Phone"/>
            <field id="Fax"/>
         </fields>
         <rowactions/>
         <massactions usefirstitemasdefault="true"/>
         <views>
            <view type="standard"/>
         </views>
         <searchfields/>
      </skootable>
   </components>
   <resources>
      <labels/>
      <css/>
      <javascript>
         <jsitem location="inline" name="phoneRenderer" cachelocation="false" url="">(function(skuid) {
    
/*
    Note - We want the below to hook in as soon as possible when the page is being
    generated.  We do not want to put this inside of another "function()" call. Using
    a "function() call inside of this block is equivalent to a document.ready listener
    and we don't want to wait until the DOM is ready.  We want to replace the stock
    renders because skuid is going to render the elements on the page and then
    add them to the DOM.  After all that, the DOM will be ready.  At that point
    it's too late in the cycle.  See Zach's original post of the sample code and
    how it doesn't use another function().
*/
    // get a shorthand reference to jquery
    var $ = skuid.$;
    
    // curry tastes good and is good for you too
    var curry = function( func ) {
        // the first parameter to this function is the function we are going to call
        // so we strip it off to get the remaining parameters
        var args = Array.prototype.slice.call(arguments, 1);
        
        // curry returns a function itself
        return function() {
            // when the function that was returned is called, we merge any parameters
            // that were passed originally to curry (minus the first param which was the function
            // itself) with any parameters that are being passed to this function
            // this allows us to support skuid adding parameters in the future and it
            // not impacting our renderer wrapper - Told you curry was good :)
            return func.apply(this, args.concat(Array.prototype.slice.call(arguments)));
        }
    }    
    
    // get shorthand to the stock skuid field renderers
    var fieldRenderers = skuid.ui.fieldRenderers
        , PHONE = skuid.ui.fieldRenderers.PHONE
        , originalPhone = {
            read: PHONE.read
            , readonly: PHONE.readonly
            , edit: PHONE.edit
        };
    
    // we'll make fax's have a green background
    var faxRenderer = function(mode, field, value) {
        // mode is in arguments[0] - we're using the named parameter
        // field is in arguments[1] - we're using the named parameter
        // value is in arguments[2] - we're using the named parameter
        // in this case, we aren't following the curry principles entirely
        // if we were we would pass the full paramater chain to the calling function
        // but we're using curry to insert a first param and allow the remaining
        // parameters to by dynamic.  So, to make skuid think we didn't do anything
        // we need to strip back off our first param and pass the remaining
        // parameters to the original renderer
        var origArgs = Array.prototype.slice.call(arguments, 1);            
        
        // invoke the original renderer
        originalPhone[mode].apply(this, origArgs);
        // make it green
        $(field.element).css('background-color', 'green');
    };
    // we'll make everything else have orange
    var skypePhoneRenderer = function(mode, field, value) {
        // mode is in arguments[0] - we're using the named parameter
        // field is in arguments[1] - we're using the named parameter
        // value is in arguments[2] - we're using the named parameter
        // in this case, we aren't following the curry principles entirely
        // if we were we would pass the full paramater chain to the calling function
        // but we're using curry to insert a first param and allow the remaining
        // parameters to by dynamic.  So, to make skuid think we didn't do anything
        // we need to strip back off our first param and pass the remaining
        // parameters to the original renderer            
        var origArgs = Array.prototype.slice.call(arguments, 1);            
        
        // invoke the original renderer        
        originalPhone[mode].apply(this, origArgs);
        // make it orange        
        $(field.element).css('background-color', 'orange');
    };
    
    // our wrapper renderer that contains the logic for which renderer to actually invoke
    // this allows the specific renderers themselves to be single purposed
    var customPhoneRenderer = function(mode, field, value) {
        // make sure to search case-insensitive
        if ((-1 != field.id.search(/fax/i)) || (-1 != field.id.search(/facsimile/i))) {
            // could just call the original but to demonstrate alternative
            // call our custom fax renderer
            //originalPhone[mode].apply(this, arguments);
            faxRenderer.apply(this, arguments);
        } else {
            skypePhoneRenderer.apply(this, arguments);
        }
    };
    
    // we need way within the renderer to know which mode we should use
    // we can't rely on field.mode because underneath, skuid directly invokes
    // READ from READONLY.  However, since we have replaced the originals, we
    // end up in a recursive loop calling back in to READONLY on the original
    // because field.mode is READONLY.  Sine we made some curry we'll add
    // a little custom renderer wrapper sauce to it and mmmmm, yummy!
    PHONE.read = curry(customPhoneRenderer, 'read');
    PHONE.readonly = curry(customPhoneRenderer, 'readonly');
    PHONE.edit = curry(customPhoneRenderer, 'edit');
})(skuid);</jsitem>
      </javascript>
   </resources>
</skuidpage>
Photo of Rob Hatch

Rob Hatch, Official Rep

  • 44,006 Points 20k badge 2x thumb
Barry,  thanks for the detailed assistance here.   Its great to see the community coming alive to help each other buld awesome stuff.. 
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
Ok Barry. As Scott once said to me.

var you = daMan

I can't wait til I can get to the point where I can take one read of code and see exactly what's not working. Barry you certainly seem to have that going on for you. +1 you anywhere I see you on the interweb.

I've taken your "curry" recipe and added the code to update the element to be clickable. Only issue now is click function does some odd things.

$(anchor).on('click', function () {
        
                var inputmodel = field.model
                , inputrow = field.row
                , whatid = inputmodel.getFieldValue(inputrow,'Id')
                , userid = skuid.utils.userInfo.userId;
            
                var popupXMLString = 
                    '<popup title="New Popup" width="90%">'
                        + '<components>'
                            + '<panelset type="custom" scroll="" cssclass="">'
                                +'<panels>'
                                    +'<panel width="100%">'
                                        +'<components>'
                                            +'<includepanel type="skuid" querystring="eaid=' + whatid + '&amp;waid=' + whatid + '&amp;ownerid=' + userid + '" pagename="SkypeClickToCallLog" module=""/>'
                                        +'</components>'
                                    +'</panel>'
                                +'</panels>'    
                            + '</panelset>'
                        + '</components>'
                    + '</popup>';

                var popupXML = skuid.utils.makeXMLDoc(popupXMLString);
                
                var popup = skuid.utils.createPopupFromPopupXML(popupXML);
        
            });
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
Here's the XML for the page. Once this is working, I'm going to make another version of this to share that includes options to comment in/out blocks of code for generic call log vs. my current skype link alteration.

<skuidpage unsavedchangeswarning="yes" showsidebar="true" showheader="true" tabtooverride="Contact">
   <models>
      <model id="Contact" limit="100" query="true" createrowifnonefound="false" sobject="Contact">
         <fields>
            <field id="FirstName"/>
            <field id="LastName"/>
            <field id="CreatedDate"/>
            <field id="Phone"/>
            <field id="HomePhone"/>
            <field id="MobilePhone"/>
            <field id="Fax"/>
         </fields>
         <conditions/>
         <actions/>
      </model>
   </models>
   <components>
      <pagetitle model="Contact">
         <maintitle>
            <template>{{Model.labelPlural}}</template>
         </maintitle>
         <subtitle>
            <template>Home</template>
         </subtitle>
         <actions/>
      </pagetitle>
      <skootable showconditions="true" showsavecancel="false" searchmethod="server" searchbox="true" showexportbuttons="false" pagesize="10" createrecords="false" model="Contact" mode="readonly">
         <fields>
            <field id="FirstName" allowordering="true" valuehalign="" type=""/>
            <field id="LastName" allowordering="true" valuehalign="" type=""/>
            <field id="MobilePhone" valuehalign="" type=""/>
            <field id="HomePhone"/>
            <field id="Phone" valuehalign="" type=""/>
            <field id="Fax"/>
         </fields>
         <rowactions/>
         <massactions usefirstitemasdefault="true"/>
         <views>
            <view type="standard"/>
         </views>
         <searchfields/>
      </skootable>
   </components>
   <resources>
      <labels/>
      <css/>
      <javascript>
         <jsitem location="inline" name="phoneRenderer" cachelocation="false" url="">(function(skuid) {
    
/*
    Note - We want the below to hook in as soon as possible when the page is being
    generated.  We do not want to put this inside of another "function()" call. Using
    a "function() call inside of this block is equivalent to a document.ready listener
    and we don't want to wait until the DOM is ready.  We want to replace the stock
    renders because skuid is going to render the elements on the page and then
    add them to the DOM.  After all that, the DOM will be ready.  At that point
    it's too late in the cycle.  See Zach's original post of the sample code and
    how it doesn't use another function().
*/
    // get a shorthand reference to jquery
    var $ = skuid.$;
    
    // curry tastes good and is good for you too
    var curry = function( func ) {
        // the first parameter to this function is the function we are going to call
        // so we strip it off to get the remaining parameters
        var args = Array.prototype.slice.call(arguments, 1);
        
        // curry returns a function itself
        return function() {
            // when the function that was returned is called, we merge any parameters
            // that were passed originally to curry (minus the first param which was the function
            // itself) with any parameters that are being passed to this function
            // this allows us to support skuid adding parameters in the future and it
            // not impacting our renderer wrapper - Told you curry was good :)
            return func.apply(this, args.concat(Array.prototype.slice.call(arguments)));
        };
    }; 
    
    // get shorthand to the stock skuid field renderers
    var fieldRenderers = skuid.ui.fieldRenderers
        , PHONE = skuid.ui.fieldRenderers.PHONE
        , originalPhone = {
            read: PHONE.read
            , readonly: PHONE.readonly
            , edit: PHONE.edit
        };
    
    // we'll make fax's have a green background
    var faxRenderer = function(mode, field, value) {
        // mode is in arguments[0] - we're using the named parameter
        // field is in arguments[1] - we're using the named parameter
        // value is in arguments[2] - we're using the named parameter
        // in this case, we aren't following the curry principles entirely
        // if we were we would pass the full paramater chain to the calling function
        // but we're using curry to insert a first param and allow the remaining
        // parameters to by dynamic.  So, to make skuid think we didn't do anything
        // we need to strip back off our first param and pass the remaining
        // parameters to the original renderer
        var origArgs = Array.prototype.slice.call(arguments, 1);            
        
        // invoke the original renderer
        originalPhone[mode].apply(this, origArgs);
        // make it green
        //$(field.element).css('background-color', 'green');
    };
    // we'll make everything else have orange
    var skypePhoneRenderer = function(mode, field, value) {
        // mode is in arguments[0] - we're using the named parameter
        // field is in arguments[1] - we're using the named parameter
        // value is in arguments[2] - we're using the named parameter
        // in this case, we aren't following the curry principles entirely
        // if we were we would pass the full paramater chain to the calling function
        // but we're using curry to insert a first param and allow the remaining
        // parameters to by dynamic.  So, to make skuid think we didn't do anything
        // we need to strip back off our first param and pass the remaining
        // parameters to the original renderer            
        var origArgs = Array.prototype.slice.call(arguments, 1);
        
        // invoke the original renderer        
        originalPhone[mode].apply(this, origArgs);
        // make it orange
        var decodedValue = skuid.utils.decodeHTML(value)
            , anchor = $(field.element).find('a:first');
        
        //$(field.element).css('background-color', 'orange');
        
                if (anchor &amp;&amp; anchor.length) {
        
            // set the href attribute value
        
            $(anchor).attr('href', 'skype:' + decodedValue);
        
            // add event listener for click
            // NOTE - This doesn't handle cases like right click or keyboard
            // events so if those are needed, this needs to be adjusted.  This will
            // only handle left-mouse click

            $(anchor).on('click', function () {
        
                var inputmodel = field.model
                , inputrow = field.row
                , whatid = inputmodel.getFieldValue(inputrow,'Id')
                , userid = skuid.utils.userInfo.userId;
            
                var popupXMLString = 
                    '&lt;popup title="New Popup" width="90%"&gt;'
                        + '&lt;components&gt;'
                            + '&lt;panelset type="custom" scroll="" cssclass=""&gt;'
                                +'&lt;panels&gt;'
                                    +'&lt;panel width="100%"&gt;'
                                        +'&lt;components&gt;'
                                            +'&lt;includepanel type="skuid" querystring="eaid=' + whatid + '&amp;amp;waid=' + whatid + '&amp;amp;ownerid=' + userid + '" pagename="CallLogTopPane" module=""/&gt;'
                                        +'&lt;/components&gt;'
                                    +'&lt;/panel&gt;'
                                +'&lt;/panels&gt;'    
                            + '&lt;/panelset&gt;'
                        + '&lt;/components&gt;'
                    + '&lt;/popup&gt;';

                var popupXML = skuid.utils.makeXMLDoc(popupXMLString);
                
                var popup = skuid.utils.createPopupFromPopupXML(popupXML);
        
            });
            
        }

    };
    
    // our wrapper renderer that contains the logic for which renderer to actually invoke
    // this allows the specific renderers themselves to be single purposed
    var customPhoneRenderer = function(mode, field, value) {
        // make sure to search case-insensitive
        if ((-1 != field.id.search(/fax/i)) || (-1 != field.id.search(/facsimile/i))) {
            // could just call the original but to demonstrate alternative
            // call our custom fax renderer
            // originalPhone[mode].apply(this, arguments);
            faxRenderer.apply(this, arguments);
        } else {
            skypePhoneRenderer.apply(this, arguments);
        }
    };
    
    // we need way within the renderer to know which mode we should use
    // we can't rely on field.mode because underneath, skuid directly invokes
    // READ from READONLY.  However, since we have replaced the originals, we
    // end up in a recursive loop calling back in to READONLY on the original
    // because field.mode is READONLY.  Sine we made some curry we'll add
    // a little custom renderer wrapper sauce to it and mmmmm, yummy!
    PHONE.read = curry(customPhoneRenderer, 'read');
    PHONE.readonly = curry(customPhoneRenderer, 'readonly');
    PHONE.edit = curry(customPhoneRenderer, 'edit');
})(skuid);</jsitem>
      </javascript>
   </resources>
</skuidpage>
Photo of Barry Schnell

Barry Schnell, Champion

  • 18,076 Points 10k badge 2x thumb
Pat -

Your problem is likely that you end up with multiple listeners registered on the anchor.  Because of the way the element gets rendered, it will get invoked multiple times and each time a listener for 'click' is added the way the code above works.  There are a few solutions to this but the easy solution should to give a namespace to the listener and remove any existing before adding.

Change the code from:

$(anchor).on('click', function () {

to

// you can choose your own namespace, I used pat for demo purposes
$(anchor).off('click.pat').on('click.pat', function () {
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
VAR YOU = DAMAN!!!!!!!!!!!!!!!!
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
AWESOME!!!!

(Edited)
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
WEEEEE!!! I refer to myself and others when I'm having fun!