Aurelia dynamic binding - aurelia

I've created a custom element that generates tabular data. For good reasons, this generates the actual HTML and inserts into the DOM without using a template.
I need to attach click observers to specific elements to I can run a function in the custom element in response to a click. If using a template, I'd use click.delegate, but I can't use that with generated HTML.
How do you attach an event handler with Aurelia other than by using jQuery?

I know this answer is late, but in case this hasn't been (properly) solved yet and/or someone else finds this in the future:
In order to make any aurelia behavior work in dynamically generated HTML, you need to compile that HTML.
I have worked on a custom element (based on how aurelia's enhance and compose work) that allows you to pass in a string of HTML and it will then be compiled, so that any behaviors like bindables, custom elements / attributes will just work. It will also re-compile when the html changes.
Here's an example: https://gist.run?id=1960218b52ba628f73774822aef55ad7
src/app.html
<template>
<dynamic-html html.bind="dynamicHtml"></dynamic-html>
</template>
src/app.ts
export class App {
public dynamicHtml: string = `
<button click.delegate="handleClick()">Click me</button>
`;
public handleClick(): void {
alert("Hello!")
}
}
src/dynamic-html.ts
import {
customElement,
TaskQueue,
bindable,
ViewCompiler,
ViewSlot,
View,
ViewResources,
Container,
ViewFactory,
inlineView,
inject,
DOM
} from "aurelia-framework";
#customElement("dynamic-html")
#inlineView("<template><div></div></template>")
#inject(DOM.Element, TaskQueue, Container, ViewCompiler)
export class DynamicHtml {
#bindable()
public html: string;
public element: HTMLElement;
private tq: TaskQueue;
private container: Container;
private viewCompiler: ViewCompile;
private runtimeView: View;
private runtimeViewSlot: ViewSlot;
private runtimeViewFactory: ViewFactory;
private runtimeViewAnchor: HTMLDivElement;
constructor(element, tq, container, viewCompiler) {
this.element = <HTMLElement>element;
this.tq = tq;
this.container = container;
this.viewCompiler = viewCompiler;
}
public bindingContext: any;
public overrideContext: any;
public bind(bindingContext: any, overrideContext: any): void {
this.bindingContext = bindingContext;
this.overrideContext = overrideContext;
if (this.html) {
this.htmlChanged(this.html, undefined);
}
}
public unbind(): void {
this.disposeView();
this.bindingContext = null;
this.overrideContext = null;
}
public needsApply: boolean = false;
public isAttached: boolean = false;
public attached(): void {
this.runtimeViewAnchor = this.element.firstElementChild;
this.isAttached = true;
if (this.needsApply) {
this.needsApply = false;
this.apply();
}
}
public detached(): void {
this.isAttached = false;
this.runtimeViewAnchor = null;
}
private htmlChanged(newValue: string, oldValue: void): void {
if (newValue) {
if (this.isAttached) {
this.tq.queueMicroTask(() => {
this.apply();
});
} else {
this.needsApply = true;
}
} else {
if (this.isApplied) {
this.disposeView();
}
}
}
private isApplied: boolean = false;
private apply(): void {
if (this.isApplied) {
this.disposeView();
}
this.compileView();
}
private disposeView(): void {
if (this.runtimeViewSlot) {
this.runtimeViewSlot.unbind();
this.runtimeViewSlot.detached();
this.runtimeViewSlot.removeAll();
this.runtimeViewSlot = null;
}
if (this.runtimeViewFactory) {
this.runtimeViewFactory = null;
}
if (this.runtimeView) {
this.runtimeView = null;
}
this.isApplied = false;
}
private compileView(): void {
this.runtimeViewFactory = createViewFactory(this.viewCompiler, this.container, this.html);
this.runtimeView = createView(this.runtimeViewFactory, this.container);
this.runtimeViewSlot = createViewSlot(this.runtimeViewAnchor);
this.runtimeViewSlot.add(this.runtimeView);
this.runtimeViewSlot.bind(this.bindingContext, this.overrideContext);
this.runtimeViewSlot.attached();
this.isApplied = true;
}
}
function createViewFactory(viewCompiler: ViewCompiler, container: Container, html: string): ViewFactory {
if (!html.startsWith("<template>")) {
html = `<template>${html}</template>`;
}
let viewResources: ViewResources = container.get(ViewResources);
let viewFactory = viewCompiler.compile(html, viewResources);
return viewFactory;
}
function createView(viewFactory: ViewFactory, container: Container): View {
let childContainer = container.createChild();
let view = viewFactory.create(childContainer);
return view;
}
function createViewSlot(containerElement: Element): ViewSlot {
let viewSlot = new ViewSlot(containerElement, true);
return viewSlot;
}

Related

Aurelia: Declaring custom element at top-level and access across application

I have a custom element called loading-bar which is used in a number of pages in my app. It's purpose is to show a status message while loading, download of content, and give response messages for backend actions.
loading-bar.html:
<template show.bind="visible">
<i class="fa fa-circle-o-notch fa-spin fa-fw" if.bind="type==1"></i>
<i class="fa fa-check fa-fw" if.bind="type==2"></i>
<span id="loading-text">${message}</span>
</template>
loading-bar.ts:
import { customElement, bindable, bindingMode, inject } from 'aurelia-framework';
#customElement('loading-bar')
export class LoadingBarCustomElement {
private visible = false;
#bindable({ defaultBindingMode: bindingMode.twoWay }) message;
#bindable({ defaultBindingMode: bindingMode.twoWay }) type = 1;
constructor() {
this.visible = false;
}
messageChanged(newValue, oldValue) {
if (newValue && newValue !== '')
this.visible = true;
else
this.visible = false;
}
}
At the moment all pages using the loading bar have this element declared in its html:
<loading-bar message.bind="loadMsg" type.bind="loadType"></loading-bar>
The loading-Bar is controlled by changing the loadMsg and loadType local variables in every page.
What I would like to do is to declare the loading-bar html at just on place, and be able (from any page) to call a method like "showBar(msg, type)" that will affect the globally declared loading-bar.
My first though is to declare the loading-bar in app.html, (just like my nav-bar is declared here) and inject a class (in all the pages' ViewModel), which contain the showBar(msg, type) method that will control the loading-bar.
I am not sure if this is the correct way forward, or how it's best implemented, and would appreciate some help.
You can use the EventAggregator to enable what you want to do.
loading-bar.ts
import { customElement, bindable, bindingMode, autoinject } from 'aurelia-framework';
import { EventAggregator, Subscription } from 'aurelia-event-aggregator';
#customElement('loading-bar')
#autoinject()
export class LoadingBarCustomElement {
private visible = false;
private sub1: Subscription;
private sub2: Subscription;
#bindable({ defaultBindingMode: bindingMode.twoWay }) message;
#bindable({ defaultBindingMode: bindingMode.twoWay }) type = 1;
constructor(private ea : EventAggregator ) {
this.ea = ea;
this.visible = false;
}
attached() {
this.sub1 = this.ea.subscribe('show-loading', ({ message, type }) => {
this.type = type;
this.message = message;
});
this.sub2 = this.ea.subscribe('hide-loading', () => {
this.message = '';
});
}
detached() {
this.sub1.dispose();
this.sub2.dispose();
}
messageChanged(newValue, oldValue) {
if (newValue && newValue !== '')
this.visible = true;
else
this.visible = false;
}
}
and then publish the events elsewhere in your app like this:
import {autoinject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
#autoinject()
export class ExamplePage {
constructor(private ea: EventAggregator){
...
}
async methodUsingTheLoadingBar(){
...
this.ea.publish( 'show-loading, { message: 'Loading...', type: 1 });
const foo = await getData();
...
...
this.ea.publish('hide-loading');
}
}
Decided to solve it like this:
<loading-bar message.bind="lbc.loadMsg" type.bind="lbc.loadType"></loading-bar>
was added to app.html
Created a controller class which will be used as a singleton:
export class LoadingBarController {
private loadMsg = '';
private loadType = 1;
public showBar(message, type) {
this.loadType = type;
this.loadMsg = message;
}
public hideBar() {
this.loadMsg = '';
}
}
This is injected in app.ts (with alias "lbc") where it actually is connected to the loading-bar element, as well injected in every viewModel of pages that wants to use the loading bar.
The loading-bar is then controlled through the injected controller like this:
#inject(LoadingBarController)
export class ExamplePage {
constructor(private lbc: LoadingBarController){
...
}
methodUsingTheLoadingBar(){
...
this.lbc.ShowBar('Loading...', 1);
...
...
this.lbc.HideBar();
}
}

Aurelia Custom element with dialog

I am having trouble with creating a custom element that will be used like
<shimmy-dialog type="video" href="/test">Hi</shimmy-dialog>
The custim element will replace this code with a href that when clicked should popup a dialog of a particular type.
Everything seems to work up until the point I try to open the dialog.
This is when I get the error
Unhandled rejection TypeError: Cannot set property 'bindingContext' of null
I do sometimes find the Aurelia errors a little cyptic.
I suspect it has something todo with the element not having a view.
The code is as follows
enum DialogType {
video = 1,
iframe
};
#inject(Bcp, DialogController)
export class ShimmyDialogModel {
private type : DialogType;
constructor(private bcp: Bcp, private controller : DialogController){
console.log("here");
}
async activate(state){
this.type = state['type'];
}
get isVideo() : boolean {
return this.type == DialogType.video;
}
get isIframe() : boolean {
return this.type == DialogType.iframe;
}
}
#noView
#processContent(false)
#customElement('shimmy-dialog')
#inject(Element, App, Bcp, DialogService)
export class ShimmyDialog {
#bindable public type : string;
#bindable public href;
#bindable public name;
private originalContent : string;
constructor(private element: Element, private app: App, private bcp: Bcp,
private dialogService: DialogService) {
this.originalContent = this.element.innerHTML;
}
bind() {
this.element.innerHTML = '' + this.originalContent + '';
}
attached() {
let self = this;
this.type = this.element.getAttribute("type");
let dialogType = DialogType[this.type];
this.element.children[0].addEventListener("click", function(){
if(dialogType == DialogType.iframe) {
self.dialogService.open({ viewModel: ShimmyDialogModel, model: {'type' : dialogType}}).then(response => {
});
}
else if(dialogType == DialogType.video) {
self.dialogService.open({ viewModel: ShimmyDialogModel, model: {'type' : dialogType}}).then(response => {
});
}
return false;
});
}
async typeChanged(newValue) {
this.type = newValue;
}
async hrefChanged(newValue) {
this.href = newValue;
}
}
The template for the dialog is below.
<template>
<require from="materialize-css/bin/materialize.css"></require>
<ai-dialog>
<ai-dialog-header>
</ai-dialog-header>
<ai-dialog-body>
<div if.bind="isVideo">
Video
</div>
<div if.bind="isIframe">
IFrame
</div>
</ai-dialog-body>
<ai-dialog-footer>
<button click.trigger="controller.cancel()">Close</button>
</ai-dialog-footer>
</ai-dialog>
</template>
Thanks for any help.
I solved this by seperating the classes into their own files.
Aurelia did no like having two export classes there.

composing html file in aurelia

I'd like to achieve something similar as "include" in android but in aurelia:
How to inject a plain html file content into my view, with binding evaluated within the parent View, and without using a custom element?
Binding innerhtml is not enough as, according to the doc, the bindings expressions are bypassed.
As already said by Ashley, using <compose view="./your-view.html"></compose> element will work with an existing HTML file and it will inherit the parent context.
If you want to compose HTML dynamically (from a file, database, or built-up programmatically) then using the ViewCompiler will give you the best performance and flexibility, as this is one layer less than compose compared to how aurelia builds custom elements internally.
I gave a similar answer to a different (but related) question here:
Aurelia dynamic binding
You'd use the text plugin to load your HTML file as text into a variable, and then pass that to the ViewCompiler. I have a custom element for this which, in terms of performance, is probably not better than compose but it does allow for more control when working with raw html as input and you could do your own performance optimizations specific to your situation as needed:
import * as markup from "text!./your-element.html";
export class SomeViewModel {
constructor() {
this.markup = markup;
}
}
And the view:
<template>
<dynamic-html html.bind="markup"></dynamic-html>
</template>
For completeness sake, here is the custom element I encapsulated the ViewCompiler in:
import {
customElement,
TaskQueue,
bindable,
ViewCompiler,
ViewSlot,
View,
ViewResources,
Container,
ViewFactory,
inlineView,
inject,
DOM
} from "aurelia-framework";
#customElement("dynamic-html")
#inlineView("<template><div></div></template>")
#inject(DOM.Element, TaskQueue, Container, ViewCompiler)
export class DynamicHtml {
#bindable()
public html: string;
public element: HTMLElement;
private tq: TaskQueue;
private container: Container;
private viewCompiler: ViewCompiler;
private runtimeView: View;
private runtimeViewSlot: ViewSlot;
private runtimeViewFactory: ViewFactory;
private runtimeViewAnchor: HTMLDivElement;
constructor(element, tq, container, viewCompiler) {
this.element = <HTMLElement>element;
this.tq = tq;
this.container = container;
this.viewCompiler = viewCompiler;
}
public bindingContext: any;
public overrideContext: any;
public bind(bindingContext: any, overrideContext: any): void {
this.bindingContext = bindingContext;
this.overrideContext = overrideContext;
if (this.html) {
this.htmlChanged(this.html, undefined);
}
}
public unbind(): void {
this.disposeView();
this.bindingContext = null;
this.overrideContext = null;
}
public needsApply: boolean = false;
public isAttached: boolean = false;
public attached(): void {
this.runtimeViewAnchor = <HTMLDivElement>this.element.firstElementChild;
this.isAttached = true;
if (this.needsApply) {
this.needsApply = false;
this.apply();
}
}
public detached(): void {
this.isAttached = false;
this.runtimeViewAnchor = null;
}
private htmlChanged(newValue: string, oldValue: void): void {
if (newValue) {
if (this.isAttached) {
this.tq.queueMicroTask(() => {
this.apply();
});
} else {
this.needsApply = true;
}
} else {
if (this.isApplied) {
this.disposeView();
}
}
}
private isApplied: boolean = false;
private apply(): void {
if (this.isApplied) {
this.disposeView();
}
this.compileView();
}
private disposeView(): void {
if (this.runtimeViewSlot) {
this.runtimeViewSlot.unbind();
this.runtimeViewSlot.detached();
this.runtimeViewSlot.removeAll();
this.runtimeViewSlot = null;
}
if (this.runtimeViewFactory) {
this.runtimeViewFactory = null;
}
if (this.runtimeView) {
this.runtimeView = null;
}
this.isApplied = false;
}
private compileView(): void {
this.runtimeViewFactory = createViewFactory(this.viewCompiler, this.container, this.html);
this.runtimeView = createView(this.runtimeViewFactory, this.container);
this.runtimeViewSlot = createViewSlot(this.runtimeViewAnchor);
this.runtimeViewSlot.add(this.runtimeView);
this.runtimeViewSlot.bind(this.bindingContext, this.overrideContext);
this.runtimeViewSlot.attached();
this.isApplied = true;
}
}
function createViewFactory(viewCompiler: ViewCompiler, container: Container, html: string): ViewFactory {
if (!html.startsWith("<template>")) {
html = `<template>${html}</template>`;
}
let viewResources: ViewResources = container.get(ViewResources);
let viewFactory = viewCompiler.compile(html, viewResources);
return viewFactory;
}
function createView(viewFactory: ViewFactory, container: Container): View {
let childContainer = container.createChild();
let view = viewFactory.create(childContainer);
return view;
}
function createViewSlot(containerElement: Element): ViewSlot {
let viewSlot = new ViewSlot(containerElement, true);
return viewSlot;
}

ChoiceBox with custom Item in a TableView

I have a
private TableView<Indicators> tableviewIndicators;
with column
private TableColumn<Indicators, WindowsItem> tablecolumnFrame;
public static class WindowsItem {
CustomInternalWindow chrt;
private WindowsItem(CustomInternalWindow _chrt) {
chrt = _chrt;
}
public String toString() {
return chrt.getTitle();
}
}
private Indicators(String tl, WindowsItem chrt, String pne, Boolean sel) {
this.tool_col = new SimpleStringProperty(tl);
if (chrt == null) {
this.chart_col = new SimpleStringProperty("");
} else {
this.chart_col = new SimpleStringProperty(chrt.toString());
}
this.pane_col = new SimpleStringProperty(pne);
this.on_col = new SimpleBooleanProperty(sel);
this.chrt = chrt;
}
public String getTool() {
return tool_col.get();
}
public void setTool(String tl) {
tool_col.set(tl);
}
public WindowsItem getChart() {
return chrt;
}
public void setChart(WindowsItem _chrt) {
System.out.println("Indicators::setChart "+chrt.toString());
chrt = _chrt;
}
public String getPane() {
return pane_col.get();
}
public void setPane(String pne) {
pane_col.set(pne);
}
public Boolean getOn() {
return on_col.get();
}
public void setOn(boolean sel) {
on_col.set(sel);
}
public SimpleBooleanProperty onProperty() {
return on_col;
}
public SimpleStringProperty toolProperty() {
return tool_col;
}
public SimpleStringProperty chartProperty() {
return chart_col;
}
public SimpleStringProperty paneProperty() {
return pane_col;
}
}
tablecolumnFrame.setCellValueFactory(new PropertyValueFactory<Indicators, WindowsItem>("chart"));
How can I add a combobox or choicebox in tablecolumnFrame?
The following code
tablecolumnFrame.setCellFactory(new Callback<TableColumn<Indicators, WindowsItem>, TableCell<Indicators, WindowsItem>>() {
#Override
public TableCell<Indicators, WindowsItem> call(TableColumn<Indicators, WindowsItem> param) {
TableCell<Indicators, WindowsItem> cell = new TableCell<Indicators, WindowsItem>() {
#Override
public void updateItem(WindowsItem item, boolean empty) {
super.updateItem(item, empty);
if(empty){
return;
}
if (item != null) {
//final ChoiceBox<WindowsItem> choice = new ChoiceBox<>();
final ComboBox<WindowsItem> choice = new ComboBox<>();
int itemsInTab = chartsInTab.getChildren().size();// dimensione del contenuto del tab, compreso il pane
CustomInternalWindow winItem;
//
for (int i = 0; i < itemsInTab; i++) {
if (chartsInTab.getChildren().get(i) instanceof CustomInternalWindow) {
winItem = (CustomInternalWindow) chartsInTab.getChildren().get(i);
choice.getItems().add(new WindowsItem(winItem));
//choice.getItems().add(winItem.toString());
System.out.println("winItem.toString() "+winItem.toString());
}
}
return this error
SEVERE: javafx.scene.control.Control loadSkinClass Failed to load skin 'StringProperty [bean: TableRow[id=null, styleClass=cell indexed-cell table-row-cell], name: skinClassName, value: com.sun.javafx.scene.control.skin.TableRowSkin]' for control TableRow[id=null, styleClass=cell indexed-cell table-row-cell]
Why do you need special property? You can create ListView with ComboBox quite easily:
ObservableList<WindowsItem> windowsItems = FXCollections.observableArrayList();
ObservableList<WindowsItem> data = FXCollections.observableArrayList();
final ListView<WindowsItem> listView = new ListView<>(data);
listView.setEditable(true);
listView.setItems(data);
listView.setCellFactory(ComboBoxListCell.forListView(windowsItems));

Binsor: How to use dictionaries whose elements are components in the container

Any idea how to configure SortedList whose elements are components register on the container in binsor.
Crud:ICrud
{
public SortedList<string, ICrudTransfer> Proxies
{
get { return _proxies;}
set { _proxies = value; }
}
}
I would like to do something like:
component ‘proxy1′, ICrudTransfer
component ‘proxy2′, ICrudTransfer
_proxies = SortedList[of string, ICrudTransfer]()
_proxies['url1']=#proxy1
_proxies['url2']=#proxy2
component ’service’, ICrud, Crud:
Proxies = _proxies
But it doesn't work
I would like to use such as properties, arrays or list. That works
component ’service’, ICrud, Crud:
CrudProxy = #proxy3
CrudProxies = (#proxy1 , #proxy2)
Thanks
Solution:
public class TestClass:ITestClass
{
IDictionary<string, IClass1> _dictGen = null;
IList<IClass1> _listGen = null;
public IList<IClass1> ListGeneric
{
get { return _listGen; }
set { _listGen = value; }
}
public IDictionary<string, IClass1> DictionaryGeneric
{
get { return _dictGen; }
set { _dictGen = value; }
}
}
component 'c1', IClass1, Class1
component 'c2', IClass1, Class1
component 'd', ITestClass, TestClass:
DictionaryGeneric = {key1:#c1,key2:#c2}
ListGeneric = (#c1,#c2)
Without generic (IList or IDictionary) seem not to work.