Skip to content

Instantly share code, notes, and snippets.

@forcementor
Last active October 11, 2015 21:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save forcementor/3921283 to your computer and use it in GitHub Desktop.
Save forcementor/3921283 to your computer and use it in GitHub Desktop.
Developing Mobile Applications with Force.com and Sencha Touch - Part 2 - Complete Project
<!--
========================================================
Name: PocketCRM
Type: Visualforce Page
Purpose: For Sencha Touch PocketCRM App
Created by: Don Robins - www.ForceMentor.com
Created on: August 1, 2012
Copyright 2012 Outformations, Inc.
Rev # Revised on Revised by Revision Description
----- ---------- -------------------------------------
1.0 08/01/2012 Don Robins Initial Release
=========================================================
-->
<apex:page docType="html-5.0" showHeader="false" standardStylesheets="false" cache="false">
<head>
<meta charset="UTF-8" />
<title>Pocket CRM</title>
<!-- Use the Visualforce tags for scripts and stylesheets rather than their HTML counterparts -->
<apex:includeScript value="http://cdn.sencha.io/try/touch/2.0.1/sencha-touch-all.js" />
<apex:stylesheet value="http://cdn.sencha.io/try/touch/2.0.1/resources/css/sencha-touch.css" />
<!-- Custom Application Style Sheet -->
<c:PocketCRM_CSS />
<!-- Our custom Visualforce component containing our Sencha Touch application -->
<c:PocketCRM_APP />
</head>
<body>
<!-- An animated image that displays while loading -->
<div id="appLoadingIndicator">
<div></div>
<div></div>
<div></div>
</div>
</body>
</apex:page>
<!--
========================================================
Name: PocketCRM_APP
Type: Visualforce Page
Purpose: For Sencha Touch PocketCRM App
Created by: Don Robins - www.ForceMentor.com
Created on: August 1, 2012
Copyright 2012 Outformations, Inc.
Rev # Revised on Revised by Revision Description
----- ---------- -------------------------------------
1.0 08/01/2012 Don Robins Initial Release
=========================================================
-->
<apex:component controller="PocketCRMLeadController" >
<script type="text/javascript">
//===============================================================================================
//APPLICATION
//The Application class is the entry point into your Sencha Touch application.
//===============================================================================================
Ext.Loader.setConfig({
enabled: true
});
Ext.application({
name: "PocketCRM",
//Load the various MVC components into memory.
models: ["Lead"],
stores: ["Leads"],
controllers: ["Leads"],
views: ["LeadsList", "LeadEditor"],
//The application's startup routine once all components are loaded.
launch: function () {
//Instantiate your main list view for Leads.
var leadsListView = {
xtype: "leadslistview"
};
var leadEditorView = {
xtype: "leadeditorview"
};
//Launch the primary fullscreen view and pass in the list view.
Ext.Viewport.add([leadsListView, leadEditorView]);
}
});
//==============================================================================================
//VIEWS
//Views display data to your users and gather input from them;
//they also emit events about your user interaction.
//==============================================================================================
//The Lead list view.
Ext.define("PocketCRM.view.LeadsList", {
extend: "Ext.Container",
//It uses the base list class.
requires: "Ext.dataview.List",
alias: "widget.leadslistview",
config: {
//Take up the full space available in the parent container.
layout: {
type: 'fit'
},
//Add the components to include within the list view.
items: [
{
xtype: "toolbar",
title: "PocketCRM",
docked: "top",
items: [
{
xtype: 'spacer'
},
{
xtype: "button",
text: 'New',
ui: 'action',
itemId: "newButton"
}
]
},
{
xtype: "toolbar",
docked: "bottom",
items: [
{
xtype: "button",
iconCls: "refresh",
iconMask: true,
itemId: "syncButton"
}
]
},
{
//The main list and its properties.
xtype: "list",
store: "Leads",
itemId: "leadsList",
onItemDisclosure: true,
indexBar: true,
grouped: true,
disableSelection: false,
//The template for display if the Store is empty of records.
//Note the style to control visual presentation.
loadingText: "Loading Leads...",
emptyText: '<div class="leads-list-empty-text">No leads found.</div>',
//The template for the display of each list item representing one record.
//One row will display for each record in the data Store.
//The fields referenced are from the entity's Model.
itemTpl: '<div class="list-item-line-main">{LastName}, {FirstName}</div>' + '<div class="list-item-line-detail">{Company}</div>' + '<div class="list-item-line-detail">{Title} - Phone: {Phone} </div>' + '<div class="list-item-line-detail">{Email}</div>',
}],
listeners: [{
delegate: "#newButton",
event: "tap",
fn: "onNewButtonTap"
}, {
delegate: "#syncButton",
event: "tap",
fn: "onSyncButtonTap"
}, {
delegate: "#leadsList",
event: "disclose",
fn: "onLeadsListDisclose"
}]
},
onSyncButtonTap: function () {
console.log("syncLeadCommand");
this.fireEvent("syncLeadCommand", this);
},
onNewButtonTap: function () {
console.log("newLeadCommand");
this.fireEvent("newLeadCommand", this);
},
onLeadsListDisclose: function (list, record, target, index, evt, options) {
console.log("editLeadCommand");
this.fireEvent('editLeadCommand', this, record);
}
});
Ext.define("PocketCRM.view.LeadEditor", {
extend: "Ext.form.Panel",
requires: "Ext.form.FieldSet",
alias: "widget.leadeditorview",
config: {
scrollable: 'vertical',
items: [{
xtype: "toolbar",
docked: "top",
title: "Edit Lead",
items: [
{
xtype: "button",
ui: "back",
text: "Home",
itemId: "backButton"
},
{
xtype: "spacer"
},
{
xtype: "button",
ui: "action",
text: "Save",
itemId: "saveButton"
}]
},
{
xtype: "toolbar",
docked: "bottom",
items: [
{
xtype: "button",
iconCls: "trash",
iconMask: true,
itemId: "deleteButton"
}]
},
{
xtype: "fieldset",
title: 'Lead Info',
items: [
{
xtype: 'textfield',
name: 'FirstName',
label: 'First Name'
},
{
xtype: 'textfield',
name: 'LastName',
label: 'Last Name',
required: true
},
{
xtype: 'textfield',
name: 'Company',
label: 'Company',
required: true
},
{
xtype: 'textfield',
name: 'Title',
label: 'Title'
},
{
xtype: 'selectfield',
name: 'Status',
label: 'Status',
required: true,
value: 'Open - Not Contacted',
options: [
{
text: 'Open - Not Contacted',
value: 'Open - Not Contacted'
},
{
text: 'Working - Contacted',
value: 'Working - Contacted'
},
{
text: 'Closed - Converted',
value: 'Closed - Converted'
},
{
text: 'Closed - Not Converted',
value: 'Closed - Not Converted'
}
],
},
]
},
{
xtype: "fieldset",
title: 'Contact Info',
items: [
{
xtype: 'textfield',
name: 'Phone',
label: 'Phone',
component: {
type: 'tel'
}
},
{
xtype: 'textfield',
name: 'MobilePhone',
label: 'Mobile',
component: {
type: 'tel'
}
},
{
xtype: 'emailfield',
name: 'Email',
label: 'Email Address'
},
]
},
],
listeners: [
{
delegate: "#backButton",
event: "tap",
fn: "onBackButtonTap"
},
{
delegate: "#saveButton",
event: "tap",
fn: "onSaveButtonTap"
},
{
delegate: "#deleteButton",
event: "tap",
fn: "onDeleteButtonTap"
}
]
},
onSaveButtonTap: function () {
console.log("saveLeadCommand");
this.fireEvent("saveLeadCommand", this);
},
onDeleteButtonTap: function () {
console.log("deleteLeadCommand");
this.fireEvent("deleteLeadCommand", this);
},
onBackButtonTap: function () {
console.log("backToHomeCommand");
this.fireEvent("backToHomeCommand", this);
}
});
//==============================================================================================================
//CONTROLLERS
//Controllers manage the communication of your application and the coordination between the views and the model;
//they listen for the events emitted by the views and react accordingly.
//==============================================================================================================
//The controller for the Leads list view
Ext.define("PocketCRM.controller.Leads", {
extend: "Ext.app.Controller",
config: {
refs: {
// We're going to lookup our views by alias.
leadsListView: "leadslistview",
leadEditorView: "leadeditorview",
leadsList: "#leadsList"
},
control: {
leadsListView: {
// The commands fired by the list container.
newLeadCommand: "onNewLeadCommand",
editLeadCommand: "onEditLeadCommand"
},
leadEditorView: {
// The commands fired by the note editor.
saveLeadCommand: "onSaveLeadCommand",
deleteLeadCommand: "onDeleteLeadCommand",
backToHomeCommand: "onBackToHomeCommand"
}
}
},
//View Transitions
slideLeftTransition: {
type: 'slide',
direction: 'left'
},
slideRightTransition: {
type: 'slide',
direction: 'right'
},
//View Transition Helper functions
activateLeadEditor: function (record) {
var leadEditorView = this.getLeadEditorView();
leadEditorView.setRecord(record);
Ext.Viewport.animateActiveItem(leadEditorView, this.slideLeftTransition);
},
activateLeadsList: function () {
Ext.Viewport.animateActiveItem(this.getLeadsListView(), this.slideRightTransition);
},
//View event handler functions
onSyncLeadCommand: function () {
console.log("onSyncLeadCommand");
//Get a ref to the store and remove it.
var leadsStore = Ext.getStore("Leads");
//Resync the proxy, reload and activate the list.
leadsStore.sync();
leadsStore.load();
this.activateLeadsList();
},
onNewLeadCommand: function () {
console.log("onNewLeadCommand");
var newLead = Ext.create("PocketCRM.model.Lead", {
Status: "Open - Not Contacted"
});
this.activateLeadEditor(newLead);
},
onEditLeadCommand: function (list, record) {
console.log("onEditLeadCommand");
this.activateLeadEditor(record);
},
onSaveLeadCommand: function () {
console.log("onSaveLeadCommand");
//Update the field values in the record.
var leadEditorView = this.getLeadEditorView();
var currentLead = leadEditorView.getRecord();
var newValues = leadEditorView.getValues();
this.getLeadEditorView().updateRecord(currentLead);
//Check for validation errors.
var errors = currentLead.validate();
if (!errors.isValid()) {
var msg = '';
errors.each(function (error) {
msg += error.getMessage() + '<br/>';
});
console.log('Errors: ' + msg);
Ext.Msg.alert('Please correct errors!', msg, Ext.emptyFn);
currentLead.reject();
return;
}
//Get a ref to the store.
var leadsStore = Ext.getStore("Leads");
//Add new record to the store.
if (null == leadsStore.findRecord('id', currentLead.data.id)) {
leadsStore.add(currentLead);
}
//Resync the proxy and activate the list.
leadsStore.sync();
this.activateLeadsList();
},
onDeleteLeadCommand: function () {
console.log("onDeleteLeadCommand");
//Get a ref to the form and its record.
var leadEditorView = this.getLeadEditorView();
var currentLead = leadEditorView.getRecord();
//Get a ref to the store and remove it.
var leadsStore = Ext.getStore("Leads");
leadsStore.remove(currentLead);
//Resync the proxy and activate the list.
leadsStore.sync();
this.activateLeadsList();
},
onBackToHomeCommand: function () {
console.log("onBackToHomeCommand");
this.activateLeadsList();
},
// Base Class functions.
launch: function () {
console.log("launch");
this.callParent(arguments);
//Load up the Store associated with the controller and its views.
console.log("load Leads");
var leadsStore = Ext.getStore("Leads");
leadsStore.load();
},
init: function () {
this.callParent(arguments);
console.log("init");
//Listen for exceptions observed by the proxy so we can report them and clean up.
//20121016 Fixed for bug on improper object references
Ext.getStore('Leads').getProxy().addListener('exception', function (proxy, response, operation, options) {
// only certain kinds of errors seem to have useful information returned from the server
if (response) {
if (response.errorMessage) {
Ext.Msg.alert('Error', response.errorMessage);
} else {
Ext.Msg.alert('Error', operation.config.action + ' failed: ' + response.errorMessage);
}
} else {
Ext.Msg.alert('Error', operation.config.action + ' failed for an unknown reason: proxy = ' + proxy);
}
});
},
});
//================================================================================================================
//PROXY RELATED EXTENSIONS
//================================================================================================================
// The RemotingProvider used by Visualforce and the one used in the latest Touch are out of sync
// so we need to adjust our read method to add a function that Touch expects to see to get Arguments.
//================================================================================================================
//AN IMPORTANT NOTE: if your org has a registered namespace, you MUST reference the Apex controller name in your
//JavaScript with your org's namespace. If you fail to do this, you will get a a JavaScript error that the Apex
//controller can NOT be found!
//================================================================================================================
PocketCRMLeadController.Query.directCfg.method.getArgs = function (params, paramOrder, paramsAsHash) {
console.log('getArgs: ' + params.data);
return [params];
}
// The DirectProxy gets properly formatted data from the API calls, but then discards it.
// This may simply be a difference between ExtJS 3 (used by Remoting) and 4...
Ext.data.proxy.Direct.prototype.createRequestCallback = function (request, operation, callback, scope) {
var me = this;
return function (data, event) {
console.log('createRequestCallback: ' + operation);
me.processResponse(event.status, operation, request, data, callback, scope);
};
};
//================================================================================================================
//MODELS
//Models are the objects on your application.
//================================================================================================================
//AN IMPORTANT NOTE: if your org has a registered namespace, you MUST reference the Apex controller name in the
//proxy your JavaScript with your org's namespace. If you fail to do this, you will get a a JavaScript error that
//the Apex controller can NOT be found!
//================================================================================================================
//================================================================================================================
//The Lead model will include whatever fields are necssary to manage.
Ext.define("PocketCRM.model.Lead", {
extend: "Ext.data.Model",
config: {
idProperty: 'Id',
fields: [
{
name: 'Id',
type: 'string',
persist: false
},
{
name: 'Name',
type: 'string',
persist: false
},
{
name: 'FirstName',
type: 'string'
},
{
name: 'LastName',
type: 'string'
},
{
name: 'Company',
type: 'string'
},
{
name: 'Title',
type: 'string'
},
{
name: 'Phone',
type: 'string'
},
{
name: 'MobilePhone',
type: 'string'
},
{
name: 'Email',
type: 'string'
},
{
name: 'Status',
type: 'string'
}
],
validations: [
{
type: 'presence',
field: 'LastName',
message: 'Enter a last name.'
},
{
type: 'presence',
field: 'Company',
message: 'Enter a company.'
},
{
type: 'presence',
field: 'Status',
message: 'Select a status.'
}
],
//Bind each CRUD functions to a @RemoteAction method in the Apex controller
proxy: {
type: 'direct',
api: {
read: PocketCRMLeadController.Query,
create: PocketCRMLeadController.Add,
update: PocketCRMLeadController.Edit,
destroy: PocketCRMLeadController.Destroy
},
limitParam: 'recordCount', // because "limit" is an Apex keyword
sortParam: 'sortParams', // because "sort" is a keyword too
pageParam: false, // we don't use this in the controller, so don't send it
reader: {
type: 'json',
rootProperty: 'records',
messageProperty: 'errorMessage'
},
writer: {
type: 'json',
root: 'records',
writeAllFields: false, // otherwise empty fields will transmit
// as empty strings, instead of "null"/not present
allowSingle: false, // need to always be an array for code simplification
encode: false // docs say "set this to false when using DirectProxy"
}
}
},
});
//================================================================================================================
//STORES
//Stored serve as the client-side cache of your data; they loading data into your app's views.
//================================================================================================================
//The Lead Store, this version will simply load with mock JSON data.
Ext.define("PocketCRM.store.Leads", {
extend: "Ext.data.Store",
requires: "Ext.data.proxy.LocalStorage",
config: {
model: "PocketCRM.model.Lead",
//MAKE SURE YOU HAVE COMPLETELY REMOVED THE OLD DATA BINDING REFERENCE FROM PART 1!
autoLoad: true,
pageSize: 50,
//Create a grouping; be certain to use a field with content or you'll get errors!
groupField: "Status",
groupDir: "ASC",
//Create additional sorts for within the Group.
sorters: [{
property: 'LastName',
direction: 'ASC'
}, {
property: 'FirstName',
direction: 'ASC'
}]
}
});
</script>
</apex:component>
<apex:component>
<style type="text/css">
/**
* Example of an initial loading indicator.
* It is recommended to keep this as minimal as possible to provide instant feedback
* while other resources are still being loaded for the first time
*/
html, body {
height: 100%;
background-color: #1985D0
}
#appLoadingIndicator {
position: absolute;
top: 50%;
margin-top: -15px;
text-align: center;
width: 100%;
height: 30px;
-webkit-animation-name: appLoadingIndicator;
-webkit-animation-duration: 0.5s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-direction: linear;
}
#appLoadingIndicator > * {
background-color: #FFFFFF;
display: inline-block;
height: 30px;
-webkit-border-radius: 15px;
margin: 0 5px;
width: 30px;
opacity: 0.8;
}
@-webkit-keyframes appLoadingIndicator {
0% {
opacity: 0.8
}
50% {
opacity: 0
}
100% {
opacity: 0.8
}
}
/**
* PocketCRM Custom styles
*/
/* Increase height of list item so title and narrative lines fit */
.x-list .x-list-item .x-list-item-label {
min-height: 4.5em!important;
}
/* Move up the disclosure button to account for the list item height increase */
.x-list .x-list-disclosure {
position: absolute;
bottom: 0.85em;
right: 0.44em;
}
.list-item-line-main {
float:left;
width:100%;
font-size:100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right:25px;
line-height:115%;
}
.list-item-line-detail {
float:left;
width:95%;
font-size:60%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right:25px;
padding-left:10px;
line-height:125%;
}
.x-item-selected .list-item-line-detail {
color:#ffffff;
font-weight: bold;
}
.leads-list-empty-text {
padding:10px;
}
</style>
</apex:component>
//========================================================
// Name: PocketCRMLeadController
// Type: Controller
// Purpose: For Sencha Touch PocketCRM App
// Created by: Don Robins - www.ForceMentor.com
// Created on: August 1, 2012
// Copyright 2012 Outformations, Inc.
//
// Rev # Revised on Revised by Revision Description
// ----- ---------- -------------------------------------
// 1.0 08/01/2012 Don Robins Initial Release
//=========================================================
public with sharing class PocketCRMLeadController {
public PocketCRMLeadController() {}
//========================================================================
//INNER CLASSES
//These support data request/response transport for remoting.
//========================================================================
// One of the parameters supplied by the DirectProxy read method.
public class QueryRequest {
Integer start;
Integer recordCount;
List < Map < String, String >> sortParams;
Public QueryRequest() {
start = 1;
recordCount = 1;
}
Public QueryRequest(Integer pStart, Integer pRecordCount) {
start = pStart;
recordCount = pRecordCount;
}
}
// The server response expected by the ExtJS DirectProxy API methods.
public class Response {
public Boolean success;
public String errorMessage;
public List < SObject > records;
public Integer total;
Response() {
records = new List < SObject > ();
success = true;
}
}
//=======================================================================
//PUBLIC CRUD REMOTE ACTION METHODS CALLED BY THE SENCHA PROXY
//=======================================================================
@RemoteAction
public static Response Query(QueryRequest qr) {
Response resp = new Response();
List < Lead > LeadList;
try {
LeadList = getAllLeads();
} catch (Exception e) {
resp.success = false;
resp.errorMessage = 'Query failed: ' + e.getMessage();
return resp;
}
//Supply only the requested records
for (Integer recno = qr.start;
recno < (qr.start + qr.recordCount) && recno < LeadList.size(); ++recno) {
resp.records.add(LeadList[recno]);
}
resp.total = LeadList.size();
resp.success = true;
return resp;
}
@RemoteAction
public static Response Edit(List < Lead > LeadData) {
return updateLeadList(LeadData);
}
@RemoteAction
public static Response Add(List < Lead > LeadData) {
return insertLeadList(LeadData);
}
@RemoteAction
public static Response Destroy(List < Lead > LeadData) {
return deleteLeadList(LeadData);
}
//=======================================================================
//PRIVATE HELPER METHODS
//=======================================================================
private static List < Lead > getAllLeads() {
return [SELECT
FirstName, LastName, Company, Title, Phone, MobilePhone, Email, Status
FROM Lead LIMIT 50];
}
private static Response insertLeadList(List < Lead > LeadData) {
Response resp = new Response();
resp.success = true;
try {
INSERT LeadData;
} catch (Exception e) {
resp.success = false;
resp.errorMessage = 'Insert failed: ' + e.getMessage();
}
return resp;
}
private static Response updateLeadList(List < Lead > LeadData) {
Response resp = new Response();
resp.success = true;
try {
UPDATE LeadData;
} catch (Exception e) {
resp.success = false;
resp.errorMessage = 'Update failed: ' + e.getMessage();
}
return resp;
}
private static Response deleteLeadList(List < Lead > LeadData) {
Response resp = new Response();
resp.success = true;
try {
DELETE LeadData;
} catch (Exception e) {
resp.success = false;
resp.errorMessage = 'Deletion failed: ' + e.getMessage();
}
return resp;
}
}
@forcementor
Copy link
Author

An important note: if your org has a registered namespace, you MUST reference the Apex controller name in your JavaScript with your org's namespace. If you fail to do this, you will get a JavaScript error that the Apex controller can NOT be found!

@forcementor
Copy link
Author

Another note: Make sure that all fields in the model are accessible in your org, available to the user and match the SOQL query in the Apex controller.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment