Inside a visual force page (as a container), I have created a custom directive in angular JS but it’s not working. In fact, the directive is not even being called!
Though this works perfectly fine in the JSFiddle
Below is the custom directive.When I use this directive in the HTML markup, it never show anything on the console log.
I have created multiple directives and seeing the same behavior.
I believe there is a bug when using custom directives inside the visualforce container.Has anyone of you have faced the same issue? Any help would be greatly appreciated.
Thanks!
-SS
UPDATE
Here is the JSFiddle for Custom directive that works fine but when I use it in visual force page, it doesn’t work.( Even though the directive has a console.log, nothing appears in console. This proves that the directive is not being called)
http://jsfiddle.net/ssah13/3y1z5943/
Please note: This directive strips off everything before and after underscore in the OppName. For example: If OppName is “111111_Test_123445" then output is “Test"
Here is the visual force page and a controller:
PAGE:
<apex:page docType="html-5.0" controller="SalesActions">
<head>
<script src="//code.jquery.com/jquery-1.11.0.min.js"/>
<apex:includeScript value="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular.min.js"/>
</head>
<!-- HTML FOR APP -->
<!-- To use bootstrap with visualforce everything needs to be wrapped with "bs" class-->
<div ng-app="salesApp" class="bs">
<div ng-controller="salesController">
<div ng-repeat="sfResult in salesforceResponse">
<table>
<tr ng-repeat="opp in sfResult.opportunities">
<td>
<span opp-name="" input="opp.Name">
{{opp.Name}}
</span>
</td>
</tr>
</table>
</div>
</div>
</div>
<!-- ACTUAL ANGULAR JS APP : Later on move this to salesworkspace.js-->
<script type = "text/javascript">
var ngApp = angular.module('salesApp', []);
//Opp Name directive
ngApp.directive('oppName', function () {
return {
restrict: 'A',
scope: {
input: '='
},
link: function (scope, element, attrs) {
console.log('Input: ', scope.input);
var input = scope.input;
if (!input) {
return;
}
// AccountName_Test_123445
if (input.indexOf('_')) {
scope.input = input.split('_')[1];
}
}
};
});
ngApp.controller('salesController', ['$scope',
function($scope) {
$scope.salesforceResponse = [];
Visualforce.remoting.Manager.invokeAction(
'{!$RemoteAction.SalesActions.getAllAccounts}',
function(result, event) {
if (event.status) {
$scope.$apply(function() { //Use Apply as the scope changed outside Angular Context?
$scope.salesforceResponse = result;
console.log($scope.salesforceResponse);
});
} else {
console.log(event);
}
}
);
} //End of function
]); //End of Controller method
</script>
</apex:page>
CONTROLLER:
salesActions.cls
public with sharing class SalesActions {
public SalesActions() { } // empty constructor
#RemoteAction
public static List<accountWrapper> getAllAccounts() {
List<accountWrapper> accountResponse = new List<accountWrapper>();
List<account> accs = [SELECT Id, Name, Type, Strategic_Account_Management__c,
(SELECT Id FROM Opportunities) ,
(SELECT Name FROM Contacts)
FROM Account
Order by Name]; //Add a Filter here. WHERE ownerId = :Userinfo.getUserId();
Set<Id> accountIds = new Set<Id>();
for(account acc : accs) {
accountIds.add(acc.Id);
}
Map<Id,Opportunity> oppIdToOpp = new Map<Id,Opportunity>([
SELECT Id,Name, Account.Name, Agency__r.Name, Campaign_EVENT__c,Rate_Type__c,StageName,Amount,CurrencyISOCode,
Probability,CampaignStartDate2__c,CampaignEndDate2__c,Contact__c,Sales_Notes__c,
(SELECT SplitAmount, SplitOwner.Name,SplitPercentage, Split__c FROM OpportunitySplits)
FROM Opportunity WHERE AccountId IN :accountIds]// Remove WHERE AccountId =:accountId and Add WHERE account.ownerId=:UserInfo.getId();
);
Map<Id,List<Partner>> accountIdToPartners = new Map<Id,List<Partner>>();
for(Partner p :[SELECT AccountFromId,AccountTo.Name FROM Partner WHERE AccountFromId IN :accountIds]) {
if(accountIdToPartners.containsKey(p.AccountFromId)) {
accountIdToPartners.get(p.AccountFromId).add(p);
} else {
accountIdToPartners.put(p.AccountFromId, new List<Partner>{p});
}
}
for(Account acc : accs) {
accountWrapper accWrapper = new accountWrapper();
accWrapper.account = acc; // This will add all the accounts and related contacts
accWrapper.opportunities = new List<Opportunity>();
accWrapper.partners = new list<Partner>();
if(accountIdToPartners.containsKey(acc.Id)){
accWrapper.partners = accountIdToPartners.get(acc.Id);
}
for(Opportunity opp : acc.Opportunities) {
accWrapper.opportunities.add(oppIdToOpp.get(opp.Id)); // This will add all the opportunties and opportunitySplits
}
accountResponse.add(accWrapper);
}
return accountResponse;
}
public class accountWrapper {
public Account account { get; set; }
public List<Partner> partners { get; set; }
public List<Opportunity> opportunities { get; set; }
}
}
For me this is how I get it to work:
ngApp.directive('testDirective', function () {
return {
restrict: 'E',
scope: {
input: '='
},
template: '<p>{{input}}</p>',
link: function (scope, element, attrs) {
console.log('Input: ', scope.input);
var input = scope.input;
if (!input) {
return;
}
// AccountName_Test_123445
if (input.indexOf('_') !== -1) {
scope.input = input.split('_')[1];
}
}
};
});
and then in the html:
<test-directive input="opp.Name"></test-directive>
Those are just some small changes. Not entirely sure why it does not work in VF before. In VF I would always try to use directives as elements as VF cannot have those empty attributes as normal HTML can.
Then you should define a template or templateUrl in the directive.
(You can use VF pages as templates here as well - even with standardControllers or custom controllers firing!)
Oh and of course have a look at ngRemote again as it can help with your AngularJS application on VF.
Hope this helps!
Florian
Related
I'm trying to dynamically add directive names to my directive from a json object. Angular however is only interpolating the directive name which is pulled from a JSON tree once, Angular is then not recognizing and compiling the dynamic children directives once the name is interpolated.
I have tried adding the interpolate service to my DDO so that I can manually interpolate the JSON values, and then have Angular compile.
I however get undefined for $interpolate(tAttrs.$attr.layout) I'm passing the json object to my isolated scope as layout, when I try to access the attr layout I get undefined. My question is how can I access layout object values in the pre link or before compile so that I can interpolate the values and inject them in.
Or do I need to have angular recompile as described here: How do I pass multiple attributes into an Angular.js attribute directive?
Any help would be great.
{
"containers": [
{
"fluid": true,
"rows": [
{
"columns": [
{
"class": "col-md-12",
"directive": "blog"
}
]
},
{
"columns": [
{
"class": "col-md-6 col-md-offset-3 col-xs-10 col-xs-offset-1",
"directive": "tire-finder"
}
]
}
]
}
]
}
...
<div layout="layout" ng-repeat="container in layout.containers" ng-class="container">
<div ng-repeat="row in container.rows">
<div ng-repeat="column in row.columns" ng-class="column.class">
<{{column.directive}}></{{column.directive}}>
</div>
</div>
</div>
...
angular.module('rpmsol').directive('wpMain', wpMainDirective);
function wpMainDirective($interpolate) {
var controller = function(brainService, $scope, $state) {
$scope.directive = {};
var currentState = $state.current.name;
brainService.getDirectiveScope('wpMain', {}).then(function(response) {
$scope.layout = response.states[currentState];
});
};
var compile = function(tElement, tAttrs, transclude) {
var directiveNames = $interpolate(tAttrs.$attr.layout);
}
return {
restrict: 'E',
// replace: true,
scope: {
layout: '=',
},
controller: controller,
templateUrl: 'directive/wpMain/wpMain.html',
compile: compile
};
};
If you're only dealing with a couple options for what a column might be, I would suggest going with #georgeawg's answer.
However, if you expect that number to grow, what you might opt for instead is something along the following lines:
<div layout="layout" ng-repeat="container in layout.containers" ng-class="container">
<div ng-repeat="row in container.rows">
<div ng-repeat="column in row.columns" ng-class="column.class">
<column-directive type="column.directive"></column-directive>
</div>
</div>
and then in your JS...
yourApp.directive('columnDirective', columnDirectiveFactory);
columnDirectiveFactory.$inject = ['$compile'];
function columnDirectiveFactory ($compile) {
return {
restrict: 'E',
scope: {
type: '='
},
link: function (scope, elem, attrs) {
var newContents = $compile('<' + scope.type + '></' + scope.type + '>')(scope);
elem.contents(newContents);
}
};
}
To the best of my knowledge, Angular doesn't have any built-in facility to choose directives in a truly dynamic fashion. The solution above allows you to pass information about which directive you want into a generic columnDirective, whose link function then goes about the business of constructing the correct element, compiling it against the current scope, and inserting into the DOM.
There was an issue with the promise in my original posted code which was preventing me from recompiling the template with the correct directive names. The issue was that I was trying to access the JSON object in the preLink function, but the promise hadn't been resolved yet. This meant that my scope property didn't yet have data.
To fix this I added my service promise to the directive scope $scope.layoutPromise = brainService.getDirectiveScope('wpMain', {}); to which I then called and resolved in my link function. I managed to have Angular compile all of my directive names from the JSON object, but I had to do it in a very hackish way. I will be taking your recommendations #cmw in order to make my code simpler and more 'Angulary'
This is currently my working code:
...
angular.module('rpmsol').directive('wpMain', wpMainDirective);
function wpMainDirective($interpolate, $compile) {
var controller = function(brainService, $scope, $state) {
$scope.currentState = $state.current.name;
$scope.layoutPromise = brainService.getDirectiveScope('wpMain', {});
};
var link = function(scope, element, attributes) {
scope.layoutPromise.then(function sucess(response) {
var template = [];
angular.forEach(response.states[scope.currentState].containers, function(container, containerKey) {
template.push('<div class="container' + (container.fluid?'-fluid':'') + '">');
//loop rows
angular.forEach(container.rows, function(row, rowkey) {
template.push('<div class="row">');
angular.forEach(row.columns, function(column, columnKey) {
template.push('<div class="' + column.class + '">');
template.push('<' + column.directive +'></' + column.directive + '>')
template.push('</div>');
});
template.push('</div>');
});
template.push('</div>');
});
template = template.join('');
element.append($compile(template)(scope));
})
};
return {
scope: true,
controller: controller,
link: link
};
};
I'm trying to take the first example from the angular.js homepage and adding in cookie support.
This is what I have so far: https://jsfiddle.net/y7dxa6n8/8/
It is:
<div ng-app="myApp">
<div ng-controller="MyController as mc">
<label>Name:</label>
<input type="text" ng-model="mc.user" placeholder="Enter a name here">
<hr>
<h1>Hello {{mc.user}}!</h1>
</div>
</div>
var myApp = angular.module('myApp', ['ngCookies']);
myApp.controller('MyController', [function($cookies) {
this.getCookieValue = function () {
$cookies.put('user', this.user);
return $cookies.get('user');
}
this.user = this.getCookieValue();
}]);
But it's not working, ive been trying to learn angular.
Thanks
I'd suggest you create a service as such in the app module:
app.service('shareDataService', ['$cookieStore', function ($cookieStore) {
var _setAppData = function (key, data) { //userId, userName) {
$cookieStore.put(key, data);
};
var _getAppData = function (key) {
var appData = $cookieStore.get(key);
return appData;
};
return {
setAppData: _setAppData,
getAppData: _getAppData
};
}]);
Inject the shareDataService in the controller to set and get cookie value
as:
//set
var userData = { 'userId': $scope.userId, 'userName': $scope.userName };
shareDataService.setAppData('userData', userData);
//get
var sharedUserData = shareDataService.getAppData('userData');
$scope.userId = sharedUserData.userId;
$scope.userName = sharedUserData.userName;
Working Fiddle: https://jsfiddle.net/y7dxa6n8/10/
I have used the cookie service between two controllers. Fill out the text box to see how it gets utilized.
ok, examined your code once again, and here is your answer
https://jsfiddle.net/wz3kgak3/
problem - wrong syntax: notice definition of controller, not using [] as second parameter
If you are using [] in controller, you must use it this way:
myApp.controller('MyController', ['$cookies', function($cookies) {
....
}]);
this "long" format is javascript uglyfier safe, when param $cookies will become a or b or so, and will be inaccessible as $cookies, so you are telling that controller: "first parameter in my function is cookies
problem: you are using angular 1.3.x, there is no method PUT or GET in $cookies, that methods are avalaible only in angular 1.4+, so you need to use it old way: $cookies.user = 'something'; and getter: var something = $cookies.user;
problem - you are not storing that cookie value, model is updated, but cookie is not automatically binded, so use $watch for watching changes in user and store it:
$watch('user', function(newValue) {
$cookies.user = newValues;
});
or do it via some event (click, submit or i dont know where)
EDIT: full working example with $scope
https://jsfiddle.net/mwcxv820/
I have a directive which is fetching data through ajax on load. But after an event in the controller which is posting some data, the Directive should re-compile with the new ajax data so that the changes can be reflected. Can you please help.
I have a compile function in the directive which takes data and puts that in HTML file and generates markup.
Then I have a save comment function in the controller which saves a new comment and so the directive gets the new data.
compile: function(tElement, tAttrs) {
var templateLoader = $http.get(base_url + 'test?ticket=' + $routeParams.ticketid, {cache: $templateCache})
.success(function(htmlComment) {
if (htmlComment != '')
tElement.html(htmlComment);
else
tElement.html('');
});
return function (scope, element, attrs) {
templateLoader.then(function (templateText) {
if (tElement.html() != '')
element.html($compile(tElement.html())(scope));
else
element.html('<div class="no-comments comment"><p>Be the first to comment</p></div>');
});
};
}
This is the compile part of the directive. I want this to be called through a normal controller event.
I would recommend #Riley Lark' response but as you already mentioned that your API returns an HTML instead of JSON, here is my take.
Your controller as:
<div ng-controller="MyCtrl">
<button ng-click="save()">Save Comment</button>
<comments></comments>
</div>
myApp.controller('MyCtrl', function($scope) {
$scope.commentHTML = '';
$scope.alert = function(salt) {
alert('You clicked, My Comment ' + salt);
}
$scope.save = function() {
// this imitates an AJAX call
var salt = Math.random(1000);
$scope.commentHTML+= '<div ng-click="alert(' + salt + ')">My Comment ' + salt + '</div>';
};
});
And the comments directive as:
myApp.directive('comments', function($compile) {
return {
restrict: 'E',
link: function(scope, element) {
scope.$watch(function() { return scope.commentHTML; }, function(newVal, oldVal) {
if (newVal && newVal !== oldVal) {
element.html(newVal);
$compile(element)(scope);
}
});
}
}
});
Hope this solves your problem..!
Working Demo
After you fetch the data you need, put the data in a $scope property. Define your template in terms of that property and it will automatically change when the data returns.
For example, your template might be
<div ng-repeat="comment in comments">
{{comment}}
</div>
You don't need a compile function or to "reload a directive" to accomplish this. The solution you posted is a sort of reimplementation of angular. It looks like you want to download a template with the data already interpolated into it, but Angular will help you the most if you separate the template from the data and let Angular interpolate it on the client.
Here is my plunker and the code I can't get to work starts on line 32
http://plnkr.co/edit/pmCjQL39BWWowIAgj9hP?p=preview
I am trying to apply an equivalent to markdown filter onto a directive... I created the filter and tested with manually applying the filter and it works that way,, but I should only use the filter conditionally when the type of content on directive is set to markdown.
I am trying to accomplish this by updating ng-model >>> ngModel.$setViewValue(html) but I am getting an error
ngModel.$setViewValue is not a function.. which makes me thing that the controller is not recognized although it is required by the directive.
Here is a working controller:
var app = angular.module('testOne', ["ngResource", "ngSanitize"]);
app.controller('testOneCtrl', function ($scope) {
$scope.product = {
id:12,
name:'Cotton T-Shirt, 2000',
description:'### markdown\n - list item 1\n - list item 2',
price:29.99
};
});
app.directive("myText", function ($parse) {
return {
restrict: "E",
require: "?ngModel",
scope:{
css: "#class", type: "#type"
},
controller: function ($scope, $element, $attrs) {},
templateUrl: "template.html",
compile: function(elm, attrs, ngModel){
var expFn = $parse(attrs.contentType + '.' + attrs.value);
return function(scope,elm,attrs){
scope.$parent.$watch(expFn, function(val){
scope.exp = { val: val };
if ( attrs.type == 'markdown'){
var converter = new Showdown.converter();
var html = converter.makeHtml(val);
//scope.exp.val = html;
ngModel.$setViewValue(html);
ngModel.$render();
}
})
scope.$watch('exp.val', function(val){
expFn.assign(scope.$parent, val)
})
}
}
}
})
This is a filter for markdown which works when applied.. (I would consider using the filter if I could figure out the way to conditionally apply it to existing directive but I'd rather do it with ng-model)
/*
app.filter('markdown', function ($sce) {
var converter = new Showdown.converter();
return function (value) {
var html = converter.makeHtml(value || '');
return $sce.trustAsHtml(html);
};
});
*/
Here is the directive template
<div ng-class="{{css}}"
ng-click="view = !view"
ng-bind-html="exp.val">
</div>
<div>
<textarea rows="4" cols="30" ng-model="exp.val"></textarea>
</div>
This is the directive in use:
<mb-text ng-cloak
type="markdown"
content-type="product"
value="description"
class="test-one-text-2">
</mb-text>
Why ngModel is empty?
When using require on a directive the controller is passed as the 4th argument to the linking function. In you code you try to reference it as an argument of the compile function. The controller is only instantiated before the linking phase so it could never be passed into the compile function anyway.
The bigger issue is that require can only get a controller of the same element ({ require: 'ngModel' }), or parent elements ({ require: '^ngmodel' } ). But you need to reference a controller from a child element (within the template).
How to get ngModel?
Do not use require at all as you cannot get child element's controller with it.
From angular.element docs:
jQuery/jqLite Extras
controller(name) - retrieves the controller of the current element or its parent. By default retrieves controller associated with the ngController directive. If name is provided as camelCase directive name, then the controller for this directive will be retrieved (e.g. 'ngModel').
Inside the linking function you can get the hold of the controller like so:
var ngModel = elm.find('textarea').controller('ngModel');
I fixed your directive:
here is a plunker: http://plnkr.co/edit/xFpK7yIYZtdgGNU5K2UR?p=preview
template:
<div ng-class="{{css}}" ng-bind-html="exp.preview"> </div>
<div>
<textarea rows="4" cols="30" ng-model="exp.val"></textarea>
</div>
Directive:
app.directive("myText", function($parse) {
return {
restrict: "E",
templateUrl: "template.html",
scope: {
css: "#class",
type: "#type"
},
compile: function(elm, attrs) {
var expFn = $parse(attrs.contentType + '.' + attrs.value);
return function(scope, elm, attrs) {
scope.exp = {
val: '',
preview: null
};
if (attrs.type == 'markdown') {
var converter = new Showdown.converter();
var updatePreview = function(val) {
scope.exp.preview = converter.makeHtml(val);
return val;
};
var ngModel = elm.find('textarea').controller('ngModel');
ngModel.$formatters.push(updatePreview);
ngModel.$parsers.push(updatePreview);
}
scope.$parent.$watch(expFn, function(val) {
scope.exp.val = val;
});
scope.$watch('exp.val', function(val) {
expFn.assign(scope.$parent, val);
});
};
}
};
});
I have a problem instanciating controller with Angular. I have a main controller AlkeTypeDefListController from which I want to dynamically create/remove controllers of type AlkeTypeDefController, so I have done that :
Code of AlkeTypeDefListController:
// Create main controller
Alke.controller('AlkeTypeDefListController', ['$scope', '$controller', function($scope, $controller)
{
var primitives =
[
];
// Add some properties to the scope
angular.extend($scope,
{
typedefs : primitives,
addTypeDef : function()
{
var controller = $controller("AlkeTypeDefController", {$scope:$scope.$new()});
$scope.typedefs.push(controller);
}
});
}]);
Code of AlkeTypeDefController:
// Create main controller
Alke.controller('AlkeTypeDefController', ['$scope', '$controller', function($scope, $controller)
{
// Add some properties to the scope
angular.extend($scope,
{
name : "New Type",
fields : [],
addField : function()
{
}
});
}]);
The html code is this one:
<div id="typedefs-editor" ng:controller="AlkeTypeDefListController">
<button ng:click="addTypeDef()">Add</button>
<button>Remove</button>
<div id="typedef-list">
<ul class="list">
<li ng:repeat="typedef in typedefs">{{typedef.name}}</li>
</ul>
</div>
</div>
The problem does not really come from the instantiation (which works fine), but from the initialization. In fact, when the new "li" appears when I push the "Add" button, the text "New type" (initialized in the controller) does not appear.
I think it is about the scope or something like that, but I can't really find how to fix this.
I wanted to know if this method seems correct, and also how could I fix the problem I have.
Thanks
Reading the code, I understand that you want to create typedefs dynamically and those typedef items have to be controlled by an AlkeTypeDefController.
In that case I would create AlkeTypeDefController using ng:controller directive, so you don't need to create the controller programmatically, because then you would need to attached it to the view and that's just what the ngController directive does for you.
Notice AlkeTypeDefListController does not create a AlkeTypeDefController controller, this is done in the view
Demo on Plunker
Controllers:
.controller('AlkeTypeDefListController', ['$scope', function($scope) {
var primitives = [];
$scope.typedefs = primitives;
$scope.addTypeDef = function() {
var typeDef = { name: 'New Type' };
$scope.typedefs.push(typeDef);
}
}])
.controller('AlkeTypeDefController', ['$scope', function($scope) {
$scope.addField = function() {
alert('add Field');
}
}]);
View (notice how ng-controller directive is specified in li element):
<div id="typedefs-editor" ng:controller="AlkeTypeDefListController">
<button ng:click="addTypeDef()">Add</button>
<button>Remove</button>
<div id="typedef-list">
<ul class="list">
<li ng:repeat="typedef in typedefs" ng:controller="AlkeTypeDefController">
{{typedef.name}}
</li>
</ul>
</div>
In the code above, ngRepeat is going to create a new $scope for each typedef. AlkeTypeDefController then decorates that scope with functions and values.
I hope it helps
When you call $controller("AlkeTypeDefController") it will essentially call new on the AlkeTypeDefController constructor and give you back the return value not the scope. You are assign the name attrubute to the scope though so it is not being accessed in your html when you have typedef.name.
Try changing your AlkeTypeDefController to this:
Alke.controller('AlkeTypeDefController', function() {
this.name = "New Type";
this.fields = [];
this.addField = function() {};
});
Then you can instantiate it with: var controller = $controller("AlkeTypeDefController"); and you shouldn't need to worry about creating nested scopes.
If I get what you're saying correctly then I think I'd try to leverage the power of a custom directive here instead of dynamically generating controllers.
plunker
Controller:
Alke.controller('alkeTypeDefListController', ['$scope', '$controller',
function($scope, $controller) {
var primitives = [];
var addTypeDef = function() {
$scope.typedefs.push({
name: 'new name'
});
};
var removeTypeDef = function(){
$scope.typedefs.pop();
};
var properties = {
typedefs: primitives,
addTypeDef: addTypeDef,
removeTypeDef: removeTypeDef
};
// Add some properties to the scope
angular.extend($scope, properties);
}
]);
Directive:
Alke.directive('alkeTypeDef', function() {
return {
restrict: 'A',
scope: {
typeDef: '=alkeTypeDef'
},
template: '{{typeDef.name}}',
link: function(scope, element, attr) {
var properties = {
fields: [],
addField: function() {
}
};
angular.extend(scope, properties);
}
};
});
HTML:
<div ng-app='Alke'>
<div id="typedefs-editor" ng-controller="alkeTypeDefListController">
<button ng-click="addTypeDef()">Add</button>
<button ng-click="removeTypeDef()">Remove</button>
<div id="typedef-list">
<ul class="list">
<li alke-type-def='typedef' ng-repeat="typedef in typedefs"></li>
</ul>
</div>
</div>
</div>
If you want a controller then you can use one in the directive instead of a linking function.