In the code snippet I try to use a controller FooCtrl which is defined in the included template app/foo.html by using the directive common.script.
angular.module('common.script', []).directive('script', function() {
return {
restrict: 'E',
scope: false,
compile: function(element, attributes) {
if (attributes.script === 'lazy') {
var code = element.text()
new Function(code)()
}
}
}
})
angular.module('app.templates', ['app/foo.html'])
angular.module("app/foo.html", []).run(function($templateCache) {
$templateCache.put("app/foo.html",
"<script data-script=\"lazy\">\n" +
" console.log('Before FooCtrl')\n" +
" angular.module('app').controller('FooCtrl', function($scope) {\n" +
" console.log('FooCtrl')\n" +
" })\n" +
"<\/script>\n" +
"<div data-ng-controller=\"FooCtrl\">app\/foo.html\n" +
"<\/div>"
)
})
angular.module('app', ['common.script', 'app.templates']).controller('ApplicationCtrl', function($scope) {
console.log('ApplicationCtrl')
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js"></script>
<div data-ng-app="app" data-ng-controller="ApplicationCtrl">
<div data-ng-include="'app/foo.html'"></div>
</div>
But instead of the expected output FooCtrl in the console AngularJS throws:
Error: [ng:areq] Argument 'FooCtrl' is not a function [...]
I don't understand why! The code in the template is executed before the exception is thrown, thus the controller should be defined. How could I fix that?
The real problem here is lazy loading of resources! There are tons of material and related posts about this topic.
The solution here could be an extended common.script directive:
'use strict'
angular.module('common.script', [])
.config(function($animateProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
angular.module('common.script').lazy = {
$animateProvider: $animateProvider,
$controllerProvider: $controllerProvider,
$compileProvider: $compileProvider,
$filterProvider: $filterProvider,
$provide: $provide
}
})
.directive('script', function() {
return {
restrict: 'E',
scope: {
modules: '=script'
},
link: function(scope, element) {
var offsets = {}, code = element.text()
function cache(module) {
offsets[module] = angular.module(module)._invokeQueue.length
}
function run(offset, queue) {
var i, n
for (i = offset, n = queue.length; i < n; i++) {
var args = queue[i], provider = angular.module('common.script').lazy[args[0]]
provider[args[1]].apply(provider, args[2])
}
}
if (angular.isString(scope.modules)) {
cache(scope.modules)
} else if (angular.isArray(scope.modules)) {
scope.modules.forEach(function(module) {
cache(module)
})
}
/*jshint -W054 */
new Function(code)()
Object.keys(offsets).forEach(function(module) {
if (angular.module(module)._invokeQueue.length > offsets[module]) {
run(offsets[module], angular.module(module)._invokeQueue)
}
})
}
}
})
The only downside of this solution is that you have to specify the module(s) you want to extend in a script tag:
<script data-script="'app'">
angular.module('app').controller('FooCtrl', function($scope) {
console.log('Works!')
})
</script>
Related
With my below angular1.x script code for image annotation I have searched for a solution to solve my conversion error but I didn't get many equalents of angular1.x code in angular4. The error is a runtime error for angular1.x when I try to convert into Angular4. See below.
For $inject I use #inject but it is quite difficult to use it in my code for converting angular 1.x image annotation to angular4 image annotation.
Image annoation in angular1.x
------------------------------
annotoriousAnnotateDirective.$inject = ['annotoriousService', 'setTimeout()'];
annotoriousAnnotateDirective(annotoriousService, setTimeout(()=> {
service = {
restrict: 'A',
link: annotateLink,
priority: 100
},
return service;
link.$inject = ['$scope', '$element', '$attributes'];
annotateLink($scope, $element, $attributes)=> {
if ($attributes.src) {
annotoriousService.makeAnnotatable($element[0]);
} else {
$element.bind('load', function () {
$scope.$apply(function () {
annotoriousService.makeAnnotatable($element[0]);
});
});
}
}
}, 200);
annotoriousDirective.$inject = ['$compile', '$rootScope', '$http', '$parse', '$timeout', 'annotoriousService'];
annotoriousDirective($compile, $rootScope, $http, $parse, $timeout, annotoriousService)=> {
let service = {
restrict: 'E',
scope: {
open: '=',
options: '=',
onOpen: '&',
onLoad: '&',
onComplete: '&',
onCleanup: '&',
onClosed: '&'
},
require: 'annotorious',
link: link,
controller: controller,
controllerAs: 'vm'
};
return service;
controller.$inject = ['$scope'];
controller($scope) {
}
link.$inject = ['$scope', '$element', '$attributes'];
function link($scope, $element, $attributes, controller) {
var cb = null;
$scope.$watch('open', function (newValue, oldValue) {
//console.log("watch $scope.open(" + $scope.open + ") " + oldValue + "->" + newValue);
if (oldValue !== newValue) {
updateOpen(newValue);
}
});
$scope.$on('$destroy', function () {
$element.remove();
});
init();
updateOpen(newValue) {
if (newValue) {
init(newValue);
} else {
if (options.annotationsFor) {
let annotatables = $(options.annotationsFor);
annotatables.each(function (idx) {
let item = this;
annotoriousService.makeAnnotatable(item);
});
}
}
}
init(open) {
let options = {
//href: $attributes.src,
annotationsFor: $attributes.annotationsFor,
onOpen() {
if (this.onOpen && this.onOpen()) {
this.onOpen()();
}
},
onLoad() {
if (this.onLoad && this.onLoad()) {
this.onLoad()();
}
},
onComplete() {
onComplete();
if (this.onComplete && this.onComplete()) {
this.onComplete()();
}
},
onCleanup() {
if (this.onCleanup && this.onCleanup()) {
this.onCleanup()();
}
},
onClosed() {
$scope.$apply(function () {
$scope.open = false;
});
if ($scope.onClosed && $scope.onClosed()) {
$scope.onClosed()();
}
}
};
if (options) {
Object.assign(options, this.options);
}
for (let key in options) {
if (options.hasOwnProperty(key)) {
if (typeof(options[key]) === 'undefined') {
delete options[key];
}
}
}
if (typeof(open) !== 'undefined') {
options.open = open;
}
$timeout(function () {
if (options.annotationsFor) {
$(options.annotationsFor).each(function (i) {
var itemToAnnotate = this;
annotoriousService.makeAnnotatable(itemToAnnotate);
});
}
}, 0);
}
My error code:
I found equalent for #Inject in angular4 ,
import { Inject } from '#angular/core';
import { ourservice } from 'servicepath';
export class{
constructor(#Inject(Ourservice / Directive/ etc) private ourservice){
//our functionality
}
}
If any corrections please let me know, I am still looking solution for Image annotation in angular4
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;
I spent more time for understand why instance of class after remove directive does not destroy. I wrote the following code. Please help me to resolve it. Code provide below. The result in the console log!!
'use strict';
var app = angular.module('app', []);
app.controller('mainController', function($scope, Service) {
$scope.service = Service;
$scope.checkAll = function () {
$scope.service.triggerListener('update');
};
$scope.add = function () {
$scope.count.push({});
};
$scope.object = {
updateAll: function () {
console.log('Count of directive "person"');
}
};
$scope.removeElement = function () {
$scope.count.splice(0, 1);
};
$scope.count = [0, 1, 2, 3, 4];
});
app.service('Service', function() {
this.listeners = [];
this.addListeners = function (object, event, callback) {
if (!this.listeners.hasOwnProperty(event)) {
this.listeners[event] = [];
}
this.listeners[event].push(object[callback]);
};
this.triggerListener = function(event) {
if (this.listeners.hasOwnProperty(event)) {
for (var i = 0; i < this.listeners[event].length; i++) {
this.listeners[event][i]();
}
}
};
});
app.directive('person', function() {
var directive = {
restrict: 'E',
template: '<button id="{{vm.index}}">Person</button> ' +
'<button ng-click="vm.add(index)">add</button>' +
'<button ng-click="vm.removeElement(index)">Clear</button>',
scope: {
index: '=',
service: '=',
removeElement: '&',
object: '=',
add: '&'
},
controller: function() {
},
link: function (scope) {
scope.vm.service.addListeners(scope.vm.object, 'update', 'updateAll');
},
controllerAs: 'vm',
bindToController: true
};
return directive;
});
<!DOCTYPE html>
<html lang="en" ng-app="app">
<head>
<meta charset="UTF-8">
<title></title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.2/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body ng-controller="mainController">
<div ng-repeat="item in count">
<person add="add()" index="$index" service="service" object="object" remove-element="removeElement()" show="show()">{{$index}}</person>
</div>
<button ng-click="checkAll()">Count of "person" directive</button>
</body>
</html>
Your directive only adds to the listeners array ... but there is nothing in your code to remove anything from that array
You need a removeListener method that would be called in the removeElement method.
$scope.removeElement = function () {
var index = // get index of element
$scope.count.splice(index, 1);
Service.removeListener(index);
};
In service:
this.removeListener = function(index){
this.listeners.splice(index,1);
};
Alternatively you could use $destroy event in directive:
link: function (scope) {
Service.addListeners(scope.vm.object, 'update', 'updateAll');
scope.$on('$destroy', function(){
Service.removeListener(scope.index);
});
},
Note that injecting Service into directive is simpler and cleaner than passing it through html attributes to scope
app.directive('person', function(Service) {
I have created a directive below:
html:
<div image-upload></div>
directive:
angular.module('app.directives.imageTools', [
"angularFileUpload"
])
.directive('imageUpload', function () {
// Directive used to display a badge.
return {
restrict: 'A',
replace: true,
templateUrl: "/static/html/partials/directives/imageToolsUpload.html",
controller: function ($scope) {
var resetScope = function () {
$scope.imageUpload = {};
$scope.imageUpload.error = false;
$scope.imageUpload['image_file'] = undefined;
$scope.$parent.imageUpload = $scope.imageUpload
};
$scope.onImageSelect = function ($files) {
resetScope();
$scope.imageUpload.image_file = $files[0];
var safe_file_types = ['image/jpeg', 'image/jpg']
if (safe_file_types.indexOf($scope.imageUpload.image_file.type) >= 0) {
$scope.$parent.imageUpload = $scope.imageUpload
}
else {
$scope.imageUpload.error = true
}
};
// Init function.
$scope.init = function () {
resetScope();
};
$scope.init();
}
}
});
This directive works fine and in my controller I access $scope.imageUpload as I required.
Next, I tried to pass into the directive a current image but when I do this $scope.imageUpload is undefined and things get weird...
html:
<div image-upload current="project.thumbnail_small"></div>
This is the updated code that gives the error, note the new current.
angular.module('app.directives.imageTools', [
"angularFileUpload"
])
.directive('imageUpload', function () {
// Directive used to display a badge.
return {
restrict: 'A',
replace: true,
scope: {
current: '='
},
templateUrl: "/static/html/partials/directives/imageToolsUpload.html",
controller: function ($scope) {
var resetScope = function () {
$scope.imageUpload = {};
$scope.imageUpload.error = false;
$scope.imageUpload['image_file'] = undefined;
$scope.$parent.imageUpload = $scope.imageUpload
if ($scope.current != undefined){
$scope.hasCurrentImage = true;
}
else {
$scope.hasCurrentImage = true;
}
};
$scope.onImageSelect = function ($files) {
resetScope();
$scope.imageUpload.image_file = $files[0];
var safe_file_types = ['image/jpeg', 'image/jpg']
if (safe_file_types.indexOf($scope.imageUpload.image_file.type) >= 0) {
$scope.$parent.imageUpload = $scope.imageUpload
}
else {
$scope.imageUpload.error = true
}
};
// Init function.
$scope.init = function () {
resetScope();
};
$scope.init();
}
}
});
What is going on here?
scope: {
current: '='
},
Everything works again but I don't get access to the current value.
Maybe I'm not using scope: { correctly.
in your updated code you use an isolated scope by defining scope: {current: '=' } so the controller in the directive will only see the isolated scope and not the original scope.
you can read more about this here: http://www.ng-newsletter.com/posts/directives.html in the scope section
I've got directive and service in my app (declared in separate files):
Service:
(function(){
angular.module('core', [])
.factory('api', function() {
return {
serviceField: 100
};
})
})();
Directive:
(function(){
angular.module('ui', ['core'])
.directive('apiFieldWatcher', function (api) {
return {
restrict: 'E',
replace: true,
scope: true,
template: '<div>+{{apiField}}+</div>',
controller: function($scope) {
$scope.apiField = 0;
},
link: function (scope) {
scope.$watch(function(){return api.serviceField}, function(apiFld){
scope.apiField = apiFld;
});
}
}
});
})();
And in another separate file I have native model:
function Model() { this.fld = 0; }
Model.prototype.setFld = function(a) { this.fld = a; }
Model.prototype.getFld = function() { return this.fld; }
How can I bind (two way) my native this.fld field to value in my AngularJS service?
The solution is in using this code:
Model.prototype.setFld = function(a) {
this.fld = a;
injector.invoke(['$rootScope', 'api', function($rootScope, api){
api.setField(a);
$rootScope.$digest();
}]);
};
Model.prototype.getFldFromApi = function() {
var self = this;
injector.invoke(['api', function(api){
self.fld = api.getField();
}]);
};
http://plnkr.co/edit/nitAVuOtzGsdJ49H4uyl
i think it's bad idea to use $digest on $rootScope, so we can maybe use
var scope = angular.element( elementObject ).scope();
to get needed scope and call $digest for it