Angular dynamic creation of Directives - angularjs

I have page setup where I want to dynamically create directives and add them to the page. I have gotten this work, but in the Directives that I'm adding are not getting compiled correctly because no interpolation is occurring and the Dependency in the constructor is not getting loaded.
Here is the code that I'm using.
page.html
<dashboard-dynamic-component ng-repeat="item in vm.parts" dashboard-part="item.part.implementationComponent" dashboard-part-id="item.id"</dashboard-dynamic-component>
dashboardDynamicComponent
function dynamicComponent($compile: ng.ICompileService, $timeout: ng.ITimeoutService) {
return {
restrict: 'E',
replace: true,
templateUrl: 'app/main/dashboard/dynamicComponent.html',
scope: {
dashboardPart: '=',
dashboardPartId: '='
},
link: (scope, element) => {
var newElement = $compile(`<${scope.dashboardPart} dashboard-part-id="${scope.dashboardPartId}" />`)(scope);
element.append(newElement);
$timeout(() => {});
}
}
}
dynamicComponent.$inject = ['$compile', '$timeout'];
angular.module('Cranalytics')
.directive('dashboardDynamicComponent', dynamicComponent);
This creating the component that I expect it to, but then in the next part is were I see the problems.
titleComponent.html - Which is what is dynamically being created above
<h3 class="title">****{{vm.title}}****</h3>
titleComponent.ts
export class TitleComponent implements ng.IDirective {
restrict = 'E';
replace = true;
templateUrl = 'app/main/dashboard/titleComponent/titleComponent.html';
scope = {
dashboardPartId: '='
};
controller = TitleController;
controllerAs = 'vm';
bindToController = true;
static create(): ng.IDirectiveFactory {
const directive = () => new TitleComponent();
return directive;
}
static instance(): ng.IDirective { return new TitleComponent(); }
}
export class TitleController {
_dashboardPartId: number;
get dashboardPartId(): number {
return this._dashboardPartId;
}
set dashboardPartId(value: number) {
this._dashboardPartId = value;
if (!value) return;
this.dataService.loadById(value)
.then((result: Models.Dashboard.dashboardPart) => {
var options = JSON.parse(result.options);
this.title = options.Title;
});
}
title: string;
static $inject = ['Cranalytics.Dashboard.dashboardPartDataService'];
constructor(private dataService: Dashboard.dashboardPartDataService) {}
}
angular.module('Cranalytics')
.directive('dashboardTitleComponent', TitleComponent.create());
So I think I'm just missing one little piece because I am getting the {{vm.title}} on the page, but the interpolation is not displaying and also in the constructor in the above code the dependency for Cranalytics.Dashboard.dashboardPartDataService is returning undefined

The problem that I had was that the property setter for dashboardPartId was being called before the constructor was being called, which resulted in the who component erroring out because this.dataService was undefined. Once I refactored the loading out of the set and checked for the dataService and added that same call into the constructor everything worked.

Related

Angular 1.5 component $postLink gets triggered too soon in when using templateUrl

I followed this post to get acquainted with Angular's 1.5 component postLink event.
I got this working in a plunker. Here's the code for the tabs component:
controller: function () {
this.$onInit = function () {
console.log("$onInit");
this.tabs = [];
};
this.addTab = function addTab(tab) {
console.log("addTab");
this.tabs.push(tab);
};
this.selectTab = function selectTab(index) {
for (var i = 0; i < this.tabs.length; i++) {
this.tabs[i].selected = false;
}
this.tabs[index].selected = true;
};
this.$postLink = function () {
console.log("$postLink. nr of tabs added: " + this.tabs.length);
this.selectTab(this.selected);
};
}
The console output:
$onInit
addTab
addTab
addTab
$postLink. nr of tabs added: 3
However, when I try to do the same in typescript, the postLink event gets triggered too soon. It gets triggered before the tabs can be added to the tabs-component.
Here's some of the code:
/tabs/tab/tab.component.ts
namespace MainApp {
const mainApp = angular.module("mainApp");
class TabComponent implements ng.IComponentOptions {
public templateUrl: string | ng.Injectable<(...args: any[]) => string>;
public controller: any;
public controllerAs: string;
public transclude: boolean;
public bindings: any;
public require: any;
constructor() {
this.templateUrl = ["rootUrl", (rootUrl) => rootUrl + "app/uitrijregelingBerekening/tabs/tab/tab.html"];
this.controller = TabController;
this.transclude = true;
this.bindings = {
label: "#",
};
this.require = {
tabs: "^^",
};
}
}
mainApp.component("tab", new TabComponent());
}
/tabs/tab/tab.controller.ts
namespace MainApp {
interface ITabBindings {
label: string;
}
export class TabController implements ITabBindings {
public label: string;
private tabs: TabsController;
public tab: any;
constructor() {
}
public $onInit() {
this.tab = {
label: this.label,
selected: false
};
this.tabs.addTab(this.tab);
}
}
}
/tabs/tabs.component.ts
namespace MainApp {
const mainApp = angular.module("mainApp");
class TabsComponent implements ng.IComponentOptions{
public templateUrl: string | ng.Injectable<(...args: any[]) => string>;
public controller: any;
public controllerAs: string;
public bindings: any;
public transclude: boolean;
constructor() {
this.templateUrl = ["rootUrl", (rootUrl) => rootUrl + "app/uitrijregelingBerekening/tabs/tabs.html"];
this.controller = TabsController;
this.bindings = {
selected:"#",
};
this.transclude = true;
}
}
mainApp.component("tabs", new TabsComponent());
}
/tabs/tabs.controller.ts
namespace MainApp {
export interface ITabsBindings {
selected: number;
}
export class TabsController implements ITabsBindings {
public selected: number;
public tabs: Array<any>;
private scope: any;
static $inject = ["$scope"];
constructor($scope: ng.IScope) {
this.scope = $scope;
}
public $onInit() {
console.log("$onInit");
this.tabs = new Array<any>();
}
public addTab(tab: any) {
console.log("addTab");
this.tabs.push(tab);
}
public selectTab(index: number) {
for (var i = 0; i < this.tabs.length; i++) {
this.tabs[i].selected = false;
}
this.tabs[index].selected = true;
}
public $postLink() {
console.log("$postLink. nr of tabs added: " + this.tabs.length);
this.selectTab(this.selected);
}
}
}
The templates are the same.
Now the console output is:
$onInit
$postLink. nr of tabs added: 0
angular.js:13920 TypeError: Cannot set property 'selected' of undefined
addTab
addTab
addTab
Am I missing something here?
Well, you are using a different approach now. Before you just pushed it into an array in one controller. Now you have two components and controllers.
From the Typescript docs, this is your problem.
/**
* Called after this controller's element and its children have been linked. Similar to the post-link function this
* hook can be used to set up DOM event handlers and do direct DOM manipulation. Note that child elements that contain
* templateUrl directives will not have been compiled and linked since they are waiting for their template to load
* asynchronously and their own compilation and linking has been suspended until that occurs. This hook can be considered
* analogous to the ngAfterViewInit and ngAfterContentInit hooks in Angular 2. Since the compilation process is rather
* different in Angular 1 there is no direct mapping and care should be taken when upgrading.
*/
$postLink?(): void;
Note that child elements that contain templateUrl directives will not
have been compiled and linked since they are waiting for their
template to load asynchronously and their own compilation and linking
has been suspended until that occurs.
Instead of using require you should maybe bind the tabs array to the children.
#kuhnroyal's answer is the correct one here. But I wanted to post a follow-up because it might be usefull for other people with the same issue. I've managed find a solution which allows me to work with templates in separate files (which improves maintainability), but still use the template-property to guarantee the correct order of postLink events.
I now work with the $templateCache object of angular. The key is to preload all the templates when the angular app starts up. And then use the $templateCache.get method to fill in the template-property on the components. This post led me to that solution.

Angular directive receiving object as attribute using Typescript

I am developing using Angular 1.5.8 and Typescript
I have a directive which is used under the scope of another directive (and another controller of course). Let's say Directive1, Controller1 and Directive2, Controller2.
Given the Controller1 already has the user information, I would like to pass this user information to the Controller2 through the Directive2, to prevent from fetching the information again from the backend.
I am not sure if this can be done, but it would be nice if that's the case :)
Below is the code to help my explanation:
Directive1 HTML:
<div>
...
<directive2 user="{{ctrl.loggedUser}}"></directive2>
...
</div>
loggedUser is loaded in Controller1 constructor through a call to the backend.
Directive2 and Directive2Ctrl Typescript code:
class Directive2 implements ng.IDirective {
controller = "Directive2Ctrl";
controllerAs = "d2Ctrl";
bindToController = {
user: "#"
};
restrict = "E";
templateUrl = "directive2.html";
static factory(): ng.IDirectiveFactory {
const directive = () => new Directive2();
return directive;
}
}
angular
.module("app")
.controller("Directive2Ctrl", Directive2Ctrl)
.directive("directive2", Directive2.factory());
class Directive2Ctrl implements IDirective2Ctrl {
public user: User;
constructor(user: User) {
// user is undefined
}
$onInit(user: User): void {
// user is undefined
}
}
I couldn't find a way of passing the user object to the Directive2Ctrl (not even sure if it is possible).
Use "scope" property instead of "bindToController" property, and replace your "#" with "=".
Then I use an interface for my specific scope to get autocompletion.
export interface IMyDirectiveScope extends ng.IScope {
user: any;
}
export class Myirective {
public restrict: string = 'E';
public templateUrl = "/mytemplate.html";
public link: (scope: IMyDirectiveScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ngModel: ng.INgModelController) => void;
public scope = {
user: "="
};
constructor() {
var context = this;
context.link = (scope: IMyDirectiveScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ngModel: ng.INgModelController) => {
//INSERT YOUR CODE
//console.log(scope.user);
};
}
public static Factory() {
var directive = () => {
return new MyDirective();
};
return directive;
}
}
In your html, remove your curly braces.
<div>
...
<directive2 user="ctrl.loggedUser"></directive2>
...
</div>
If you want to share data between different locations in your application, just put it in a service and use DI wherever you need the data.
That is, fetch the data, store it in a service and use DI to make the data available in different locations. There is no need to pass data through bindings over several layers, much easier to use a service.
var mod = angular.module('testApp', ['ngRoute']);
mod.config(['$routeProvider',
function($routeProvider) {
$routeProvider.
when('/intern', {
template: '<div class="outer" ng-controller="InternController">{{User.firstName}} <div class="nested" ng-controller="NestedController">{{NestedUser.lastName}}<test-dir></test-dir></div></div>',
resolve: {
userResolve: function($q, $timeout, User) {
var deferred = $q.defer();
// mock server call, which returns server data
$timeout(function() {
var mockUserResp = {
firstName: 'John',
lastName: 'Rambo'
};
User.setUser(mockUserResp);
deferred.resolve();
}, 1000);
return deferred.promise;
}
}
}).
otherwise({
redirectTo: '/intern'
});
}]);
mod.factory('User', function() {
var _user = null;
return {
setUser: function(user) {
_user = user;
},
getUser: function() {
return _user;
}
}
});
mod.controller('InternController', function($scope, User) {
$scope.User = User.getUser();
});
mod.controller('NestedController', function($scope, User) {
$scope.NestedUser = User.getUser();
});
mod.directive('testDir', function(User) {
return {
restrict: 'EA',
scope: {},
template: '<div class="dir">{{firstName}} is a cool guy.</div>',
link: function(scope) {
scope.firstName = User.getUser().firstName;
}
};
});
.outer {
border: 1px solid green;
}
.nested {
border: 1px solid blue;
}
.dir {
border: 1px solid orange;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.12/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.12/angular-route.min.js"></script>
<div ng-app="testApp">
<div ng-view></div>
</div>
It should be like this. But if it's still not working for you, you can create a simple plunker and I will fix it there. Cheers!
<div>
...
<directive2 user="ctrl.loggedUser"></directive2>
...
</div>
`
class Directive2 implements ng.IDirective {
controller = "Directive2Ctrl";
controllerAs = "d2Ctrl";
scope = {},
bindToController = {
user: "="
};
restrict = "E";
templateUrl = "directive2.html";
static factory(): ng.IDirectiveFactory {
return () => new Directive2();
}
}
angular
.module("app")
.controller("Directive2Ctrl", Directive2Ctrl)
.directive("directive2", Directive2.factory());
class Directive2Ctrl implements IDirective2Ctrl {
public user: User;
$onInit(user: User): void {
// user should be available
console.log(this.user);
}
}
Sorry, I wonder if you still need to set scope = {}
To do what I asked in the first place, the scope needs to be correctly used.
Here is another question which is nicely explained which uses the scope correctly:
How can I define my controller using TypeScript?

Can there be an AngularJS directive to manage an array of conditions?

I'm new to AngularJS and my project at the moment has a menu that only needs to be displayed sometimes.
I therefore have:
<div class="iframe-hide"
ng-show="$state.includes('deposit.card.start')||
$state.includes('deposit.card.3ds')||
$state.includes('deposit.card.waiting')||
$state.includes('deposit.bank')||
$state.includes('deposit.x')||
$state.is('deposit.x.start')||
$state.is('deposit.y.start')||
$state.is('deposit.y.frame')">
As you can imagine, as the project grows this becomes unmanageable, so I want to look into tidying it up and creating perhaps a custom directive that will handle these conditions better.
I've been thinking of adding a custom data parameter like this:
.state("deposit.card.waiting", {
url: "/waiting",
templateUrl: "app/deposit/templates/card/waiting.html",
data: { includeMenu: true }
})
The Html instead would be:
<div class="iframe-hide" show-if-true="includeMenu">
And then a directive that will check whether includeMenu is true. I wrote it here:
export class showIfTrueDirective {
static $inject = ["$", "$rootScope"];
static $rootScope: any;
public static build($, $rootScope) {
var directive: ng.IDirective = {
link: (scope, element, attributes: any) => {
var itemToShow = attributes["showIfTrue"];
// this correctly prints "includeMenu"
// grab the data from current state?. If includeMenu == true then show element, otherwise hide element
}
};
return directive;
}
}
if I hook that up:
.directive("showIfTrue", ["$", "$rootScope", (r, s) => { return ShowIfTrueDirective.build(r,s); }])
If I manage to grab the scope data then this might work but this is my first week using Anglular and not entirely sure what I'm doing. Is there a better solution for this scenario?
I managed to solve it:
export class NgHideDirective {
static $inject = ["$rootScope"];
static $rootScope: any;
public static build($rootScope) {
var directive: ng.IDirective = {
link: (scope, element, attributes: any) => {
var itemToHide = attributes["ngHide"];
$rootScope.$on('$stateChangeStart',
(event, toState) => {
if (toState.data.hasOwnProperty(itemToHide)) {
element.hide();
} else {
element.show();
}
});
}
};
return directive;
}
}
So if we now do this on an element:
<div class="iframe" ng-hide="hideMenu">
And this on the state:
.state("deposit.x.rejected", {
url: "/rejected",
templateUrl: "app/deposit/templates/x/rejected.html",
data: { hideDepositMenu: null }
Then the div will be hidden.
However this doesn't work when page is refreshed for some reason.

TypeScript + AngularJS directive: cannot call 'new' on class

I have some issue with declaring a class. I have the following code:
module Directive.toast{
import ToastService = Toaster.ToasterService;
'use strict';
interface IToastScope extends ng.IScope {
message:string;
display:boolean
}
class ToastDirective implements ng.IDirective {
templateUrl= '/toast.html';
restrict= 'AE';
replace= true;
scope = {
};
public message:any;
public display:boolean;
constructor(public Toast:ToastService, public $timeout:angular.ITimeoutService) {
}
link:ng.IDirectiveLinkFn = (scope:IToastScope, element:ng.IAugmentedJQuery, attributes:ng.IAttributes) => {
scope.message = this.Toast.message.text;
this.$timeout(function() {
this.message.display= false;
element.remove();
}, 5000);
};
static factory():ng.IDirectiveFactory {
return () => new ToastDirective();
}
}
var app = AppModule.getModule();
app.directive('toast', ToastDirective.factory());
}
Which gives me an error:
error TS2346: Supplied parameters do not match any signature of call target.
However, if I delete the injections from the constructor, it will pass through the compiler - but I do need to inject those dependencies, to use them in the linking functions to the directive. Any ideas?
I would do something like this;
export var ToastDirective = (public Toast:ToastService, public $timeout:angular.ITimeoutService) : angular.IDirective =>
{
return {
templateUrl: '/toast.html',
restrict: 'AE',
replace: true,
scope: {
message: "=",
display: "="
},
link: (scope: IToastScope, element:angular.IAugmentedJQuery, attributes: angular.IAttributes) => {
scope.message = Toast.message.text;
$timeout(()=> {
scope.message.display=false;
element.remove();
}, 5000);
}
}
ToastDirective.$inject = ["ToastService", "$timeout"];
var app = AppModule.getModule();
app.directive('toast', ToastDirective);
}

Angularjs+Typescript directive implementing $compile

I am having issues injecting $compile in the following directive.
export class Element {
public link(scope:dirScopeInterface, element:any, attrs:ng.IAttributes, formCtrl:ng.IFormController) {
var attr = this.arrayJoiner(scope.standard, scope.attrs || {}, scope.ignore || {});
element.html(this.compiler(attr));
$compile(element.contents())(scope);
}
}
At the moment it is throwing an $compile is undefined error. I have tried using
static $inject = ['$compile'];
But it disappears from the transpiled script for some reason.
Here is the full code used.
Include the static $inject and a constructor:
export class Element {
// $compile can then be used as this.$compile
constructor(private $compile: ng.ICompileService){};
public link(scope:dirScopeInterface, element:any, attrs:ng.IAttributes, formCtrl:ng.IFormController) {
var attr = this.arrayJoiner(scope.standard, scope.attrs || {}, scope.ignore || {});
element.html(this.compiler(attr));
this.$compile(element.contents())(scope);
}
}
EDIT
To register this directive with angular, this is what I always do (there are multiple solutions):
export class Element implements angular.IDirective {
public static ID = "element";
// This can then be removed:
// static $inject = ["$compile"];
// ..
/**
* The factory function that creates the directive
*/
static factory(): angular.IDirectiveFactory {
const directive = ($compile) => new Element($compile);
directive.$inject = ["$compile"];
return directive;
}
}
and to register:
angular.module("myModule" [])
.directive(Element.ID, Element.factory());
So I found a way to get it to work but it is not as elegant as I would have liked.
angular.module('formDirectives', [], function($compileProvider){
$compileProvider.directive('catElement', ($compile) => {
return new Element($compile);
});
})
my implementation for jQuery Wizard Steps with AngularJS + Typescripts
It should work well for other $compile function too.
AppDirective.ts
export class stepwizard implements ng.IDirective {
constructor() {
console.log("construtor step wizard directive");
}
link($scope: ng.IScope, $element: JQuery, attributes: ng.IAttributes, ngModel: ng.INgModelController, ctrl: any) {
console.log("start step wizard link function");
$element.wrapInner('<div class="steps-wrapper">');
const steps = $element.children(".steps-wrapper").steps({
headerTag: "h3",
bodyTag: "section",
transitionEffect: "slideLeft",
autoFocus: true
});
const compiled = ctrl.$compile(steps);
}
public static Factory() {
var directive = () => {
return new stepwizard();
};
directive.$inject = ['$compile'];
console.log("initial step wizard");
return directive;
}
}
AppController.ts
export class pageController{
// your declaraction
static $inject: Array<string> = [
$compile',
];
constructor(
$compile: ng.ICompileService,
) {
// your constructor declaraction
}
HTML
// sample take from official website
<div stepwizard>
<h3>Keyboard</h3>
<section>
<p>Try the keyboard navigation by clicking arrow left or right!</p>
</section>
<h3>Effects</h3>
<section>
<p>Wonderful transition effects.</p>
</section>
<h3>Pager</h3>
<section>
<p>The next and previous buttons help you to navigate through your content.</p>
</section>
</div>
directives.stepwizard = <namespace>.directives.stepwizard.Factory();
app.directive(directives);

Resources