Sign Up for Free

RunKit +

Try any Node.js package right in your browser

This is a playground to test code. It runs a full Node.js environment and already has all of npm’s 400,000 packages pre-installed, including aurelia-class-enhancements with all npm packages installed. Try it out:

var aureliaClassEnhancements = require("aurelia-class-enhancements")

This service is provided by RunKit and is not affiliated with npm, Inc or the package authors.

aurelia-class-enhancements v2.0.1

Enhance your Aurelia's classes with high order functionality

aurelia-class-enhancements

Travis Coveralls github David

Enhance your Aurelia's classes with high order functionality

Introduction

If you are wondering why I built this, go to the Motivation section.

Here's a really basic example of what you can achieve using this library:

// myComponent.js
import enhance from 'aurelia-class-enhancements';
import LogStatus from './logStatus';

@enhance(LogStatus)
class MyComponent {}

export { MyComponent };

// logStatus.js
class LogStatus {
  attached() {
    console.log('attached :D!');
  }
  detached() {
    console.log('detached D:!');
  }
}

When your component gets attached to the DOM (or gets detached), via the enhancement class LogStatus, it will log a message on the console; all of this without having to write the functionality on your component class and without needing class inheritance.

Usage

As you saw on the example above, the way it works is pretty straightforward:

  1. Define an enhancement class with methods you want to be called when the ones of the target class are triggered.
  2. Add the enhancement to the target class using the enhance decorator.

Access the target class

When instantiating an enhancement, the library sends to a reference of the target class instance to the enhancement constructor, so you can access methods and properties on your custom methods.

Let's say you have a component that renders a form where the user saves some important information, and if the user were to leave the route without saving the information, you would want to use a prompt to ask for confirmation.

A basic approach would be to have a flag indicating if the changes are saved and then use the canDeactivate lifecycle method to decide whether to show the prompt or not:

class MyForm {
  isSaved = false;
  ...
  canDeactivate() {
    // If everything is saved, go away.
    if (isSaved) {
      return true;
    }

    // Let's make the prompt async.
    return new Promise((resolve) => {
      // ask for the user confirmation.
      const answer = confirm('Confirm that you want to leave without saving');
      // resolve the promise with the answer.
      resolve(answer);
    });
  }
}

export { MyForm };

It works, is simple and you didn't have to involve any external logic. But now, what if you have to implement this same logic on four more forms? That's when an enhancement is useful.

Let's write an enhancement that uses the reference for the ViewModel in order to verify if the user needs to be prompted.

class FormConfirmation {
  constructor(viewModel) {
    this._viewModel = viewModel;
  }

  canDeactivate() {
    if (this._viewModel.isSaved) {
      return true;
    }

    return new Promise((resolve) => {
      const answer = confirm('Confirm that you want to leave without saving');
      resolve(answer);
    });
  }
}

It's the same functionality, but it now checks the isSaved property form the ViewModel.

In this case it was really easy because the property is a boolean and the if checks with falsy, but we could've check if the property was defined and use a different default, like "if the ViewModel doesn't have the property, the user can always leave without confirmation".

Ok, let's add it to the form:

import enhance from 'aurelia-class-enhancements';
import { FormConfirmation } from '...';

@enhance(FormConfirmation)
class MyForm {
  isSaved = false;
  ...
}

export { MyForm };

And that's all, you can now add it to the other four forms using the enhancement.

Dependency injection

Just like on any other class you use in the Aurelia context, you can use the @inject decorator on an enhancement in order to inject dependencies.

We'll use the enhancement from the first example and trigger an event before the log messages.

import { inject } from 'aurelia-framework';
import { EventAggregator } from 'aurelia-event-aggregator';

@inject(EventAggregator);
class LogStatus {
  constructor(viewModel, ea) {
    this._viewModel = viewModel;
    this._ea = ea;
  }
  attached() {
    this._ea.publish('something:attached', this._viewModel);
    console.log('attached :D!');
  }
  detached() {
    this._ea.publish('something:detached', this._viewModel);
    console.log('detached D:!');
  }
}

Enhance an enhancement

Since the library was made to enhance any kind of class, that means that you could also enhance an enhancement class.

Let's take the example about dependency injection and move the events part to another enhancement:

import { inject } from 'aurelia-framework';
import { EventAggregator } from 'aurelia-event-aggregator';

@inject(EventAggregator);
class PublishStatus {
  constructor(viewModel, ea) {
    this._viewModel = viewModel;
    this._ea = ea;
  }
  attached() {
    this._ea.publish('something:attached', this._viewModel);
  }
  detached() {
    this._ea.publish('something:detached', this._viewModel);
  }
}

export { PublishStatus };

Now we can create an enhanced LogStatus with PublishStatus:

import enhance from 'aurelia-class-enhancements';
import { PublishStatus } from '...';

@enhance(PublishStatus)
class LogStatus {
  attached() {
    console.log('attached :D!');
  }
  detached() {
    console.log('detached D:!');
  }
}

That was just to prove the point that you can enhance an enhancement, but there are two other, and simpler, ways in which you can achieve the same result:

The decorator as a function

Decorators are just functions, in this case, a function that returns a function:

enhance(...Enhancements)(TargetClass): Proxy<TargetClass>

So, instead of enhancing LogStatus with PublishStatus, we can create a new enhancement with both of them:

import enhance from 'aurelia-class-enhancements';
import { LogStatus } from '...';
import { PublishStatus } from '...';

export const PublishAndLogStatus = enhance(PublishStatus)(LogStatus);

Multiple enhancements at once

The enhance decorator supports multiple enhancements as parameters, so we could just send LogStatus and then PublishStatus to the class with want to enhance and the result would be the same:

import enhance from 'aurelia-class-enhancements';
import { LogStatus } from '...';
import { PublishStatus } from '...';

@enhance(LogStatus, PublishStatus)
class MyComponent {}

Lifecycle method

Let's say we have this enhancement:

class FormConfirmation {
  constructor(viewModel) {
    this._viewModel = viewModel;
  }

  canDeactivate() {
    if (this._viewModel.isSaved) {
      return true;
    }

    return new Promise((resolve) => {
      const answer = confirm('Confirm that you want to leave without saving');
      resolve(answer);
    });
  }
}

But on the ViewModel, you want to add some other functionality that also needs to run on the canDeactivate, but only if the enhanced method returned false.

Modifying the signature of the enhanced method to add an extra parameter wasn't an option, as it could end up messing up methods with optional parameters.

The easiest way to solve this was adding a lifecycle method the library will call, if defined, with whatever was returned from the enhanced method.

The name of the method is dynamically generated based on the name of the original method: enhanced[OriginalMethodName]Return (even if the method name starts with lowercase, the library will make the first letter uppercase).

This is its signature:

enhancedCanDeactivateReturn(value, enhancementInstance): void

Development

NPM/Yarn tasks

TaskDescription
testRun the project unit tests.
lintLint the modified files.
lint:allLint the entire project code.
docsGenerate the project documentation.
todoList all the pending to-do's.

Repository hooks

I use husky to automatically install the repository hooks so the code will be tested and linted before any commit and the dependencies updated after every merge. The configuration is on the husky property of the package.json and the hooks' files are on ./utils/hooks.

Testing

I use Jest with Jest-Ex to test the project. The configuration file is on ./.jestrc.json, the tests are on ./tests and the script that runs it is on ./utils/scripts/test.

Linting

I use ESlint with my own custom configuration to validate all the JS code. The configuration file for the project code is on ./.eslintrc and the one for the tests is on ./tests/.eslintrc. There's also an ./.eslintignore to exclude some files on the process. The script that runs it is on ./utils/scripts/lint.

Documentation

I use ESDoc to generate HTML documentation for the project. The configuration file is on ./.esdoc.json and the script that runs it is on ./utils/scripts/docs.

To-Dos

I use @todo comments to write all the pending improvements and fixes, and Leasot to generate a report. The script that runs it is on ./utils/scripts/todo.

Motivation

I put this at the end because no one usually reads it :P.

The example about prompting the user when there's a form unsaved was a real case requirement for me, and while discussing which would be the best approach to implement it, there were three conclusions:

  1. OOP inheritance on the ViewModels sounds like a terrible idea; and you are limited to only one "enhancement".
  2. Mixins on "real classes" was a no-go.
  3. It should be something like React's high order components (I know high order functionality is not specific to React, but that's where it shines).

So, I wanted something similar to React's HOCs, but for classes, and a little bit closer to a HOF (function), since there's no JSX on Aurelia.

Lately, I've been playing around with Proxies on some of my other libraries, and they are really powerful; so I thought I could use a proxy on top of a class and the only complicated part would be to solve the dependency injection, as I wanted the enhancements to be able to access other services.

I got a prototype working and that's when I realized that Aurelia makes "heavy use" of decorators, so instead of having a function to enhance the class before exporting it (like the React approach), I could "decorate the class":

@enhance(MyEnhancement)
class MyComponent {}

Yay :D!

RunKit is a free, in-browser JavaScript dev environment for prototyping Node.js code, with every npm package installed. Sign up to share your code.
Sign Up for Free