Aurelia Custom element with dialog - aurelia

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.

Related

Angular 5 BehaviorSubject data not update UI

I use to change dynamic child component in body and keep static header, bottom and menu.
My problem: When use BehaviorSubject as shared-data between components, then UI (*ngFor) not be updated event shared-data transferred well. I am using Angular 5.2.0, RxJs 5.5.6
My app has flow:
user click search button on Layout-top.component.ts -> fetch data from Backend server by Home.service.ts-> set data in BehaviorSubject object.
On Home.component.ts constructor always subscribe shared-data from Home.service.ts -> change data of Home.component.ts -> display them.
1. App.compoenet.ts
#Component({
selector: 'xxx',
template:
`
<gotop position="200"></gotop>
<layout-top></layout-top>
<router-outlet></router-outlet>
<layout-bottom></layout-bottom>
`
})
export class AppbComponent implements OnInit, AfterViewInit{
public ngAfterViewInit(): void {
this.spinner.hide();
}
message:string;
constructor(private spinner:Spinner){
}
public ngOnInit(){
this.spinner.show();
}
}
Layout-top.component.ts
public doSearch(){
let filter = {
xx:'XXX'
};
this.homeService.setData(filter);
}
3.Home.service.ts
#Injectable()
export class HomeService extends BaseService{
public data =new BehaviorSubject<DataType>(<DataType>{});
public eventFilter: EventEmitter<{}> = new EventEmitter();
public constructor(private http: HttpClient,
private _const: Const,
private util:Util,
private appref: ApplicationRef) {
super(_const, util);
}
public listProduct(filter):Observable<any>{
const url = url to my backend api
let headers:HttpHeaders = this.util.header(this._const, null, 'application/json');
return this.http.post(
url,
filter,
{headers})
.map(res => {
return res;
});
}
public getData():Observable<DataType>{
return this.data.asObservable();
}
public setData(filter:any):void {
const listProduct$ = this.listProduct(filter);
listProduct$.subscribe(res => {
this.data.next({res:res, filter:filter});
});
}
public cleanData() {
this.data.next(null);
}
}
layout-top.html
5.home.html
<div class="product-item"
*ngFor="let item of listProducts">
<!--display some thing here-->
</div>
6.home.component.ts
constructor(private service: HomeService,
private cdRef:ChangeDetectorRef,
private zone:NgZone,private appref: ApplicationRef ){
this.subsListProduct = this.service.getData().subscribe(obj=>{
this.zone.run(()=>{
$("#in-blur").css("display", "block");
if(!obj){
return;
}
const res = obj.res;
const filter = obj.filter;
if(res && filter){
this.listProducts = res.list;
this.cdRef.detectChanges();
}
});
setTimeout(()=>{
$("#in-blur").css("display", "none");
}, 1000);//for test loading spinner. will be remove in product
});
}
"this.listProducts = res.list;" work fine, ther listProducts be updated, but UI is not any change.
Many people advised use zone.run() or ChangeDetectorRef.detectChanges() but not work in my app. Plz support me.
The best way to map observable data to views is to use the async pipe - https://angular.io/api/common/AsyncPipe - this way all the change management and unsubscribing form the observable is handled for you. So in your example:
The view:
<div class="product-item"
*ngFor="let item of (listProducts | async)?.list">
<!--display some thing here-->
</div>
The ? before .list makes the property optional, so nothing will break if list is is null or undefined
The home component:
constructor(private service: HomeService,
private appref: ApplicationRef ){
this.listProducts = this.service.getData();
}
If you need to manipulate any of the data from the observable before displaying in the view do that in a .map, eg.
constructor(private service: HomeService,
private appref: ApplicationRef ){
this.subsListProduct = this.service.getData()
.map(obj => {
if (obj.list && obj.filter) {
return obj;
} else {
return null;
}
});
}
You should also consider dropping jquery for doing your css changes and use ngClass in your view to do this instead - https://angular.io/api/common/NgClass

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();
}
}

Binding and Displaying Data With Aurelia

I am new to Aurelia. I have a WebApi that will return some data that I would like to populate into my exported model and then display the info on the screen.I'm thinking it would go into my run event but am not sure. Can anybody tell me how to do this . Any information would be most appreciated. My code is below.
--Jason
import 'fetch';
import {HttpClient, json} from 'aurelia-fetch-client';
import {inject} from 'aurelia-dependency-injection';
declare var window: { wcApiUrl: string, wcAmtInstanceId: string };
#inject(HttpClient)
export class BureauModUpdate {
files: string;
constructor(private http: HttpClient) {
http.configure(x => {
x.defaults.headers = { 'Authorization': 'Basic ' + window.wcAmtInstanceId }
});
}
public run(): void {
//Would I put it here ??
}
upload(): void {
var form = new FormData()
for (var i = 0; i <= this.files.length; i++) {
form.append('file', this.files[i])
this.http.fetch(window.wcApiUrl + '/Lookup/BureauModUpdate/CreateBureauModUpdates', {
method: 'post',
body: form
})
}
}
}
export class BureauModUpdateHistory {
public IndexId: number;
public UploadID:number;
public EmployeeNum: number;
public filename: string;
public Bureau: string;
public UploadedDate: Date;
public UploadedStatus: string;
public ErrorInfo: string;
public RecordCount: number;
}
To make something happen when the page loads, use the attached() Aurelia component lifecycle method, like this:
attached() {
// do something here
}
For more information on the Component Lifecycle, see the documentation on Aurelia's DocHub.
Example using Fetch to get data:
For getting HTTP data using fetch (or any other web service), you need to use an async call (using .then to chain the next event). For example:
this.http.fetch(url).then(response => {
this.data = response;
}
Then, just bind your data to this.data (depending on what type of data you're getting). For example:
<template>
Hello, ${data.fname}!
</template>

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;
}

Aurelia dynamic binding

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;
}