My components often start out by having multiple @Input
and @Output
properties. As I add properties, it seems cleaner to switch to a single config
I would say it could be OK to use single config objects for Input
s but you should stick to Output
s at all the time. Input
defines what your component requires from outside and some of those may be optional. However, Output
s are totally component's business and should be defined within. If you rely on users to pass those functions in, you either have to check for undefined
functions or you just go ahead and call the functions as if they are ALWAYS passed within config which may be cumbersome to use your component if there are too many events to define even if the user does not need them. So, always have your Output
s defined within your component and emit whatever you need to emit. If users don't bind a function those event, that's fine.
Also, I think having single config
for Input
s is not the best practice. It hides the real inputs and users may have to look inside of your code or the docs to find out what they should pass in. However, if your Input
s are defined separately, users can get some intellisense with tools like Language Service
Also, I think it may break change detection strategy as well.
Let's take a look at the following example
@Component({
selector: 'my-comp',
template: `
<div *ngIf="config.a">
{{config.b + config.c}}
</div>
`
})
export class MyComponent {
@Input() config;
}
Let's use it
@Component({
selector: 'your-comp',
template: `
<my-comp [config]="config"></my-comp>
`
})
export class YourComponent {
config = {
a: 1, b: 2, c: 3
};
}
And for separate inputs
@Component({
selector: 'my-comp',
template: `
<div *ngIf="a">
{{b + c}}
</div>
`
})
export class MyComponent {
@Input() a;
@Input() b;
@Input() c;
}
And let's use this one
@Component({
selector: 'your-comp',
template: `
<my-comp
[a]="1"
[b]="2"
[c]="3">
</my-comp>
`
})
export class YourComponent {}
As I stated above, you have to look at the code of YourComponent
to see what values you are being passed in. Also, you have to type config
everywhere to use those Input
s. On the other hand, you can clearly see what values are being passed in on the second example better. You can even get some intellisense if you are using Language Service
Another thing is, second example would be better to scale. If you need to add more Input
s, you have to edit config
all the time which may break your component. However, on the second example, it is easy to add another Input
and you won't need to touch the working code.
Last but not least, you cannot really provide two-way bindings with your way. You probably know that if you have in Input
called data
and Output
called dataChange
, consumers of your component can use two-way binding sugar syntax and simple type
<your-comp [(data)]="value">
This will update value
on the parent component when you emit an event using
this.dataChange.emit(someValue)
Hope this clarifies my opinions about single Input
Edit
I think there is a valid case for a single Input
which also has some function
s defined inside. If you are developing something like a chart component which often requires complex options/configs, it is actually better to have single Input
. It is because, that input is set once and never changes and it is better to have options of your chart in a single place. Also, the user may pass some functions to help you draw legends, tooltips, x-axis labels, y-axis labels etc.
Like having an input like following would be better for this case
export interface ChartConfig {
width: number;
height: number;
legend: {
position: string,
label: (x, y) => string
};
tooltip: (x, y) => string;
}
...
@Input() config: ChartConfig;
Are there any downsides to having a single config object as an input to a component, instead of having multiple input and outputs?
Yes, when you want to switch to the onpush change detection strategy, which is often needed in bigger projects to mitigate performance issues caused by too many render-cycles, angular will not detect changes that happened inside your config object.
Will avoiding @Output and EventEmitter like this cause any issues that might catch up to me later?
Yes, if you start to move away from @Output
and in your template directly operate on the config object itself, then you are causing side-effects in your view, which will be the root of hard-to-find bugs in the future. Your view should never modify the data it get's injected. It should stay "pure" in that sense and only inform the controlling component via events (or other callbacks) that something happened.
Update: After having a look at the example in your post again, it looks like you did not mean that you want to directly operate on the input model but pass event emitters directly via the config object. Passing callbacks via @input
(which is what you are implicitly doing) also has it's drawbacks, such as:
The point of having the Input
besides its obvious functionality, is to make your component declarative and easy to understand.
Putting all the configs in one massive object, which will grow definitely (trust me) is a bad idea, for all the above reasons and also for testing.
It's much easier to test a component's behaviour with a simple input
property, rather than supplying a giant confusing object.
You're going backwards and thinking like the way jQuery plugins used to work, where you'd call a function called init
and then you provide a whole bunch of configuration which you don't even remember if you should provide or not, and then you keep copy pasting this unknown and ever-growing object across your components where they probably don't even need them
Creating defaults is extremley easy and clear with simple Input
s whereas it becomes a little bit messy with objects to created defaults.
If you have too many similar Input
, Output
s, you can consider below :
1- You can create a Base
class and put all your Input/Output
s that are similar and then extend all your components from it.
export class Base{
@Input() prop1: number;
@Output() prop1Change = new EventEmitter<number>();
@Input() prop2: number;
@Output() prop2Change = new EventEmitter<number>();
}
@Component({})
export class MyComponent extends from Base{
constructor(){super()}
}
2- If you don't like this, you can use composition and create a reusable mixin
and apply all your Input/Output
s like that.
Below is an example of a function that can be used to apply mixins, NOTE may not necessarily be exactly what you want, and you need to adjust it to your needs.
export function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
And then create your mixins :
export class MyMixin{
@Input() prop1: number;
@Output() prop1Change = new EventEmitter<number>();
@Input() prop2: number;
@Output() prop2Change = new EventEmitter<number>();
}
applyMixins(MyComponent, [MyMixin]);
3- You can have default properties for inputs so you only override them if you need:
export class MyComponent{
@Input() prop1: number = 10; // default
}
If you want to bundle input parameters as an object, i'd suggest to do it like this:
export class UsingConfig implements OnInit {
@Input() config: any;
@Output() configChange = new EventEmitter<any>();
ngOnInit() {
// Simulate something that changes prop1
setTimeout(() =>
this.configChange.emit({
...this.config,
prop1: this.config.prop1 + 1
});
);
}
}
Both points ensure that ChangeDetection will work properly (assuming you use the more efficient OnPush strategy). Plus it's easier to follow the logic in case of debugging.
Edit: Here's the obvious part within the parent component.
Template:
<using-config [config]="config" (configChange)="onConfigChange($event)"></using-config>
Code:
export class AppComponent {
config = {prop1: 1};
onConfigChange(newConfig: any){
// if for some reason you need to handle specific changes
// you could check for those here, e.g.:
// if (this.config.prop1 !== newConfig.prop1){...
this.config = newConfig;
}
}
personally if I see I need more than 4 inputs+outputs, I will check my approach to create my component again , maybe it should be more than one component and I'm doing something wrong. Anyway even if I need that much of input&outputs I won't make it in one config, for this reasons :
1- It's harder to know what should be inside inputs and outputs,like this: (consider a component with to html inputs element and labels)
imagine if you got only 3 of this component and you should comeback to work on this project after 1 or 2 month, or someone else gonna collaborate with you or use your code!. it's really hard to understand your code.
2- lack of performance. it's way cheaper for angular to watch a single variable rather than watching an array or object. beside consider the example i gave you at first one, why you should force to keep track of labels in which may never change alongside with values which always are changing.
3- harder to track variables and debug. angular itself comes with confusing errors which is hard to debug, why should I make it harder. tracking and fixing any wrong input or output one by one is easier for me rather than doing it in one config variable which bunch of data.
personally I prefer to break my components to as small as possible and test each one. then make bigger components out of small ones rather than having just a big component.
Update : I use this method for once input and no change data ( like label )
@Component({
selector: 'icon-component',
templateUrl: './icon.component.html',
styleUrls: ['./icon.component.scss'],
inputs: ['name', 'color']
});
export class IconComponent implements OnInit {
name: any;
color: any;
ngOnInit() {
}
}
Html:
<icon-component name="fa fa-trash " color="white"></icon-component>
with this method angular wont track any changes inside your component or outside. but with @input method if your variable changes in parent component, you will get change inside component too.