Download Firefox -  a safer, easier to use web browser. Return to iribbit.net - Leap into the online experience! Return to iribbit.net - Leap into the online experience! iribbit.net - Leap into the online experience!

Project News :.

The latest project to launch was the site for Gorilla Offroad Company. Aside from their main site, a social media strategy was develop to launch the company into various industry specific automobile enthusist discussion board communities as well as popular social media fronts like Facebook, Pinterest, and Twitter.


Valid XHTML 1.0 Transitional

Valid CSS!

Section 508 Compliant

powered by: Macromedia ColdFusion MX

made with: Macromedia Dreamweaver MX

What is RSS

XML - often denotes RSS Feed information.

Macromedia - ColdFusion Programming
white horizontal rule

ColdFusion News :.

To bring a little life to my site, I've pulled a couple What is RSS Feeds into this page. You can currently choose between the technology related news stories from the following news sources:



You are currently viewing and RSS Feed from Raymond Camden's Blog.



Follow up to ColdFusion Axis2 Web Service Issue

Last month I blogged about an odd issue with Axis2 web services. Basically, the CFC was persisting past the initial hit. Normally CFCs are recreated on every request. This didn't seem to be case with Axis2 web services.

I filed a bug report for the issue and today I got a response form an engineer:

This is a change in behavior in how we designed the Axis 2 web services support.

In Axis 1, For every web service request we created a new CFC instance and the request was handled by this new instance. That is why the variables always gets re initialized for every request.

In Axis 2, we are creating the CFC instance the first time only. For every subsequent requests the same instance is used. So you are seeing the caching there. A new CFC instance will be created only if service CFC is modified.

Normally if you are deploying an axis2 based aar, in tomcat, you will see the same behavior.

This was a design decision we took. Are you facing any issues because of this caching? Do you have a requirement to use different CFC instances for each web service request?

So, long story short, this is expected. I want to be clear so people understand the issue. This is not like trusted cache, where changing the code doesn't reflect on the server. This is not like the case where you change a method and have to tell CF to refresh the WSDL. No, this is data persistence, as my example demonstrates. In every other case of using a CFC - either via Ajax or WSDL - this is not how CFCs act.

So... um... yeah. I guess watch out for it. Or take the advice I gave on the last blog entry and avoid WSDL like the plague on humanity it is. If you feel like this was a bad design by the ColdFusion engineers, please post your thoughts on the bug entry, not here. (Or both - just keep in mind I've got no power to change things there. ;)


(Thu, 21 Aug 2014 09:09:00 -0400)
[view article in new window]

Ionic releases an easier path to Cordova / Android development

I promise - I'm not turning into a complete Ionic fan boy, but if you want to call me an unofficial evangelist for them (especially since I don't do that for Adobe anymore), I certainly won't mind. This weekend I was working on the book I'm writing covering Cordova, and as part of that process I worked through the complete setup for doing Cordova development with Android. Most of the time I use iOS but for the book I wanted a platform anyone could use. In fact, I specifically did my testing in Windows since I know that many people use Windows and Cordova development has been a bit tricky there in the past.

It had been a long time since I went through the complete process. What I discovered is that while it is relatively straightforward, there are more steps than I remembered and you really need to pay attention or it is easy to screw up one of the prerequisites. I think my chapter does a great job of discussing this process, but as I said, it was surprising just how much you have to do to get up and running.

So I find this new post by the Ionic team to be very timely: Faster Hybrid Dev with Ionic Vagrant. I had some exposure to Vagrant at CFObjective a few weeks back but I had not actually used it for anything. Vagrant allows you to build development environments - think operating systems with applications and configurations - via a scriptable interface. What this means is - in an ideal world - a person can download a Vagrant image - set it up - and have everything ready to go. I've been through enough projects to know that something like this could be a huge timesaver.

To help with Android/Cordova development, the Ionic folks have released an Ionic Vagrant image. What this means is - after installing Vagrant - you can download their zip, unzip, and type vagrant up. And that's it. The first time you run this be prepared for a long wait. For me it took about 20-25 minutes, and during part of the process I saw some red error messages, but it eventually wrapped up at the end - I had a complete operating system.

With Android.

With Cordova.

And with Ionic.

Hell, screw Ionic (sorry guys!), even if you don't give a rat's mustache about Ionic, this is a huge timesaver in terms of getting up and running with Cordova. If you do decide to check it out, here are a few things to watch out for.

Number one - the commands to check for a connected device require sudo. This is covered on the GitHub and from what I can tell, you will run into this issue. Just do exactly as described:

sudo /home/vagrant/android-sdk-linux/platform-tools/adb kill-server sudo /home/vagrant/android-sdk-linux/platform-tools/adb start-server

And follow up with sudo /home/vagrant/android-sdk-linux/platform-tools/adb devices to ensure your device shows up.

Number two - how do you edit files? For my first test I used ed, but I wouldn't recommend that. (Old school MUD coder here - I used to be a wiz with ed.) Vagrant supports the concept of synced folders, which means you can edit using your preferred browser tools while the files are available to the Vagrant image as well. Thanks to Max Lynch for helping me out with this as I found the docs to be a bit weird. Here is how it works in a nutshell.

If you expanded the image zip to /Users/ray/Downloads/ionic-cordova-android-vagrant-0.1.1, then that folder is available on the image as /vagrant. So while SSHed into the image, I went to /vagrant and made a new Ionic project called android2. It showed up in Finder right way.

I opened this up in Sublime, edited, and went back in Terminal, and sent the project to my Android device. It felt completely the same as normal development. (To be clear, when I SSHed into the image, I did it via Terminal, and you end up ... in Terminal, so this is probably assumed, but I wanted to be clear this wasn't like a VMWare visual type thing.)


(Tue, 19 Aug 2014 16:43:00 -0400)
[view article in new window]

PhoneGap/Cordova Example - Getting File Metadata (and an update to the FAQ)

I decided to move my PhoneGap/Cordova FileSystem FAQ from a Google Doc to my Cordova Examples repository. I figured this would make it a bit easier for folks to edit and simpler for me to commit those changes. You can find the FAQ here:

PhoneGap Cordova File System FAQ

In doing so I decided to quickly knock out one of the questions - how to get metadata about files. This is rather trivial, so my demo app is rather trivial, but hopefully it will help folks. The basic gist is - once you have a FileEntry object, you can then fetch the File object itself (MDN Docs) and fetch various properties. Here is a super simple demo roughly based on the ones I showed earlier. It uses window.resolveLocalFileSystemURL to get index.html.

document.addEventListener("deviceready", init, false);
function init() {
	
	//This alias is a read-only pointer to the app itself
	window.resolveLocalFileSystemURL(cordova.file.applicationDirectory + "www/index.html", gotFile, fail);

}

function fail(e) {
	console.log("FileSystem Error");
	console.dir(e);
}

function gotFile(fileEntry) {

	fileEntry.file(function(file) {
		var s = "";
		s += "<b>name:</b> " + file.name + "<br/>";
		s += "<b>localURL:</b> " + file.localURL + "<br/>";
		s += "<b>type:</b> " + file.type + "<br/>";
		s += "<b>lastModifiedDate:</b> " + (new Date(file.lastModifiedDate)) + "<br/>";
		s += "<b>size:</b> " + file.size + "<br/>";
		
		document.querySelector("#status").innerHTML = s;
		console.dir(file);
	});
}

Here is a sample of it running in iOS. (Be aware - there is a bug with Android that may prevent this from working. It is reported at the Cordova site.)

So - yeah - not rocket science - but hopefully helpful. This "application" can be grabbed from my Cordova repo: https://github.com/cfjedimaster/Cordova-Examples/tree/master/getfiledata.


(Mon, 18 Aug 2014 16:58:00 -0400)
[view article in new window]

Ionic and Cordova's DeviceReady - My Solution

Folks know that I've been madly in love with the Ionic framework lately, but I've run into an issue that I'm having difficulty with. I thought I'd blog about the problem and demonstrate a solution that worked for me. To be clear, I think my solution is probably wrong. It works, but it just doesn't feel right. I'm specifically sharing this blog entry as a way to start the discussion and get some feedback. On the slim chance that what I'm showing is the best solution... um... yes... I planned that. I'm brilliant.

The Problem

So let's begin by discussing the problem. Given a typical Ionic app, your Angular code will have a .run method that listens for the ionicPlatform's ready event. Here is an example from the "base" starter app (https://github.com/driftyco/ionic-app-base/blob/master/www/js/app.js):

// Ionic Starter App

// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
angular.module('starter', ['ionic'])

.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      // Set the statusbar to use the default style, tweak this to
      // remove the status bar on iOS or change it to use white instead of dark colors.
      StatusBar.styleDefault();
    }
  });
})

The ionicPlatform.ready event is called when Cordova's deviceready event fires. When run on a desktop, it is fired when on window.load. Ok, so in my mind, this is where I'd put code that's normally in a document.ready block. So far so good.

Now let's imagine you want to use a plugin, perhaps the Device plugin. Imagine you want to simply copy a value to $scope so you can display it in a view. If that controller/view is the first view in your application, you end up with a race condition. Angular is going to display your view and fire off ionicPlatform.ready asynchronously. That isn't a bug of course, but it raises the question. If you want to make use of Cordova plugin features, and your application depends on it immediately, how do you handle that easily?

One way would be to remove ng-app from the DOM and bootstrap Angular yourself. I've done that... once before and I see how it makes sense. But I didn't want to use that solution this time as I wanted to keep using ionicPlatform.ready. I assumed (and I could be wrong!) that I couldn't keep that and remove the ng-app bootstraping.

So what I did was to add an intermediate view to my application. A simple landing page. I modified the stateProvider to add a new state and then made it the default. In my ionicPlatform.ready, I use the location service to do a move to the previously default state.

.run(function($ionicPlatform,$location,$rootScope) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      // org.apache.cordova.statusbar required
      StatusBar.styleDefault();
    }

	  $location.path('/tab/dash');
	  $rootScope.$apply();
  });
})

This seemed to do the trick. My controller code that's run for the views after this was able to use Cordova plugins just fine. How about a real example?

The Demo

One of the more recent features to land in Ionic is striped-style tabs. This is an Android-style tab UI and it will be applied automatically to apps running on Android. The difference is a bit subtle when the tabs are on the bottom:

But when moved to the top using tabs-top, it is a bit more dramatic.

Ok... cool. But I wondered - how can I get tabs on top just for Android? While I'm not one of those people who believe that UI elements have to be in a certain position on iOS versus Android, I was curious as to how I'd handle this programmatically.

Knowing that it was trivial to check the Device plugin, and having a way now to delay the view until my plugins were loaded, I decided to use the approach described above to ensure I could access the platform before that particular view loaded.

Here is the app.js file I used, modified from the tabs starter template.

// Ionic Starter App

// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
// 'starter.services' is found in services.js
// 'starter.controllers' is found in controllers.js
angular.module('starter', ['ionic', 'starter.controllers', 'starter.services'])

.run(function($ionicPlatform,$location,$rootScope) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      // org.apache.cordova.statusbar required
      StatusBar.styleDefault();
    }

	  $location.path('/tab/dash');
	  $rootScope.$apply();
  });
})

.config(function($stateProvider, $urlRouterProvider) {

  // Ionic uses AngularUI Router which uses the concept of states
  // Learn more here: https://github.com/angular-ui/ui-router
  // Set up the various states which the app can be in.
  // Each state's controller can be found in controllers.js
  $stateProvider

    // setup an abstract state for the tabs directive
  	.state('home', {
		url:"/home",
		templateUrl:'templates/loading.html',
		controller:'HomeCtrl'
	})
    .state('tab', {
      url: "/tab",
      abstract: true,
      templateUrl: function() {
		  if(window.device.platform.toLowerCase().indexOf("android") >= 0) {
			  return "templates/tabs_android.html";			  
		  } else {
			  return "templates/tabs.html";
		  }
	  },
    })

    // Each tab has its own nav history stack:

    .state('tab.dash', {
      url: '/dash',
      views: {
        'tab-dash': {
          templateUrl: 'templates/tab-dash.html',
          controller: 'DashCtrl'
        }
      }
    })

    .state('tab.friends', {
      url: '/friends',
      views: {
        'tab-friends': {
          templateUrl: 'templates/tab-friends.html',
          controller: 'FriendsCtrl'
        }
      }
    })
    .state('tab.friend-detail', {
      url: '/friend/:friendId',
      views: {
        'tab-friends': {
          templateUrl: 'templates/friend-detail.html',
          controller: 'FriendDetailCtrl'
        }
      }
    })

    .state('tab.account', {
      url: '/account',
      views: {
        'tab-account': {
          templateUrl: 'templates/tab-account.html',
          controller: 'AccountCtrl'
        }
      }
    });

  // if none of the above states are matched, use this as the fallback
  $urlRouterProvider.otherwise('/home');

});

You can see where I use the location.path mechanism after the ionicPlatform.ready event has fired. You can also see where I sniff the device platform to determine which template to run. tabs_android.html is the exact same as tabs.html - but with the tabs-top class applied (*). The biggest drawback here is that the application would error when run on the desktop. That could be avoided by sniffing for the lack of window.device and just setting it to some default: window.device = {platform : "ios"};

So that's it. What do you think? I have to imagine there is a nicer way of handling this. Maybe I'm being lazy but I want to use Ionic's killer directives and UX stuff along with Cordova plugins and not have to use an awkward workaround like this.

* A quick footnote. I noticed that if I tried to add tabs-top to the ion-tabs directive, it never worked. For example, this is what I tried first: <ion-tabs ng-class="{'tabs-top':settings.isAndroid}"> I used code in my controller that always set it to true (I wasn't worried about the device plugin yet) and it never actually updated the view. It's like the controller scope couldn't modify the view for some odd reason.
(Sat, 16 Aug 2014 11:25:00 -0400)
[view article in new window]

Eventbrite API Demos

A few days ago a client of mine, Rich Swier of HuB, asked if I could build him two quick demos that made use of the Eventbrite API. I whipped up the two demos for him and once done, he graciously allowed me to share it on my blog. (Thank you Rich!) I will warn you that this code was written for ColdFusion 8 so it is entirely tag based. At the very end a mod was made for ColdFusion 9. Obviously it could be converted to script and perhaps improved in other ways as well, but I hope this is useful for folks who want to play with the Eventbrite API in the future.

Before making use of these demos yourself, you will want to get yourself an authorization token for your account. After going to developer.eventbrite.com, click Get Started. Note the green button on the right.

When I tested this a few days ago, it worked fine for me, but failed for Rich as he wasn't the primary account owner for his organization. Unfortunately the "failure" was a blank white screen. I got a response from Eventbrite on their forums (https://groups.google.com/forum/#!forum/eventbrite-api) about this within a day, but I'm not sure if it is fixed yet. If you do get a "white screen of death" error though check to see if there are other people in your group using Eventbrite.

Once logged in, add a new app, and when done, make note of the OAuth token. My API makes use of this to make authenticated calls for your organization. Finding the ID of your organization can be a bit weird too. Unfortunately I can't find the tip that helped me with this before (I believe it was an Eventbrite staff member), but in order to get your organization ID, login, go to your profile page, and find your organizer page URL:

The organizer ID is the numeric part at the end of your URL. I've highlighted it in the screen shot above. It is a bit silly that the value isn't called out specifically either on the developer page or on your profile, but, there it is.

Ok, so finally, with your token and organizer ID, you can perform some basic searches against the API. For Rich, I only needed Search, but their API supports pretty much everything imaginable as far as I can tell. I created a simple CFC that is initialized with the auth token. First, the Application.cfc used to set up the CFC.

<cfcomponent output="false">
	
	<cfset this.name = "RichSwierEventBrite">
	
	<cffunction name="onApplicationStart" access="public" returntype="boolean" output="false">
	
		<cfset application.eventBrite = {}>
		<cfset application.eventBrite.token = "secret">
		<cfset application.eventBrite.orgid = "3983270067">
		
		<cfset application.eventBriteApi = createObject("component", "eventbrite").init(application.eventBrite.token)>
		
		<cfreturn true>
	</cffunction>
	
	<cffunction name="onRequestStart" access="public" returntype="boolean" output="false">

		<!--- TODO: remove 1 --->
		<cfif structKeyExists(url, "init") or 1>
			<cfset onApplicationStart()>	
		</cfif>
		
		<cfreturn true>
	</cffunction>
	
</cfcomponent>

And here is the CFC itself. Again, this was written for ColdFusion 8, so pardon all the darn tags. I could have used script I suppose, but I ran into some issues with even script-based UDFs on his server so I went the safe route. Also note I ran into cfhttp issues with the https server Eventbrite used. This is why I used a workaround described a few years ago on my blog. On ColdFusion 10 I didn't have the issue at all.

<cfcomponent output="false">

	<cffunction name="init" access="public" returnType="eventbrite" output="false">
		<cfargument name="token" type="string" required="true">
		<cfset variables.token = arguments.token>		
		<cfreturn this>
	</cffunction>
	
	<cffunction name="getEvents" access="public" returnType="struct" output="false">
		<cfargument name="organizationid" type="string" required="true">
		<cfargument name="startdate" type="date" required="false">
		<cfargument name="enddate" type="date" required="false">

		<cfset var result = "">
		<cfset var content = "">
		<cfset var apiurl = "https://www.eventbriteapi.com/v3/events/search?organizer.id=#arguments.organizationid#">
		<cfset var startUTC = "">
		<cfset var endUTC = "">
		<cfset var objSecurity = "">
		<cfset var storeProvider = "">
		
		<cfif structKeyexists(arguments, "startdate")>
			<cfset startUTC = getIsoTimeString(arguments.startdate, true)>
			<cfset apiurl &= "&start_date.range_start=#startUTC#">
		</cfif>
		<cfif structKeyexists(arguments, "enddate")>
			<cfset endUTC = getIsoTimeString(arguments.enddate, true)>
			<cfset apiurl &= "&start_date.range_end=#endUTC#">
		</cfif>
		
		<cfset apiurl &= "&token=#variables.token#">

		<cfset objSecurity = createObject("java", "java.security.Security") />
		<cfset storeProvider = objSecurity.getProvider("JsafeJCE") />
		<cfset objSecurity.removeProvider("JsafeJCE") />
		
		<cfhttp url="#apiurl#" result="result">

		<cfset objSecurity.insertProviderAt(storeProvider, 1) />
		
		<cfset content = deserializeJSON(result.filecontent)>
		<cfreturn content>

	</cffunction>
	
	<cfscript> 
	// I take the given date/time object and return the string that
	// reprsents the date/time using the ISO 8601 format standard.
	// The returned value is always in the context of UTC and therefore
	// uses the special UTC designator ("Z"). The function will
	// implicitly convert your date/time object to UTC (as part of
	// the formatting) unless you explicitly ask it not to.
	function getIsoTimeString(datetime) {
	 
	 	if(!structKeyExists(arguments, "convertToUTC")) {
	 		convertToUTC = true;	
	 	}
	 	
	    if ( convertToUTC ) {
	        datetime = dateConvert( "local2utc", datetime );
	    }
	 
		// When formatting the time, make sure to use "HH" so that the
		// time is formatted using 24-hour time.
		return(dateFormat( datetime, "yyyy-mm-dd" ) &
		    "T" &
	    	timeFormat( datetime, "HH:mm:ss" ) &
		    "Z"
	        );
	 
	}
	</cfscript>
	
</cfcomponent>

Nothing special really. I just construct the URL and fire off the call. That UDF at the end may be found on CFLib: getIsoTimeString. As I said, I built a grand total of one function for the API because that's all he needed. Now let's look at the actual demos. The first demo makes use of Google Maps. I perform a search for future events, get their locations, and display them on a map.

For the most part, this was simple, but the issue I ran into (and this is a reported issue with the API), is the location information for the venue wasn't including the full address. There is a venue API that returns the proper information, but that would mean adding N more calls over http to fetch the data. I simply warned Rich that the address won't be working properly until they fix the API. To be clear, I'm talking about the textual part of the address. The longitude and latitude work just fine. Right now when you click you get some detail and the ability to click for the Eventbrite URL.

Now let's look at the code that generates the map.

<!--- only events today and onwards --->
<cfset dtNow = now()>

<cfif structKeyExists(url, "clearcache")>
	<cfset cacheRemove("eventbrite.futureEvents")>
</cfif>

<cfset events = cacheGet("eventbrite.futureEvents")>
<cfif isNull(events)>
	<cfset eventData = application.eventBriteApi.getEvents(application.eventBrite.orgid,dtNow)>

	<cfset cachePut("eventbrite.futureEvents", eventData.events,0.5)>
	<cfset events = eventData.events>
</cfif>

<!DOCTYPE html>
<html>

<head>
<style type="text/css">
#map_canvas { width: 500px; height: 500px; }
</style>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript">
function initialize() {
	
    var latlng = new google.maps.LatLng(41.5, -98);
    var myOptions = {
        zoom: 3,
        center: latlng,
        mapTypeId: google.maps.MapTypeId.ROADMAP
    };
    var map = new google.maps.Map(document.getElementById("map_canvas"),myOptions);

	<cfloop index="x" from="1" to="#arrayLen(events)#">
		<cfset event = events[x]>
		<!--- conditionally format address --->
		<cfset address = "">
		<cfif structKeyExists(event.venue, "name") and event.venue.name is not "undefined">
			<cfset address &= event.venue.name>
		</cfif>
		<cfif structKeyExists(event.venue, "address_1") and event.venue.address_1 is not "undefined">
			<cfset address &= "<br/>" & event.venue.address_1>
		</cfif>
		<cfif structKeyExists(event.venue, "address_2") and event.venue.address_2 is not "undefined">
			<cfset address &= "<br/>" & event.venue.address_2>
		</cfif>
		<cfif structKeyExists(event.venue, "city") and event.venue.city is not "undefined">
			<cfset address &= "<br/>" & event.venue.city>
		</cfif>
		<cfif structKeyExists(event.venue, "state") and event.venue.state is not "undefined">
			<cfset address &= ", " & event.venue.state>
		</cfif>
		<!--- format times --->
		<cfset startDate = listFirst(event.start.local, "T")>
		<cfset startDate = dateFormat(startDate, "mm/dd/yy")>
		<cfset startTime = listLast(event.start.local, "T")>
		<!--- strips off seconds, which is silly --->
		<cfset startTime = mid(startTime, 1, len(startTime)-3)>
		<cfset startTime = timeFormat(startTime, "h:mm tt")>
		
		<cfset endDate = listFirst(event.end.local, "T")>
		<cfset endTime = listLast(event.end.local, "T")>
		<!--- strips off seconds, which is silly --->
		<cfset endTime = mid(endTime, 1, len(endTime)-3)>
		<cfset endTime = timeFormat(endTime, "h:mm tt")>
		
		<!--- currently assumes same date --->
		<cfset dateStr = startDate & " at " & startTime & " to " & endTime>
		<cfoutput>
		var pos = new google.maps.LatLng(#event.venue.latitude#,#event.venue.longitude#);
	 	var marker#x# = new google.maps.Marker({
	    	map: map, 
	        position: pos,
	        title: "#jsStringFormat(event.name.text)#"
	    });

		var infowindow#x# = new google.maps.InfoWindow({
			content: '<b>#jsStringFormat(event.name.text)#</b><p/>#jsStringFormat(address)#<br/>#jsStringFormat(dateStr)#<br/><a href="#event.url#">Details</a>',
			maxWidth: 250
		});
		
		google.maps.event.addListener(marker#x#, 'click', function() {
			infowindow#x#.open(map,marker#x#);
		});
  	    </cfoutput>
	</cfloop>
}
</script>
</head>

<body onload="initialize()">

<div id="map_canvas"></div>

</body>
</html>

Again - pretty simple. Get the events and then just iterate over them to create map markers using the Google API. I don't like the ColdFusion code inside the JavaScript block there and I'd probably consider moving that outside. That way when I create the markers the code would be simpler. (And obviously, if this were a production application I'd use something like FW/1 and do most of that work in the controller/service layer anyway.)

So that's maps. The next demo he requested was a calendar. I decided to use the FullCalendar jQuery plugin as it worked well for me before. (See my blog post from a few years back.) In order to work with the client-side code, I wrote a quick CFC to handle calls to my application scoped Eventbrite API. It also handles shaping the data so it makes the FullCalendar happy. I could have added some RAM-based caching here, but I found that the API was fast enough for month based searches where it didn't feel necessary. Then again - I'd probably consider adding it anyway just to ensure you don't hit any rate limits.

<!---
Main wrapper to calls to the EB api. Handles the caching so EB can be a bit simpler.
Also handles converting EB events for FullCalendar
--->
<cfcomponent output="false">
	
	<cffunction name="getEvents" access="remote" output="false" returnformat="json">
		<cfargument name="start" type="string" required="true">
		<cfargument name="end" type="string" required="true">
		
		<cfset var eventData = application.eventBriteApi.getEvents(application.eventbrite.orgid,parseDateTime(arguments.start), parseDateTime(arguments.end))>
		<cfset var result = []>
		<cfset var x = "">
		<cfset var event = "">
		
		<cfloop index="x" from="1" to="#arrayLen(eventData.events)#">
			<cfset event = {}>
			<cfset event["title"] = eventData.events[x].name.text>
			<cfset event["start"] = eventData.events[x].start.local>
			<cfset event["end"] = eventData.events[x].end.local>
			<cfset event["url"] = eventData.events[x].url>
			<cfset arrayAppend(result, event)>	
		</cfloop>
		
		<cfreturn result>
	</cffunction>
	
</cfcomponent>

Finally, here is the front end using FullCalendar. It is a page with just the calendar and a DOM element used to handle the loading of remote data. I could have designed that a bit better but I assumed the client would have their own ideas about that.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width">
		<link rel='stylesheet' href='fullcalendar/fullcalendar.css' />
		<script src='fullcalendar/lib/jquery.min.js'></script>
		<script src='fullcalendar/lib/moment.min.js'></script>
		<script src='fullcalendar/fullcalendar.min.js'></script>
		<script>
		$(document).ready(function() {

		    $('#calendar').fullCalendar({
		    	eventSources: [
		    		{
			    		url:'api.cfc?method=getevents'
			    	}
		    	],
		    	loading:function(isLoading, view) {
		    		if(isLoading) {
			    		$("#loading").html("<i>Loading data...</i>");
			    	} else {
			    		$("#loading").html("");
			    	}
		    	}
		    });

		});		
		</script>

    </head>
    <body>
    	
		<div id="loading"></div>
		<div id='calendar'></div>

    </body>
</html>

I have attached the code as a zip to this blog entry. Note that the token and organization ID are not in the Application.cfc file so you will need to set them yourself.


(Fri, 15 Aug 2014 05:50:00 -0400)
[view article in new window]

Review: Code School's JavaScript Best Practices

Yesterday evening I finally wrapped up Code School's JavaScript Best Practices course and I thought my readers might like to know what I think. In general, this is a great course marred by poor challenges, surprisingly poor challenges, but still worth your time.

First and foremost, the content of this course is incredible. For a while now I've been thinking about what it means for a developer to improve their JavaScript skills. To go from a "user", someone who knows basic syntax and can copy and paste code to a "developer", someone who is comfortable with the language and understands performance and organizational principles. I'm not talking about Ninja level skills here. Just good, intermediate level skill levels. I think there is a dearth of material aimed at developers who want to achieve this level and it is something I'm personally trying to help with as well. With that context in mind, this course is exactly what I think developers should be taking. Let's look at the material.

Section one, "The Sword of Syntax", covers ternary conditionals, logical assignments (OR and AND), and Switch blocks. In general, this was the section that was probably least useful to me, but I still found the material useful. Just the presentation of the topics themselves were a bit different and I felt like I got something from it. I do think this section will be the one most folks will skip over, but I recommend going through it anyway. Don't forget you can watch the videos at 1.5X speed to move things along a bit quicker.

Section two is "The Pendant of Performance". Topics cover loop optimization, script execution, general performance tips, and measuring. Performance topics can be very confusing, and sometimes a bit difficult to relate to your own development, but the course does a great job at keeping this material practical, which is an important thing for me.

Section three is "The Crystal of Caution". This one covers comparisons, error handling, and some oddities of the language itself. This section in particular serves as a great warning to the types of mistakes developers may make in their code and not realize until later.

The final section, "The Mail of Modularity", discusses namespacing and modularity. It does a good job of introducing the basics of encapsulation in JavaScript. If you are currently writing JavaScript and discovering that you need help with organization, this will be a great lesson for you.

The speaker, Jason Millhouse, is great. He has a good pace, a clear voice, and a sense of humor. I think he may be my favorite speaker at Code School so far and I'm hoping he has done other courses that I can take.

So, that's the good. What's the bad? In general, I've been very impressed with how Code School handles challenges for its material. I find the questions targeted well, interesting, and just enough of a challenge to motivate you to pay attention well and think before you answer. My experience with the challenges with this course though were very poor. It got to the point where I was dreading the challenges and even considering just skipping them. Let me give you an example of one of the first questions:

Given foo is true, what is the result of name = foo ? "ray" : "beer".

If you had said, ray, you would have been wrong. Nope, the answer is "ray". And... ok... I get it. Including the quotes is a bit more precise, but given the nature of the challenge (type your answer in this box), it seems overly specific and unnecessary. And yes, that's a minor quibble, but it was only the beginning.

Next, I ran into a question where the answer didn't make sense. It discussed an aspect of performance that was not covered in the video. While it was possible I had missed it, I discovered other people had the same issue as well. There is a post on their forums, but what concerns me is that there is no official response yet. Remember, Code School is a paid course, so I'd expect some kind of response on this thread.

How about another example? One of the things Code School does well in their code challenges is to provide an editor where you can only edit in certain parts. So they may show a function and ask you to fix one part of it. Only that part will be editable. It is a nice way to focus your efforts and ensure you don't spend too much time writing code that isn't relevant to the challenge. In general - I dig this. But then I encountered the challenge where I was asked to move a script tag from the head block to right before the body. There were - literally - two places in the file you could edit. So the challenge was - move the line of code to the other place - the only other place. I guess an "easy win" is nice every now and then but that was just ridiculous.

Speaking of ridiculous, towards the end of the course there were a few questions that looked like this:

My ____ is ____ and I ____ about ____ a day when I ___ to the ____ and do ____ after I ____ so and _____. Then I ____ while ____ the ____.

After this were four answers that looked like this: Moo, Zoo, Something, AnotherWord, More, Less, Foo, Etc, OMG, Etc. So the intent is - take each word from the list and see if it makes sense in the paragraph. Speaking just for myself - that made my brain explode and it felt like a horrible challenge. After trying one of these, I simply skipped the rest of them.

I had more examples, but honestly, I can't remember them. Most likely I got so frustrated towards the end that I was probably getting annoyed by things I wouldn't have normally. It was a big disappointment.

However...

Despite all of this, I cannot stress enough how much I enjoyed the video content, and how much I felt that this was exactly the course that a lot of people need. Honestly, screw what I said about the challenges. I really hope Code School takes a look at them, but I wouldn't hesitate at all to pay for the course. I mean, we're talking 20 bucks or so for about 4 hours of material (and that's just one course), so this is a no-brainer.


(Wed, 13 Aug 2014 06:08:00 -0400)
[view article in new window]

Presentation tonight on Apache Cordova

I was originally going to be presenting just to the Memphis Technology User Groups tonight, but they have graciously said they don't mind if I open this up to the public. If you want to hear me give an introduction to Cordova, join me online tonight at 6:30 CST here: https://my.adobeconnect.com/raysworld/. I will be giving priority for QA to the Memphis group of course, but all our welcome to join in and - hopefully - learn something as well. This will be over Adobe Connect so be sure to use a Flash-enabled browser.


(Tue, 12 Aug 2014 09:07:00 -0400)
[view article in new window]

Centering a Google Map on America

Earlier this morning I was building a Google Map demo for a client (using EventBrite data - I'll share that if I can) and I needed to center a Google Map on America. There are a couple ways of doing this and I thought I'd share them along with some screen shots so you can see the results.

The first Google result I found led to this Stack Overflow result: How to center Google Map on a country by name. The answer uses Geocoding to translate a simple country name, like America, into the right results. You'll need to scroll down a bit to see a version of the code for the current version of Google's API, or just check out the full sample below.

<!DOCTYPE html>
<html>

<head>
<title>Demo One</title>
<style type="text/css">
#map_canvas { width: 500px; height: 500px; }
</style>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript">
function initialize() {

	var country = "United States"

    var myOptions = {
        zoom: 3,
        mapTypeId: google.maps.MapTypeId.ROADMAP
    };
	
    var map = new google.maps.Map(document.getElementById("map_canvas"),myOptions);

	var geocoder = new google.maps.Geocoder();
	geocoder.geocode( { 'address': country }, function(results, status) {
		if (status == google.maps.GeocoderStatus.OK) {
			map.setCenter(results[0].geometry.location);
		} else {
			alert("Could not find location: " + location);
		}
	});

}
</script>
</head>

<body onload="initialize()">

<div id="map_canvas"></div>

</body>
</html>

This gives you:

Looks good to me - but the geocoding bugs me. For every single visitor to your site, a request will be made to Google to ask it where America is. Most likely, America is not going to move. Probably. And while this request is pretty darn fast, there's no real reason for you to geocode this constantly. I did another quick Google and discovered this Wikipedia page: Geographic center of the contiguous United States. It defined the longitude and latitude for America as 39°50?N 98°35?W. I rewrote my code to simply use these hard coded values.

<!DOCTYPE html>
<html>

<head>
<title>Demo Two</title>
<style type="text/css">
#map_canvas { width: 500px; height: 500px; }
</style>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript">
function initialize() {

    var latlng = new google.maps.LatLng(39.5, -98.35);
    var myOptions = {
        zoom: 3,
        center: latlng,
        mapTypeId: google.maps.MapTypeId.ROADMAP
    };

	var map = new google.maps.Map(document.getElementById("map_canvas"),myOptions);

}
</script>
</head>

<body onload="initialize()">

<div id="map_canvas"></div>

</body>
</html>

And the result:

Looks a tiny bit different to me. So I went back to the first demo and added this line: console.log(results[0].geometry.location.lat(), results[0].geometry.location.lng());. I checked the console for the values and simply updated the numbers.

I zipped up the demos (including a third demo with the values returned via the console) and included it as a zip.


(Sat, 09 Aug 2014 11:22:00 -0400)
[view article in new window]

Avoid Zero (or super short) Application Timeouts

So here's a doozy for you. Over the past week or so I exchanged emails with a reader who was having an odd issue with ColdFusion mappings. Specifically the code he wrote to actually use the mappings would fail to work if he used application specific mappings. Switching to mappings defined on the server fixed it.

After I failed to notice it (and you can guess what's coming up based on the title of the entry), user BKBK on the Adobe forums pointed out that he had a 0-length application timeout: this.applicationTimeout = createTimeSpan(0,0,0,0);. Basically the application, and its settings, including the application mappings, timed out before his code could use it.

Why did he have it in the first place? He wanted a simple way to reload the Application on every request. For that I'd simply use a hook in your onRequestStart to run onApplicationStart():

function onRequestStart(string req) {
  onApplicationStart();
}

To be clear, this is not really reloading the application. It is just manually running your onApplicationStart method which is - 99% of the time - probably want you want. (During testing anyway, not in production.) You can also wrap that with a simple if(structKeyExists(url, "init")) block as well.


(Fri, 08 Aug 2014 15:14:00 -0400)
[view article in new window]

Keep your DOM from shifting around...

I've been meaning to write this up for a while now, but I never got around to it till today when a meeting got cancelled suddenly. It was this or get on the treadmill, and unfortunately, the treadmill lost. Lately I've noticed a common problem with both web apps and native apps. The problem is this: The application renders some sort of dynamic content. In that content are various UI elements you can click. At the same time, the app is fetching additional content asynchronously. When that content comes in, it is displayed then and the layout of the content is adjusted as the new stuff comes in. The problem is that the user may have been just about to click on a button, link, or whatever, and now finds that their click action has done nothing. Or worse - has activated another action that they didn't want. TweetDeck is especially bad about this. Facebook, surprisingly, is actually pretty darn good about this. Let's look at a simple example in case I'm not making sense.

I've built a simple application that lets you view, and like, pictures of cats. Let me be clear, this is just a proof of concept, but if someone builds this site I'll be hitting that every five minutes or so. When the application loads, you get a picture of the cat, and some, but not all, of the UI.

As you move your mouse, or finger, over the Like button, all of sudden the UI updates to show stats about the picture. The original developer thought it would be cool to load this after the rest of the page. Not a terribly bad idea, right? If the main focus is to show pictures of cats then loading the stats later makes a bit of sense. But see how the DOM changes after the stats were loaded:

As you can see, the Like button was shifted down. In this case the worst you get is a click event that didn't trigger anything, but it is still annoying. You can demo this yourself here: http://www.raymondcamden.com/demos/2014/aug/5/test1.html. Let's quickly look at the code so you can see how the original version was built. First, the HTML.

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
		<title></title>
		<meta name="description" content="">
		<meta name="viewport" content="width=device-width">
		<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
		<script src="app1.js"></script>
	</head>
	<body>

		<div id="content">
			<img src="http://placekitten.com/300/300">
			<div id="stats"></div>
			<button>Like!</button>
		</div>
		
	</body>
</html>

And here is the JavaScript. I used a simple setTimeout to fake a slow AJAX request.

$(document).ready(function() {

	//fake a delayed update
	window.setTimeout(function() {
		$("#stats").html("<b>Likes:</b> 912");
	},2000);
	
});

Ok, so how can we fix this? One approach may be to simply specify a set height for the DOM item we are updating. That way there won't be a "shift" when the content is uploaded. For example:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
		<title></title>
		<meta name="description" content="">
		<meta name="viewport" content="width=device-width">
		<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
		<script src="app2.js"></script>
		<style>
			#stats {
				height: 30px;
				background-color: #c0c0c0;
			}
		</style>
	</head>
	<body>

		<div id="content">
			<img src="http://placekitten.com/300/300">
			<div id="stats"></div>
			<button>Like!</button>
		</div>
		
	</body>
</html>

Notice I added both a height and a background-color. The color change was simply to ensure that my height was working right. It also gives the user a bit of a clue that something is going to be there. (I won't pretend this is pretty, but hopefully you get the idea.) You can try this version here: http://www.raymondcamden.com/demos/2014/aug/5/test2.html.

But we can do even better, right? I don't like the big empty box. Let's modify the stats area to include the labels for our stats (well, our stat), so that the update is a bit less jarring. While we're at it, our image service (in this case, the epic placekitten.com) can also be a source of DOM shifting as the image loads. I should have added specific height and width to the image.

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
		<title></title>
		<meta name="description" content="">
		<meta name="viewport" content="width=device-width">
		<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
		<script src="app3.js"></script>
		<style>
			#stats {
				height: 30px;
			}
		</style>
	</head>
	<body>

		<div id="content">
			<img src="http://placekitten.com/300/300" width="300" height="300">
			<div id="stats">Likes: <span id="likes"></span></div>
			<button>Like!</button>
		</div>
		
	</body>
</html>

I modified the JavaScript now to both add a loading message and to just change the span.

$(document).ready(function() {

	$("#likes").html("<i>Fetching</i>");

	//fake a delayed update
	window.setTimeout(function() {
		$("#likes").html("912");
	},2000);
	
});

You can run this version here: http://www.raymondcamden.com/demos/2014/aug/5/test3.html.

This isn't rocket science, but as I said in the beginning, I find myself surprised by how many sites and apps seem to have this problem. Keep it in mind when working on your next project.


(Tue, 05 Aug 2014 08:07:00 -0400)
[view article in new window]

MockData CFC Released

A while ago I wrote a Node.js service called MockData. The idea behind it was to create a quick way to generate mock data for client-side applications that were purely driven by URL parameters. So for example, I could get 10 random people by doing an XHR request to http://myserver:myport?num=10&author=name. The service I built supported a few different types of mock data (names, emails, addresses, telephone numbers, etc), and was, I think, pretty flexible. I thought it would be interesting to rewrite the core logic in ColdFusion, specifically ColdFusion 11, to see how much of the JS code had to be re-engineered.

To be fair, this wasn't meant to be a complete rewrite. My Node.js version is its own little server. It runs on a port and could easily be used with anything - ColdFusion, PHP, another Node.js app, or even a static web site. The ColdFusion version doesn't have to worry about firing up a server or taking over a port. It simply has to look at the query string and generate the proper data.

So how did it go? All in all it was mostly painless. The biggest annoyance I ran into was... and yes... you can taunt me with these words later... was ColdFusion starting arrays at 0. I will go to my grave convinced that 0-based indexes came about because the original designer was drunk and needed to cover his ass later (cue the outraged comments in 3...2...1...) but at the end of day... I'm just more used to 0 now and I wish ColdFusion had a toggle to use it too.

There. I said it.

The other big thing I ran into was also array based. I kept typing someArr.length when I needed to do someArr.len(). I'm happy ColdFusion 11 allows me to run methods on variables, but I do wish they had added a simple length property too.

Speaking of methods, I did find myself forgetting to make use of them. For example, I had structKeyExists(foo, "goo") where foo.keyExists("goo") works now. If you look over the code you'll probably find other places where I forgot this as well. (In fact, I just checked and found another one I missed: arr.each). The code isn't terribly long so I'll share the entire thing below, but you can download, and fork it, here: https://github.com/cfjedimaster/MockDataCFC

component {

	cfheader(name="Access-Control-Allow-Origin", value="*");
	
	//So you can skip passing it...
	url.method="mock";

	//Defaults used for data, may move to a file later
	fNames = ["Andy","Alice","Amy","Barry","Bob","Charlie","Clarence","Clara","Danny","Delores","Erin","Frank","Gary","Gene","George","Heather","Jacob","Leah","Lisa","Lynn","Nick","Noah","Ray","Roger","Scott","Todd"];
	lNames = ["Anderson","Bearenstein","Boudreaux","Camden","Clapton","Degeneres","Hill","Moneymaker","Padgett","Rogers","Smith","Sharp","Stroz","Zelda"];
	emailDomains = ["gmail.com","aol.com","microsoft.com","apple.com","adobe.com"];
	lorem = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";

	defaults = ["name","fname","lname","age","all_age","email","ssn","tel","gps","num"];
	function isDefault(s) {
		return defaults.findNoCase(s) >= 0; 
	}

	function generateFirstName() {
		return fNames[randRange(1, fNames.len())];
	}

	function generateLastName() {
		return lNames[randRange(1, lNames.len())];
	}

	function generateFakeData(type) {
		if(type == "string") return "string";
		if(type == "name") return generateFirstName() & " " & generateLastName();
		if(type == "fname") return generateFirstName();
		if(type == "lname") return generateLastName();
		if(type == "age") return randRange(18,75);
		if(type == "all_age") return randRange(1,100);
		if(type == "email") {
			var fname = generateFirstName().toLowerCase();
			var lname = generateLastName().toLowerCase();
			var emailPrefix = fname.charAt(1) & lname;
			return emailPrefix & "@" & emailDomains[randRange(1, emailDomains.len())];
		}
		if(type == "ssn") {
			return randRange(1,9) & randRange(1,9) & randRange(1,9) & "-" &
				    randRange(1,9) & randRange(1,9) & "-" & 
				    randRange(1,9) & randRange(1,9) & randRange(1,9) & randRange(1,9);
		}
		if(type == "tel") {
			return "(" & randRange(1,9) & randRange(1,9) & randRange(1,9) & ") " &
				    randRange(1,9) & randRange(1,9) & randRange(1,9) & "-" & 
				    randRange(1,9) & randRange(1,9) & randRange(1,9) & randRange(1,9);
	
		}
		if(type.find("num") == 1) {
			//Support num, num:10, num:1:10
			if(type == "num") return randRange(1,10);
			if(type.find(":") > 1) {
				var parts = type.listToArray(":");
				if(parts.len() == 2) return randRange(1,parts[2]);
				else return randRange(parts[2],parts[3]);
			}
		}
		if(type.find("oneof") == 1) {
			//Support oneof:male:female, ie, pick a random one
			var items = type.listToArray(":");
			items.deleteAt(1);
			return items[randRange(1,items.len())];
		}
		if(type.find("lorem") == 1) {
			if(type == "lorem") return lorem;
			if(type.find(":") > 1) {
				var parts = type.listToArray(":");
				var result = "";
				var count = "";
				if(parts.len() == 2) count = parts[2];
				else count = randRange(parts[2],parts[3]);
				for(var i=0; i<count; i++) result &= lorem & "\n\n";
				return result;
			}
		}
		return "";
	}

	function generateNewItem(model) {
		var result = {};

		model.each(function(field) {
			if(!field.keyExists("name")) {
				field.name = "field"&i;
			}
	
			if(!field.keyExists("type")) {
				//if we are a default, that is our type, otherwise string
				if(isDefault(field.name)) field.type = field.name;
			}
			result[field.name] = generateFakeData(field.type);
			
		});
		
		return result;
	}
	
	remote function mock() returnformat="json" {
		
		//Did they specify how many they want?
		if(!arguments.keyExists("num")) arguments.num = 10;

		if(!isNumeric(arguments.num) && arguments.num.find(":") > 0) {
			var parts = arguments.num.listToArray(":");
			if(parts[1] != "rnd") {
				throw("Invalid num prefix sent. Must be 'rnd'");
			}
			// format is rnd:10 which means, from 1 to 10
			if(parts.len() == 2) {
				arguments.num = randRange(1,parts[2]);
			} else {
				arguments.num = randRange(parts[2],parts[3]);
			}
		}

		var fieldModel = [];
		for(var key in arguments) {
			if(key != "num") {
				fieldModel.append({name:key,type:arguments[key]});
			}
		}
	
		var result = [];
		for(var i=1; i<=arguments.num; i++) {
			result.append(generateNewItem(fieldModel));
		}
			
		cfheader(name="Content-Type", value="application/json");

		return result;
	}

}

(Mon, 04 Aug 2014 16:38:00 -0400)
[view article in new window]

How to make a phone call via GMail

This falls under the "obvious" category (like probably most of my posts), but as I had trouble finding it before, I figured I'd share this tip. To make a phone call from GMail, with the new Hangouts features, follow these steps:

First, sign into Hangouts. I used to keep my account signed out as I used Adium for IM, but I've switched to using Hangouts for all my IM conversations now. (Note, in the screen shot below, I'm using Firefox and an older account of mine, which accounts for lack of crap in my inbox ;)

After you sign in, click the Search button:

In the popup, notice it has a phone icon there, that's your first clue. But it may not be obvious how to dial. Just enter a number.

In case you're curious, I used sophisticated NSA-level methods to obfuscate the number in the screen shot. And that's it - just click the number and it will dial. A popup window for a new Hangout will launch and that's it. Obviously you get a dial pad (I use it a lot for Adobe's phone call meeting service) and you can mute yourself as well. You may want to check the settings to ensure it picks up your mic. My laptop has a mic, but I have an external one as well, and on a recent call it was using the wrong one.


(Fri, 01 Aug 2014 13:20:00 -0400)
[view article in new window]

Read Holly's guide to the iOS Status Bar

It's kinda funny - but something as simple as the iOS status bar can be confusing as quantum mechanics. Holly wrote a blog post, and an interactive web-based simulator, that explains the various settings and configurations for the status bar. It is definitely recommended reading: PhoneGap Developer's Guide to the iOS Status Bar.

P.S. Yes, this is a super short blog post, but I like to share important blog posts like this for the members of my audience lucky enough to avoid Twitter. Trust me - stay away...


(Thu, 31 Jul 2014 14:43:00 -0400)
[view article in new window]

Linting your ColdFusion code

Earlier this week I discovered a new project on GitHub, CFLint. For my readers who may not be aware, linters are tools that inspect your code for bugs, best practices, and other issues. Numerous different types of linters exist, but as far as I know this is the first one for ColdFusion. It is still a bit rough (in my tests it would routinely have parsing issues on some of my files) but it is a good start and I think it could be a great tool for ColdFusion developers.

You can test this yourself on the project's releases page. They included scripts for both OS X and Windows. The core code is Java based if you want to peak under the hood. Like most linters, you can also disable rules you don't agree with. As I said, it isn't perfect, but I'd strongly urge folks to check it out and - even better - help improve it.

Want to test it out with ColdFusion Builder? I worked on an extension for it last night. You can right-click on a project, folder, or file and scan it using CFLint. As an example:

You can click on each line item to open the file right to the line reported by CFLint. Currently the biggest issue is that it doesn't provide feedback while it is working. If you test it on a large project, keep that in mind. You can download my extension here: https://github.com/cfjedimaster/CFLint-Extension.


(Thu, 31 Jul 2014 06:59:00 -0400)
[view article in new window]

Check out CodeSchool's Angular course

I've raved about CodeSchool before so I thought folks might be interested in knowing they have a really cool new course for AngularJS: Shaping Up with AngularJS. Best of all - the course is free. I didn't learn a lot from it as I think I've got the basics covered well, but I enjoyed going through the lessons to help reinforce my existing knowledge. As with other courses, they do a real good job of building lessons that let you code without being particular about things like tabs, spaces, whatever.

There were a couple of issues I had with the course however. The smallest one was, and this may sound petty, but the "cutsey" song before each lesson almost made me jump off a cliff. I like the fact that CodeSchool creates these cute little musical intros, but if you are doing an entire course in one afternoon, it gets old extremely quickly. In my opinion they should really consider skipping the intros after the first lesson. On the flip side, I liked that they include outtakes at the end of each video.

The other issue I ran into was with the lessons. So I know I just praised how they let you answer solutions without being picky about how you write your code, but sometimes it was picky in ways that could be confusing to users. Here is a great example. You are asked to modify an HTML template to add a value. So basically going from this: <span>-- </span> to this: <span>-- {{author}}</span>. Notice how the author token comes after the two dashes with a space? If you didn't edit the template to match that exactly, you were told you had not completed the task correctly. To me, this is something that could have been handled much easier. I don't know how they do their validation, but if they were checking for the exact string "-- {{author}}" they could have switched to something more generic. I know this probably sounds a bit petty too, but I worry about it confusing/blocking students.

The most surprising issue, and one that will probably cause Angular folks to scratch their heads, is that none of the examples makes use of $scope. Now... as a technical blogger, I know for a fact I've sometimes skipped best practices when teaching some particular concept. I'll make that call when I think it makes sense. But for the life of me I can't imagine why they would skip showing $scope when it seems like such an important facet of Angular development. I'm still a newbie of course so maybe I'm overstating it, but it seems truly odd. I think it will trip people up who go from this course to another one.

Finally, I wish the course's code was available for download. When you do the lessons, you only have to write a few lines at a time. So for example, they may have you modify a JavaScript file but you only need to worry about three lines. I liked the examples (despite the issue above) and I wish I could have had them locally to play with later. Yes, I could cut and paste, but it seems like an obvious thing they could do to improve the course.

So, all in all, I definitely recommend it, and heck, you can't beat the price. CodeSchool will be offering a commercial course later on and I plan on taking that too.

p.s. I'm currently taking the JavaScript Best Practices course. I'll review it here when I'm done.


(Tue, 29 Jul 2014 13:19:00 -0400)
[view article in new window]


© The connection to the CAMDEN's RSS feed has timed out - please try again later. We are sorry for any inconvenience this may have caused.