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?
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
originalPhonermode].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
originalPhonermode].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>
Barry, thanks for the detailed assistance here. Its great to see the community coming alive to help each other buld awesome stuff…
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); });
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 argumentse0] - we're using the named parameter // field is in argumentse1] - we're using the named parameter // value is in argumentse2] - 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 originalPhonehmode].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 argumentse0] - we're using the named parameter // field is in argumentse1] - we're using the named parameter // value is in argumentse2] - 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 originalPhonehmode].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 // originalPhonehmode].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>
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 () {
VAR YOU = DAMAN!!!
AWESOME!!!
WEEEEE!!! I refer to myself and others when I’m having fun!
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?
Enter your E-mail address. We'll send you an e-mail with instructions to reset your password.