How to Inject/Replace part of the view and view model in Aurelia
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
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
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
Disclaimer: This content is shared under creative common license cc-by-sa 3.0. It is generated from StackExchange Website Network.