UIB (Bootstrap) Component Modals Made Easy

So UIB decided to support “component modals”. Unfortunately, the way I see it, the components need to be “dirtied” with the modal logic, meaning you will also need to include resolve and close in every component you want to display as a modal. Inside the components constructor or $onInit you then proceed to “resolve the resolve” (i.e. if(resolve){ this.myBinding = this.resolve.myBinding; ... }) which is quite annoying. Why isn’t it possible to JUST WRAP A COMPONENT IN A MODAL? So that’s what I set out to do: I created a wrapper-modal-component for my modal-components.

The idea behind was, to abstract away the modal logic from the component logic. Most of the component doesn’t need to know that it’s wrapped by a modal.

Untitled drawing (22)

Now in this sketch, the inner component functionality, doesn’t need to know ANYTHING about the modal. The only thing where it might be useful to be aware that it’s a component, is the UPDATE button, since you might want to close the modal after the update. But you can easily and beautifully do this by emitting an event.

But first things first. So I thought what’s the minimal information that a component-modal-opener requires? I.e. openModal, what does it need to know?

  1. It needs to know the name of the modal
  2. It needs to know the bindings, in case there are any

So an implementation of `openModal` could look something like this:

public openComponentModal (componentName: string, bindings?) {

  let resolve = {
    component: () => {
      return componentName;
    },
    bindings: () => {
      return bindings;
    }
  };

  return this.$uibModal.open({
    animation: true,
    component: 'modal',
    resolve: resolve
  });

}

Okay, so what happened here? Lets say this is in my Utils service, so from now on, I can always open modals just with e.g.

this.Utils.openComponentModal('my-component', {
  bindingOne: this.bindingOne,
  bindingTwo: this.bindingTwo
  ...
});

Handy, isn’t it? The modals will look consistent across your app, and the component isn’t spoiled with modal logic. But how is this achieved?

Notice how I DON’T open my-component directly, but instead I open the component modal. The component modal is where the magic happens and it looks like this:

Controller:

@Component('myApp', 'modal', {
  templateUrl: '/components/directives/modal/modal-component.html',
  bindings: {
    resolve: "<",
    close: "&"
  }
})
class ModalComponent {

  public resolve;
  public close;

  static $inject: string[] = ['Utils', '$compile', '$scope', '$log'];

  constructor(
    private Utils: Utils,
    private $compile,
    private $scope,
    private $log: ILogService
  ) {}

  public style;

  $onInit() {

    this.initModal();
    this.initCloseButton();

    this.$scope.$on(EmitEvents.MODAL_CLOSE, () => {
      this.close();
    });

  }

  private initModal() {
    if (this.resolve && this.resolve.component) {
      let bindingsString = (bindings): string => {
        let str = "";
        if (bindings) {
          for (var property in bindings) {
            if (bindings.hasOwnProperty(property)) {
              str += " " + this.Utils.camelCaseToDash(property) + "=\"$ctrl." + property + "\"";
              this[property] = bindings[property];
            }
          }
        }
        return str;
      };

      let completeString = `<${this.resolve.component}` + bindingsString(this.resolve.bindings) +`><${this.resolve.component}>`;
      let compiledHtml = this.$compile(completeString)(this.$scope);
      $('#tb-modal').html(compiledHtml);
    } else {
      this.$log.error("Modal not correctly initialized");
    }
  };

  private initCloseButton() {

    this.style = {
      position: 'absolute',
      top: '-20px',
      right: '8px',
      'font-size': 35 + 'px',
      'z-index': 1000,
      color: this.Utils.primaryColor
    };

  }

  public open

}

View:

<div class='modal-wrapper'>
  
  <div style="position: relative">
    <button ng-click="$ctrl.close()" ng-style="$ctrl.style">
      <span style="position: relative">
        <i class="fa fa-circle" aria-hidden="true" style="position: absolute; color: {{$ctrl.color}}"></i>
        <i class="fa fa-times" aria-hidden="true" style="position: absolute; color: white; font-size: 20px; top: 7px; left: 7px;"></i>
      </span>
    </button>
  </div>
  
  
  <div id="tb-modal"></div>
  
</div>

Uff, pretty crazy. But the beauty about it is, that the crazyness is abstracted away in an isolated component. All the other things left and right of it are left untouched. So the crazyness quickly explained:

  1. The view has simply a close button and a div which is replaced with the actual component by jquery
  2. The controller takes a resolve from uib.resolve, resolves it, and compiles the component.
  3. The controller binds to an emit event.

Now, if e.g. the UPDATE button in the component should close the modal, you simply make it run:

this.$scope.$emit(EmitEvents.MODAL_CLOSE);

Meaning the component is still coupled to a modal, but much more lightly than before.

I hope this gets you started with your modal-wrapper-component. 🙂

Leave a Reply

Your email address will not be published.