Dynamic controller for directives with ECMA6 - javascript

I'm trying to set a controller dynamically to my directive using the name property. So far this is my code.
html
<view-edit controller-name="vm.controller" view="home/views/med.search.results.detail.resources.audios.html" edit="home/views/med.media.resources.edit.html"></view-edit>
js
export default class SearchResultsCtrl extends Pageable {
/*#ngInject*/
constructor($injector, $state, api) {
super(
{
injector: $injector,
endpoint: 'mediaMaterialsList',
selectable:{
itemKey: 'cid',
enabled:true,
params: $state.params
},
executeGet: false
}
);
this.controller = SearchResultsResourcesAudiosCtrl;
}
}
Directive
export default class ViewEditDirective {
constructor() {
this.restrict = 'E';
this.replace = true;
this.templateUrl = 'home/views/med.view.edit.html';
this.scope = {};
this.controller = "#";
this.name = "controllerName";
this.bindToController = {
'view': '#?',
'edit': '#?'
};
this.open = false;
this.controllerAs = 'ctrl';
}
}
I get undefined for vm.controller. I guess that it's rendering before the controller can assign the controller to the variable (I debbuged it, and it's setting the controller in the variable).
I'm following this answer to achieve this, but no luck so far.
How to set the dynamic controller for directives?
Thanks.

The problem is not related to ES6 (which is a sugar syntax coating over ES5), this is how Angular scope life cycle works.
This directive may show what's the deal with attribute interpolation
// <div ng-init="a = 1"><div sum="{{ a + 1 }}"></div></div>
app.directive('sum', function () {
return {
scope: {},
controller: function ($attrs) {
console.log($attrs.sum) // {{ a + 1 }}
// ...
},
link: function (scope, element, attrs) {
console.log(attrs.sum) // 2
}
};
});
And $attrs.sum may still not be 2 in link if a value was set after that (i.e. in parent directive link).
It is unsafe (and wrong per se) to assume that the value on one scope can be calculated based on the value from another scopes at some point of time. Because it may be not. That is why watchers and data binding are there.
All that controller: '#' magic value does is getting uninterpolated attribute value and using it as controller name. So no, it won't interpolate controller name from vm.controller and will use 'vm.controller' string as controller name.
An example of a directive that allows to set its controller dynamically may look like
// dynamic-controller="{{ ctrlNameVariable }}"
app.directive('dynamicController', function () {
return {
restrict: 'A',
priority: 2500,
controller: function ($scope, $element, $attrs, $interpolate, $compile) {
var ctrlName = $interpolate($attrs.dynamicController)($scope);
setController(ctrlName);
$attrs.$observe('dynamicController', setController);
function setController (ctrlName) {
if (!ctrlName || $attrs.ngController === ctrlName) {
return;
}
$attrs.$set('ngController', ctrlName);
$compile($element)($scope);
}
}
};
});
with all the side-effects that re-compilation may bring.

Related

How to get access to controller function from directive in AngularJS

So, this is my controller with some tiny function:
class DashboardCtrl {
constructor ($scope, $stateParams) {
"ngInject";
this.$scope = $scope;
this.title = 'Dashboard';
}
loadCharts () {
// some logic here
}
}
export default DashboardCtrl;
And now my directive:
class TreeNodeTooltip {
constructor ($scope, $state) {
"ngInject";
this.$scope = $scope;
this.$state = $state;
this.scope = {
loadCharts: '&'
};
}
link (scope, elem, attrs) {
$(elem).on('click', '.node-name', (e) => {
console.log(this.$scope.loadCharts());
console.log(this.scope.loadCharts());
console.log(this.$scope.main.loadCharts());
}
}
}
This is not working, i've tried couple of ways but ... How to get access to controller from directive, to controller function? Thx for help.
I think you are not fully understanding what Angular's .directive() is doing. It is not registering class for directive instances, but it is registering directive factory class.
I guess that TreeNodeTooltip from your question is what you register as the directive factory. In that case $injector can inject services in the constructor, but not any actual $scope (not sure how it is possible you are not getting error from that).
The actual directive instance is being initialized through the postLink / link function. There you also have directive's scope available. However you are accessing scope from the directive definition this.scope and that does not make sense.
The following modification provides a bit sense to the provided code:
class TreeNodeTooltip {
constructor () {
this.scope = {
loadCharts: '&'
};
}
link (scope, elem, attrs) {
$(elem).on('click', '.node-name', (e) => {
console.log(scope.loadCharts());
}
}
}
And then in the template:
<!-- ngController only required if it is not registered as state controller -->
<div ng-controller="DashboardCtrl as dashboardCtrl">
<tree-node-tooltip load-charts="dashboardCtrl.loadCharts()" />
</div>

Binding a Directive Controller's method to its parent $scope

I will explain what exactly I'm trying to do before explaining the issue. I have a Directive which holds a form, and I need to access that form from the parent element (where the Directive is used) when clicking on a submit button to check fi the form is valid.
To do this, I am trying to use $scope.$parent[$attrs.directiveName] = this; and then binding some methods to the the Directive such as this.isValid which will be exposed and executable in the parent.
This works fine when running locally, but when minifying and building my code (Yeoman angular-fullstack) I will get an error for aProvider being unknown which I traced back to a $scopeProvider error in the Controller.
I've had similar issues in the past, and my first thought was that I need to specifically say $inject for $scope so that the name isn't lost. But alas.....no luck.
Is something glaringly obvious that I am doing wrong?
Any help appreciated.
(function() {
'use strict';
angular
.module('myApp')
.directive('formDirective', formDirective);
function formDirective() {
var directive = {
templateUrl: 'path/to/template.html',
restrict: 'EA',
scope: {
user: '='
},
controller: controller
};
return directive;
controller.$inject = ['$scope', '$attrs', 'myService'];
function controller($scope, $attrs, myService) {
$scope.myService = myService;
// Exposes the Directive Controller on the parent Scope with name Directive's name
$scope.$parent[$attrs.directiveName] = this;
this.isValid = function() {
return $scope.myForm.$valid;
};
this.setDirty = function() {
Object.keys($scope.myForm).forEach(function(key) {
if (!key.match(/\$/)) {
$scope.myForm[key].$setDirty();
$scope.myForm[key].$setTouched();
}
});
$scope.myForm.$setDirty();
};
}
}
})();
Change the directive to a component and implement a clear interface.
Parent Container (parent.html):
<form-component some-input="importantInfo" on-update="someFunction(data)">
</form-component>
Parent controller (parent.js):
//...
$scope.importantInfo = {data: 'data...'};
$scope.someFunction = function (data) {
//do stuff with the data
}
//..
form-component.js:
angular.module('app')
.component('formComponent', {
template:'<template-etc>',
controller: Controller,
controllerAs: 'ctrl',
bindings: {
onUpdate: '&',
someInput: '<'
}
});
function Controller() {
var ctrl = this;
ctrl.someFormThing = function (value) {
ctrl.onUpdate({data: value})
}
}
So if an event in your form triggers the function ctrl.someFormThing(data). This can be passed up to the parent by calling ctrl.onUpdate().

Angular 1.4 ES6 Class Directive

I am working on seeing if I can take a directive in 1.4 and trying to resemble a 1.5 component. I am using bindToController and controllerAs to use the controller in my directive instead of a separate controller. I have done this successfully with exporting as a function, but wanted to see if I could export as a class, and see if there is a good reason to do so. I am running into a bindToController error right now with the following code:
export default class recordingMenuComponent {
constructor(RecordingCtrl) {
'ngInject';
this.restrict = 'E',
this.scope = {},
this.templateUrl = '/partials/media/recording/recording-menu.html',
this.controller = RecordingCtrl,
this.controllerAs = 'record',
this.bindToController = {
video: '='
}
}
RecordingCtrl($log, $scope, $state, $timeout, RecordingService) {
'ngInject';
const record = this;
Object.assign(record, {
recentCalls: record.recentCalls,
startRecording() {
let video = {
accountId: $scope.accountId,
title: record.sipUrl
};
RecordingService
.recordVideoConference(video, record.sipUrl, record.sipPin, 0)
.then(result => {
video.id = result.id;
$timeout(() => $state.go('portal.video', {videoId: video.id}), 500);
}, error => $log.error('Error starting the recording conference: ', error));
},
getRecentCalls() {
RecordingService
.recentVideoConferenceCalls()
.then(result => {
record.recentCalls = result;
}, error => $log.error('There was an error in retrieving recent calls: ', error));
}
});
}
static recordingFactory() {
recordingMenuComponent.instance = new recordingMenuComponent();
return recordingMenuComponent.instance;
}
}
and then importing:
import angular from 'angular'
import recordingMenuComponent from './recordingMenuComponent'
angular.module('recordingModule', [])
.directive(recordingMenuComponent.name, recordingMenuComponent.recordingFactory);
There is some of the module that I have left out for brevity that did not have to do with trying to turn this directive into a component. Note that I am trying to not use the .controller() before the .directive().
When I try to use this, I get this error:
angular.js:9490 Error: [$compile:noctrl] Cannot bind to controller without directive 'recordingMenuComponent's controller
I am not sure I am going on the right track or this is not the right road to be going on.
Thank you for any help.
You should implement RecordingCtrl as a class
const app = require('../app');
class RecordingCtrl {
static $inject = ['$log', 'RecordingService'];
constructor($log, recordingService) {
this.$log = $log;
this.recordingService = recordingService;
}
startRecording() {
// . . .
}
recentCalls() {
// . . .
}
}
// for ng15 component
const recordingMenu = {
restrict: 'E',
scope = {},
templateUrl = '/partials/media/recording/recording-menu.html',
controller = RecordingCtrl,
controllerAs = 'record',
bindToController = {
video: '='
}
}
app.component('recordingMenu', recordingMenu);
// or ng1.4 directive
function recordingMenu() {
return {
restrict: 'E',
scope = {},
templateUrl = '/partials/media/recording/recording-menu.html',
controller = RecordingCtrl,
controllerAs = 'record',
bindToController = {
video: '='
}
}
}
app.directive('recordingMenu', recordingMenu);
It does't make sense to implement a controller as a class method.
This means you will have two classes... unless you just want to make the Directive Definition Object factory a plain-old-function or a static method of your controller.
I was able to get this working even if it is not the best approach. It is for experimentation and the way that I was able to get it working:
.directive(recordingMenuComponent.name, () => new recordingMenuComponent())

Passing data from controller to directive

I 'm trying to handle data that is coming from my database in a directive , however these data are being pulled by a controller and being assigned to scope like this:
Calendar Controller:
'use strict';
var CalendarController = ['$scope', 'EventModel', function(scope, EventModel) {
scope.retrieve = (function() {
EventModel.Model.find()
.then(function(result) {
scope.events = result;
}, function() {
});
}());
}];
adminApp.controller('CalendarController', CalendarController);
Calendar Directive:
'use strict';
var calendarDirective = [function($scope) {
var Calendar = {
init: function(events) {
console.log(events);
}
};
return {
link: function(scope) {
Calendar.init(scope.events);
}
};
}];
adminApp.directive('calendarDirective', calendarDirective);
But the data is undefined in the directive, and in the controller the data appears to be ok.
Thanks!
This is a common error for people starting out with AngularJS. This is a load order issue. The events scope variable is not defined when the directive link function is executed. One solution is to use a watch on the variable passed into the directive and load once it is defined.
return {
link: function(scope) {
scope.$watch('events', function() {
if(scope.events === undefined) return;
Calendar.init(scope.events);
});
}
};
In the above scenario controller and directives have isolated scope. One solution will be to use factory/service to communicate with the server and inject the service to controller and directive. Since service/factory is singleton you can cache the data & share is between controllers & directives.
try this way.
Directive html
<calendar-directive objEvent="events" ></calendar-directive >
JS :
'use strict';
var calendarDirective = [function($scope) {
return {
scope : { objEvent : '=objEvent'}
template: '<div>{{ objEvent }}</div>',
};
}];
Use angular isolated scopes.

angular.js directive two-way-binding scope updating

I wanted to use a directive to have some click-to-edit functionality in my front end.
This is the directive I am using for that: http://icelab.com.au/articles/levelling-up-with-angularjs-building-a-reusable-click-to-edit-directive/
'use strict';
angular.module('jayMapApp')
.directive('clickToEdit', function () {
return {
templateUrl: 'directives/clickToEdit/clickToEdit.html',
restrict: 'A',
replace: true,
scope: {
value: '=clickToEdit',
method: '&onSave'
},
controller: function($scope, $attrs) {
$scope.view = {
editableValue: $scope.value,
editorEnabled: false
};
$scope.enableEditor = function() {
$scope.view.editorEnabled = true;
$scope.view.editableValue = $scope.value;
};
$scope.disableEditor = function() {
$scope.view.editorEnabled = false;
};
$scope.save = function() {
$scope.value = $scope.view.editableValue;
$scope.disableEditor();
$scope.method();
};
}
};
});
I added a second attribute to the directive to call a method after when the user changed the value and then update the database etc. The method (´$onSave´ here) is called fine, but it seems the parent scope is not yet updated when I call the method at the end of the directive.
Is there a way to call the method but have the parent scope updated for sure?
Thanks in advance,
Michael
I believe you are supposed to create the functions to attach inside the linking function:
Take a look at this code:
http://plnkr.co/edit/ZTx0xrOoQF3i93buJ279?p=preview
app.directive('clickToEdit', function () {
return {
templateUrl: 'clickToEdit.html',
restrict: 'A',
replace: true,
scope: {
value: '=clickToEdit',
method: '&onSave'
},
link: function(scope, element, attrs){
scope.save = function(){
console.log('save in link fired');
}
},
controller: function($scope, $attrs) {
$scope.view = {
editableValue: $scope.value,
editorEnabled: false
};
$scope.enableEditor = function() {
$scope.view.editorEnabled = true;
$scope.view.editableValue = $scope.value;
};
$scope.disableEditor = function() {
$scope.view.editorEnabled = false;
};
$scope.save = function() {
console.log('save in controller fired');
$scope.value = $scope.view.editableValue;
$scope.disableEditor();
$scope.method();
};
}
};
});
I haven't declared the functions inside the controller before, but I don't see why it wouldn't work.
Though this question/answer explain it Link vs compile vs controller
From my understanding:
The controller is used to share data between directive instances, not to "link" functions which would be run as callbacks.
The method is being called but angular doesn't realise it needs to run the digest cycle to update the controller scope. Luckily you can still trigger the digest from inside your isolate scope just wrap the call to the method:
$scope.$apply($scope.method());

Categories

Resources