Skip to content
Fix Code Error

How to Inject/Replace part of the view and view model in Aurelia

May 2, 2021 by Code Error
Posted By: Anonymous

I’m migrating from KnockoutJS to Aurelia and having a hard time trying to figure out how I can swap out some HTML/JS from a view. I have some existing Knockout code as follows:

$.ajax({
    url: "/admin/configuration/settings/get-editor-ui/" + replaceAll(self.type(), ".", "-"),
    type: "GET",
    dataType: "json",
    async: false
})
.done(function (json) {

    // Clean up from previously injected html/scripts
    if (typeof cleanUp == 'function') {
        cleanUp(self);
    }

    // Remove Old Scripts
    var oldScripts = $('script[data-settings-script="true"]');

    if (oldScripts.length > 0) {
        $.each(oldScripts, function () {
            $(this).remove();
        });
    }

    var elementToBind = $("#form-section")[0];
    ko.cleanNode(elementToBind);

    var result = $(json.content);

    // Add new HTML
    var content = $(result.filter('#settings-content')[0]);
    var details = $('<div>').append(content.clone()).html();
    $("#settings-details").html(details);

    // Add new Scripts
    var scripts = result.filter('script');

    $.each(scripts, function () {
        var script = $(this);
        script.attr("data-settings-script", "true");//for some reason, .data("block-script", "true") doesn't work here
        script.appendTo('body');
    });

    // Update Bindings
    // Ensure the function exists before calling it...
    if (typeof updateModel == 'function') {
        var data = ko.toJS(ko.mapping.fromJSON(self.value()));
        updateModel(self, data);
        ko.applyBindings(self, elementToBind);
    }

    //self.validator.resetForm();
    switchSection($("#form-section"));
})
.fail(function (jqXHR, textStatus, errorThrown) {
    $.notify(self.translations.getRecordError, "error");
    console.log(textStatus + ': ' + errorThrown);
});

In the above code, the self.type() being passed to the url for an AJAX request is the name of some settings. Here is an example of some settings:

public class DateTimeSettings : ISettings
{
    public string DefaultTimeZoneId { get; set; }

    public bool AllowUsersToSetTimeZone { get; set; }

    #region ISettings Members

    public string Name => "Date/Time Settings";

    public string EditorTemplatePath => "Framework.Web.Views.Shared.EditorTemplates.DateTimeSettings.cshtml";

    #endregion ISettings Members
}

I use that EditorTemplatePath property to render that view and return it in the AJAX request. An example settings view is as follows:

@using Framework.Web
@using Framework.Web.Configuration
@inject Microsoft.Extensions.Localization.IStringLocalizer T

@model DateTimeSettings

<div id="settings-content">
    <div class="form-group">
        @Html.LabelFor(m => m.DefaultTimeZoneId)
        @Html.TextBoxFor(m => m.DefaultTimeZoneId, new { @class = "form-control", data_bind = "value: defaultTimeZoneId" })
        @Html.ValidationMessageFor(m => m.DefaultTimeZoneId)
    </div>
    <div class="checkbox">
        <label>
            @Html.CheckBoxFor(m => m.AllowUsersToSetTimeZone, new { data_bind = "checked: allowUsersToSetTimeZone" }) @T[FrameworkWebLocalizableStrings.Settings.DateTime.AllowUsersToSetTimeZone]
        </label>
    </div>
</div>

<script type="text/javascript">
    function updateModel(viewModel, data) {
        viewModel.defaultTimeZoneId = ko.observable("");
        viewModel.allowUsersToSetTimeZone = ko.observable(false);

        if (data) {
            if (data.DefaultTimeZoneId) {
                viewModel.defaultTimeZoneId(data.DefaultTimeZoneId);
            }
            if (data.AllowUsersToSetTimeZone) {
                viewModel.allowUsersToSetTimeZone(data.AllowUsersToSetTimeZone);
            }
        }
    };

    function cleanUp(viewModel) {
        delete viewModel.defaultTimeZoneId;
        delete viewModel.allowUsersToSetTimeZone;
    }

    function onBeforeSave(viewModel) {
        var data = {
            DefaultTimeZoneId: viewModel.defaultTimeZoneId(),
            AllowUsersToSetTimeZone: viewModel.allowUsersToSetTimeZone()
        };

        viewModel.value(ko.mapping.toJSON(data));
    };
</script>

Now if you go back to the AJAX request and see what I am doing there, it should make more sense. There is a <div> where I am injecting this HTML, as follows:

<div id="settings-details"></div>

I am trying to figure out how to do this in Aurelia. I see that I can use Aurelia’s templatingEngine.enhance({ element: elementToBind, bindingContext: this }); instead of Knockout’s ko.applyBindings(self, elementToBind); so I think that should bind the new properties to the view model. However, I don’t know what to do about the scripts from the settings editor templates. I suppose I can try keeping the same logic I already have (using jQuery to add/remove scripts, etc)… but I am hoping there is a cleaner/more elegant solution to this with Aurelia. I looked at slots, but I don’t think that’s applicable here, though I may be wrong.

Solution

As discussed in the comments, my answer to your other question should do the trick here. Given this runtime-view element:

TypeScript

_x000D_

_x000D_

    import { bindingMode, createOverrideContext } from "aurelia-binding";_x000D_
    import { Container } from "aurelia-dependency-injection";_x000D_
    import { TaskQueue } from "aurelia-task-queue";_x000D_
    import { bindable, customElement, inlineView, ViewCompiler, ViewResources, ViewSlot } from "aurelia-templating";_x000D_
    _x000D_
    @customElement("runtime-view")_x000D_
    @inlineView("<template><div></div></template>")_x000D_
    export class RuntimeView {_x000D_
      @bindable({ defaultBindingMode: bindingMode.toView })_x000D_
      public html: string;_x000D_
    _x000D_
      @bindable({ defaultBindingMode: bindingMode.toView })_x000D_
      public context: any;_x000D_
    _x000D_
      public el: HTMLElement;_x000D_
      public slot: ViewSlot;_x000D_
      public bindingContext: any;_x000D_
      public overrideContext: any;_x000D_
      public isAttached: boolean;_x000D_
      public isRendered: boolean;_x000D_
      public needsRender: boolean;_x000D_
    _x000D_
      private tq: TaskQueue;_x000D_
      private container: Container;_x000D_
      private viewCompiler: ViewCompiler;_x000D_
    _x000D_
      constructor(el: Element, tq: TaskQueue, container: Container, viewCompiler: ViewCompiler) {_x000D_
        this.el = el as HTMLElement;_x000D_
        this.tq = tq;_x000D_
        this.container = container;_x000D_
        this.viewCompiler = viewCompiler;_x000D_
        this.slot = this.bindingContext = this.overrideContext = null;_x000D_
        this.isAttached = this.isRendered = this.needsRender = false;_x000D_
      }_x000D_
    _x000D_
      public bind(bindingContext: any, overrideContext: any): void {_x000D_
        this.bindingContext = this.context || bindingContext.context || bindingContext;_x000D_
        this.overrideContext = createOverrideContext(this.bindingContext, overrideContext);_x000D_
    _x000D_
        this.htmlChanged();_x000D_
      }_x000D_
    _x000D_
      public unbind(): void {_x000D_
        this.bindingContext = null;_x000D_
        this.overrideContext = null;_x000D_
      }_x000D_
    _x000D_
      public attached(): void {_x000D_
        this.slot = new ViewSlot(this.el.firstElementChild || this.el, true);_x000D_
        this.isAttached = true;_x000D_
    _x000D_
        this.tq.queueMicroTask(() => {_x000D_
          this.tryRender();_x000D_
        });_x000D_
      }_x000D_
    _x000D_
      public detached(): void {_x000D_
        this.isAttached = false;_x000D_
    _x000D_
        if (this.isRendered) {_x000D_
          this.cleanUp();_x000D_
        }_x000D_
        this.slot = null;_x000D_
      }_x000D_
    _x000D_
      private htmlChanged(): void {_x000D_
        this.tq.queueMicroTask(() => {_x000D_
          this.tryRender();_x000D_
        });_x000D_
      }_x000D_
    _x000D_
      private contextChanged(): void {_x000D_
        this.tq.queueMicroTask(() => {_x000D_
          this.tryRender();_x000D_
        });_x000D_
      }_x000D_
    _x000D_
      private tryRender(): void {_x000D_
        if (this.isAttached) {_x000D_
          if (this.isRendered) {_x000D_
            this.cleanUp();_x000D_
          }_x000D_
          try {_x000D_
            this.tq.queueMicroTask(() => {_x000D_
              this.render();_x000D_
            });_x000D_
          } catch (e) {_x000D_
            this.tq.queueMicroTask(() => {_x000D_
              this.render(`<template>${e.message}</template>`);_x000D_
            });_x000D_
          }_x000D_
        }_x000D_
      }_x000D_
    _x000D_
      private cleanUp(): void {_x000D_
        try {_x000D_
          this.slot.detached();_x000D_
        } catch (e) {}_x000D_
        try {_x000D_
          this.slot.unbind();_x000D_
        } catch (e) {}_x000D_
        try {_x000D_
          this.slot.removeAll();_x000D_
        } catch (e) {}_x000D_
    _x000D_
        this.isRendered = false;_x000D_
      }_x000D_
    _x000D_
      private render(message?: string): void {_x000D_
        if (this.isRendered) {_x000D_
          this.cleanUp();_x000D_
        }_x000D_
    _x000D_
        const template = `<template>${message || this.html}</template>`;_x000D_
        const viewResources = this.container.get(ViewResources) as ViewResources;_x000D_
        const childContainer = this.container.createChild();_x000D_
        const factory = this.viewCompiler.compile(template, viewResources);_x000D_
        const view = factory.create(childContainer);_x000D_
    _x000D_
        this.slot.add(view);_x000D_
        this.slot.bind(this.bindingContext, this.overrideContext);_x000D_
        this.slot.attached();_x000D_
    _x000D_
        this.isRendered = true;_x000D_
      }_x000D_
    }

_x000D_

_x000D_

_x000D_

ES6

_x000D_

_x000D_

    import { bindingMode, createOverrideContext } from "aurelia-binding";_x000D_
    import { Container } from "aurelia-dependency-injection";_x000D_
    import { TaskQueue } from "aurelia-task-queue";_x000D_
    import { DOM } from "aurelia-pal";_x000D_
    import { bindable, customElement, inlineView, ViewCompiler, ViewResources, ViewSlot } from "aurelia-templating";_x000D_
    _x000D_
    @customElement("runtime-view")_x000D_
    @inlineView("<template><div></div></template>")_x000D_
    @inject(DOM.Element, TaskQueue, Container, ViewCompiler)_x000D_
    export class RuntimeView {_x000D_
      @bindable({ defaultBindingMode: bindingMode.toView }) html;_x000D_
      @bindable({ defaultBindingMode: bindingMode.toView }) context;_x000D_
    _x000D_
      constructor(el, tq, container, viewCompiler) {_x000D_
        this.el = el;_x000D_
        this.tq = tq;_x000D_
        this.container = container;_x000D_
        this.viewCompiler = viewCompiler;_x000D_
        this.slot = this.bindingContext = this.overrideContext = null;_x000D_
        this.isAttached = this.isRendered = this.needsRender = false;_x000D_
      }_x000D_
    _x000D_
      bind(bindingContext, overrideContext) {_x000D_
        this.bindingContext = this.context || bindingContext.context || bindingContext;_x000D_
        this.overrideContext = createOverrideContext(this.bindingContext, overrideContext);_x000D_
    _x000D_
        this.htmlChanged();_x000D_
      }_x000D_
    _x000D_
      unbind() {_x000D_
        this.bindingContext = null;_x000D_
        this.overrideContext = null;_x000D_
      }_x000D_
    _x000D_
      attached() {_x000D_
        this.slot = new ViewSlot(this.el.firstElementChild || this.el, true);_x000D_
        this.isAttached = true;_x000D_
    _x000D_
        this.tq.queueMicroTask(() => {_x000D_
          this.tryRender();_x000D_
        });_x000D_
      }_x000D_
    _x000D_
      detached() {_x000D_
        this.isAttached = false;_x000D_
    _x000D_
        if (this.isRendered) {_x000D_
          this.cleanUp();_x000D_
        }_x000D_
        this.slot = null;_x000D_
      }_x000D_
    _x000D_
      htmlChanged() {_x000D_
        this.tq.queueMicroTask(() => {_x000D_
          this.tryRender();_x000D_
        });_x000D_
      }_x000D_
    _x000D_
      contextChanged() {_x000D_
        this.tq.queueMicroTask(() => {_x000D_
          this.tryRender();_x000D_
        });_x000D_
      }_x000D_
    _x000D_
      tryRender() {_x000D_
        if (this.isAttached) {_x000D_
          if (this.isRendered) {_x000D_
            this.cleanUp();_x000D_
          }_x000D_
          try {_x000D_
            this.tq.queueMicroTask(() => {_x000D_
              this.render();_x000D_
            });_x000D_
          } catch (e) {_x000D_
            this.tq.queueMicroTask(() => {_x000D_
              this.render(`<template>${e.message}</template>`);_x000D_
            });_x000D_
          }_x000D_
        }_x000D_
      }_x000D_
    _x000D_
      cleanUp() {_x000D_
        try {_x000D_
          this.slot.detached();_x000D_
        } catch (e) {}_x000D_
        try {_x000D_
          this.slot.unbind();_x000D_
        } catch (e) {}_x000D_
        try {_x000D_
          this.slot.removeAll();_x000D_
        } catch (e) {}_x000D_
    _x000D_
        this.isRendered = false;_x000D_
      }_x000D_
    _x000D_
      render(message) {_x000D_
        if (this.isRendered) {_x000D_
          this.cleanUp();_x000D_
        }_x000D_
    _x000D_
        const template = `<template>${message || this.html}</template>`;_x000D_
        const viewResources = this.container.get(ViewResources);_x000D_
        const childContainer = this.container.createChild();_x000D_
        const factory = this.viewCompiler.compile(template, viewResources);_x000D_
        const view = factory.create(childContainer);_x000D_
    _x000D_
        this.slot.add(view);_x000D_
        this.slot.bind(this.bindingContext, this.overrideContext);_x000D_
        this.slot.attached();_x000D_
    _x000D_
        this.isRendered = true;_x000D_
      }_x000D_
    }

_x000D_

_x000D_

_x000D_

Here are some ways in which you can use it:

dynamicHtml is a property on the ViewModel containing arbitrary generated html with any kind of bindings, custom elements and other aurelia behaviors in it.

It will compile this html and bind to the bindingContext it receives in bind() – which will be the viewModel of the view in which you declare it.

<runtime-view html.bind="dynamicHtml">
</runtime-view>

Given a someObject in a view model:

this.someObject.foo = "bar";

And a dynamicHtml like so:

this.dynamicHtml = "<div>${foo}</div>";

This will render as you’d expect it to in a normal Aurelia view:

<runtime-view html.bind="dynamicHtml" context.bind="someObject">
</runtime-view>

Re-assigning either html or context will trigger it to re-compile. Just to give you an idea of possible use cases, I’m using this in a project with the monaco editor to dynamically create Aurelia components from within the Aurelia app itself, and this element will give a live preview (next to the editor) and also compile+render the stored html/js/json when I use it elsewhere in the application.

Answered By: Anonymous

Related Articles

  • Aurelia with knockout secure binding
  • Aurelia bundling issue with virtual directory
  • Can't correctly import external JS into Aurelia application…
  • SystemJS (Aurelia with jspm) fails to load…
  • using d3.js with aurelia framework
  • What is the copy-and-swap idiom?
  • aurelia-bootstrapper not found after upgrading to jspm beta
  • Aurelia UX showcase app fails to load
  • Java ElasticSearch None of the configured nodes are…
  • Downloading jQuery UI CSS from Google's CDN

Disclaimer: This content is shared under creative common license cc-by-sa 3.0. It is generated from StackExchange Website Network.

Post navigation

Previous Post:

How to tear down an enhanced fragment

Next Post:

TailwindCSS + NextJS: Integrating with PostCSS and IE11 support (custom properties)

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

  • Get code errors & solutions at akashmittal.com
© 2022 Fix Code Error