Create viewmodel/view dynamicaly without compose


#1

I’m assuming this is possible because aurelia-dialog does it. How can invoke a viewmodel without using compose?

For nearly 2 years I’ve been relying on a wrapper class that makes working with dialogs much easier in our project. Today I’m converting it to a plugin and updating to the latest version. I was on 1.0rc3

I have 3 methods with some overrides. each dialog type has a variety of options
alert, confirm, and custom

The reason I spent time making this is my project relies on a lot of dialogs and the dialog plugin in my opinion has too much boiler plate code. It also protects me if there are any major API changes since I only have to update my code in one place.
My team can trigger dialogs like this

Alert
await this.dialogFactory.alert("Simple alert", "It doesn't get any easier than this");

Confirm
let result = await this.dialogFactory.confirm("Simple confirm", "It doesn't get any easier than this");

Custom

let settings: CustomSettings<CustomDemoModel> = {
      viewModel: "customDialogs/customDemo",
      model: { name: "Joe Smith" }
    };

let result = await this.dialogFactory.custom<CustomDemoModel, CustomDemoResult>("My custom", settings);

The developer will get a strongly typed result back as long as pass in an interface/class as a type in the CustomSettings class.

Where my question comes about is I currently require the developer to set the viewModel as a string in the CustomSettings object. I’d really like for that to be a class. Is this possible? I tried following the dialog plugin code to see how the dialogService does it but I get lost.

What I have works. Since we rely on typescript so heavily, I’m trying to limit the areas we use strings.

(Update) - To make what I have work, i have a viewModel that gets passed into dialogService.open method that I call DialogContainer. That viewmodel does the rest of the magic to make my factory work. It’s on that viewmodel where the custom dialogs viewmodel gets rendered.


#2

Every composition in Aurelia is done by CompositionEngine, what you need to give it is a CompositionContext with viewModel property that is a string to point to a module, which will be loaded later via loader, or a direct class constructor to instantiate it directly.

@inject(CompositionEngine)
export class MyCustomComposer {
  constructor(compositionEngine) {
    this.compositionEngine = compositionEngine;
  }

  compose(viewModel: Function) {
    this.compositionEngine.compose({
      viewModel: viewModel
    });
  }
}

#3

Thank you! I have working code already


#4

Glad you see the requirement got resolved :+1:


#5

While I figured it out, this is another area that lacks nice documentation. I know it’s not something that needs to be done on a regular basis. Thanks again for the help. I got what I needed working and I need to set this plugin aside before I keep feature creeping it.


#6

Hey @elitemike, could you provide an example of your solution using this CompositionEngine? I’m currently working on similar concept (custom wrapper class handling multiple types of dialogs) and I would love to see your approach to this topic.


#7

I certainly will share some. I’m hoping my company will let me actually put the plugin out there, but if not, I’ll be able to share how I approached things. I’m sure someone else can make it much better. When I have a moment tomorrow I’ll get some code snippets up


#8

The way I’m doing compose is this. I have a tag on the view that I’m watching to make sure it’s ready and it is called body on my viewModel

  bodyChanged() {
    if (this.model.type === DialogType.custom) {
      let context = this.createContext((<CustomModel>this.model).bodyViewModel, this.body, (<CustomModel>this.model).model);
      let _this = this;
      this.compositionEngine.compose(context).then((view: any) => {
        _this.viewRef = view.viewModel;
        if ((<CustomModel>_this.model).footerViewModel) {
          _this.composeFooter();
        }
      });
    }
  }

  createContext(viewModel: object, host: Element, model: object): CompositionContext {
    return {
      container: this.container.createChild(),
      viewModel: viewModel,
      model: { dialogContainer: this, model: model },
      host: host,
      bindingContext: null,
      viewResources: null,
      viewSlot: new ViewSlot(host, true)
    };
  }

I am storing reference to the created viewModel because currently I need that for other work I’m doing but I’m trying to eliminate that need . I need to refactor my code so it’s more evident what (<CustomModel>this.model).model means. this.model is the object directly from the activate function and that object has it’s own model which is the model for my view to be composed.

My view looks something like this at the moment. I am working to eliminate the separate need for a footer view. My current WORKING implementation has the body as a separate view/viewModel and if a custom footer is desired that gets it’s own view/viewModel. The footer then gets the body viewmodel injected so i can handle logic with the buttons nicely. I could let this custom view contain the footer, but my goal is to not require a developer to have to create buttons each time.

If you see my most recent post about content projection, I am trying to make a wrapper for the custom case. The footer would be default buttons like I pasted above unless overridden, but I can handle closing the dialog with less code than I currently need.

<template>
  <require from="./style.css"></require>
  <require from="./customContainer"></require>

  <ux-dialog ref="dialogRef" with.bind="model">
    <ux-dialog-header style="padding:5px;" if.bind="title">
      <div roles="alert" style="padding:5px; margin-bottom:2px; margin-top:2px;">
        <div style="float: left;">
          <span style="font-size: 24px; font-weight:bold; ">${title}</span>
        </div>
        <div style="float:right">
          <loading-spinner loading.bind="isLoading"></loading-spinner>
          <a if.bind="showCloseButton" style="cursor: pointer;" id="ux-dialog-header-close" title="Close" aria-hidden="true"
            aria-label="Close" click.trigger="cancel()"><i class="fal fa-times"></i> </a>
        </div>
        <div style="clear:both;"></div>
      </div>

    </ux-dialog-header>
    <!-- This is where the custom view gets injected -->
    <div if.bind="type===0" element.ref="body"></div>
    <template else>
      <ux-dialog-body class="ux-dialog-body" id="ux-dialog-body" css="width: ${width}px; height: ${height}px;" style="overflow:auto; padding:5px;">
        <div class="container-fluid" if.bind="type !== 0" style="min-width:415px;">
          <div class="row" if.bind="iconName">
            <div class="col col-11 align-self-center" innerhtml.bind="message | sanitizeHTML">
            </div>
            <div class="col col-1 align-self-center">
              <i class="fal fa-${iconName} ${iconSizeString}"></i>
            </div>
          </div>
          <div else innerhtml.bind="message | sanitizeHTML">
          </div>
        </div>
      </ux-dialog-body>
      <ux-dialog-footer>
        <div element.ref="footer" if.bind="!hideFooter">
          <template if.bind="!footerViewModel">
            <button id="dgCancel" if.bind="type === 2 || (type === 0 && cancelButtonVisible)" disabled.bind="cancelDisabled"
              click.trigger="cancel()" class="btn btn-default" type="button">${cancelText}
              <b if.bind="autoCloseCancels && count">${countdown}</b>
            </button>
            <button id="dgOk" disabled.bind="okDisabled" click.trigger="ok()" type="submit" class="btn btn-primary">
              ${okText} <b if.bind="!autoCloseCancels && count">${countdown}</b>
            </button>
          </template>
        </div>
      </ux-dialog-footer>
    </template>
  </ux-dialog>
</template>

I can’t post the entire project unless I get permission. I am trying…

My alert signature looks like this

public alert(title: string, message: string, openDialogPromise: boolean, settings?: AlertSettings, immediate?: boolean): Promise<AlertDialog>;
public alert(title: string, message: string, settings?: AlertSettings, immediate?: boolean): Promise<void>;
public alert(title: string, message: string, arg2: any, arg3?: any, arg4?: any): Promise<any> 

Here is my confirm

public confirm(title: string, message: string, openDialogPromise: boolean, settings?: ConfirmSettings, immediate?: boolean): Promise<ConfirmDialog>;
public confirm(title: string, message: string, settings?: ConfirmSettings, immediate?: boolean): Promise<boolean>;
public confirm(title: string, message: string, arg2: any, arg3?: any, arg4?: any): Promise<any> {

I have a variety of settings for each that control the header, icons for alert/confirm. The ability to get the dialogPromise back.

Other methods on my dialogFactory class include the ability to externally enable disable ok/cancel, close the open dialog and destroy and queued ones, show a loading/status icon and the ability to update a dialogs title

I wanted to work async await with my dialogs so I wrap the actual dialogservice.open in my own promise implementation

My custom dialogs works the same as the above 2 except it will return the object you are expecting back.

This lets me just await an alert for close if I want, or to await a true or false out of my confirm. I also have queuing logic in place that lets the dialogs wait so I don’t end up with dialogs on top of each other unless I want to (This is more about protection, myself and the rest of us better not need the stacking feature. I hate nested modals)


#9

Very nice! Thank you for your sample code with description. It looks very promising. I wish you luck with your further improvements :slight_smile:.

Do you think this solution (using CompositionEngine) is faster and performs better than using traditionally <compose> element?


#10

I’m not the one to answer that, but ultimately I think it does the same thing, the big difference for me is I have to wait for the element where I’m placing it to be available.