30

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?

15
  • 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 to get started Commented Dec 11, 2017 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? Commented Dec 11, 2017 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. Commented Dec 11, 2017 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. Commented Dec 11, 2017 at 16:04
  • 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, as opposed to the model with several input/output properties, which still works. Commented Nov 24, 2018 at 16:39

5 Answers 5

16
+75

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.

Sign up to request clarification or add additional context in comments.

6 Comments

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).
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
labels into @Input parameters fix the performance issue? Wouldn't Angular would still be tracking them?
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.
"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).
|
8

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;

9 Comments

"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!
@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
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.
Those configs 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 Inputs is better. Also, other than very few cases, having Outputs within config is a bad idea and may be confusing to the users.
@ritaj I edited my answer based on your input, thanks.
|
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 
}

3 Comments

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.
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?
@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 ...
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

6 Comments

That makes no sense. Config is an input, I will trigger change detection even with OnPush
@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...)
@ritaj - You can see what he means in the stackblitz demos mentioned in my comment to the question.
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.
@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.
|
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;
    }
  }

3 Comments

But I don't see how the parent component will know which property has changed.
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).
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.