aurelia / templating

An extensible HTML templating engine supporting databinding, custom elements, attached behaviors and more.
MIT License
116 stars 103 forks source link

Feature request: templated lifecycle callbacks #378

Closed davismj closed 8 years ago

davismj commented 8 years ago

Problem: Lifecycle callbacks are only available to the view/viewModel pair

I have an application that is closely tied to the DOM. I need to keep track of the size and position of the elements that represent the objects behind them. I need to access lifecycle callbacks on the various elements composed into the view. I could achieve this by creating custom elements or composed viewModels for each of the elements, but this would decouple the elements from their parent, and that will not work because these are tightly coupled.

myViewModel.js

export class MyViewModel {

    // my root view model has important properties 
    // that all other functions and objects need to use
    constructor() {
        this.importantProperty = 'veryimportant';
        this.things = [];
    }

    // i create things in the view model that are
    // represented in the dom
    createThing() {
        this.things.push({
            isAThing: true
        });
    }

    // i do things with things in the view model 
    // that depend strongly on the root view model
    doSomethingWithThing(thing, property) {
        thing[property] = `${this.importantProperty}${property}`;
    }

    // but i need to know all about the dom representation
    // of the things in the view model
    doAnotherThingWithThing(thing) {
        console.log(`the height of the thing is ${thing.height}`);
    }

    lookAndSeeWhatSizeThisThingIs(element, thing) {
        thing.height = element.clientHeight;
        thing.width = element.clientWidth;
        console.assert('That was easy!');
    }
}

myViewModel.html

<template>

    <!-- these things can change in size and shape, and I have
        no idea what they will be until runtime, so ideally I'd like to 
        write something like this -->
    <div repeat.for="thing of things"
        attached.delegate="lookAndSeeWhatSizeThisThingIs($element, thing)">
        <img src="img/${$index}.png" />
    </div>

</template>

Proposal: Templated Lifecycle Callbacks

We have a convention over configuration system in place for the composition lifecycle that depends on named methods on the root viewModel. We could extend that directly into our system today by firing CustomEvents that match the lifecycle stage names if no viewModel method was found.

The biggest problem with this implementation is bubbling. Currently, if I were to manually fire an 'attached' CustomEvent on the above element,. I must set bubbles: true on the EventInit, or the attached binding will not fire, even if the event is triggered on the same element where the binding is located. However, if the event bubbles, this could erroneously trigger parent callbacks as well, which is undesirable.

EisenbergEffect commented 8 years ago

Why not just use a custom attribute that grabs the view model and makes any associations or calls any methods?

export class LifecycleCustomAttribute {
  bind(bindingContext, overrideContext) {
    //this gives you the data...so you can do whatever you want with it
    //custom attributes can implement all lifecycle hooks
  }
}
davismj commented 8 years ago

So I took your advice with the custom attribute route, and it works pretty well. Here's the code:

attachable.js

import {autoinject} from 'aurelia-framework';

@inject(Element)
export class AttachableCustomAttribute {

    constructor(element) {
        this.element = element;
    }

    attached() {
      this.element.dispatchEvent(
          new CustomEvent('attached'));
    }
}

view.html

<div repeat.for="thing of things"
    attached.trigger="lookAndSeeWhatSizeTheThingIs($event, thing)" attachable>

This approach works pretty well, as it allows me to achieve what I'm looking to do declaratively. Additionally, I was able to encapsulate the behavior by not bubbling and using a trigger binding.

Still, I imagine this is something that might have a place in the default templating system, for the same reasons as the ref attribute mentioned here: https://github.com/aurelia/templating/issues/376