Angular 2: Best Practice for routing properties of a subcomponent

182 views
Skip to first unread message

JM

unread,
May 3, 2016, 11:41:10 PM5/3/16
to AngularJS
What is the best practice for routing input/output properties of a subcomponent including the event logic in angular 2?
The following solution is the only way I found so far.

@Component({
    selector: "clock",
template: `
<timer [(seconds)]="seconds" [(minutes)]="minutes"></timer>
        `
})
export class Clock{

  _seconds:number = 0;
@Input("seconds")
set seconds(v:number) {
this._seconds = v;
this.secondsChange.emit(v);
}
get seconds() {
return this._seconds ;
}
@Output("secondsChange") secondsChange:EventEmitter<number> = new EventEmitter();

  _minutes:number = 0;
@Input("minutes")
set minutes(v:number) {
this._minutes= v;
this.minutesChange.emit(v);
}
get minutes() {
return this._minutes;
}
@Output("minutesChange") minutesChange:EventEmitter<number> = new EventEmitter();

}

Now I can detect Changes of the subcomponent <timer> and can pass the event to the component <clock>.
But, it looks wrong. Especially if you use a lot of properties.
Is there a shorter or recommended way?

Best

Jan
 

Sander Elias

unread,
May 4, 2016, 2:01:59 AM5/4/16
to AngularJS
Hi Jan,

With my limited experience of NG2, I think this is the only way. But you are right, it does not feel right. This throws out DRY altogether. 
What came to mind was writing a 'proxy' decorator, that handles all this in a single line of code. However, this takes care of the symptoms, and I feel I need to dig a bit deeper to see if there is a cure.

Regards
Sander

JM

unread,
May 4, 2016, 1:09:30 PM5/4/16
to AngularJS
Hi Elias,

thanks for you fast reply. At least I am relieved now, to doubt my own results.
I will check out, your advice to think about using a "proxy" decorator.

Regards,

Jan

JM

unread,
May 4, 2016, 5:11:14 PM5/4/16
to AngularJS
Hi Elias,

what I have so far is.
 
 
@Component({
selector: "clock",
template: `
<timer [(seconds)]="seconds" [(minutes)]="minutes"></timer>
`
})
export class Clock{

    @InAndOut
public seconds:number;
    @Output("secondsChange") secondsChange:EventEmitter<number> = new EventEmitter();


    @InAndOut
public minutes:number;
    @Output("minutesChange") minutesChange:EventEmitter<number> = new EventEmitter();

}


And as decorator definiton

import {EventEmitter} from "angular2/core";

export function InAndOut(target:any, name:any) {

// property value
var _val = this[name];

// property getter
var getter = function () {
return _val;
};

// property setter
var setter = function (newVal:any) {
_val = newVal;
// emit
if( this[name + "Change"] ) {
this[name + "Change"].emit(newVal);
}
};

// Delete property.
if (delete this[name]) {

// Create new property with getter and setter
Object.defineProperty(target, name, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});

}

}

Is there a chance to include the creation of secondsChange with the @Output decorator from inside the @InAndOut decorator.
So I could reduce the lines:


@InAndOut public seconds:number = 0;
@Output("secondsChange") secondsChange = new EventEmitter();


to:


@InAndOut public seconds:number = 0;



Regards,

Jan



Am Mittwoch, 4. Mai 2016 08:01:59 UTC+2 schrieb Sander Elias:

Zlatko Đurić

unread,
May 5, 2016, 2:22:30 AM5/5/16
to AngularJS


On Wednesday, May 4, 2016 at 11:11:14 PM UTC+2, JM wrote:

Is there a chance to include the creation of secondsChange with the @Output decorator from inside the @InAndOut decorator.
So I could reduce the lines:


@InAndOut public seconds:number = 0;
@Output("secondsChange") secondsChange = new EventEmitter();


to:


@InAndOut public seconds:number = 0;





Hi Jan,

Without actually trying anything, I would say that your @InAndOut type should not be a number, but rather an event emitter if you want it to emit values (e.g. @InAndOut public seconds:EventEmitter<number>  = new EventEmitter();).  But then we lost the @In part, so it proabbly would not merge the two lines into one.

With a disclaimer on lack of my expertise here, I would say if you want something to be shared among multiple components in NG2, that it goes into its own component/service. So, if somebody besides the "Clock" here wanted to know about "Timer.seconds", they'd have to listen to timer directly. But then you can't wrap that whole functionality around the "Clock" nicely, so that external components don't need to know, so we're back at square 1. An interesting problem.

In any case, the @InAndOut looks like a great deal of help there with this kind of work.

Sander Elias

unread,
May 5, 2016, 11:38:16 AM5/5/16
to AngularJS
Hi Jan,

I think there is, you can import the @output decorator in your own decorator, and set it up from there. Might hurt testability of the decorator a bit, and it overrules the injector. However, I think this is fair game in this case :)

Regards
Sander

JM

unread,
May 5, 2016, 2:26:27 PM5/5/16
to AngularJS
Hi Elias, Hi Zlatko,

I have a first solution.


"use strict";

import {EventEmitter} from "angular2/core";
import {Output} from "angular2/core";

export function InAndOut(target:any, name:any) {


    ///// CREATE GETTER UND SETTER FOR PROPERTY

var descriptor:PropertyDescriptor = Object.getOwnPropertyDescriptor(target, name);
if (descriptor) {

// f.i. handle an previous decorator for the property

Object.defineProperty(target, name, {
get: function ():any {
return descriptor.get.call(this);
},
set: function (newValue:any) {
descriptor.set.call(this, newValue);
// emit
this[name + "Change"].emit(newValue);
            },
enumerable: true,
configurable: true
});

    } else {

// store property
var _val:any;

// Create new property
Object.defineProperty(target, name, {
get: function ():any {
return _val;
},
set: function (newValue:any) {
_val = newValue;
// emit
this[name + "Change"].emit(newValue);
            },
enumerable: true,
configurable: true
});

}

    ///// CREATE EMITTER FOR PROPERTY

// emitter
var _emitter = new EventEmitter();

// Create new emitter property
Object.defineProperty(target, name + "Change", {
get: function () {
return _emitter;
},
set: function (newVal:any) {
_emitter = newVal;
        },
enumerable: true,
configurable: true
});

    // create @Output decorator for the emitter
Reflect["decorate"]([
Output()
], target, name + "Change");


}


With this decorator I can now write


@InAndOut seconds:number = 0;



The decorator function is a bit more complicated now. But now you can combine the decorator with other decorators, like:


@logProperty
@InAndOut
test:number = 111;


Still, there are some questions:

1) I recognized that I get an error if I write Reflect.decorate(...) so I cheated with Reflect["decorate"](...). Do you have any idea what I am doing wrong?

2) I am wondering, why this decorator is not part of the angular 2, while it is so easy to write. Is there a good reason, why I should avoid using @InAndOut?

Thanks for your suggestions guys. 

Regards,

Jan

Sander Elias

unread,
May 6, 2016, 2:36:54 AM5/6/16
to AngularJS
Hi Jan,

As you are already in a decorator, do you really need the call to reflect.decorate at all?
I would have gone for:

output(target, name+'Change')

But as I'm not that experienced with NG2, that might be a shortcut that has unexpected features ;)

Regards
Sander

JM

unread,
May 6, 2016, 10:45:15 AM5/6/16
to AngularJS
Hi Elias,

thanks for your suggestion. Your right, it seems the logic way. 


Output(target, name);


But it seems like you can not call the decorator function Output() directly.
I get error-messages:

IDE says: "Invalid number of argument, expected 0...1"
compiler says: " error TS2346: Supplied parameters do not match any signature of call target. " 

Maybe cause its part of an DecoratorFactory?

So I still stuck with:

    // create @Output decorator for the emitter
Reflect["decorate"]([
Output()
], target, name + "Change");


Regards,

Jan

JM

unread,
May 6, 2016, 3:01:08 PM5/6/16
to AngularJS
Hi Elias, Hi Zlatko,

thanks for the leading suggestions. I really appreciate your help.
I think the new decorator @InAndOut is ready.

"use strict";

import {EventEmitter} from "angular2/core";
import {Output,Input} from "angular2/core";

export function InAndOut(target:any, name:any) {

    ///// CREATE EMITTER FOR PROPERTY /////

// emitter
var _emitter = new EventEmitter();
// Create new emitter property
Object.defineProperty(target, name + "Change", {
        value: _emitter,
        enumerable: true,
configurable: true
});
    // create @Output decorator for the emitter
    Output()(target, name + "Change");


///// CREATE GETTER UND SETTER FOR PROPERTY /////

var descriptor:PropertyDescriptor = Object.getOwnPropertyDescriptor(target, name);
if (descriptor) {

        // handle an previous descriptor (getter and setter)
        Object.defineProperty(target, name, {
get: function ():any {
return descriptor.get.call(this);
},
set: function (newValue:any) {
descriptor.set.call(this, newValue);
// emit
                this[name + "Change"].emit(descriptor.get.call(this));

},
enumerable: true,
configurable: true
});

} else {

        // first descriptor
// store property value
        var _val:any;
// Create new property
Object.defineProperty(target, name, {
get: function ():any {
return _val;
},
set: function (newValue:any) {
_val = newValue;
// emit
                _emitter.emit(newValue);
            },
enumerable: true,
configurable: true
});

}
    // create @Input decorator
Input()(target, name);


}



All my tests seem to run smoothly. 
So, I still ask myself:

Why is an similiar decorator like @InAndOut not part of the angular 2? Is there a good reason?

I remember I read in an article, that there was a lot of discussions about two-way-bindind, but I didnt get into this topic.
Reply all
Reply to author
Forward
0 new messages