I have the following declaration of a annotation in Angular:
class ModalContainer {
public destroy: Function;
public closeModal() {
this.destroy();
}
}
export function Modal() {
return function (target: any) {
Object.assign(target.prototype, ModalContainer.prototype);
};
}
I want to use the annotation inside a component:
#Component({
selector: 'my-component',
templateUrl: 'my-component.template.html',
styleUrls: ['my-component.style.scss']
})
#Modal()
export class MyComponent implements OnInit {
private EVENT_SAVE = 'EVENT_SAVE';
private EVENT_CANCEL = 'EVENT_CANCEL';
private buttonClick(event: any, eventType: string){
if (eventType === this.EVENT_CANCEL){
this.closeModal();
} else if(eventType === this.EVENT_SAVE){
this.closeModal();
}
}
}
The problem is, TypeScript is not able to compile, since this method is not known during compile time. However, when I'm using the same method call inside the template, then it works. That means, that the the prototype was assigned.
The compiler shows this error message:
ERROR in [at-loader] ./src/main/webapp/ui/src/my-component.component.ts:128:18
TS2339: Property 'closeModal' does not exist on type 'MyComponent'.
Does anyone know, how I cann solve this?
By adding this line the compiler will not complain anymore and it works.
private closeModal: Function;
The class then looks like this:
#Component({
selector: 'my-component',
templateUrl: 'my-component.template.html',
styleUrls: ['my-component.style.scss']
})
#Modal()
export class MyComponent implements OnInit {
private EVENT_SAVE = 'EVENT_SAVE';
private EVENT_CANCEL = 'EVENT_CANCEL';
private closeModal: Function;
private buttonClick(event: any, eventType: string){
if (eventType === this.EVENT_CANCEL){
this.closeModal();
} else if(eventType === this.EVENT_SAVE){
this.closeModal();
}
}
}
Related
When passing an argument, the context is lost to the function. How can I bind it in my typescript file?
import { Component } from "#angular/core";
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
name = "Angular";
getName(): string {
return this.name;
}
onAction(cb: Function) {
return cb();
}
}
<p>
{{onAction(getName)}}
</p>
Error: Cannot read property 'name' of undefined
If you make the binding in the template, then this will work.
<p>
{{onAction(getName).bind(this)}}
</p>
But I would like to make the linking in the controller.
I would be grateful for any help!
If I understand you correctly, you can do the binding in the constructor in AppComponent:
export class AppComponent {
name = "Angular";
constructor() { // ***
this.getName = this.getName.bind(this); // ***
} // ***
getName(): string {
return this.name;
}
onAction(cb: Function) {
return cb();
}
}
#Component {
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: 'app.component.css'
}
export class App implements OnChanges, OnInit {
prop = '';
ngOnInit() {
this.prop = 'something';
}
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
}
}
I'm expecting to get the console log from ngOnChanges() when I change the prop property via ngOnInit() method. But it's not working. Any help?
The lifeCycle hook OnChanges is only triggered for an #Input() property when the value is changed in the parent component.
Edit regarding the comments :
In order to listen to a prop changes inside a component, you can change it to a Subject :
prop: Subject<string> = new Subject();
ngOnInit() {
this.prop.next('something');
this.prop.subscribe(value => console.log(value));
}
onClick(value: string) {
this.prop.next(value);
}
Problem
I am trying to add functionality to the built-in NgForm directive by intercepting the onSubmit function in order to prevent double-submission and invalid submission, but I haven't been able to find a way to do so without monkey patching.
Failed Attempt 1: Decorator via Dependency Injection
I didn't really expect this to work with directives since they aren't really "providers", but I tried it anyway (to no avail).
import { Injectable } from '#angular/core';
import { NgForm } from '#angular/forms';
#Injectable()
export class NgFormDecorator extends NgForm {
constructor() {
super(null, null);
}
onSubmit($event: Event): boolean {
// TODO: Prevent submission if in progress
return super.onSubmit($event);
}
}
// Module configuration
providers: [{
provide: NgForm,
useClass: NgFormDecorator
}]
Working Attempt 2: Monkey Patch with Secondary Directive
This works great but is obviously not ideal.
import { Directive, Output, EventEmitter } from '#angular/core';
import { NgForm } from '#angular/forms';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/finally';
import { noop } from 'rxjs/util/noop';
#Directive({
selector: 'form',
exportAs: 'NgFormExtension'
})
export class NgFormExtensionDirective {
private onSubmitBase: ($event: Event) => void;
submitting: boolean;
constructor(private ngForm: NgForm) {
this.onSubmitBase = ngForm.onSubmit;
ngForm.onSubmit = this.onSubmit.bind(this);
}
private onSubmit($event: FormSubmitEvent): boolean {
if (this.submitting || this.ngForm.invalid) {
return false;
}
this.submitting = true;
const result = this.onSubmitBase.call(this.ngForm, $event);
if ($event.submission) {
$event.submission
.finally(() => this.submitting = false)
.subscribe(null, noop);
} else {
this.submitting = false;
}
return result;
}
}
export class FormSubmitEvent extends Event {
submission: Observable<any>;
}
Question
Is there a way to decorate/intercept a built-in directive in Angular 4 without monkey patching?
You can always just override the ngForm selector, and extend the NgForm class:
#Directive({
selector: 'form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]',
})
export class CNgFormDirective extends NgForm {
constructor(
#Optional() #Self() #Inject(NG_VALIDATORS) validators: any[],
#Optional() #Self() #Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) {
super(validators, asyncValidators);
}
onSubmit($event: Event): boolean {
console.log(`I'm custom!`);
return super.onSubmit($event);
}
}
working stack
I'm implemeting dynamic form system in my current project using ANgular 2, and so far is going good but I found the following problem:
I have two components that represent a form control like for example:
#Component({
selector: 'app-text-area',
templateUrl: './text-area.component.html',
styleUrls: ['./text-area.component.css']
})
export class TextAreaComponent implements OnInit {
label: string;
formGroup: FormGroup;
formControlName: string;
constructor(private injector: Injector) { }
ngOnInit() {
this.label = this.injector.get('label');
this.formGroup = this.injector.get('formGroup');
this.formControlName = this.injector.get('formControlName');
}
}
And:
#Component({
selector: 'app-input-text',
templateUrl: './input-text.component.html',
styleUrls: ['./input-text.component.css']
})
export class InputTextComponent implements OnInit{
label: string;
formGroup: FormGroup;
formControlName: string;
constructor(private injector: Injector) { }
ngOnInit() {
this.label = this.injector.get('label');
this.formGroup = this.injector.get('formGroup');
this.formControlName = this.injector.get('formControlName');
}
}
As you see both are identical except for the templateUrl, which is displaying different html elements.
So I would like to refactor the code and to create an abstract component to provide the common attributes and logic, and make then, the child classes to inherit the base class (as I would do when using Java). So I have created this class:
export class AbstractFormControl implements OnInit {
label: string;
formGroup: FormGroup;
formControlName: string;
constructor(private injector: Injector) { }
ngOnInit() {
this.label = this.injector.get('label');
this.formGroup = this.injector.get('formGroup');
this.formControlName = this.injector.get('formControlName');
}
}
And I have make the child classes extend the base class like this:
#Component({
selector: 'app-input-text',
templateUrl: './input-text.component.html',
styleUrls: ['./input-text.component.css']
})
export class InputTextComponent extends AbstractFormControl{
}
However now I'm getting the following error:
Uncaught Error: Can't resolve all parameters for InputTextComponent: (?).
Can someone explain me what's the right way to do this, or what I'm doing wrong?
Angular dependency injection system should know which type has been passed to constructor. When you inherit component such way typescript won't keep information about parameter private injector. You have two options:
1) Duplicate initializing
#Component({
...
})
export class InputTextComponent extends AbstractFormControl{
constructor(injector: Injector) { super(injector);}
}
But in your case you have the same number of parameters in your base and inherited classes and this solution seems redundant because we can omit constructor in our derived class.
We can omit constructor in derived class if we want to only use dependencies from parent class.
So let's say we have parent class like:
abstract class Parent {
constructor(private a: string, private b: number) {}
}
we can extend this class either
class Foo extends Parent {
constructor(a: string, b: number) {
super(a, b);
}
}
or
class Foo extends Parent {
}
because the second option will generate code like
function Foo() {
return _super !== null && _super.apply(this, arguments) || this;
}
Plunker Example
2) Use #Injectable for base class.
#Injectable()
export class AbstractFormControl {
this way typescript will translate the code above into
AbstractFormControl = __decorate([
core_1.Injectable(),
__metadata("design:paramtypes", [core_1.Injector])
], AbstractFormControl);
Plunker Example
and angular reflector can easily read this information
3) Use #Inject() for each of parameters
export class AbstractFormControl implements OnInit {
constructor(#Inject(Injector) private injector: Injector) { }
Is there a way to write a global selfmade mylogger function that I could use in Angular2 typescript project for my services or components instead of console.log function ?
My desired result would be something like this:
mylogger.ts
function mylogger(msg){
console.log(msg);
};
user.service.ts
import 'commons/mylogger';
export class UserService{
loadUserData(){
mylogger('About to get something');
return 'something';
};
};
You could write this as a service and then use dependency injection to make the class available to your components.
import {Injectable, provide} from 'angular2/core';
// do whatever you want for logging here, add methods for log levels etc.
#Injectable()
export class MyLogger {
public log(logMsg:string) {
console.log(logMsg);
}
}
export var LOGGING_PROVIDERS:Provider[] = [
provide(MyLogger, {useClass: MyLogger}),
];
You'll want to place this in the top level injector of your application by adding it to the providers array of bootstrap.
import {LOGGING_PROVIDERS} from './mylogger';
bootstrap(App, [LOGGING_PROVIDERS])
.catch(err => console.error(err));
A super simple example here: http://plnkr.co/edit/7qnBU2HFAGgGxkULuZCz?p=preview
The example given by the accepted answer will print logs from the logger class, MyLogger, instead of from the class that is actually logging.
I have modified the provided example to get logs to be printed from the exact line that calls MyLogger.log(), for example:
get debug() {
return console.debug.bind(console);
}
get log() {
return console.log.bind(console);
}
I found how to do it here: https://github.com/angular/angular/issues/5458
Plunker: http://plnkr.co/edit/0ldN08?p=preview
As per the docs in developers.mozilla,
The bind() method creates a new function that, when called, has its
this keyword set to the provided value, with a given sequence of
arguments preceding any provided when the new function is called.
More information about bind here:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
If you want to use 'console.log' function just in your component you can do this:
import { Component, OnInit } from '#angular/core';
var output = console.log;
#Component({
selector: 'app-component',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
constructor() { }
ngOnInit() { }
printFunction(term: string): void {
output('foo');
}
}
How about using console on your main service, So we can customize and apply console.log conditionally:
myComponent.ts
export class myComponent implements OnInit {
constructor(
private config: GlobalService
) {}
ngOnInit() {
this.config.log('func name',{a:'aval'},'three');
}
}
global.service.ts
#Injectable()
export class GlobalService {
constructor() { }
this.prod = true;
public log(one: any, two?: any, three?: any, four?: any) {
if (!this.prod) {
console.log('%c'+one, 'background:red;color:#fff', two, three, four);
}
}
}
(Note: first parameter should be string in this example);
For toggling console.log ON\OFF:
logger.service.ts:
import { Injectable } from '#angular/core';
#Injectable()
export class LoggerService {
private oldConsoleLog = null;
enableLogger(){
if (this.oldConsoleLog == null) { return; }
window['console']['log'] = this.oldConsoleLog;
}
disableLogger() {
this.oldConsoleLog = console.log;
window['console']['log'] = function () { };
};
}
app.component.ts:
#Component({
selector: 'my-app',
template: `your templ;ate`
})
export class AppComponent {
constructor(private loggerService: LoggerService) {
var IS_PRODUCTION = true;
if ( IS_PRODUCTION ) {
console.log("LOGGER IS DISABBLED!!!");
loggerService.disableLogger();
}
}
}
I created a logger based on the provided information here
Its very basic (hacky :-) ) at the moment, but it keeps the line number
#Injectable()
export class LoggerProvider {
constructor() {
//inject what ever you want here
}
public getLogger(name: string) {
return {
get log() {
//Transform the arguments
//Color output as an example
let msg = '%c[' + name + ']';
for (let i = 0; i < arguments.length; i++) {
msg += arguments[i]
}
return console.log.bind(console, msg, 'color:blue');
}
}
}
}
Hope this helps
type safer(ish) version with angular 4, typescript 2.3
logger.service.ts
import { InjectionToken } from '#angular/core';
export type LoggerService = Pick<typeof console,
'debug' | 'error' | 'info' | 'log' | 'trace' | 'warn'>;
export const LOGGER_SERVICE = new InjectionToken('LOGGER_SERVICE');
export const ConsoleLoggerServiceProvider = { provide: LOGGER_SERVICE, useValue: console };
my.module.ts
// ...
#NgModule({
providers: [
ConsoleLoggerServiceProvider,
//...
],
// ...
my.service.ts
// ...
#Injectable()
export class MyService {
constructor(#Inject(LOGGER_SERVICE) log: LoggerService) {
//...
There is now an angular2 logger component on NPM which supports log levels.
https://www.npmjs.com/package/angular2-logger