1

I am building an Aurelia app which will basically allow users to show different lists for different resources. These lists share a couple of functions like a toolbar with search and refresh capabilities and a paginated list of resources.

I created this for a single resource and everything worked like a charm, but I would now need to duplicate big parts of both TypeScript code as well as HTML for the other resource lists. I decided to go for a different approach, create a custom-element with some named view-slots and an abstract view-model. This worked for the first run, but immediately after manipulating the list it would stop updating the slot contents.

Is there a way to achieve what I am trying to do?

Any help would be appreciated.

side note: I tried to create a simple Gist to demonstrate the issue, but it seems like the latest CDN version I could find does not support view-slots yet (I couldn't get it to work). :(

Basically what I am trying to do is something like:

list.html

<template>
    <div>list: ${counter}</div>
    <slot></slot>
    <button click.delegate="load()">Increase counter</button>
</template>

list.ts

import { autoinject } from 'aurelia-dependency-injection';
import { customElement } from 'aurelia-templating';

@customElement('list')
@autoinject
export abstract class List {
    public abstract counter;
    public abstract load();
}

resource.html

<template>
    <require from="./list"></require>

    <list>
        <div>resource: ${counter}</div>
    </list>
</template>

resource.ts

import { autoinject } from 'aurelia-dependency-injection';
import { List } from './list';

@autoinject
export class App extends List {
    public counter = 0;

    constructor() {
        super();
    }

    public load() {
        this.counter++;
    }
}

This gives the following output:

list: 0
resource: 0
<button>Increase counter</button>

After the button is clicked it should have increased both the list as well as the resource counter, but instead it still shows "0" for the resource. My guess is that the button is calling the load function on the abstract parent class that has no implementation and is therefor doing nothing. Changing the button to the child class would work, but is not really helping my code reduction goal. How can I keep using the implementer's (resource) binding context? Is there a way? Or am I completely off-track?

EDIT
So I found a way to work around it, but it just doesn't feel right. I changed the following bit in the List:

@bindable({defaultBindingMode: bindingMode.twoWay}) public abstract counter:number;

And then in the resource HTML:

<list counter.bind="counter">
    <div>users: ${counter}</div>
</list>

This way it synchronises the binding context for the counter property. I would prefer if there was a way to tell the List not to have its own binding context at all as this could theoretically lead to many, many bindables. Does anyone know how I could tell the List to inherit the binding context?

EDIT 2
Found another workaround for the previous EDIT, removed the bindable behaviour and implemented the bind callback in the List component. This gives me access to the parent binding context as first parameter. I save locally in the List component and then manually update the parent binding context when the counter property changes. This is still a workaround as this would make the List component quite big, but allows me not worry about bindings in the resources. The List now looks like this:

import { customElement } from 'aurelia-templating';
import { autoinject } from 'aurelia-dependency-injection';
import { observable } from 'aurelia-binding';

@customElement('list')
@autoinject
export abstract class List {
    private parentContext;
    @observable public abstract counter:number;

    bind(bindingContext) {
        this.parentContext = bindingContext;
    }

    counterChanged(newValue) {
        if (this.parentContext) this.parentContext.counter = newValue;
    }

    public abstract load();
}

I would still be interested in a way to remove the binding context for the List component altogether letting it inherit the parent binding context.

Note instruction.inheritBindingContext mentioned in this GitHub issue is acting like a one-time initial inherit and was thus not useful in my case, but might be useful for someone else.

Patrick
  • 179
  • 1
  • 8
  • Inspecting the binding context, I can see the context in `List` is updated to `new content`, but the context in your `resource.html` is still bound to `app message` – Jesse Aug 21 '18 at 14:35
  • @JessedeBruijne thanks for the comment, this makes it even more interesting. The parent context is updated, while the child context implementation to change the context was used..? – Patrick Aug 21 '18 at 14:43
  • @JessedeBruijne Updated the question to highlight the differences between the binding contexts of `List` and `resource` – Patrick Aug 21 '18 at 15:00

2 Answers2

1

Have a look at this blog post Aurelia Dynamic Compose Using compose and v-model and model, you can have different custom elements in the same list

Bender
  • 96
  • 3
  • Thanks for the link, but I can't use compose in combination with replaceable parts nor with view-slots. I could make separate compose parts, but I'd really prefer to have one single view and view-model as like a parent abstract List component that can be extended for each resource. – Patrick Aug 22 '18 at 06:06
  • Whilst this may theoretically answer the question, [it would be preferable](//meta.stackoverflow.com/q/8259) to include the essential parts of the answer here, and provide the link for reference. See [here](https://meta.stackexchange.com/a/94027/285661) for instructions how to write *better* "link-based" answers. – Jesse Aug 22 '18 at 06:41
0

After Edit 2 I was still getting issues when I had multiple resource lists implemented. It seemed like it was doing all kinds of functions x amount of times. So instead of manually syncing the binding contexts I now only update the parent (resource) context, like so:

List.ts

import { customElement } from 'aurelia-templating';

@customElement('list')
export abstract class List {
    public parentContext;
    public abstract counter:number;

    bind(bindingContext) {
        this.parentContext = bindingContext;
    }

    public abstract load();
}

List.html

<template>
    <div>list: ${parentContext.counter}</div>
    <slot></slot>
    <button click.delegate="parentContext.load()">Increase counter</button>
</template>

This is not only decreasing the complexity of my List, but also making a lot more sense and is less error prone.

Patrick
  • 179
  • 1
  • 8
  • Does the class still need to be abstract for this to work? – Jesse Aug 22 '18 at 14:42
  • @JessedeBruijne Probably doesn't, but it would make it more clear that this class is not to be used standalone. This also allows properties and functions to be abstract too, requiring the resource classes to implement those. – Patrick Aug 22 '18 at 15:21