Directive to wrap html element in hyperlink - angularjs

I'd like to write own directive which can wrap any html element in hyperlink using the passed parameter, so for example:
<button myDirective="parameter">...</button>
will have following effect
<button>...</button>
I'm beginner in AngularJS. Unfortunately I didn't find any helpful tutorial for making this in typescript.
I created sth like that:
export default class LinksHelperDirective implements ng.IDirective {
public static Name = "kb-link";
public restrict = "A";
public urlTemplate = "";
constructor(private readonly $parse: ng.IParseService) {
}
public static Factory() : any {
const directive = ($parse: ng.IParseService) => {
return new LinksHelperDirective($parse);
};
directive["$inject"] = ["$parse"];
return directive;
}
link = (scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes,
ngModel: ng.INgModelController) => {
const linkId = this.$parse(attrs["kb-link"])(scope);
const wrapper = angular.element('');
element.wrap(wrapper);
};
}
But unfortunately it doesn't work... even constructor is not called. I registered directive in index file like below:
module.directive(LinksHelperDirective.Name, LinksHelperDirective.Factory(), ["$parse"]);
and in html file:
<button kb-link="1234">Help</button>
Anybody knows what's wrong with that?

You need to use ng-transclude directive for that
class WrapDirective {
restrict = 'E';
transclude = true;
scope = { someLink: '<' };
template: '<a ng-href="url/{{ someLink }}"><ng-transclude></ng-transclude></a>';
}
But you need to wrap your element like <wrap-directive some-link="$ctrl.link"><button>...</wrap-directive>

Related

How to dynamically bind a custom AngularJS modal?

For an educational side-project I am working on, I want to avoid using AngularJS Material Design, UI Bootstrap, or any custom libraries that provide modal functionality.
However, I've hit a snag. I've created a service that is supposed to manage and dynamically create modals. It provides an open function that accepts a spec object, which it then reproduces in the DOM.
What this code actually does:
1. The modal is correctly appended to the DOM.
2. The modal controller's $onInit function fires.
What this code does not do:
1. Bind the $ctrl.message property in the markup to the instance of the controller that we know starts.
Normally, I would ask my question after providing code, however there's a good bit of code required to reproduce this problem (it's below, sans some AngularJS boilerplate.) As that's the case, though, here's my question:
In what way can I get the modals being spun off by this service to properly bind their contents to their given controller?
What I've tried:
As you can see in ModalService.bindModalDiv, I've tried a few avenues of thought, mostly using $compile. Yet, $compile and the resulting link function don't actually seem to be binding the new DOM elements to Angular.
I've tried using $controller to explicitly bind the new scope being generated to the someModalCtrl being instantiated, but that doesn't seem to help at all.
Because I can hit breakpoints on the someModalCtrl, and see the console.log message I used as a sanity check, I think I'm misunderstanding how exactly I'm supposed to bind the new DOM elements to Angular. I'm sure I'm missing something basic that I've managed to forget about or disregard, somehow.
One more note:
I'm sure my problems with getting the modal to bind to AngularJS properly aren't the only problems present. Please remember, I'm doing this partially as a learning excersize; if y'all can help me figure out my modal problem, I'll keep on doing my due diligence and hunting down the flaws I've doubtless built into this approach. Therefore, if you see something that's not a modal problem, it's OK to draw my attention to it, but I won't rewrite the question to fix whatever you find - unless it's absolutely essential that I do. As an example - I know that ModalService.open has some issues in how I'm implementing the promise setup. A $rootScope.$watch is probably more reasonable.
modalSvc.ts:
export interface IModalSpecObject {
parent?: string | Element | JQuery;
templateURL: string
controller: string;
controllerAs?: string;
data: object;
}
export class ModalInstance {
public isOpen: boolean = true;
public returnData: object = null;
public element: JQLite = null;
public $parent: JQuery = null;
public constructor(
public specObject: IModalSpecObject
) {
}
public close(returnData: object): void {
if (this.element)
this.element.remove();
this.isOpen = false;
this.returnData = returnData;
}
}
export class ModalService {
public pollRate: number = 250;
public instance: ModalInstance = null;
public static $inject: string[] = [
'$q', '$rootScope', '$compile', '$controller'
];
public constructor(
public $q: ng.IQService,
public $rootScope: ng.IRootScopeService,
public $compile: ng.ICompileService,
public $controller: ng.IControllerService
) {
}
public open(specObject: IModalSpecObject): ng.IPromise<{}> {
if (this.instance && this.instance.isOpen)
this.instance.close(null);
this.instance = new ModalInstance(specObject);
const $parent: JQuery = this.setParent(specObject);
const modalDiv: JQLite = this.buildModal(specObject);
this.bindModalDiv(modalDiv, $parent);
const result: ng.IPromise<{}> = this.$q((resolve) => {
setInterval(() => {
if (!this.instance.isOpen) {
resolve(this.instance.returnData);
}
}, this.pollRate);
});
return result;
}
private buildModal(specObject: IModalSpecObject): JQLite {
const modalDiv: JQLite = angular.element('<div/>');
modalDiv.addClass('modal');
const $modalPanel: JQuery = $('<div/>');
$modalPanel.addClass('modal-panel');
// Inject HTML template...
$modalPanel.load(specObject.templateUrl);
// Set up the angular controller...
const controllerAs: string = specObject.controllerAs
? specObject.controllerAs
: '$ctrl';
$modalPanel.attr('ng-controller', `${specObject.controller} as ${controllerAs}`);
modalDiv.append($modalPanel);
this.instance.element = modalDiv;
return modalDiv;
}
private setParent(specObject: IModalSpecObject): JQuery {
let $parent: JQuery;
if(!specObject.parent)
$parent = $(document);
else if (typeof specObject.parent === "string"
|| specObject.parent instanceof Element)
$parent = $(specObject.parent);
else if (specObject.parent instanceof jQuery)
$parent = specObject.parent;
else
$parent = $(document);
this.instance.$parent = $parent;
return $parent;
}
// !!!! !!!! I suspect this is where my problems lie. !!!! !!!!
private bindModalDiv(modalDiv: JQLite, $parent: JQuery): void {
const newScope: ng.IScope = this.$rootScope.$new(true);
// Try #1: Bind generated element to parent...
//$parent.append(this.$compile(modalDiv)(newScope));
// Try #1a: Generate bindings, then append to parent...
//const element: JQLite = this.$compile(modalDiv)(newScope);
//$parent.append(element);
// Try #2: Bind element to parent, then generate ng bindings...
//$parent.append(modalDiv);
//this.$compile(modalDiv)(newScope);
// Try #3: Well, what if we bind a controller to the scope?
const specObject: IModalSpecObject = this.instance.specObject;
const controllerAs: string = specObject.controllerAs
? specObject.controllerAs
: '$ctrl';
this.$controller(`${specObject.controller} as ${controllerAs}`, {
'$scope': newScope
});
const element = this.$compile(modalDiv)(newScope);
$parent.append(element);
}
}
angular
.module('app')
.service('modalSvc', ModalService);
SomeController.ts:
SomeController.ts pretty much just controls a button to trigger the modal's appearance; I've not included the markup for that reason.
export class SomeController {
public static $inject: string[] = [ 'modalSvc' ];
public constructor(
public modalSvc: ModalService
) {
}
public $onInit(): void {
}
public openModal(): void {
const newModal: IModalSpecObject = {
parent: 'body',
templateUrl: '/someModal.html',
controller: 'someModalCtrl',
data: {
'message': 'You should see this.'
}
};
this.modalSvc.open(newModal)
.then(() => {
console.log('You did it!');
});
}
}
angular.module('app').controller('someCtrl', SomeController);
someModal.html:
<div class="modal-header">
Important Message
</div>
<!-- This should read, "You should see this." -->
<div class="modal-body">
{{ $ctrl.message }}
</div>
<!-- You should click this, and hit a breakpoint and/or close the modal. -->
<div class="modal-footer">
<button ng-click="$ctrl.close()">Close</button>
</div>
someModal.ts:
export class SomeModalController {
public message: string = '';
public static $inject: string[] = [ 'modalSvc' ];
public constructor(
public modalSvc: ModalService
) {
}
public $onInit(): void {
console.log('$onInit was triggered!');
this.message = this.modalSvc.instance.specObject.data['message'];
}
public close(): void {
this.modalSvc.instance.close(null);
}
}
angular
.module('app')
.controller('someModalCtrl', SomeModalController);
I figured out where I went wrong - I needed to use $().load()'s callback. JQuery load is asynchronous, which meant that $compile was working correctly; however, the HTML in my modal partial wasn't loaded by the time $compile had done its job, thus the unbound HTML.
A slight modification of my ModalService got around this, though.
Revised fragment of ModalSvc.ts:
// This is just a convenience alias for void functions. Included for completeness.
export type VoidFunction = () => void;
// ...
public open(specObject: IModalSpecObject): ng.IPromise<{}> {
if (this.instance && this.instance.isOpen)
this.instance.close(null);
this.instance = new ModalInstance(specObject);
const $parent: JQuery = this.setParent(specObject);
// open already returned a promise before, we just needed to return
// the promise from build modal, which in turn sets up the true
// promise to resolve.
return this.buildModal(specObject)
.then((modalDiv: JQLite) => {
this.bindModalDiv(modalDiv, $parent);
const result: ng.IPromise<{}> = this.$q((resolve) => {
// Also, side-note: to avoid resource leaks, always make sure
// with these sorts of ephemeral watches to capture and release
// them. Resource leaks are _no fun_!
const unregister: VoidFunction = this.$rootScope.$watch(() => {
this.instance.isOpen
}, () => {
if (! this.instance.isOpen) {
resolve(this.instance.returnData);
unregister();
}
});
});
return result;
});
}
private buildModal(specObject: IModalSpecObject): ng.IPromise<{}> {
const modalDiv: JQLite = angular.element('<div/>');
modalDiv.addClass('modal');
this.instance.element = modalDiv;
const $modalPanel: JQuery = $('<div/>');
$modalPanel.addClass('modal-panel');
// By wrapping $modalPanel.load in a $q promise, we can
// ensure that the modal is fully-built before we $compile it.
const result: ng.IPromise<{}> = this.$q((resolve, reject) => {
$modalPanel.load(specObject.templateUrl, () => {
modalDiv.append($modalPanel);
resolve(modalDiv);
});
});
return result;
}
private setParent(specObject: IModalSpecObject): JQuery {
let $parent: JQuery;
if(!specObject.parent)
$parent = $(document);
else if (typeof specObject.parent === "string"
|| specObject.parent instanceof Element)
$parent = $(specObject.parent);
else if (specObject.parent instanceof jQuery)
$parent = specObject.parent;
else
$parent = $(document);
this.instance.$parent = $parent;
return $parent;
}
private bindModalDiv(modalDiv: JQLite, parent: JQLite): void {
// parent should be a JQLite so I can use the injector() on it.
parent.injector().invoke(['$rootScope', '$compile', ($rootScope, $compile) => {
const newScope: ng.IScope = $rootScope.$new(true);
this.$controller(this.getControllerAsString(), {
'$scope': newScope
});
const element: JQLite = $compile(modalDiv)(newScope);
parent.append(element);
}]);
}
private getControllerAsString(): string {
const specObject: IModalSpecObject = this.instance.specObject;
const controllerAs: string = specObject.controllerAs
? specObject.controllerAs
: '$ctrl';
return `${specObject.controller} as ${controllerAs}`;
}
I figured this out by going back and doing the step-by-step engineering. I first ensured that $compile was working by creating an element whose contents were {{ 2 + 2 }}, compiling it, then injecting it. When I saw 4 added to my page, I knew that the compile-then-inject aspect of the program worked just fine.
From there, I started building up the modal's construction, and found it working flawlessly...up until I got to jQuery load. When I read the documentation, I saw the error of my ways.
TL;DR: Read the friendly manual!

AngularJS, TypeScript, Component Architecture weird behavior

I've jumped for a while from Angular 6 to Angular JS and I'm trying to code in Component Architecture. The problem is - I'm trying to use ng-show="somevariable" to hide / show a div.
AppRootModule:
export const appRootModule: IModule = module('datawalk', ['asyncFilter'])
.component('appRoot', new AppRootComponent())
.component('postModal', new PostModalComponent())
.service('componentsDataService', ComponentDataService);
PostModalComponent:
export class PostModalComponent {
public template: string;
public controller: Injectable<IControllerConstructor>;
public controllerAs: string;
constructor() {
this.controller = PostModalController;
this.controllerAs = 'postM';
this.template = PostModalTemplateHtml;
}
PostModalController
export class PostModalController implements IController {
public modalVisible: boolean;
/../
constructor(componentsDataService: ComponentDataService) {
/../
this.modalVisible = false;
/../
this.$cds.getModalData().subscribe((data: any) => {
if (data.showMod === true) {
this.modalOpen(data);
}
});
}
public $onInit = async (): Promise<void> => {};
public modalOpen(post: any): void {
console.error(this.modalVisible); // false
this.modalVisible = true;
console.error(this.modalVisible); // true
/../
}
And the template:
<div class="modal" ng-show="modalVisible">
<div class="modal-body">
/../
</div>
</div>
Anyone can tell me what I'm doing wrong?
console logs shows that the variable changes, but nothing happen, modal div is still hidden.
OK. So I've figured it out.
That's not a problem of code, but the problem is inside my IDE.
WebStorm has an issue - it does not recognize a controllerAs property in HTML templates of AngularJS in component architecture.
Also tslint works faulty.
I don't recommend to use that with AngularJS with component architecture.
Vs Code works perfect with that.

Typescript private and protected members exposed to angular 1.x view

It seems like when combining TS and angular, everything i have on a controller is exposed to the view. In my case, myPrivate will appear on $ctrl.
class MyController extends BaseController implements SomeInterface {
private myPrivate: string = 'myPrivateString';
}
Is there any workaround around this issue?
It's pretty obvious why when you look at the generated javascript.
var MyController = (function (_super) {
__extends(MyController, _super);
function MyController() {
_super.apply(this, arguments);
this.myPrivate = 'myPrivateString';
}
return MyController;
}(BaseController));
Your private property ends up as any other property on your controller.
You can see the full transpilation here.
A solution would be to have a parameterized base controller able to set something like a view model for the view to use, instead of the regular $ctrl.
It would look something like this:
class BaseController<T> {
protected scope;
protected viewModel: T;
constructor(scope: any, modelType: { new (): T; }) {
this.scope = scope;
this.viewModel = new modelType();
this.scope["viewModel"] = this.viewModel;
}
}
class MyParticularViewModel {
public somethingForTheView: string;
}
class MyController extends BaseController<MyParticularViewModel> implements SomeInterface {
private myPrivate: string = 'myPrivateString';
constructor(scope) {
super(scope, MyParticularViewModel);
}
}
In the view you can then use the viewModel property to access the needed properties.
I have used this in a project in practice and it worked out pretty well. You can see a starter template that I used here for more info.

Angular 1.5 components : Component based application architecture

According to the Angular 1.5 documentation components should only control their own View and Data.
Instead of changing properties of objects passed to the component, a component should create an internal copy of the original data and use callbacks to inform the parent component when this copy has changed.
In this plunk I created a small demo illustrating my problem.
interface IStudent {
id: number,
name: string;
}
/* SERVICE: StudentService */
public class StudentsService {
static $inject = ['$q'];
constructor(private $q: ng.IQService) {
}
public getStudents() : ng.IPromise<IStudent[]> {
return this.$q.when([
{ id: 1, name: 'Adam' },
{ id: 2, name: 'Ben' }
}]);
}
}
/* COMPONENT: student */
class StudentsComponent implements ng.IComponent {
public template = `<student-list on-selected="$ctrl.onSelected(student)"></student-list>
<edit-student student="$ctrl.student" ng-show="$ctrl.student" on-changed="$ctrl.copyChanged(copy)"></edit-student>`;
public controller = StudentsController;
}
class StudentsController {
private student: IStudent;
protected onSelected(student: IStudent) {
this.student = student;
}
protected copyChanged(copy: IStudent) {
this.student.name = copy.name;
}
}
/* COMPONENT: student-list */
class StudentListComponent implements ng.IComponent {
public template = '<ul><li ng-repeat="student in $ctrl.students"><a ng-click="$ctrl.onClick(student)">{{ student.name }}</a></li></ul>';
public controller = StudentListController;
public bindings : any = {
onSelected: '&'
}
}
class StudentListController {
protected students: IStudent[];
static $inject = ['studentsService'];
constructor(private studentsService: StudentsService) {
}
public $onInit() {
this.studentsService.getStudents().then(data => this.students = data);
}
protected onClick(student: IStudent) {
this.onSelected({ student: student });
}
}
/* COMPONENT: edit-student */
class EditStudentComponent implements ng.IComponent {
public template = `<form class="form">
<div class="input-group">
<label for="#" class="control-label">Original</label>
<input type="text" class="form-control" ng-model="$ctrl.student.name" readonly>
</div>
</form>
<form class="form">
<div class="input-group">
<label for="#" class="control-label">Copy</label>
<input ng-change="$ctrl.changed()" type="text" class="form-control" ng-model="$ctrl.copy.name">
</div>
</form>`;
public controller = EditStudentController;
public bindings :any = {
student: '<',
onChanged: '&'
};
}
class EditStudentController {
protected copy: IStudent;
public $onInit() {
console.log('EditStudentComponent.$onInit', this.student);
}
public $onChange() {
console.log('EditStudentComponent.$onChange', this.student);
this.copy = angular.copy(this.student);
}
protected changed() {
console.log('EditStudentController.changed', this.copy);
this.onChanged({ copy: this.copy });
}
}
/* Bootstrap */
angular
.module('app', [])
.component('students', new StudentsComponent())
.component('studentList', new StudentListComponent())
.component('editStudent', new EditStudentComponent())
.service('studentsService', StudentsService)
;
angular.bootstrap(document, ['app']);
I have a list iterating over students. When the user selects a student, a textbox is shown in which the user can change the name of the student. Whenever the name changes, this change is propagated to the parent component which updates the list.
The problem is that after selecting a student in the list, the edit-user component is not initialized and still shows the name of the copy created when the component was created (null).
Can someone tell me how to fix this plunk such that, when clicking a student in the list, the edit component gets initialized with a copy of the selected student?
Edit: changed the plunk, as I accidentally removed the script tag instead of the style tag.
I thought this plunk represented my problem, but alas it didn't. The plunk didn't work because I implemented $onChange instead of $onChanges. I fixed the plunk such that it works as expected.
The cause of my original problem was a completely different one. In my business application I used another component with a ng-transclude directive around my edit component, like this:
<modal-editor>
<edit-student data="$ctrl.data">
<edit-student>
</modal-editor>
As the edit-student component was defined in the isolated scope of the modal-editor component, it didn't receive any changes made to the data variable in the outer scope (but somehow it could still access the data from this outer scope).
After modifying the modal-editor component such that it passed the data to the child component, everything worked as expected:
<modal-editor data="$ctrl.data">
<edit-student data="$ctrl.data">
<edit-student>
</modal-editor>

cloning an element in angular, and tying up the attached directives

I have built a function that correctly appends a new row to a table when the last row gets the focus.
This works successfully, but the directive is no longer triggering the cloned row
How do I fix the clone so that the elements are added with directives attached.
I need to trigger the directive link after the clone is complete.
each row has a directive attached.
<tr add-table-row-empty>
<td>...
And this is the directive.
module Panda {
#directive('$log', '$compile')
export class AddTableRowEmpty implements ng.IDirective
{
public restrict: string = "A";
constructor(public $log: ng.ILogService, public $compile: ng.ICompileService)
{
}
public link: Function = ($scope: any, element: angular.IAugmentedJQuery, attrs: angular.IAttributes) => {
var inputs = $('input', element);
inputs.on('focus', () => this.addIfLastRow(element));
}
private addIfLastRow(element: angular.IAugmentedJQuery) {
if (!($(element).is(':last-child')))
return;
this.addRow(element);
}
private addRow(element: angular.IAugmentedJQuery)
{
// this should do a deep clone including all events etc.
var clone = element.clone(true, true);
$("input", clone).each((i, _) => $(_).val(""));
element.after(clone);
clone
.hide()
.fadeIn(1000);
}
}
panda.directive("addTableRowEmpty", <any>AddTableRowEmpty);
}

Resources