28

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 object as input.

For example, here's a component with multiple inputs and outputs:

export class UsingEventEmitter implements OnInit {
    @Input() prop1: number;
    @Output() prop1Change = new EventEmitter<number>();
    @Input() prop2: number;
    @Output() prop2Change = new EventEmitter<number>();

    ngOnInit() {
        // Simulate something that changes prop1
        setTimeout(() => this.prop1Change.emit(this.prop1 + 1));
    }
}

And its usage:

export class AppComponent {
    prop1 = 1;

    onProp1Changed = () => {
        // prop1 has already been reassigned by using the [(prop1)]='prop1' syntax
    }

    prop2 = 2;

    onProp2Changed = () => {
        // prop2 has already been reassigned by using the [(prop2)]='prop2' syntax
    }
}

Template:

<using-event-emitter 
    [(prop1)]='prop1'
    (prop1Change)='onProp1Changed()'
    [(prop2)]='prop2'
    (prop2Change)='onProp2Changed()'>
</using-event-emitter>

As the number of properties grows, it seems that switching to a single configuration object might be cleaner. For example, here's a component that takes a single config object:

export class UsingConfig implements OnInit {
    @Input() config;

    ngOnInit() {
        // Simulate something that changes prop1
        setTimeout(() => this.config.onProp1Changed(this.config.prop1 + 1));
    }
}

And its usage:

export class AppComponent {
    config = {
        prop1: 1,

        onProp1Changed(val: number) {
            this.prop1 = val;
        },

        prop2: 2,

        onProp2Changed(val: number) {
            this.prop2 = val;
        }
    };
}

Template:

<using-config [config]='config'></using-config>

Now I can just pass the config object reference through multiple layers of nested components. The component using the config would invoke callbacks like config.onProp1Changed(...), which causes the config object to do the reassignment of the new value. So it seems we still have one-way data flow. Plus adding and removing properties doesn't require changes in intermediate layers.

Are there any downsides to having a single config object as an input to a component, instead of having multiple input and outputs? Will avoiding @Output and EventEmitter like this cause any issues that might catch up to me later?

Frank Modica
  • 10,238
  • 3
  • 23
  • 39
  • yes. there will be change detection fired for each changes. So I recommend using `state-management` to share the data across components. Consider reading my [**medium post**](https://medium.com/@aravindfz/setting-up-storemodule-in-ngrx-4-0-b7c60732aa64) to get started – Aravind Dec 11 '17 at 15:45
  • @Aravind Thanks for the article. I'm not trying to share data across multiple components here. I'm trying to pass multiple inputs and callbacks to one child component. Are you saying that change detection will not work if a child component invokes callbacks on a config object, rather than emitting events? Also - what if I'm building an open source component and I don't want to assume the consumer is using a store? – Frank Modica Dec 11 '17 at 15:53
  • if it is a open source project its fine to have this way. But more the input and output more the change detection is triggered. – Aravind Dec 11 '17 at 15:57
  • @Aravind I see, so your concern is with having many inputs/outputs in general. Then it would seem that having a single config object instead of multiple inputs/outputs might be more performant in some cases. – Frank Modica Dec 11 '17 at 16:04
  • Yup. You got my point. its good to have it this way so open source projects work like this! – Aravind Dec 11 '17 at 16:07
  • Thank you @ritaj for opening the bounty! – Frank Modica Nov 23 '18 at 22:31
  • 1
    Does the "config model" assume that `ChangeDetectionStrategy.OnPush` is not used in the child component? Because if that strategy is set, the single object model [does not appear to work](https://stackblitz.com/edit/angular-cspaax), as opposed to the model with several input/output properties, which [still works](https://stackblitz.com/edit/angular-sfxq5k). – ConnorsFan Nov 24 '18 at 16:39
  • @ConnorsFan I wouldn’t expect `onPush` to work when mutating the config, because that strategy requires the object reference to change. Usually I’m ok with this, as long as components nested inside the `using-config` component pick up on property changes. For example, if `using-config` passes `config.prop1` into another component, I want that other component to pick up changes to `config.prop1`. – Frank Modica Nov 24 '18 at 16:58
  • @SiddAjmera Your edit removes the parenthesis from the “banana box” syntax, but as I understand it’s the parenthesis that cause the reassignment, which I wanted in the example. I’ll add them back. – Frank Modica Nov 25 '18 at 12:57
  • In your "config" model, is `config.onProp1Changed` supposed to be the equivalent of the event handler with the same name in `AppComponent` in the input/output model? – ConnorsFan Nov 27 '18 at 20:15
  • @ConnorsFan Pretty much yes. But with ``, the reassignment is done to a property on `AppComponent` via "banana box" syntax, and `onProp1Changed` is there just in case you need to do something else. But with `` the reassignment is done to a property on the config through `config.onProp1Changed`. – Frank Modica Nov 27 '18 at 20:30
  • I was asking the question because I see a difference of context in these two situations. In the "normal" input/output model, `this` in the event handler refers to the `AppComponent`, whereas it refers to the configuration object in the "config" model. It could be a problem if you want to do some extra processing involving `AppComponent` properties in the event handler, and confuse a developper who expects the standard event handler usage. See [this stackblitz](https://stackblitz.com/edit/angular-rd6wph) for an example. – ConnorsFan Nov 27 '18 at 20:45
  • @ConnorsFan Yes that's a good point. You'd have to do something like `self = this` outside of the config to distinguish context, and perhaps add a fake `this` parameter in the config methods to help with typing, like `onProp1Changed(this: MyConfigType, val: number)`, which doesn't look so great. If you switch to an arrow function, you could reference the config itself through closure (by variable name), but that's a little messy too. But I assume that's a problem with any object that's built inline and has methods. – Frank Modica Nov 27 '18 at 20:58
  • Maybe you could get around this by having the component `new` up the config object (or get it from a service), but then if the component wanted to add on its own processing it would have to pass in a callback or similar, which is more indirection. – Frank Modica Nov 27 '18 at 20:58
  • Another solution is for the callback to be an arrow function, with the config object itself to be passed in as a parameter (also potentially weird for people who have never seen this done). – Frank Modica Nov 28 '18 at 13:54

5 Answers5

15

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.

molikh
  • 1,274
  • 13
  • 24
  • Thank you for your response. 1 - Aside from the fact that I'm not using established practices, is there another reason this is hard to understand? It seems similar to refactoring a method which takes many parameters into a method that takes one object. Sure, there's a bit of overhead now since you have to look at the type of the object to understand the properties. But the clutter is removed, so it seems like a decent tradeoff to me (but you're right, perhaps it's worse to other people). – Frank Modica Nov 24 '18 at 15:34
  • 1
    2 - Usually I do create small components using `@Input` and `@Output`, but sometimes an outer component groups them and ends up needing the large number of parameters. This outer component is the one I like to refactor. It doesn't usually need to track changes to each value, as it's just forwarding them to other small components. But in a situation where it does need to track each property, you're right that now it's tracking the callback methods as well, which is unnecessary. But when you said "why you should force to keep track of labels in which may never change", how does turning those – Frank Modica Nov 24 '18 at 15:35
  • labels into `@Input` parameters fix the performance issue? Wouldn't Angular would still be tracking them? – Frank Modica Nov 24 '18 at 15:35
  • 3 -That's a good point. Maybe Angular's errors would help you pinpoint the location of the error more quickly with `@Input` and `@Output`. – Frank Modica Nov 24 '18 at 15:35
  • "it's way cheaper for angular to watch a single variable rather than watching an array or object" It's not cheaper, it's the same. It just won't work for a changed array/object (assuming OnPush strategy). – Martin Cremer Nov 25 '18 at 00:29
  • @FrankModica 1- In my experience when I had multiple variable in on object, it became harder during the development process, each month I had to add more component to my project ( It was a big project with multiple apps in which all apps used same component ) and it became harder by each component added to know what parameters it's take. so I prefer know to create my components in the way i can undrestand what is inside only by looking at html part. – molikh Nov 27 '18 at 10:15
6

I would say it could be OK to use single config objects for Inputs but you should stick to Outputs at all the time. Input defines what your component requires from outside and some of those may be optional. However, Outputs 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 Outputs 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 Inputs 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 Inputs 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 Inputs. 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 Inputs, 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 functions 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;
Bunyamin Coskuner
  • 8,719
  • 1
  • 28
  • 48
  • "Also, I think it may break change detection strategy as well.” This was the point I was hoping for the most. Do you have any proof of that? Otherwise great answer, thanks! – Roberto Zvjerković Nov 24 '18 at 22:27
  • @ritaj as other people mentioned in the comments, angular checks if the reference of the object has changed to trigger change detection. If the parent component has `OnPush` strategy on, any change within `config` may not trigger change detection within child component. I haven't tested myself but I can try and let you know – Bunyamin Coskuner Nov 24 '18 at 22:31
  • Oh well, that happens with every Input property. Still, people use config objects for input all the time. Question was more about putting Output inside the config too. As far as I have understood it, that is. – Roberto Zvjerković Nov 24 '18 at 22:33
  • Those `config`s are usually like constants, set once and unlikely to be changed later. If that's your case, it is fine to use single `Input`. However, if your inputs may change over time, then having separate `Input`s is better. Also, other than very few cases, having `Output`s within `config` is a bad idea and may be confusing to the users. – Bunyamin Coskuner Nov 24 '18 at 22:36
  • @ritaj I edited my answer based on your input, thanks. – Bunyamin Coskuner Nov 24 '18 at 22:43
  • Nice answer. Regarding checking for `undefined`, I didn't think about this but you're right. With `Output` you can just emit and forget, but with callbacks you can potentially call a function that's not there (although perhaps I'd want an error in that case sometimes). Intellisense is a great point too which I hadn't considered. Regarding refactoring - this is where I may disagree. One of the reasons I liked the config approach was that it seemed to make refactoring easier. If you have to pass data through multiple nested components, it can be a pain when you later need to add more inputs and – Frank Modica Nov 25 '18 at 04:00
  • outputs to each intermediate component. With a config you make a change in one place and that's it. Although some might argue that if you're dealing with lots of nesting then you should switch to sharing data via a service (not always great either). Regarding not being able to use banana box syntax, that's true, but with the config approach you're moving that code from the template to the config (which does the value reassignment and leads to equivalent "two way" binding), although I suppose now you have less control and maybe there's more code (although there's less clutter due to just one – Frank Modica Nov 25 '18 at 04:00
  • config and no inputs/outputs). Thanks for your input, you made some really good points. – Frank Modica Nov 25 '18 at 04:06
  • Yes, you are right you can simply add new fields to `config` and pass it down the component tree. However, it may get messier as the `config` gets bigger. Yes, it may require less code but more time to understand the code whose main purpose is to be easily understandable by other people. For example, you may be passing 30 fields from the top component but you may not know which component uses which field. I, also, think you should not be passing some attributes too many level down the tree. If you are doing so, there may be something wrong with your architecture. – Bunyamin Coskuner Nov 25 '18 at 10:40
6
  • 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 Inputs whereas it becomes a little bit messy with objects to created defaults.

If you have too many similar Input, Outputs, you can consider below :

1- You can create a Base class and put all your Input/Outputs 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/Outputs 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 
}
Milad
  • 27,506
  • 11
  • 76
  • 85
  • 1
    The base class approach is kind of nice because it cleans up the internal implementation of the component a bit, while sticking with the native `@Input` / `@Output`. It's interesting that the consensus seems to be that it's harder to understand an object than numerous parameters. I actually like when everything is wrapped up in an object. It seems clutter-free, and if I want to understand the details I can go to the type definition. But what I perceive as clutter, others perceive as clarity and transparency. It's good to get other people's perspective on it, thanks for your response. – Frank Modica Nov 26 '18 at 14:01
  • There are definitely some situations where default parameters could reduce clutter (components that wrap form elements with many attributes, maybe even the chart example someone else mentioned). Mixins are also a cool idea - have you used it in a team setting? – Frank Modica Nov 26 '18 at 14:27
  • @FrankModica, no worries. I also added another note on defaults, because it's harder and less performant with objects to create defaults. You have to clone it all the time and ... – Milad Nov 26 '18 at 22:21
3

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:

  • your component gets harder to understand and reason about (what are its inputs vs its outputs?)
  • cannot use banana box syntax anymore
Felix K.
  • 14,171
  • 9
  • 58
  • 72
  • That makes no sense. Config is an input, I will trigger change detection even with OnPush – Roberto Zvjerković Nov 24 '18 at 21:49
  • @ritaj yes, with onpush, if the config object itself changes it will trigger a rerender. But angular will only do a shallow comparison of inputs, which is why changes of properties of the config object would be ignored if the reference of the config object itself stays the same. (I now made the word "inside" bold in my answer, since it seems you overread it...) – Felix K. Nov 24 '18 at 21:54
  • 1
    @ritaj - You can see what he means in the stackblitz demos mentioned in [my comment](https://stackoverflow.com/questions/47743281/refactoring-angular-components-from-many-inputs-outputs-to-a-single-config-objec#comment93792010_47743281) to the question. – ConnorsFan Nov 24 '18 at 22:25
  • Your edited answer gets to what I’m saying. I’m not trying to modify the object in the child - I’m pretty much passing callbacks as input. Good point about no longer being able to use banana box syntax. As for making it harder to reason about, sometimes I think “so what, I’ll go to the type definition for the config and find the definitions there, is that so bad?”. But it is overhead, so it’s a fair point. – Frank Modica Nov 25 '18 at 03:28
  • @FrankModica "so what, I’ll go to the type definition for the config and find the definitions there", Yes, personally I also think the distinction between input and ouput somewhat bloats the framework. For example in react js you do not have a "hard" distinction between output and input props and simply pass callbacks as (input) props and that works pretty well. In case of angular, I would probably stick with "@output" though, as it is a core framework design principle that angular developers are used to. – Felix K. Nov 25 '18 at 10:25
  • 1
    @B12Toaster I also didn't have a problem with callbacks in React, and it made me think about why we use events in Angular. In the end though, as you say it's best to stick with what people on your team are used to. – Frank Modica Nov 26 '18 at 14:14
2

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
          });
        );
    }
}
  • You are creating a new config object when changing a property.
  • You are using an Output-Event to emit the changed config object.

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;
    }
  }
Martin Cremer
  • 5,191
  • 2
  • 32
  • 38
  • But I don't see how the parent component will know which property has changed. – Bunyamin Coskuner Nov 24 '18 at 23:23
  • Sure, this is basically the recommended way of doing things (while also enforcing immutability). I know I could (and maybe should) do it this way, but I was more looking for drawbacks to the other way I suggested. You briefly touch on it by saying that this way ensures change detection works (but I’m pretty sure it can work my way too) and that it’s easier to debug (probably a fair point). – Frank Modica Nov 25 '18 at 03:23
  • You’re right though that I can’t switch to the “On Push” strategy unless I make the config immutable too. This is usually not a problem for me, because the `using-config` component often just passes a config property into another smaller component which does its own change tracking. But this is not always the case, so you have a point. – Frank Modica Nov 25 '18 at 19:36