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())
Related
I am trying to bind to an angularjs 1.5 component a resolve value without success, In the state definition, I have replaced the common value of template properties with a value of the name of my new component. like this:
.state('eventslogs.create', {
url: '/create',
template: '<addevent data="$resolve.addevent"></addevent>.',
resolve: {
addevent: newEventslog
},
data: {
roles: ['admin'],
pageTitle: 'Eventslogs Create'
}
})
NewEventslog is a function that injects one of my services
newEventslog.$inject = ['EventslogsService'];
function newEventslog(EventslogsService) {
return new EventslogsService();
}
In my controller I have tried several ways but nothing works
angular.module('eventslogs')
.component('addevent', {
templateUrl: 'addevent.client.component.view.html',
bindings: {
data: '<'
},
controller: function($scope, $element) {
var vm = this;
vm.eventslog = vm.data;
}
But vm.eventslog always results in an undefined value, what's wrong with my aproach?, If I use "#" instead "<" in bindings, vm.addevent results in a string with value "$resource.addevent" and not like an instance of newEventslog function.
I am using ui-route version 0.2.18
not sure but try this inside the component
this.$onInit = () => {
vm.eventslog = vm.data;
}
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.
I have a component :
import template from './login.html';
import controller from './login.controller';
import './login.less';
let loginComponent = {
restrict: 'E',
bindings: {
redirect: '#'
},
template,
controller,
controllerAs: 'vm'
};
export default loginComponent;
Which uses a basic controller:
import angular from 'angular';
let LoginController = [
'AuthService',
'$resource',
'$state',
'config',
(AuthService, $resource, $state, config) => {
var vm = {
redirect: ''
};
var initialize = () => {
if (AuthService.authenticated()) {
loginSuccess();
}
};
var loginSuccess = () => {
console.log(vm.redirect);
if (vm.redirect) {
$state.go(vm.redirect);
}
};
initialize();
return vm;
},
];
export default LoginController;
The issue is that the redirect attribute is not available at the time of construction (when the initialize method is called). How do I access this attribute during controller construction? If there is no way, what's the best way to watching for this attribute to become available, and then calling the initialize function?
Solution:
$onInit was exactly the method I was looking for. Here's the corrected controller for future users:
import angular from 'angular';
let LoginController = [
'AuthService',
'$resource',
'$state',
'config',
(AuthService, $resource, $state, config) => {
var vm = {
redirect: ''
};
vm.$onInit = () => {
if (AuthService.authenticated()) {
loginSuccess();
}
};
var loginSuccess = () => {
console.log(vm.redirect);
if (vm.redirect) {
$state.go(vm.redirect);
}
};
return vm;
},
];
export default LoginController;
You can't access the binding at controller construction because of how Angular directive linking works.
It will be available in link function in directive, in component (which is sugar syntax for directive) it is available in magic $onInit controller method:
Called on each controller after all the controllers on an element have
been constructed and had their bindings initialized (and before the
pre & post linking functions for the directives on this element). This
is a good place to put initialization code for your controller.
The problem with listed controller is that it uses arrow function instead of regular one. The controller is an instance of controller constructor and provides this context.
There is no need to invent initialize method, it should be this.$onInit now.
Following is my code for angular Directive written in typescript class.
When i run the directive in browser. I get error as
Failed to load template: ../Templates/inputcontrol.html (HTTP status: 404 Not Found) - Error: $compile:tpload
Error Loading Template
Im using asp.net mvc
How to set the path or templateUrl in angular ?
<input-control a="shan"></input-control>
Although the relative path is fine.
class InputControl implements ng.IDirective {
restrict = "E";
scope = {
a: "=a"
};
templateUrl = "../Templates/inputcontrol.html";
controller = ["$scope", ($scope: any) => {
console.log("this this controller", $scope.a);
}];
controllerAs = "inputcontroller";
constructor(private $location: ng.ILocationService) {
}
link = (scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ctrl: any) => {
console.log(this.$location);
};
static factory(): ng.IDirectiveFactory {
const directive = ($location: ng.ILocationService) => new InputControl($location);
directive.$inject = ["$location"];
return directive;
}
}
angular.module("SheetApp").directive("inputControl", InputControl.factory());
Don't use relative pathing - rather, path according to how you'd expect to retrieve the element from your server if you were doing a Get request.
Assuming AngularApp is at the top level of your project (same tree-indentation as say the Controllers or Scripts folders are by default), use the following for your templateURL rather than relative pathing.
"/AngularApp/Templates/main.html"
I made it work by changing the code like this.
class InputControl implements ng.IDirective {
restrict = "E";
scope = {
a: "="
};
templateUrl = "";
controller = ["$scope", ($scope: any) => {
console.log("this this controller", $scope.a);
}];
controllerAs = "inputcontroller";
constructor(private $location: ng.ILocationService , private $sce:ng.ISCEService) {
var a : InputControl = this;
a.templateUrl=$sce.trustAsUrl("http://"+window.location.host +"/AngularApp/Templates/inputcontrol.html");
}
link = (scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ctrl: any) => {
console.log(this.$location);
};
static factory(): ng.IDirectiveFactory {
const directive = ($location: ng.ILocationService , $sce : ng.ISCEService) => new InputControl($location,$sce);
directive.$inject = ["$location","$sce"];
return directive;
}
}
angular.module("SheetApp").directive("inputControl", InputControl.factory());
It's worked for me when I changed only:
templateUrl: "http://" + window.location.host + "/AngularApp/Templates/inputcontrol.html"
I am in the middle of writing some tests for my application (AngularJS).
As we speak I encountered a problem with verifying if onEnter property of my state was called correctly.
Let me share some code with You
describe('Midway :: routesTest', function () {
var $state,
$rootScope,
$injector,
navigationService;
beforeEach(function () {
module('springatom', function ($provide) {
$provide.value('navigationService', navigationService = {});
});
states.configure();
inject(function (_$rootScope_, _$state_, _$injector_, $templateCache) {
$rootScope = _$rootScope_;
$state = _$state_;
$injector = _$injector_;
// We need add the template entry into the templateCache if we ever
// specify a templateUrl
$templateCache.put('/static/sa/views/home/home.html', '');
$templateCache.put('/static/sa/tpls/grid.html', '');
});
navigationService.getNavigationModel = jasmine.createSpy('getNavigationModel').and.returnValue([]);
navigationService.setNavigatorModel = jasmine.createSpy('setNavigatorModel').and.callFake(function (arg) {
});
});
it("should have a working home route", inject(function () {
var homeState = $state.get('home');
expect(homeState).toBeDefined();
expect($state.href(homeState)).toEqual('#/sa');
$rootScope.$apply(function () {
$state.go(homeState);
});
var current = $state.current;
expect($injector.invoke(current.resolve.actionModel)).toEqual([]);
expect($injector.invoke(current.onEnter)).toHaveBeenCalled();
}));
});
The failing assertion is the last one I am trying to verify therefore mentioned onEnter.
Error is:
Error: [$injector:unpr] Unknown provider: actionModelProvider <- actionModel
http://errors.angularjs.org/1.3.8/$injector/unpr?p0=actionModelProvider%20%3C-%20actionModel
As it is expected Angular tries to resolve actionModel which is the property from the resolve.
I dont know what I might be doing wrong here, so any help will be gladly welcomed.
I am attaching the state configuration as well:
define(
[
'views/home/homeController',
'views/home/recentlyUpdatedController',
// angular deps
'services/navigation'
],
function homeState(homeController, recentlyUpdatedController) {
return {
name : 'home',
definition: {
url : '/sa',
templateUrl: '/static/sa/views/home/home.html',
resolve : {
actionModel: function (navigationService) {
return navigationService.getNavigationModel('main.navigation')
}
},
onEnter : function (actionModel, navigationService) {
navigationService.setNavigatorModel('main.navigation');
},
views : {
'': {
controller : recentlyUpdatedController,
templateUrl: '/static/sa/tpls/grid.html'
}
}
}
}
}
);