I have a very particular scenario, and I can't identify the cause of it.
Brief overview of my situation:
I'm developing a small little application, as part of my learning TypeScript, and using AngularJS seemed to be a challenge almost everyone new to the TS system faces, so why not.
I have an app.ts file, that basically sets up required global variables and initializes the data-ng-app="" part required by AngularJS.
The app.ts file:
module Learning.AngularTS.Core
{
"use strict";
// ... environmental variables left out
export var AngularTSApp: ng.IModule = angular.module("angularTSApp", []);
export var ngCompileProvider: ng.ICompileProvider = null;
AngularTSApp.config(function (
$compileProvider: ng.ICompileProvider,
$controllerProvider: ng.IControllerProvider): void
{
(<any>$controllerProvider).allowGlobals();
ngCompileProvider = $compileProvider;
});
}
Nothing special, a module just creates a 'namespaced' reference for me, and in here I export some variables, but the one of interest is the AngularTSApp variable, which needs it's configuration properties set.
In the .config() I'm attaching the reference to the Angular's CompileProvider to the variable ngCompileProvider, as per the code I posted.
The particular 'issue', I'm experiencing is when I'm trying to attach a Directive to the page, where the .config() only gets called after the directive, and I don't know why.
The directive:
module Learning.AngularTS.Directives
{
"use strict";
Learning.AngularTS.Core.ngCompileProvider.directive.apply(null,
["MyTestDirective",
["$parse", "$compile",
function ($parse: ng.IParseService,
$compile: ng.ICompileService): ng.IDirective
{
return <ng.IDirective>{
restrict: "E",
replace: false,
transclude: true,
templateUrl: "",
scope: {
value: "=",
edit: "#"
},
link: function ($scope: any, $rootElement: ng.IRootElementService, $attributes: ng.IAttributes): any
{
return null;
}
}
}]]);
}
On face value, this looks fine, when the JS file loads, Learning.AngularTS.Core.ngCompileProvider is null, because it has not been initialized as per the .config() section in app.ts.
Is there something wrong with my approach, and if so, is there a better way to do this?
Related
I'm very much trying to get my head around angularJS and directives still.
I have an existing REST service that outputs JSON data as follows (formatted for readability):
{"ApplicationType":
["Max","Maya","AfterEffects","Nuke","WebClient","Other"],
"FeatureCategory":
["General","Animation","Compositing","Management","Other"],
"FeatureStatus":
["Completed","WIP","NotStarted","Cancelled","Rejected","PendingReview"],
"BugStatus":
["Solved","FixInProgress","NotStarted","Dismissed","PendingReview"]}
I then have a service (which appears to be working correctly) to retrieve that data that I wish to inject into my directive.
(function () {
'use strict';
var enumService = angular.module('enumService', ['ngResource']);
enumService.factory('Enums', ['$resource',
function ($resource) {
return $resource('/api/Enums', {}, {
query: { method: 'GET', cache: false, params: {}, isArray: false }
});
}
]); })();
My intentions are to use the data from the json response to bind to html selector 'options' for the purposes of keeping the data consistent between the code behind REST service and the angular ( ie. the json data is describing strongly typed model data from c# eg. Enum.GetNames(typeof(ApplicationType)) )
projMgrApp.directive('enumOptions', ['Enums',
function (Enums) {
return {
restrict: 'EA',
template: '<option ng-repeat="op in options">{{op}}</option>',
scope: {
key: '#'
},
controller: function($scope) { },
link: function (scope, element, attrs) {
scope.options = Enums.query(function (result) { scope.options = result[scope.key]; });
}
};
}
]);
the intended usage would be to use as follows:
<label for="Application" class="control-label col-med-3 col">Application:</label>
<select id="Application" class="form-control col-med-3 col pull-right">
<enum-options key="ApplicationType"></enum-options>
</select>
which would then produce all of the options consistent with my c# enums.
In this case it appears the directive is never being called when the tag is used.
Note. I assume the factory is working fine, as i can inject it into a separate controller and it works as anticipated.
1) I guess projMgrApp is the main module. Have you included enumService as dependency to this module?
angular.module('projMgrApp',['enumServices'])
Otherwise your main module won't be aware of the existence of your service.
2) Are you aware how the directives are declared and used. When you declare
projMgrApp.directive('EnumOptions', ['Enums', function (Enums) {}])
actually should be used in the html code as:
<enum-options></enum-options>
I m not quite sure about the name but it should start with lowercase letter like enumOptions
3) I don't know why you use this key as attribute. You don't process it at all. scope.key won't work. You either have to parse the attributes in link function (link: function(scope, element, attributes)) or create isolated scope for the directive.
Add as property of the return object the following:
scope : {
key:'#' //here depends if you want to take it as a string or you will set a scope variable.
}
After you have this , you can use it in the link function as you did (scope.key).
Edit:
Here is a working version similar (optimized no to use http calls) to what you want to achieve. Tell me if I'm missing anything.
Working example
If you get the baddir error try to rename your directive name to enumOptions according to the doc (don't forget the injection):
This error occurs when the name of a directive is not valid.
Directives must start with a lowercase character and must not contain leading or trailing whitespaces.
Thanks to Tek's suggestions I was able to get this fully functioning as intended. Which means now my angularJS + HTML select tags/directives are completely bound to my APIs enums. I know down the line that I am going to be needing to adjust add to these occasionally based on user feedback, plus I use these enums to represent strongly typed data all over the app. So it will be a big time saver and help reduce code repetition. Thanks to everyone that helped!
The service and directive that I used is copied below, in case other people starting out with Angular run into similar issues or have similar requirements.
Service:
(function () {
'use strict';
var enumService = angular.module('enumService', [])
.service('Enums', ['$http', '$q', function ($http, $q) {
return {
fetch: function () {
var defer = $q.defer();
var promise = $http.get("/api/Enums").then(function (result) {
defer.resolve(result);
});
return defer.promise;
}
}
}]);})();
Directive:
angular.module('projMgrApp').directive('enumOptions', ['Enums', function (Enums) {
return {
restrict: "EA",
scope: {
key: "#"
},
template: "<select><option ng-repeat='enum in enumIds' ng-bind='enum'></option><select>",
link: function (scope) {
Enums.fetch().then(function (result) {
scope.enumIds = result.data[scope.key];
});
}
};
}]);
Given following common setup:
CtrlA (page level controller)
|- directiveAA (component e.g. button bar)
|- directiveAAA (sub-component e.g. button)
I would like to call CtrlA.methodA() from directiveAAA by passing the methodA down the chain using directive attributes - CtrlA -> directiveAA -> directiveAAA. So for example my directiveAAA "Save" button can call controller method "Save". Components directiveAA and directiveAAA are dumb components and only know about their environment given their attribute settings.
Before Typescript I would make use of inherited scope down the chain to call controller method $scope.save() from directiveAAA.
How would this work with Typescript? Would we still have to make use of injected scope into our controller, directive controller classes or can this be done without using scope, based on class inheritance?
So here's my question in code - its probably not perfect but gives the gist - the nub of the problem is marked with comment "this is where i need help":
module app.page {
class CtrlPage {
private ctrlPageBtnActions: string[] = ['goPrev', 'goNext'];
goPrev(){
console.log('go previous');
}
goNext(){
console.log('go next');
}
}
function pageDirective(){
return {
restrict: 'E',
replace: true,
template: '<button-bar actions="CtrlPage.ctrlPageActions"></button-bar>',
controller: CtrlPage,
controllerAs: 'ctrlPage',
bindToController: true
}
}
angular.module('app')
.directive('page', pageDirective);
}
module app.page.directives {
class CtrlBtnBar {
private actions: string[];
}
function buttonBar() {
return {
restrict: 'E',
replace: true,
template: '<div class="buttonBar"><btn action="CtrlBtnBar.actions[0]"></btn><btn action="CtrlBtnBar.actions[1]"></btn></div>'
controller: CtrlBtnBar,
controllerAs: 'CtrlBtnBar',
bindToController: {
actions: '='
}
}
}
angular.module('app')
.directive('buttonBar', buttonBar);
}
module app.page.directives {
class CtrlBtn {
private action: string;
handleClick(){
if (action === 'goNext'){
CtrlPage.goNext(); /// Here is where I need help
}
}
}
function btnDirective(){
return {
restrict: 'E',
replace: true,
template: '<input type="button" value="someCaption" ng-click="CtrlBtn.handleClick()"/>',
controller: CtrlBtn,
controllerAs: 'ctrlBtn',
bindToController: {
action: '#'
}
}
}
angular.module('app')
.directíve('btn', btnDirective);
}
If you run the code in http://www.typescriptlang.org/Playground you will see that typescript understandably objects to CtrlPage reference from within btnDirective controller CtrlBtn, because within this context CtrlPage does not exist. Must we use angular $scope to access the "goNext" method, given that the btnDirective is dumb and is not aware of its parents controllers and only receives inputs from its attributes? Taking radim's tip into consideration I guess the answer is yes.
Typescript with AngularJS (ver 1) does NOT bring any change to angular's architecture/design. So scopes are scopes, and they will be inherited as they did (via .$new())
Also, any Typescript class inheritance has no impact on $scope inheritance. And that won't change even with Angular 2. If some component (a bit like controller class in Typescript today) will be using code from its parent (derive from it) - in runtime it will has no effect on its context.
So, use the angular as you did, just profit from strongly typed language support.
Check these Q & A with working examples with directives:
How can I define my controller using TypeScript?
How To bind data using TypeScript Controller & Angular Js
Angular Ui-router can't access $stateParams inside my controller
I'm building an app in AngularJS that uses LeafletJS for interacting with a map, offering different possible interactions separated across what I call phases. For each of those phases there is a UIRouter state, with its controller and template.
I'm currently providing the leaflet functionality through a service. The idea was for that service to initialise a Leaflet map and provide some limited access to the state's controller. Those controllers would thus call service functions such as setupMarkersInteractions to setup callbacks that enable marker placement on the map, for example.
However, I'm running into a problem when initialising the map through Leaflet's leaflet.map() function, namely: Error: Map container not found. This is related to Leaflet's inability to find the HTML element with which the map should be associated.
Currently, I'm kinda doing this:
function mapService() {
var map;
return {
initializeMap : initializeMap,
setupMarkersInteractions : setupMarkersInteractions
};
function initializeMap() {
map = leaflet.map('map');
}
function setupMarkersInteractions() {
map.on('click', markerPlacementCallback);
}
}
The initializeMap function tells leaflet.map to look for a HTML element with id='map', which is declared on the state's template.
Now, for the actual question, is this related to some kind of AngularJS services' inability to access the HTML template? I couldn't find anything on the matter, but I thought that it would make sense for services to not directly access the view...
If it is, what kind of workaround should I explore? I've looked into leaflet-directive, but it doesn't seem to offer the possibility to add and remove custom callbacks with the flexibility I would like to (things get complex when I add free draw functionality with Leaflet-Freedraw, for example).
I considered using leaflet.map directly with an HTMLElement argument for the element but still I couldn't make it work - although there is a probability that I'm not passing what is supposed to.
What's happening is that at the moment L.Map tries to access the DOM from your service, the template is available yet. Normally services get loaded and injected into controllers, controllers initialize their scopes, after that the templates get initialized and added to DOM. You'll see if you'll put a large timeout on your map initialization that it will find it's DOM element. But that's a very ugly hack. In Angular you should use a directive to add logic to DOM elements.
For example, a template: <leaflet></leaflet> and it's very basic directive:
angular.module('app').directive('leaflet', [
function () {
return {
replace: true,
template: '<div></div>',
link: function (scope, element, attributes) {
L.map(element[0]);
}
};
}
]);
You can hook that up to your service and pass the element to your initialization method:
angular.module('app').directive('leaflet', [
'mapService'
function (mapService) {
return {
replace: true,
template: '<div></div>',
link: function (scope, element, attributes) {
mapService.initializeMap(element[0]);
}
};
}
]);
That way the initializeMap method will only be called once the actual DOM element is available. But it presents you with another problem. At the moment your controller(s) are initialized, your service is not ready yet. You can solve this by using a promise:
angular.module('app').factory('leaflet', [
'$q',
function ($q) {
var deferred = $q.defer();
return {
map: deferred.promise,
resolve: function (element) {
deferred.resolve(new L.Map(element));
}
}
}
]);
angular.module('app').directive('leaflet', [
'leaflet',
function (leaflet) {
return {
replace: true,
template: '<div></div>',
link: function (scope, element, attributes) {
leaflet.resolve(element[0]);
}
};
}
]);
If you want to use the map instance in your controller you can now wait untill it's resolved:
angular.module('app').controller('rootController', [
'$scope', 'leaflet',
function ($scope, leaflet) {
leaflet.map.then(function (map) {
var tileLayer = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap',
maxZoom: 18
}).addTo(map);
map.setView([0, 0], 1);
L.marker([0, 0]).addTo(map);
});
}
]);
Here's an example of the concept on Plunker: http://plnkr.co/edit/DoJpGqtR7TWmKAeBZiYJ?p=preview
I am new to angular and have a problem using my custom directive with ng-repeat. I want to display some posts I get from a rest interface and then use their _id property inside the directive for other purposes. However, it turns out that the property is always the one from the last displayed post when used from inside a function (test in the sample below). When trying to display the id directly over the viewmodel it shows the right one. Hope this makes sense. Any help would be appreciated.
//directive.js
angular.module('app').directive('gnPost', myGnPost);
function myGnPost() {
return {
restrict: 'E',
controller: 'postCtrl',
controllerAs: 'postCtrl',
bindToController: {
post: '='
},
scope: {},
templateUrl: 'template.html'
};
};
//controller.js
angular.module('app').controller('postCtrl', myPostCtrl);
function myPostCtrl(postRestService) {
vm = this;
vm.test = function () {
return vm.post._id;
};
};
// template.html
<p>{{postCtrl.post._id}}</p>
//displays the right id
<p>{{postCtrl.test()}}</p>
//displays the id of the last element of ng-repeat
//parent page.html
<gn-post ng-repeat="singlePost in posts.postList" post="singlePost"></gn-post>
In your controller, you have the following line:
vm = this;
It should be:
var vm = this;
By omitting the var, there is a vm variable created on the global scope instead of a local one per controller instance. As a result each iteration when vm.test is called, it's always pointing at the function defined on the last controller.
Fiddle - try including/omitting the var in postCtrl
It's good practice to use strict mode in Javascript to prevent that issue and others. In strict mode, it impossible to accidentally create global variables, as doing so will throw an error and you'll see the problem straight away. You just need to add this line at the start of your file or function:
"use strict";
To get familiar with directive testing I created the simple example shown below. Unfortunately, the test is failing and it seems that the link function is never called. The directive does work when used within an app.
I have tried hardcoding the message attribute, removing the condition within the link function and even extracting the attr set from within the $watch, but the test still fails.
There has been other posts like this and the reason for those was due to the lack of a $digest call, but I do have that and I have tried moving it into the it spec block.
If I run a console.log(elem[0].otherHTML) call the scope binding seems to work
<wd-alert type="notice" message="O'Doole Rulz" class="ng-scope"></wd-alert>
What am I missing?
alert.spec.js
"use strict";
describe('Alert Specs', function () {
var scope, elem;
beforeEach(module('myapp'));
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope;
scope.msg = "O'Doole Rulz";
elem = angular.element('<wd-alert type="notice" message="{{msg}}"></wd-alert>');
$compile(elem)(scope);
scope.$digest();
}));
it('shows the message', function () {
expect(elem.text()).toContain("O'Doole Rulz");
});
});
alert.js
angular.module('myapp').directive('wdAlert', function() {
return {
restrict: 'EA',
replace: true,
template: '<div></div>',
link: function(scope, element, attrs) {
attrs.$observe('message', function() {
if (attrs.message) {
element.text(attrs.message);
element.addClass(attrs.type);
}
})
}
}
});
Turns out the issue was a Karma configuration for the way I had the files organized. I will leave this question up just in case it bites someone else in the ass.
files: [
...,
'spec_path/directives/*js' // what I originally had
'spec_path/directives/**/*.js' // what I needed
]
I'm adding this to expand on the answer for anyone else who wants to know.
I installed a third-party directive via bower. It worked in the app, but caused tests to break.
The problem is that Grunt/Karma doesn't read the scripts in your index.html. Instead, you need to make sure you let karma know each installed script.
For example, I used the directive ngQuickDate (a datepicker). I added it to index.html for the app, but I needed to add it to karma.conf.js as well:
files: [
...
'app/bower_components/ngQuickDate/dist/ng-quick-date.js'
]
The answer above does the same thing by means of using the ** wildcard to mean "in all subdirectories recursively".
Hope that helps!