Generic UI Component Development!

At some point in our application development a decision is made. To create a component library. This decision is accepted by all, and development starts. We start with the requirements given from product/design and we create what we think at that point is the entire component. But with in time the requirements of the component grows, and as a result the complexity increases and maintainability decreases. Most designers/products don’t know/care about the development process and see each element as a unique instance and therefore they feel free to make crazy requirements.

I will Use a Button component for the ongoing example. I use Angular in this example but this pattern can be done with every framework.

@Component({
  selector: 'demo-button',
  template: `
        <button (click)="onClick.emit($event)">
          {{text}}
        </button>
  `,
})
export class ButtonComponent {
  @Input() text: string
  @Output() onClick: EventEmitter<any> = new EventEmitter<any>();
}

This how every button component ever made begins. This is amazing, it works just as expected. It gets a text that will be present in the button and has an on-click that will emit to the consumer Then the designer asks to add a loading indicator… and as a result we add the loading parameter and state.

@Component({
  selector: 'demo-button',
  template: `
        <button (click)="onClick.emit($event)">
          {{loading ? 'loading...' : text}}
        </button>
  `,
})
export class ButtonComponent {
  @Input() text: string
  @Output() onClick: EventEmitter<any> = new EventEmitter<any>();
  @Input() loading: boolean
}

Here begins the interesting part. Why does the button component need to know the text that is inside the button? The answer: it shouldn’t. The button component gives us an accepted behaviour and design which we accept once we agree to use the component. The content is by definition replaceable and changeable, and more importantly, the component doesn’t care, as it doesn’t have an affect or impact on it.

@Component({
  selector: 'demo-button',
  template: `
   <button (click)="onClick.emit()">
     <ng-content></ng-content> <--- will put what ever i want inside
   </button>
  `,
})
export class ButtonComponent {
  @Output() onClick: EventEmitter<any> = new EventEmitter<any>();
}


// consumer component

// just text
<demo-button>
   {{loading ? 'loading' : text}}
</demo-button>

// example of passing on a different component
<demo-button>
    <demo-other-component></demo-other-component
</demo-button>

Now we have a generic button, the consumer can put anything they want, and is not limited to specific type of content which will give us quicker development on UI elements in the future and keeps our component lean and clean.

The upcoming question is… “ why do we need a shared component if we pass all the hard work?“ Good question, The shared component has 2 main agendas.

  1. Force a systematic design and behaviour
  2. Have a single job which is handled internally , which at some point will update the consumer with information.

The component will have wrapper elements which will create an agreed upon layout.(see code bellow) Regardless the content, my button will always have padding 5px and background red. any additional content will be affected by this design forced by the component. We can define different sizes and responsive layouts in order to keep our accepted design.

@Component({
  selector: 'demo-button',
  template: `
        <button (click)="onClick.emit($event)" style="padding: 5px; background: red">
            <ng-content></ng-content>
        </button>
  `,
})
export class ButtonComponent {
  @Output() onClick: EventEmitter<any> = new EventEmitter<any>();
}

Specific use case which is handled internally

All the our button component needs to do, is to tell the consumer that it was clicked (very simple in our example), nothing else is important nothing else matters. What we can do is pass into the button component helpers that modify that single job (modifiers). For example : Disabled

@Component({
  selector: 'demo-button',
  template: `
        <button
         (click)="!disabled ?? oClick.emit() : void"
          style="padding: 5px; background: red"
         [ngClass]="size">
            <ng-content></ng-content>
        </button>
  `,
})
export class ButtonComponent {
  @Input() disabled: boolean; <--- helper
  @Input() size: ButtonSize = ButtonSize.Default <--- ButtonsSizes
  @Output() onClick: EventEmitter<any> = new EventEmitter<any>();
}

Its not about practices, it’s a MENTALITY

Components are built to be used. We Preach about reusability but in reality we develop for a specific use case which as a result are not reusable. The button is a simple example, but components can be complex elements with moving parts (Still with one job), but the ideas stay the same. I would like to reiterate the second point from before.

A Shared Component must have a single job which is handled internally, which at some point will update the consumer with information.

If we understand what is that single job, we can separate between the job requirements and everything else. Anything that isn’t coupled to that single job, should not be in the component’s responsibility ( i.e the text in our example), and should be handled one level above. Only then can we make truly reusable components that are easy to use, read and maintain.