Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lifecycle Events Promise Proposal #367

Closed
MarkHerhold opened this issue Mar 17, 2016 · 28 comments
Closed

Lifecycle Events Promise Proposal #367

MarkHerhold opened this issue Mar 17, 2016 · 28 comments

Comments

@MarkHerhold
Copy link
Contributor

I like the concept of the lifecycle events, namely created(), bind(), attached(), detached(), and unbind(). I do think that Aurelia could provide more functionality behind these, especially in regards to waiting on async operations.

Ideally, I would like to be able to implement a hook for the created() callback, return a promise, and have Aurelia wait for it to resolve before invoking the attached() callback. I am not sure what the idea behavior should be if detached() needs to be invoked before the promise from created() resolves, but I imagine the promise would be canceled.

I feel like there is a disconnect between views that have been routed to via the router, as they get the activate() hook which respects and waits for promises, as compared to custom elements which don't respect promises at all.

As an end-user of Aurelia, I would expect to be able to do this:

import { HttpClient } from 'aurelia-fetch-client';

export class Books {
    static inject = [HttpClient];
    constructor(http) {
        this.http = http;
    }

    created() {
        // return a promise that resolves when the data is retrieved
        return this.http.fetch('/books/')
            .then(data => { this.books = data; }); // store the books locally
    }

    // fires only after the promise from `created()` either resolves or rejects
    attached() {
        // update the DOM here, e.g. draw a chart, etc
        this.numBooks = this.books.length; // the user is guaranteed that this.books will be availaible
    }
}

I hope this makes sense. I feel like Aurelia is really well thought out, so it wouldn't surprise me if there is already a way to do this.

@EisenbergEffect
Copy link
Contributor

We can't provide this across all components. It would be a disaster for performance and would no longer map in any way to web components. If you don't care about web components, you can use the new CompositionTransaction: http://aurelia.io/docs.html#/aurelia/templating/1.0.0-beta.1.1.2/doc/api/class/CompositionTransaction Simply have that injected into your component constructor and then call enlist() this will return you a CompositionTransactionNotifier: http://aurelia.io/docs.html#/aurelia/templating/1.0.0-beta.1.1.2/doc/api/interface/CompositionTransactionNotifier You can call done on that when your async operation is complete. The global composition will wait to attach until after you are done.

@MarkHerhold
Copy link
Contributor Author

This works really well and was exactly what I was after, @EisenbergEffect! You have my thanks.

Here is what @EisenbergEffect said translated into code:

import { HttpClient } from 'aurelia-fetch-client';
import { CompositionTransaction } from 'aurelia-framework';

export class YearToDateGauge {
    static inject = [HttpClient, CompositionTransaction];
    constructor(http, compositionTransaction) {
        this.http = http;

        // https://github.com/aurelia/framework/issues/367
        this.compositionTransactionNotifier = compositionTransaction.enlist();
    }

    created() {
        // retrieve the data
        this.http.fetch('/books/')
            .then(data => {
                this.books = data; // store locally

                // done loading data, allow the attached() hook to fire
                this.compositionTransactionNotifier.done();

                return data;
            });
    }

    // fires only after `compositionTransactionNotifier.done()` is called
    attached() {
        // update the DOM here, e.g. draw a chart, etc
        this.numBooks = this.books.length; // the user is guaranteed that this.books will be availaible
    }
}

@wshayes
Copy link
Contributor

wshayes commented Mar 17, 2016

Great question and answer - I saved this to my offbrain storage: http://blog.williamhayes.org/2016/03/aurelia-custom-element-async-life-cycle.html http://blog.williamhayes.org/2016/03/aurelia-custom-element-async-life-cycle.html

On Mar 17, 2016, at 6:45 PM, Mark Herhold notifications@github.com wrote:

This works really well and was exactly what I was after, @EisenbergEffect https://github.com/EisenbergEffect! You have my thanks.

Here is what @EisenbergEffect https://github.com/EisenbergEffect said translated into code:

import { HttpClient } from 'aurelia-fetch-client';
import { CompositionTransaction } from 'aurelia-framework';

export class YearToDateGauge {
static inject = [HttpClient, CompositionTransaction];
constructor(http, compositionTransaction) {
this.http = http;

    // https://github.com/aurelia/framework/issues/367
    this.compositionTransaction = compositionTransaction;
    this.compositionTransactionNotifier = this.compositionTransaction.enlist();
}

created() {
    // return a promise that resolves when the data is retrieved
    return this.http.fetch('/books/')
        .then(data => {
            this.books = data; // store locally

            // done loading data, allow the attached() hook to fire
            this.compositionTransactionNotifier.done();

            return data;
        });
}

// fires only after the promise from `created()` either resolves or rejects
attached() {
    // update the DOM here, e.g. draw a chart, etc
    this.numBooks = this.books.length; // the user is guaranteed that this.books will be availaible
}

}

You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub #367 (comment)

@EisenbergEffect
Copy link
Contributor

Glad it worked for you! :)

@wshayes
Copy link
Contributor

wshayes commented Mar 17, 2016

And Dwayne is adding it to his book as well :)

On Mar 17, 2016, at 7:05 PM, Rob Eisenberg notifications@github.com wrote:

Glad it worked for you! :)


You are receiving this because you commented.
Reply to this email directly or view it on GitHub #367 (comment)

@atsu85
Copy link
Contributor

atsu85 commented Mar 18, 2016

@MarkHerhold
following line from Your example isn't needed as i understand:
this.compositionTransaction = compositionTransaction;

@MarkHerhold
Copy link
Contributor Author

@atsu85 you are correct. The constructor could be changed to:

    constructor(http, compositionTransaction) {
        this.http = http;

        // https://github.com/aurelia/framework/issues/367
        this.compositionTransactionNotifier = compositionTransaction.enlist();
    }

@atsu85
Copy link
Contributor

atsu85 commented Mar 20, 2016

@MarkHerhold,
one more thing. I guess it doesn't make sense to return a promise from created(), as the framework doesn't do anything with it. So probably there is one more thing You should change:
To avoid misunderstanding, You should replace
return this.http.fetch('/books/')
with
this.http.fetch('/books/').

@MarkHerhold
Copy link
Contributor Author

@atsu85 Great point! I updated the code block above to reflect both changes discussed.

@EisenbergEffect
Copy link
Contributor

@MarkHerhold Does it work equally well if you enlist inside of the bind callback? and kick off the data load during bind?

@MarkHerhold
Copy link
Contributor Author

@EisenbergEffect please disregard my previous commment (deleted)

@MarkHerhold
Copy link
Contributor Author

@EisenbergEffect Yes is does. 👍

Here is an equivalent-example of what the code looks like that I tried:

import { HttpClient } from 'aurelia-fetch-client';
import { CompositionTransaction } from 'aurelia-framework';

export class YearToDateGauge {
    static inject = [HttpClient, CompositionTransaction];
    constructor(http, compositionTransaction) {
        this.http = http;

        this.compositionTransaction = compositionTransaction;
        this.compositionTransactionNotifier = null;
    }

    bind() {
        // enlist
        this.compositionTransactionNotifier = this.compositionTransaction.enlist();

        // retrieve the data
        this.http.fetch('/books/')
            .then(data => {
                this.books = data; // store locally

                // done loading data, allow the attached() hook to fire
                this.compositionTransactionNotifier.done();

                return data;
            });
    }

    // fires only after `compositionTransactionNotifier.done()` is called
    attached() {
        // update the DOM here, e.g. draw a chart, etc
        this.numBooks = this.books.length; // the user is guaranteed that this.books will be availaible
    }
}

@atsu85
Copy link
Contributor

atsu85 commented Mar 20, 2016

@EisenbergEffect

@MarkHerhold Does it work equally well if you enlist inside of the bind callback?

what would be the benefit of this approach? The down side of this approach is

  1. simplicity (more code without much difference)
  2. holding references to 2 objects instead of 1 (in fields)
    so i'd probably only store the compositionTransactionNotifier to the field (in the constructor) if there are no good reasons of doing what You asked about.

@EisenbergEffect
Copy link
Contributor

If you ever wanted to use view caching, the first option would not work because when a view is cached its created callback is only called once on the initial creation, not when it is reused. However, bind, attached, detached and unbind are always called. Also, in many cases, the async composition operation is dependent on some data, which would not usually be available until the bind phase. For the specific example above, it might not matter.

@atsu85
Copy link
Contributor

atsu85 commented Mar 21, 2016

If you ever wanted to use view caching, the first option would not work because when a view is cached its created callback is only called once on the initial creation, not when it is reused.

Good point, thanks!

@doktordirk
Copy link

any suggestion on howto handle a rejected promise here? can i cancel attaching, unbind myself or something?

edit:
stupid question. i obviously just display (in the element), whatever i like

@EisenbergEffect
Copy link
Contributor

You can't cancel those events. You probably want to plan, from a UX perspective for failure and have the component render differently if that happens.

However, my personal recommendation is to not put data access inside of "wdigets" but instead to put that inside "screens" that are controlled by the router. Then you can control navigation if there is a failure. Those screens can then bind the data into the widgets which encapsulate some common rendering.

@istrau3
Copy link

istrau3 commented May 6, 2016

@EisenbergEffect Can you expand on:

The global composition will wait to attach until after you are done.

Does this mean that if I use this method in any of my custom elements, all of my custom-elements will wait until that one promise resolves?

@EisenbergEffect
Copy link
Contributor

It means that the the highest level component at the time which is adding nodes to the DOM will wait to add those nodes until after the transaction completes,

A common example is the router. The router processes a navigation and eventually wants to add the dom for the new view into the document. However, if there's a compose element in the new view, it may be asynchronously composing another widget. The router uses the above mechanism to know when the internal composition is complete. When it is, it then adds the view to the document. This allows complex compositions that may be async to result in a single batch modification of the document once all internal async compositions are done.

@diefans
Copy link

diefans commented Jul 26, 2016

@EisenbergEffect the doc location for http://aurelia.io/docs.html#/aurelia/templating/1.0.0-beta.1.1.2/doc/api/interface/CompositionTransactionNotifier appear to have been changed. Can you point me to the right direction?

@3cp
Copy link
Member

3cp commented Aug 26, 2016

@EisenbergEffect though the current Aurelia doc didn't mention, component can implement an activate() callback which is called even before created() and attached().
The activate() callback does accept an promise as return value. I tried this on my root component (not loaded by router) and it works.

I saw the implementation here https://github.com/aurelia/templating/blob/10ccb740597fee8a42a40c70201c0cacab2fb47f/src/composition-engine.js#L69

I tested a bit, the code above does not run if the component is loaded by router. But router still triggers this activate() callback before created() and attached(). So the behaviour is consistent.

I vaguely remember activate() is one of the lifecycle hook in old Aurelia doc. Why it's removed from the doc but still stays in source code? Is it going to be deprecated? Should I use it on component which is not loaded by router?

@EisenbergEffect
Copy link
Contributor

The activate callback only works in conjunction with the compose element (or the dynamic composition apis). It's not part of the standard model.

@don-bluelinegrid
Copy link

don-bluelinegrid commented Feb 12, 2017

It means that the the highest level component at the time which is adding nodes to the DOM will wait to add those nodes until after the transaction completes,

A common example is the router. The router processes a navigation and eventually wants to add the dom for the new view into the document. However, if there's a compose element in the new view, it may be asynchronously composing another widget. The router uses the above mechanism to know when the internal composition is complete. When it is, it then adds the view to the document. This allows complex compositions that may be async to result in a single batch modification of the document once all internal async compositions are done.

@EisenbergEffect

Rob,

I have a scenario like this:

AppRouter - tabA | tabB | tabC
ChildRouter - tabA.1 | tabA.2 | tabA.3

tabA and tabB have child routers, and the child views depend on the result/Promise of an API call that's made in the parent's (tabA) constructor() method. So, this means that really, we shouldn't display the tabA child, or allow the tabA child bind/attached methods to be called, until the tabA parent Promise resolves - so that the child lifecycle methods can get access to the Promise result.

I haven't seen a straightforward way to do this. In Angular, the router provides an optional 'resolve:{}' config object, which awaits for a Promise to resolve before completing the active routing. Is there anything you can suggest to emulate that type of behavior? Or would it make a difference if I moved my API async call from the tabA parent's constructor() to a different method, like attached() or bind() or activate()?

Thanks,
Don

@EisenbergEffect
Copy link
Contributor

@don-bluelinegrid Definitely move that out of the constructor. If you return a promise from activate, you should get the behavior you desire.

@don-bluelinegrid
Copy link

@EisenbergEffect

Rob,

Thanks, I see this in the docs now, which I had missed before.

I had tried to attach a new .then() to my existing Promise that returns the required data, which worked. But returning a Promise in the activate() of the parent VM is clearly the correct, and more elegant, way to cause routing to wait for required/resolved data.

Thanks,
Don

@frvncis
Copy link

frvncis commented Jul 19, 2018

Hi @EisenbergEffect,
I'm currently working on a Aurelia SSR app.
This app is just a front-end displayer. Most of the data displayed are coming from an external API. As this app must be SEO friendly, I need to have these data displayed in the source code at the page rendering.
Right now I have the API call with aurelia-http-client in the activate() method of my VM. WIth this setup,
the data appears in the template after the page was rendered. This is not an issue for the UX as it's pretty fast but it is for the SEO as the data loaded from the API are not in the source code of the HTML page.
I tried 3 ways to have these data in the source code before the page is rendered:

  1. Add async before the activate method and await on the HTTP call
  2. Return a promise in the activate method
  3. Use the solution above with the CompositionTransaction

None of them helped me to reach my goal of having the data in the source code.

Here is my implementation of the third solution :

@inject(CompositionTransaction, HttpClient)
export class MyVM {
  http: HttpClient;
  compositionTransaction: CompositionTransaction;
  compositionTransactionNotifier: CompositionTransactionNotifier;

  constructor(compositionTransaction, httpClient) {
    this.http = httpClient;
    this.compositionTransaction = compositionTransaction;
    this.compositionTransactionNotifier = null;
  }

  bind() {
    this.compositionTransactionNotifier = this.compositionTransaction.enlist();
    const url = "test.json";
    this.http.get(url)
      .then(data => {
        let dataParsed = JSON.parse(data.response)
        this.processData(dataParsed);
        this.compositionTransactionNotifier.done();
        return data;
      });
 }

This implementation makes the app lag in the browser without any error server side or client side.
Am I doing something wrong?

Is there a better solution to have the data in the source code before the page is rendered with Aurelia SSR?

Thanks a lot for your help.

@bigopon
Copy link
Member

bigopon commented Sep 28, 2018

@frvncis If you need data to be loaded, and loaded fast, maybe you should load early. I see that you mentioned employing activate, why it didn't work for you?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests