Content projection (fallback slot limitations)

I hope i’m doing something wrong because according to the docs this isn’t a limitation. I have the following view which is created by compose. What I’ve found is that the only properties that get properly bound must exist on this views viewmodel. I want dialogFactory to be set via the activate method. Activate gets called and I can see the value set, but it’s as if I end up with a different instance of the viewModel. Is this a limitation with content projection fallback slots?

<template>
  <div class="ux-dialog-body">
    <slot name="body">
    </slot>
  </div>
  <slot name="footer">
    <button id="dgCancel"
      if.bind="dialogContainer.model.type === 2 || (dialogContainer.model.type === 0 && dialogContainer.model.cancelButtonVisible)"
      disabled.bind="dialogContainer.cancelDisabled" click.delegate="cancel()" class="btn btn-default"
      type="button">${dialogContainer.model.cancelText}
      <b
        if.bind="dialogContainer.model.autoCloseCancels && dialogContainer.model.count">${dialogContainer.model.countdown}</b>
    </button>

    <button id="dgOk" disabled.bind="dialogContainer.model.okDisabled" click.delegate="ok($this)" type="submit"
      class="btn btn-primary">
      ${dialogContainer.model.okText} <b
        if.bind="!dialogContainer.model.autoCloseCancels && dialogContainer.model.count">${dialogContainer.model.countdown}</b>
    </button>
  </slot>
</template>
1 Like

Activate gets called and I can see the value set, but it’s as if I end up with a different instance of the viewModel

From your view, it seems unclear to understand what you meant with different instance of the viewModel.

What I’ve found is that the only properties that get properly bound must exist on this views viewmodel

Can you elaborate this?

1 Like

Here is my view model . The console messages show proper objects in the activate method. The view sees those as being null. when ok() is called, the properties are still null

import { DialogContainer } from "./dialogContainer";

export class CustomContainer {
    private model: any = null;
    private dialogContainer: any = null;

    activate(model: { dialogContainer: DialogContainer, model: any }) {
        this.model = model.model;
        this.dialogContainer = model.dialogContainer;

        console.log("dialog container model", this.dialogContainer);
        console.log("model", this.model);
    }

    ok() {
        console.log("this", this)
        this.dialogContainer.ok()
    }

    cancel() {
        this.dialogContainer.cancel();
    }
}

The dialogContainer properly works due to injection this way, but I don’t want to rely on injection as it needs to be transient so i need that instance to come through the activate call.

import { DialogContainer } from "./dialogContainer";
import { autoinject, computedFrom } from "aurelia-framework";

@autoinject
export class CustomContainer {
    private model: any = null;

    constructor(private dialogContainer: DialogContainer) {
    }

    activate(model: { dialogContainer: DialogContainer, model: any }) {
        this.model = model.model;       
    }

    ok() {
        console.log("this", this)
        this._dialogContainer.ok(this)
    }

    cancel() {
        this._dialogContainer.cancel();
    }
}

1 Like

I ended up taking a different approach after realizing what I posted above isn’t 100% what I wanted. Below is what I wanted to do (customContainer became dialogContainer). Activate is called on the viewModel of this file containing dialogContainer and and I get the properties I want inside of the dialogContainer class from the bindingContext during the bind() call. All seems to work as I hoped. I just need to update the fallback ok and cancel functions.

<template>
    <dialog-container>
        <div slot="body">
            <div class="form-group">
                <label for="nameField">Name</label>
                <input class="form-control" value.bind="model.name" type="text" id="nameField" />
            </div>
        </div>
        <!-- <div slot="footer">
            <h4>My custom footer</h4>
            <button click.trigger="cancel()">
                Custom Cancel
            </button>
            <button click.trigger="ok()">
                Custom OK
            </button>
        </div> -->
    </dialog-container>
</template>

I now almost have my dialog-factory working as I want it to. I’m still hoping to open source this project at some point. I think what I have is what most people would expect how aurelia-dialog should function.

1 Like

I can now define my dialogs as simple as this after injecting my factory class. All 3 methods accept a settings class that controls the dialog configuration. My goal was to eliminate as much copy and paste code for these scenarios and I’m happy to say I succeeded finally.

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

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

Custom

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

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

Custom viewModel


import { CustomDemoResult } from "./customDemoResult";
import { DialogContainerBase, DialogOkArgs } from "dialog-factory";

export class CustomBody extends DialogContainerBase<CustomDemoModel> {
  public async ok(args: DialogOkArgs) {
    let result = new CustomDemoResult();
    result.newName = this.model.name;
    await this.doWork();
    return result;
  }

  doWork(): Promise<void> {
    return new Promise<void>((resolve) => {
      setTimeout(() => {
        resolve();
      }, 500);
    });
  }
}

export class CustomDemoModel {
  public name: string = null;
}

custom view

<template>
    <dialog-container>
        <div slot="body">
            <div class="form-group">
                <label for="nameField">Name</label>
                <input class="form-control" value.bind="model.name" type="text" id="nameField" />
            </div>
        </div>
    </dialog-container>
</template>

custom view with footer

<template>
    <dialog-container>
        <div slot="body">
            <div class="form-group">
                <label for="nameField">Name</label>
                <input class="form-control" value.bind="model.name" type="text" id="nameField" />
            </div>
        </div>
        <div slot="footer">
            <h4>My custom footer</h4>
            <button click.trigger="cancelClicked()">
                Custom Cancel
            </button>
            <button click.trigger="okClicked()">
                Custom OK
            </button>
        </div>
    </dialog-container>
</template>