AngularJS Seed Project

Best practice of custom SPA solutions for Salesforce

#angular, #ng, #spa, #salesforce, #bestpractice

Project boot

Template

Controller

Service

Remote Action

AJAX

JS

APEX

Visualforce component

Dropbox

 - Hash link

 - No link to folder

 - Long synch

 

+ Quick start

+ Public changes

Local server

- Private changes

+ No hashed link

+ Relative path

+ Git

+ Gulp tasks

+ Quick synch

Setup

  • install npm, git
  • init project (npm init / npm install)
  • install gulp
  • create gulpfile.js
  • run gulp 
  • enjoy :)
$ git clone https://github.com/vladborsh/angular-sf-seed.git

$ cd angular-sf-seed

$ npm install -g bower

$ npm install

$ npm install -g gulp-cli 

$ gulp

...

$ gulp zip-vendor

$ gulp zip-app

Gulpfile

var gulp = require('gulp');
var webserver = require('gulp-webserver');
 
gulp.task('webserver', function() {
  gulp.src('app')
    .pipe(webserver({
      livereload: true,
      directoryListing: {
      	enable: true,
        path:   'app'
      },
      open: true,
      https: true
    }));
});

/* ... */

gulp.task('default', [ 'webserver']);

Key files

Salesforce

  • Fruits.page
  • Templates.component
  • Banana.component
  • FruitsCtrl.cls
  • BananaSrv.cls
  • BananaSrvTest

Local server

  • app.js
  • app.config.js
  • sf-remote.srv.js
  • banana.ctrl.js
  • banana.srv.js
  • banana.fct.js

Front-end

  1. Project structure
  2. Templates separation
  3. Salesforce remote service
  4. Salesforce call, Chaining, Parallel calls
  5. General signature
  6. Data sharing through factories

...

tip #1 Project structure

  • Implicit separation of concerns
  • In large project is hard to identify relations
  • Similar to other "sample" SPA demos
  • Explicit separation of concerns
  • Easy to edintify related items and scale
  • Ready to hierarchy separation

#bro

#nonbro

tip #2 Templates separation

<apex:page showHeader="false" sidebar="false" 
    controller="FruitsCtrl">
<html lang="en">
    <head>
        <meta charset="UTF-8"/> 
        <title>Fruits</title>

        <!-- Vendor scripts -->

        <!-- Client scripts -->

    </head>

    <body ng-app="App">
        <!-- Templates storage -->
  	<c:Templates /> 

  	<div class="container-fluid">
            <!-- Main router view-->
            <div class="animate-opacity" ui-view="main"></div>
  	</div>

  </body>
	
</html>
</apex:page>
<apex:component >
    <script type="text/ng-template" id="Banana.html">
	<c:Banana /> 
    </script>
    <script type="text/ng-template" id="Opportunity.html">
	<c:Opportunity /> 
    </script>
    <script type="text/ng-template" id="OpportunityDetails.html">
	<c:OpportunityDetails /> 
    </script>
    <!-- Other components -->
</apex:component>
<apex:component >
    <div class="row">
        <div class="col-md-12">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <!-- Some header -->
                </div>
                <div class="panel-body">
                    <!-- Page body -->
                </div>
	    </div>
        </div>
    </div>
</apex:component>

Fruits.page

Templates.component

Banana.component

tip #2 Templates separation

Config.$inject = ['$stateProvider', '$urlRouterProvider']

function Config($stateProvider, $urlRouterProvider) {
  $stateProvider
  .state('banana', {
    url: '/',
    views: {
      "main": {
        templateUrl: "Banana.html",
        controller: 'BananaCtrl',
        controllerAs: 'vm'
      }
    }            
  })
  /* other states */
  $urlRouterProvider.otherwise('/');
};

angular.module('App').config(Config);
<apex:component >
    <script type="text/ng-template" id="Banana.html">
	<c:Banana /> 
    </script>
    <script type="text/ng-template" id="Opportunity.html">
	<c:Opportunity /> 
    </script>
    <script type="text/ng-template" id="OpportunityDetails.html">
	<c:OpportunityDetails /> 
    </script>
    <!-- Other components -->
</apex:component>

app.config.js

Templates.component

Part 2

tip #3 Salesforce remote service

function SfRemote($q, $cookies, $log) {
  /* Explicit remote method invocation */
  this.do = function(name, arg) {
    var deffered = $q.defer();
    $log.debug('\n I ~ '+ name);
    /* Middleware */
    Visualforce.remoting.Manager.invokeAction(name, arg,
      function(result,event){
        if(event.status){
          deffered.resolve(result);
        } else {
          deffered.reject(event);
        }
      },
      {buffer: true,escape :false}
    );
    return deffered.promise;
  }
};
angular.module('App').service('SfRemote', SfRemote); 

sf-remote.srv.js

/* */

this.getAllBananas = function(arg) {
    return SfRemote.do( 'FruitsCtrl.getAllBananas', 
        arg );
}

/* */

banana.srv.js

/* */

this.getAllBananas = function(arg) {
    return SfRemote.do( 'FruitsCtrl.getOpenOpportunities', 
        arg );
}

/* */

opportunity.srv.js

tip #4 Salesforce call, Chaining, Parallel calls

/* */

this.getAllBananas = function(arg) {
    return SfRemote.do( 
        'FruitsCtrl.getAllBananas', arg 
    );
}

/* */

banana.srv.js

/* */
vm.getAllBananas = function(ownerId, createdDate) {
  BananaSrv.getAllBananas(
    {
      ownerId : ownerId, 
      createdDate : createdDate
    }
  ).then (
    function(data) {
      vm.alerts.push({type:data.type, msg:data.msg});
      vm.model.bananas = data.items;
    }, function(err) {
      vm.alerts.push(err);
    }
  )
}

banana.ctrl.js

sf-remote.srv.js

tip #4 Salesforce call, Chaining, Parallel calls

console.clear();
$scope.processed = true;
$scope.uploadIdPic(jsonObj)
.then (
  function (data) { 
    return $scope.uploadCustomerPic (jsonObj); 
  }
)
.then (
  function (data) { 
    return $scope.uploadAdditionalIdPic (jsonObj); 
  }
)
/* ... */
)
.then (
  function (data) { 
    return $scope.clearAndClose (); 
  }
)
.catch (
  function(err) {
    console.log(err);
    $scope.processed = false;
  }
);

Chaining 

Part 2

$q.all({
  'statuses'            : ScreeningCustomerSrv.getCustomerStatuses(),
  'sate2CityMap'        : ScreeningCustomerSrv.getTerritories(),
  /* ... */
  'idType2LinkMapping'  : ScreeningCustomerSrv.getIdType2LinkMapping(),
})
.then (
  function(data) {
    _.forEach(data, function(val, key) {
      console.log(key, val);
      CustomerFct.set(key, val);
    })
  }
)
.catch(
  function(err) {
    $scope.model.alerts.push({type:'danger', msg: err.message});
  }
);

Parallel

tip #5 General signature

/* ... */
vm.getAllBananas = function(ownerId, createdDate) {
  BananaSrv.getAllBananas(
    {
      ownerId : ownerId, 
      createdDate : createdDate
    }
  ).then (
    function(data) {
      vm.alerts.push({type:data.type, msg:data.msg});
      vm.model.bananas = data.items;
    }, function(err) {
      vm.alerts.push(err);
    }
  )
}

JS Controller

public class BananaService {
  /* ... */
  public static Map<String,Object> getAllBananas (
    Map<String,Object> arg ) 
  {
    Map<String,Object> result = new Map<String,Object>();
    String ownerId = (String)arg.get('ownerId');
    Savepoint sp = Database.setSavepoint();
    try {
      /* Do somthing useful */
    } catch (Exception e) {
      Database.rollback(sp);
      result = new Map<String,Object> {
        'type' => 'error',
        'msg'  => (e.getMessage())
      };
    }
    return result;
  }
}

Apex service

Middleware

tip #6 Data sharing through factories

function BananaFct() {

  var model = {};

  return {
    get : function(key) {
      return model[key];
    },
    getCopy : function(key) {
      return angular.copy(model[key]);
    },
    getModel : function() {
      return model;
    },
    getModelCopy : function() {
      return angular.copy(model);
    },
    set : function(key, val) {
      model[key] = val;
    },
    setModel : function(_model) {
      model = _model;
    }
  };
};
angular.module('App').service('BananaFct', BananaFct); 

Best factory

  • Collects data
  • Shares data beetween controllers
  • Saves data beetween states
  • Contains global application config and metadata

tip #6 Data sharing through factories

Part 2

Child controller

Parent ctrl

Factory

Ctrl 1

Ctrl 2

State change

Factory

tip #6 Data sharing through factories

Part 3

function BananaCtrl (BananaSrv, BananaFct) {
  vm .model = {
    _BananaFct : BananaFct.getModel(),
    lastName   : BananaFct.getLastName,
    /* ... */
  }
  /* ... */
}
angular.module('App')
  .controller('BananaCtrl', BananaCtrl); 

 Angular Controller

<input 
    aria-describedby="basic-addon1" 
    class="form-control ng-invalid" 
    name="FirstName" 
    ng-model="vm.model._BananaFct['FirstName']"
    ng-required="true" 
    type="text" />
<!-- -->
<input 
    aria-describedby="basic-addon1" 
    class="form-control ng-invalid" 
    name="LastName" 
    ng-model="vm.model.lastName()"
    ng-required="true" 
    type="text" />

Related Component

Back-end

  1. Direct data transfer
  2. Service Layer (Lasagna)
  3. General signature

Direct data transfer

public with sharing class FruitsCtrl {

  public String portalSettings       { get; set; }
  public String portalSettingsKeySet { get; set; }
  public String mainFruit            { get; set; }
  public String mainFruitIsApple    { get; set; }

  /* Main initialization */
  public FruitsCtrl() {

    Map<String,Fruits_Portal_Settings__c> portalSettingsMap 
      = Fruits_Portal_Settings__c.getall();
    if(portalSettingsMap.containsKey('Main Fruit')) {
      mainFruit = portalSettingsMap.get('Main Fruit').Value__c;
      /* Dummy actions, just for demo */
      mainFruitIsApple = JSON.serialize(
        !(portalSettingsMap.get('Main Fruit').Value__c == 'Banana'));
    }
    portalSettings       = JSON.serialize(portalSettingsMap);
    portalSettingsKeySet = JSON.serialize(portalSettingsMap.keySet());

  }
  /* ... */
}
<script type="text/javascript">
      var portalSettings        =  {!portalSettings};
      var portalSettingsKeySet  =  {!portalSettingsKeySet};
      var mainFruit             =  '{!mainFruit}';
      var mainFruitIsApple      =  {!mainFruitIsApple};
</script>

Service Layer

public with sharing class MyCtrl {

  /* Main initialization */

  public MyCtrl() {
    /* ... */
    portalSettings       = JSON.serialize(portalSettingsMap);
    portalSettingsKeySet = JSON.serialize(portalSettingsMap.keySet());
  }

  @RemoteAction
  public static Account getAccountByPerc(Integer percent, Account acc) {
    /* ... */
    return result;
  }

  @RemoteAction
  public static List<Contact> getCustomersForContact (String Id) {
    /* ... */
    return result;
  }

}

MyCtrl.cls

public with sharing class MyExtension{

  /* Main initialization */

  public MyExtension() {
    /* ... */
  }

  @RemoteAction
  public static Contact getExtendedContactInfo (String Id) {
    /* ... */
    return result;
  }

  @RemoteAction
  public static List<Integer> getSomth (Boolean value) {
    /* ... */
    return result;
  }

}

MyExtension.cls

OpportunityExtension.cls

CustomerExtension.cls

...

Service Layer

public static void applyDiscounts(
  Set<ID> opportunityIds,
  Decimal discountPercentage) 
{
  // Query Opportunities and Lines
  List<Opportunity> opportunities = // ...

  // Update Opportunities and Lines (if present)
  SObjectUnitOfWork uow = new SObjectUnitOfWork(SERVICE_SOBJECTS);
  for(Opportunity opportunity : opportunities) {
    // Apply to Opportunity Amount or Product Lines?
    // ...
    uow.registerUpdate(opportunity);
    // or
    uow.registerUpdate(opportunityLine);
  }

  // Update the database
  uow.commitWork();

}

Part 2

public PageReference applyDiscount() {
  try {
    // Apply discount entered to the current Opportunity
    OpportunitiesService.applyDiscounts(
      new Set<ID> { standardController.getId() }, DiscountPercentage);
  }
  catch (Exception e) {
    ApexPages.addMessages(e);
  }     
  return ApexPages.hasMessages() ? null : standardController.view();
}

Service Layer

public with sharing class FruitsCtrl {

   /* ... */

  public FruitsCtrl() {

    /* ... */
    portalSettings       = JSON.serialize(portalSettingsMap);
    portalSettingsKeySet = JSON.serialize(portalSettingsMap.keySet());

  }

  @RemoteAction
  public static Map<String,Object> getAllBananas(Map<String,Object> arg) {
    return BananaService.getAllBananas(arg);
  }

  @RemoteAction
  public static Map<String,Object> getAllOpportunities (Map<String,Object> arg) {
    makeMiddleware(arg);
    return OpportunityService.getAllOpportunities(arg);
  }

  /* ... */

}

Part 3

public class BananaService {
  
  public static Map<String,Object> getAllBananas(
    Map<String,Object> arg) 
  {
    Map<String,Object> result = new Map<String,Object>();
    String ownerId = (String)arg.get('ownerId');
    try {
      /* Do something usefull */
    } catch (Exception e) {
      result = new Map<String,Object> {
        'type' => 'error',
        'msg'  => (e.getMessage())
      };
    }
    return result;
  }

}

Controller

Service

General signature

public with sharing class FruitsCtrl {

   /* ... */

  @RemoteAction
  public static Map<String,Object> getAllOpportunities (Map<String,Object> arg) {
    makeMiddleware(arg);
    return OpportunityService.getAllOpportunities(arg);
  }

  /* ... */

}
public class BananaService {
  public static Map<String,Object> getAllBananas (Map<String,Object> arg) {
    Map<String,Object> result = new Map<String,Object>();
    String ownerId = (String)arg.get('ownerId');
    try {
      /* Do something usefull */
    } catch (Exception e) {
      result = new Map<String,Object> {
        'type' => 'error',
        'msg'  => (e.getMessage())
      };
    }
    return result;
  }
}

Controller

Service

/* ... */

  BananaSrv.getAllBananas(
    {
      ownerId : ownerId, 
      createdDate : createdDate
    }
  ).then (
    function(data) {
      vm.alerts.push({
        data
      });
      vm.model.bananas = data.items;
    }, function(err) {
      vm.alerts.push(err);
    }
  )

/* ... */

JS

AngularJS

good parts:

  • Data binding
  • Amazing UI/UX (SPA, custom animation, ...)
  • Easy to start, and maintain
  • Powerfull and flexible form validation
  • UI-Router
  • Reusable components aka Directives
  • Set of ready-to-use solutions
  • Separation of concerns
  • Unit testing

AngularJS

not too good parts :C

  • Non-native technology in SF
  • Lightining is comming ...
  • Too old

But...

Thanks!

You are awesome!

 

deck

By vladborsh

deck

  • 914