Consider the initial app main state:
$stateProvider.state('main', {
url: '',
views: {
'nav#': {
templateUrl: baseTemplatesUrl + 'nav.main.html',
controllerAs: 'vm',
controller: 'mainNavController',
resolve: {
timelinesService: 'timelinesService'
}
},
'content#': {
template: ''
}
}
});
Inside the nav.main.html template I'm trying to use a simple slider directive:
<ul>
<li ng-repeat="t in vm.timelines">
<!-- ui-sref=".timeline({yearFrom: t.yearFrom, yearTo: t.yearTo})" -->
<a class="button tiny" ui-sref-active="active">{{t.text}}</a>
</li>
</ul>
<div simple-slider items="vm.timelines" on-item-clicked="vm.timelineClicked"></div>
The directive is defined as:
function simpleSlider() {
return {
restrict: 'EA',
templateUrl: baseTemplatesUrl + 'directive.simpleSlider.html',
replace: true,
scope: {
items: '=',
onItemClicked: '&'
},
link: function(scope, el, attr, ctrl) {
scope.curIndex = 0;
scope.next = function() {
scope.curIndex < scope.items.length - 1 ? scope.curIndex++ : scope.curIndex = 0;
};
scope.prev = function() {
scope.curIndex > 0 ? scope.curIndex-- : scope.curIndex = scope.items.length - 1;
};
scope.$watch('curIndex', function() {
scope.items.forEach(function(item) {
item.active = false;
});
scope.items[scope.curIndex].active = true;
});
},
};
}
The problem
On a page load the directive's $watch throws an exception saying that scope.items[scope.curIndex] is undefined, whereas the ng-repeat="t in vm.timelines" inside the nav.main.html template renders successfully.
Why the watch fails / how to pass vm.timelines to directive's scope?
The fiddle
https://jsfiddle.net/challenger/Le5p9aup/
Update 1. Regarding #Lex answer
The fiddle is working. But my setup differs from the given fiddle. In my setup the timelinesService returns the $http promise:
var service = {
getTimelines: function() {
return $http.get('/api/timelines');
}
};
then inside the navMainController the vm.timelines array gets populated:
vm.timelines = [];
timelinesService.getTimelines().then(function(response) {
vm.timelines = response.data.timelines;
}).catch(function(error) {
console.log(error);
});
Then, unless you click the directive's prev/next button, the $watch will fail:
// scope.items[scope.curIndex] is undefined
scope.items[scope.curIndex].active = true;
Related
I'm working on a hybrid Ionic1 app and I am not able to call a controller method from inside a directive.
The problem is that inside the directive I'm able to pass controller's variables (such as variable apps) but I am not able to call removeCredentials() inside card-app directive.
I've tried so many ways to make it work but none of them solved the problem. I've also tried using the & to bind the controller method inside the directive but clicking on the X icon in the directive template triggers nothing.
Right now the code looks like this:
Controller AppsCtrl in apps.js
angular.module('testprofile')
.controller('AppsCtrl', function($scope, $state, $log, $ionicHistory, $filter, $ionicPopup, UserData, CredentialStoreService, ionicMaterialMotion, ionicMaterialInk, Auth, Effects, CredentialStoreService, Globals, Validations) {
$scope.onlyMyApps = true;
$scope.showNoItems = false;
var translate = $filter('translate');
$scope.apps = [];
$scope.mapAppsFromCredentials = function(credentials) {
var map = credentials.map(function(cred) {
/* app: "App 1"
appId: 7
profile: "Default Profile"
profileId: 0
*/
var app = UserData.getLocalApp(cred.app);
if (!app) {
$log.debug("apps.js::mapAppsFromCredentials - no local app matching credentials: "+JSON.stringify(cred));
return null;
}
var curHelper = UserData.getCurrentAppHelper(app);
return {
used: Boolean(cred.used),
id: app.id,
title: app.nameApp,
profile: cred.profile,
desc: curHelper ? curHelper.name : 'NO HELPER'
};
})
return map;
}
$scope.refresh = function() {
$log.debug("apps.js::AppsCtrl - REQUEST CREDENTIAL LIST...");
CredentialStoreService.getCredentialList(
//ON SUCCESS
function(response) {
$log.debug("apps.js::AppsCtrl - ...CREDENTIAL LIST RESPONSE: " + JSON.stringify(response));
$scope.apps = $scope.mapAppsFromCredentials(response);
Effects.init();
$scope.showNoItems = true;
for (var i = 0; i < $scope.apps.length; i++) {
if ($scope.apps[i].used) {
$scope.showNoItems = false;
break;
}
}
},
//ON ERROR
function() {
$log.debug("apps.js::AppsCtrl - ...CREDENTIAL LIST ERROR");
}
)
}
$scope.$on("$ionicView.enter", function () {
$log.debug("apps.js::$ionicView.enter - ENTER");
$scope.refresh();
});
//REMOVE CREDENTIALS
$scope.removeCredentials = function() {
$log.debug("app-details.js::removeCredential CONFIRMATION...");
$ionicPopup.confirm({
title: translate('Remove Credentials'),
template: translate('Are you sure you want to delete current credentials') + ' ' + app.nameApp + '?',
cancelText: translate('No'), // String (default: 'Cancel'). The text of the Cancel button.
//cancelType: '', // String (default: 'button-default'). The type of the Cancel button.
okText: translate('Yes'), // String (default: 'OK'). The text of the OK button.
okType: 'button-energized-900' // String (default: 'button-positive'). The type of the OK button.
}).then(function(res) {
if(res) {
$log.debug("app-details.js::removeCredential CONFIRMED!");
Auth.askPIN(true)
.then( function() {
CredentialStoreService
.removeCredential(
//authenticationProfileName
UserData.getAuthProfile(app).name,
//appProfileName
app.name,
// ON SUCCESS
onRemovedCredential,
// ON ERROR
Globals.onError);
});
} else {
$log.debug("app-details.js::removeCredential REJECTED.");
}
});
};
var onRemovedCredential = function() {
$log.debug("app-details.js::removeCredential CREDENTIALS REMOVED!");
Effects.showToast(translate('CREDENTIALS REMOVED')+'.');
$ionicHistory.nextViewOptions({
disableBack: true
});
$state.go('menu.apps', {}, {reload:true});
};
})
Directives in inclusions.js
angular.module('testprofile')
.directive('cardProfile', function() {
return {
scope: {
data:"=cardSource"
},
restrict: 'E',
templateUrl: 'app/templates/card-profile.html'
};
})
.directive('cardApp', function() {
return {
scope: {
data:"=cardSource",
},
restrict: 'E',
templateUrl: 'app/templates/card-app.html',
controller: 'AppsCtrl',
controllerAs: "ac",
bindToController: true,
link: function(scope, element, attrs, ctrl) {
scope.removeCredentials = function() {
return scope.ac.removeCredentials();
};
}
};
})
.directive('pinRequired', function(Auth) {
return {
restrict: 'A',
link: function(scope, element, attrs, ctrl) {
attrs.authorized = false;
element.on('click', function(e){
// console.log("\t\t + attrs.authorized:"+attrs.authorized);
if (attrs.authorized) {
if (attrs.href) location.href = attrs.href;
return true;
} else {
e.preventDefault();
e.stopPropagation();
Auth.askPIN(true)
.then(function() {
// console.log("inclusions.js::pinRequired - askPin - AUTHORIZED");
//THE PIN IS CORRECT!
attrs.authorized = true;
element.triggerHandler('click');
attrs.authorized = false;
});
}
});
}
};
})
;
apps.html
<ion-view view-title="Apps">
<ion-content padding="true" class="has-header bg-stable">
<h4 class="list-title"><i class="material-icons"></i> {{ 'App List' | translate }}
<!--label class="toggle toggle-calm pull-right toggle-small">
<span class="toggle-label" translate>My Apps</span>
<input type="checkbox" ng-model="onlyMyApps">
<div class="track">
<div class="handle"></div>
</div>
</label-->
</h4>
<ion-list class="">
<card-app ng-repeat="app in apps | filter:(onlyMyApps || '') && {used: true}"
card-source="app"
class="{{app.used ? 'used' : 'unused'}}"></card-app>
<div class="item padding" ng-show="showNoItems"><p class="text-center">{{ 'No saved credentials' | translate}}</span>.</p>
</div>
</ion-list>
</ion-content>
</ion-view>
card-app.html
<ion-item
menu-close=""
class="card item-thumbnail-left item-icon-right app-item">
<!--<img ng-src="{{data.thumb}}">-->
<span style="line-height:70px;vertical-align:middle" class="item-image avatar-initials h2">{{data.title|initials}}</span>
<div class="item">
<h2 class="inline-block">
<span ng-bind="data.title"></span>
</h2>
<span class="icon-right material-icons" ng-click='removeCredentials()'></span>
<div>
<span>{{'Active helper' | translate}}</span>
<h3>{{ data.desc | translate }}</h3>
</div>
</div>
</ion-item>
Solution: at the end the click event was not triggered because I had to add pointer-events: auto; and cursor: pointer; in my css
If your directive needs to call just a single function from a parent controller, just pass it as parameter (like you tried).
First ensure that controller scope is available for directive. For example create simple method and call it before directive:
// to ensure scope is ok
<button ng-click="alertMethod()"></button>
<card-app ng-repeat='' card-source='' class='' my-method="alertMethod()"></card-app>
// directive
//
scope: {
myMethod: '&'
},
template: "<button ng-click='myMethod()'>Click</button>"
That should work, but if your controller scope is not available maybe you need to use named controllers:
https://docs.angularjs.org/api/ng/directive/ngController
Using controller as makes it obvious which controller you are accessing in the template when multiple controllers apply to an element.
Than in your code:
// controller
this.alertMethod = function() {
alert('Hello');
}
// view
<div ng-app="myApp" ng-controller="testCtrl as ctrl">
<card-app ng-repeat='' card-source='' class='' my-method="ctrl.alertMethod()"></card-app>
I have a directive called tabset
angular.module('widgets')
.directive('tabset', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
controllerAs: 'tabs',
bindToController: {
tabInfo: '=tabdata'
},
templateUrl: 'Template.html',
link: function(scope, element, attrs, tabs) {
//var tabs = this;
scope.$watch('tabs.tabInfo', function() {
tabs.populateDataProvider();
console.log('this just kicked');
}, true);
},
controller: ['$filter', '$state', function($filter, $state) {
this.activeIndex = this.activeIndex < 0 ? 0 : this.activeIndex;
this.selectTab = function selectTab(tab) {
$state.go(tab.state);
};
}]
};
});
I now have a dependent directive called checkTab which requires "tabset" and on change to tabset.tabInfo, teh watch needs to kick in and populatedataprovider needs to be triggered. However that does not seem to be teh case.
Here is the checkTab directive,
angular.module('widgets')
.directive('checkTab', ['XXXService',
function(XXXService) {
return {
restrict: 'A',
require: 'tabset',
link: function(scope, element, attrs, ctrls) {
var tabCtrl = ctrls;
tabCtrl.tabInfo.push({
name: 'newtab',
state: 'newtab'
});
**var selectFn = tabCtrl.populateDataProvider;**
tabCtrl.populateDataProvider = function() {
selectFn.apply(tabCtrl, arguments);
(function recurse(dataObj) {
for (var key in dataObj) {
var obj = dataObj[key];
if (obj.hasOwnProperty('children')) {
for (var i = 0; i < obj.children.length; i++) {
recurse(obj.children[i]);
}
}
}
})(tabCtrl.dataprovider);
};
}
};
}
]);
However , when the new tab is added to tabset.tabInfo. I get an error Cannot read property 'apply' of undefined where teh selectFn is undefined.
I currently have an issue when I call ui-tinymce directive in a custom directive. The custom directive is used to load dynamically links from backend for tinymce advlink plugin (+ load tinymce options object associated with a key passed as an attribute to the directive).
Here is my controller :
module.controller('Ctrl', function ($scope) {
$scope.test = {
val: "gfsgfdgh"
};
});
Here is how I call the directive in HTML:
<tinymce-custom type="minimal" ng-model="test.val"></tinymce-custom>`
And here is my directive :
module.directive('tinymceCustom', function($location, TinyService, Module, GenerateurPage) {
return {
restrict: 'E',
replace: true,
require:"ngModel",
scope: {
ngModel: '='
},
link: function(scope, element, attrs, ngModel){
scope.loaded = {
modules: false,
pages: false,
tinymce: false
};
scope.tinyOptions = {};
var link_list = [];
var modules = [];
var pages = [];
Module.findByOrganisme({}, function (data) {
data.forEach(function(module) {
modules.push({title: module.libelle, value: "/modules/"+module.id});
});
link_list.push({title: "Modules", menu: modules});
scope.loaded.modules = true;
initTiny();
});
GenerateurPage.findByOrganisme({}, function(data) {
data.forEach(function(page) {
pages.push({title: page.titre, value: "/#/generateurPage/afficherPage?id=/"+page.id});
});
link_list.push({title: "Pages", menu: pages});
scope.loaded.pages = true;
initTiny();
});
function initTiny() {
if (!scope.loaded.modules || !scope.loaded.pages) {
return false;
}
scope.tinyOptions = TinyService.options(attrs.type);
console.log(scope);
scope.tinyOptions.link_list = link_list;
scope.loaded.tinymce = true;
}
},
template: '<div ng-if="loaded.tinymce"><textarea ui-tinymce="tinyOptions" ng-model="ngModel"></textarea></div>'
};
});
The problem is that the model passed to ui-tinymce directive is not updated when changing the text with the editor, and the text in the editor is not updated when the model from the controller is changed... BUT, the initial ngModel value is passed to ui-tinymce directive, so I think that is the data binding that is broken. Tried to watch it with $watch but nothing happens.
I can't figure how to fix it so I'm now looking for some help...
Thx
Finaly fixed it changing the approach :
<textarea tinymce-custom="minimal" ng-model="myVar"></textarea >
The final directive :
module.directive('tinymceCustom', function($location, $compile, $q, TinyService, Module, GenerateurPage) {
return {
restrict: 'A',
priority:999,
terminal:true, // prevent lower priority directives to compile after it
scope: true,
require: ['?ngModel'],
link: function(scope, el, attrs) {
// default is basic template
var type = attrs.tinymceCustom ? attrs.tinymceCustom : 'basic';
function loadTinyOptions(name) {
var loaded = {
modules: false,
pages: false,
tinymce: false
};
var link_list = [];
var deferred = $q.defer();
var initTiny = function() {
if (!loaded.modules || !loaded.pages) {
return false;
}
var tinyOptions = TinyService.options(name);
tinyOptions.link_list = link_list;
deferred.resolve(tinyOptions);
};
Module.findByOrganisme({}, function (data) {
var modules = [];
data.forEach(function(module) {
modules.push({title: module.libelle, value: "/modules/"+module.id});
});
link_list.push({title: "Modules", menu: modules});
loaded.modules = true;
initTiny();
});
GenerateurPage.findByOrganisme({}, function(data) {
var pages = [];
data.forEach(function(page) {
pages.push({title: page.titre, value: "/#/generateurPage/afficherPage?id=/"+page.id});
});
link_list.push({title: "Pages", menu: pages});
loaded.pages = true;
initTiny();
});
return deferred.promise;
}
loadTinyOptions(type).then(function(data) {
scope._tinyOptions = data;
el.removeAttr('tinymce-custom'); // necessary to avoid infinite compile loop
el.attr('ui-tinymce', '{{_tinyOptions}}');
$compile(el)(scope);
});
}
};
Hope this can help.
Im trying to implement directive pagination and i want to pass the number of the current page from the directive to a controller and then run a function from the controller with this argument but im getting undefined.
<account-pagination pagination-config="paginationConfig" on-change="pageChanged()">
</account-pagination>
controller:
$scope.pageChanged = function(page) {
console.log(page); // undefined
}
directive:
scope: {
paginationConfig: '=',
onChange: '&',
}
$scope.moveToPage = function(numPage) {
$scope.currentPage = numPage;
getPaginData(numPage);
}
function getPaginData(numPage) {
$scope.onChange({page: numPage});
}
template directive:
<li ng-repeat="num in numPages"><a ng-click="moveToPage(num)" ng-class="{pageActive: isActive(num)}">{{num}}</a></li>
OK i find the solution:
<account-pagination pagination-config="paginationConfig" on-change="pageChanged">
</account-pagination>
scope: {
paginationConfig: '=',
onChange: '=',
}
$scope.pageChanged = function(page) {
console.log(page);
}
$scope.moveToPage = function(numPage) {
$scope.currentPage = numPage;
$scope.onChange(numPage);
}
I am trying to update testVar1 the ng-model attr for the input. The value succesfully gets
$scope.testVar1 = menuElements[$scope.element.id].value;
But when i change the value of
menuElements[$scope.element.id].value;
I want testVar1 to update along with its input view
Is this possible? if so what am i doing wrong? I made a function below to try and hard set the code to val = 2 but it was not succesful it seems that the scope variables only update when you build the page(at least the way ive written it)
HTML:
<div class="well">
<label for="{{element.id}}">{{element.info}}:</label>
<input class="ui-slider" type="range" ng-model="testVar1" ng-change="changeValue(element.id)" name="{{element.id}}" min="{{element.min}}" max="{{element.max}}" id="{{element.id}}"/>
<button ng-click="setTestValue()">Test</button>
</div>
Directive and controller
cordovaAngular.directive('myCustomer', function () {
return {
restrict: 'A',
scope: {
element: '=',
elementArray: '='
},
templateUrl: elementURL,
controller: function ($scope) {
var test = JSON.stringify($scope.elementArray);
$scope.selectedOption = "Success"
$scope.testVar1 = menuElements[$scope.element.id].value;
console.log($scope.testVar1);
console.log($scope.element.id);
$scope.changeOption = function (selectedItem) {
$scope.selectedOption = selectedItem;
// alert(1);
}
$scope.changeValue = function (id) {
menuElements[id].onChange();
}
$scope.setTestValue = function () {
menuElements[$scope.element.id].value = 2;
$scope.testVar1.
console.log($scope.testVar1);
}
}
};
});
I think you can use $watch
Assign menuElements to a $scopevariable and add a $watch listener to it.
$scope.$watch('menuElements', function(newVal, oldVal){
// When menuElementes change, update testVar1 here
$scope.testVar1 = menuElements[$scope.element.id].value;
}, true);
The AngularJS docs for $watch
You need menuElements to be part of the scope in order to be able to watch changes in it. Since your directive has isolated scope, it should be in the scope of your directive. Here is an example of doing it:
HTML
<body ng-controller="ctrl" id="ctrl">
<ul>
<li ng-repeat="element in data.elementArray">{{element.id}} - {{data.menuElements[element.id].value}}</li>
</ul>
<div my-customer="" element="data.element" element-array="data.elementArray" menu-elements="data.menuElements"></div>
</body>
JavaScript
angular.module('app', []).
controller('ctrl', ['$scope', function($scope) {
$scope.data = {
elementArray: [{
id: 'el1',
info: 'Element Info 1',
min: 0,
max: 9
}, {
id: 'el2',
info: 'Element Info 2',
min: 10,
max: 19
}],
menuElements: {
'el1': {
value: 1
},
'el2': {
value: 15
}
}
};
$scope.data.element = $scope.data.elementArray[0];
}]).
directive('myCustomer', function() {
return {
template: '<div class="well">' +
'<label for="{{element.id}}">{{element.info}}:</label>' +
'<input class="ui-slider" type="range" ng-model="testVar1" ng-change="changeValue(element.id)" name="{{element.id}}" min="{{element.min}}" max="{{element.max}}" id="{{element.id}}"/>' +
'<button ng-click="setTestValue()">Test</button>' +
'</div>',
scope: {
element: '=',
elementArray: '=',
menuElements: '=' // <= add menuElements to scope
},
controller: ['$scope', function($scope) {
$scope.setTestValue = function() {
$scope.menuElements[$scope.element.id].value = 5;
}
}],
link: function(scope, element, attr) {
scope.$watch(function() { // <= Watch changes of scope.menuElements[scope.element.id].value
return scope.menuElements[scope.element.id].value;
}, function(value) {
scope.testVar1 = value;
});
}
}
});
Plunker: http://plnkr.co/edit/FLl6pLBdCWAfTbvGSBxQ?p=preview
Edit:
If you need to modify scope from outside of Angular, you can still do it, by fetching scope of DOM element related to that scope:
function setValue() {
var scope = angular.element(document.getElementById('ctrl')).scope();
scope.$apply(function() {
scope.data.menuElements[scope.data.element.id].value = 7;
});
}
Plunker: http://plnkr.co/edit/LXwJEtCIxVNgAb5AdJM6?p=preview