jaanekoja
jaanekoja

Reputation: 3

Aurelia: Issue with using a shared class specification for properties in parent and child custom elements

I have a code like below where a Project has a Timeline.

I have created a class named TimelineProperties which acts as a shared specification for a properties I want to pass from a project to a child Timeline.

I have planned to use TimelineProperties class in Project custom element like this:

import {TimelineProperties} from "./timeline";

@customElement('project')
export class Project {
   timeline: TimelineProperties = new TimelineProperties();
}
<template>
   ...
   <timeline list.bind="timeline.list" loading.bind="timeline.loading" error.bind="timeline.error" />
   ...
</template>

And also inside a Timeline custom element (to share code):

Either via inheritance like this:

export class Timeline extends TimelineProperties {}

Or via composition like this:

export class Timeline {
  // TimelineProperties class has @bindable properties defined
  timeline: TimelineProperties = new TimelineProperties();
}

// and then use bindings from project.html like this:
<timeline timeline.list.bind="timeline.list" timeline.loading.bind="timeline.loading" timeline.error.bind="timeline.error" />

The issue is that I cannot use a shared specification class TimelineProperties inside a Timeline custom element either via inheritance nor composition.

  1. Issue with inheritance - https://github.com/aurelia/templating/pull/507
  2. Issue with composition - Exception in runtime: "Error: Attempted to register an Element when one with the same name already exists. Name: project"

So now I have copied the TimelineProperties fields also into a Timeline Custom Element class (see 3 @bindable properties inside timeline.ts code below) just to make it work. But I would like to avoid that code duplication.

My question is, is there some way I could use TimelineProperties class inside a Timeline Custom Element to bind data from Project Element directly into a Timeline's TimelineProperties?

Here is my full code that works by code duplication and not using a shared TimelineProperties class:

  1. project.ts - I have a parent Custom Element like this:

import {TimelineProperties} from "./timeline";

@customElement('project')
export class Project {
   timeline: TimelineProperties = new TimelineProperties();
}
<template>
   ...
   <timeline list.bind="timeline.list" loading.bind="timeline.loading" error.bind="timeline.error" />
   ...
</template>

  1. timeline.ts - And child Custom Element like this:

import {DataLoading} from "./api";

export class TimelineProperties extends DataLoading {
    @bindable list: Task[] = [];
}

@customElement('timeline')
export class Timeline {
    @bindable list: Task[] = [];
    @bindable loading: boolean = false;
    @bindable error: any;
    ...
}
<template>
...
</template>

  1. api.ts

export class DataLoading {
    @bindable loading: boolean = false;
    @bindable error: any;
}

UPDATE - Satisfied with this solution

Based on Ashley Grant's suggestions to use a decorator to initialize the bindings, I have modified my code based on that input and I am happy with it now. Here is my code now using the "extends" and a decorator to initialize the bindings in child classes:

import {DataLoading} from "./api";

export class TimelineProperties extends DataLoading {
    list: Task[] = [];
}

// To ensure a compile time errors if property names are changed in TimelineProperties
function propertyName<T>(name: keyof T){
    return name;
}
// Now define all bindings in a decorator instead of inside the classes
function timelineBindings() {
  return function(target) {
    bindable(propertyName<TimelineProperties>("loading"))(target);
    bindable(propertyName<TimelineProperties>("error"))(target);
    bindable(propertyName<TimelineProperties>("list"))(target);
  }
}

@customElement('timeline')
@timelineBindings()
export class Timeline extends TimelineProperties {
    ...
}

Upvotes: 0

Views: 153

Answers (1)

Ashley Grant
Ashley Grant

Reputation: 10887

I'm not sure why you are trying to create such a deep hierarchy of classes for this. These classes have one or two properties, so I'm not seeing what the benefit is of the inheritance tree. You're writing TypeScript, so you'd probably be better served by using interfaces for this stuff.

How many other places is TimelineProperties used in your codebase? If this is the only place then there isn't really code duplication in reality. The code duplication is caused by your insistence on using a bunch of classes to unnecessarily complicate the design.

I'm sorry if I'm misreading this from your example code, but I very often see developers going crazy with inheritance and such thinking it will help them not repeat themselves, but in the end the benefits of DRY are vastly outweighed by the increased complexity of the solution they produce.

So anyways, assuming there actually is a need to reuse these classes in multiple places, I would recommend using decorators to accomplish this.

For example: https://gist.run/?id=6ad573e051f53c8e163d36dc31dc36b6

timeline-props.js

import {bindable} from 'aurelia-framework';

export function timelineProps() {
  return function(target) {
    bindable('list')(target);
  }
}

timeline.js

import {timelineProps} from 'timeline-props';

@timelineProps()
export class Timeline {

}

timeline.html

<template>
  <ul>
    <li repeat.for="item of list">
      ${item}
    </li>
  </ul>
</template>

app.html

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

  <timeline list.bind="items"></timeline>
</template>

This is the exact type of use-case that decorators were created to handle.

Upvotes: 0

Related Questions