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

Feature Request: Callback handler on view when rendered #192

Closed
ghost opened this issue Oct 20, 2015 · 29 comments
Closed

Feature Request: Callback handler on view when rendered #192

ghost opened this issue Oct 20, 2015 · 29 comments

Comments

@ghost
Copy link

ghost commented Oct 20, 2015

A lot of jQuery-style components and frameworks (including Material Design Lite) are designed to bind to DOM elements after they have been rendered. MeteorJS for example allows a "rendered" event handler to be added to a view that is called when the view is rendered into the DOM and provides an ideal place to enable jQuery components, etc. The event also provides the root DOM element for the particular view.

Right now the only way to do this seems to be to create a custom element or attribute and use the attached() callback. It would be nice to have a callback like this on the view.

@charlespockert
Copy link

@chrismbeckett I believe this has been discussed a few times in the gitter channel

The conclusion has usually been; whilst it's possible to add a hook that does something along these lines, there isn't really such a thing as "rendered" since the binding system is async, so calling it "rendered" is a bit of a misnomer.

Usually the issue with using attached() lies in that the DOM may not be in the state you expect (e.g. if show/if bindings are due to run etc), so in order to wait until the binding system has finished the best way is to push your jQuery stuff to the back of the micro task queue

Something like:

import {TaskQueue} from 'aurelia-framework';

// stuff ... inject task queue etc

attached() {
    this.taskQueue.queueMicroTask(() => {
        // Do your stuff
    });
}

So I believe the best that can be done is the above - I'm not sure if that causes any issues though but maybe it could be run after attached()

@ghost
Copy link
Author

ghost commented Oct 22, 2015

Hey, I appreciate the feedback, the info on the taskQueue is much appreciated.

@ghost ghost closed this as completed Oct 22, 2015
@schatekar
Copy link

I am trying to somehow notify phantomjs that my aurelia app is ready. I am using phantom2-render-stream library to save a page of my application as pdf. I have following code in my view model. Setting window.renderable to true signals phantomjs that page is loaded and ready for export.

  attached() {
    this.taskQueue.queueMicroTask(() => {
        window.renderable = true;
    });
  }

The statement window.renderable = true; is called before the view is actually is displayed. My pdf is coming out blank. Any idea what's wrong here?

@zewa666
Copy link
Member

zewa666 commented Jan 25, 2016

@schatekar you could setup a listener for the aurelia-composed event. Take a look at our protractor plugin which does exactly that. https://github.com/aurelia/skeleton-navigation/blob/master/skeleton-es2016/aurelia.protractor.js#L25

@empz
Copy link

empz commented Mar 18, 2016

+1 on this

I have no idea how to run code (i.e. initialize/retrigger a jquery plugin) after a binding has been rendered. Yeah, I can use attached() but that only works the first time.

@doktordirk
Copy link

@doktordirk
Copy link

@EisenbergEffect That.S exactly the issue i had the others day with the changed behaviour/order of view attaching. I d suggest to have a Blog/Doc Entry about it

@EisenbergEffect
Copy link
Contributor

@emzero What do you mean by "attached only works the first time"?

@empz
Copy link

empz commented Mar 18, 2016

@EisenbergEffect Maybe I'm not doing things the Aurelia way but consider the following case:

I have a repeat.for="item of myArray" that iterates over myArray and renders a div for each item. After the divs have been attached to the DOM, I use the attached() method to run some jQuery plugin code that does something with those attached divs.
Then whenever myArray changes (i.e. an item is added) I see the repeat.for updates and adds the item to the DOM, but attached() is not executed again so I have no way to know whenever a binding has been executed and rendered.

And as OP says, the need to do something with dynamically added elements to the DOM is very common in the world of jQuery plugins.

Is there a way to set a callback to be executed everytime a binding is updated and rendered?
If not, how would you go about running code and using the updated DOM elements?

@EisenbergEffect
Copy link
Contributor

Wait...what? You are adding a component and its attached isn't being called? That would be a serious bug. Can you provide some concrete code examples here of what you are doing? I may no be quite understanding the details...

@empz
Copy link

empz commented Mar 18, 2016

@EisenbergEffect No, it's not a component. It just HTML with Aurelia string interpolation binding.

myViewModel.html

<div repeat.for="item of myArray">
  <div class="text">${item.text}</div>
</div>

myViewModel.js

export class MyViewModel() {
  activated() {
    this.myArray = GetFromSomeExternalAPI();
  }
  attached() {
    // do something with those divs of class="text" (i.e. run some jQuery plugin)
  }

  // Here I also have set a setTimeOut function that will poll for new data from that External API and concat the response into this.myArray (which triggers the render of the repeat.for)
}

So the first time the repeat.for runs, the attached() method of the view-model executes and I can do stuff with those div.text. Problem is, whenever an item is added to myArray I see it's being attached and rendered in the DOM, but I don't know how to attach a handler to that moment so I can do something with them.

Am I making sense?

@ghost
Copy link
Author

ghost commented Mar 18, 2016

@EisenbergEffect - should this issue be re-opened and tagged as a feature request or documentation request?

The original issue was how to make Aurelia play nicely with jQuery. Either it does, and people don't understand how to do it (doc request), or Aurelia doesn't play that nicely with jQuery but people wish it would (feature request).

@EisenbergEffect
Copy link
Contributor

To solve this, you want to create a custom attribute to encapsulate your jQuery widget. Here's the general pattern for that:

@inject(Element)
export class MyPluginCustomAttribute {
  constructor(element) {
    this.element = element;
  }

  attached() {
    this.plugin = jQuery(this.element).myPlugin();
  }

  detached() {
    this.plugin.destroy();
  }
}

Then you apply it on the items you want to apply the plugin to:

<div repeat.for="item of myArray">
  <div class="text" my-plugin>${item.text}</div>
</div>

@ghost
Copy link
Author

ghost commented Mar 18, 2016

So, let's assume that this plugin requires a complex hierarchy of DOM elements to function correctly (tabs, or a complex list, etc), and let's say the template source in a VM that ultimately renders the DOM hierarchy that the jQuery UI plugin requires is a combination of components. When the attached() method for the custom attribute you demonstrated above run on that one particular component, does it ensure that the DOM has been fully rendered for all sub-components used by that component in its own template? Does that also extend to transcluded content injected from the parent, and the components it might contain, as well as conditional rendering, bindings, etc?

I think the question remains unanswered. The intention of a rendered() method on the VM is to know that all of the components on the VM have been rendered out so you can use jQuery plugins against the DOM safely. From the response by @charlespockert above, there seems to be some confusion that the attached() event in a custom attribute doesn't ensure that result?

@EisenbergEffect
Copy link
Contributor

Yes, when attached runs, all of that has happened. The only thing that has not happened is the attached callbacks being run on the children, because those callbacks run from parent to child, to model the web components specs. But, all elements will be in the dom, including the result of all conditional bindings.

There is no way to have a render hook beyond this because the browser simply doesn't tell us when it's done "painting". The attached callback is aurelia's way of telling you that the dom tree that the component is a part of is now in the document along with all children (and any parents).

@ghost
Copy link
Author

ghost commented Mar 18, 2016

Awesome - thank you for that clarification, and the sample. I have been using the attached() method, but for simple use cases, and was never sure it was going to be reliable under load. Your response has certainly addressed my concerns.

@empz
Copy link

empz commented Mar 20, 2016

@EisenbergEffect I didn't mean a render hook, I know browsers doesn't tell us when are done "painting". But the attached() method runs the first time binding has already been done and the elements are attached to the DOM. But when the underlying data changes and the view is updated, we have no way to know something has been updated and it's already attached to the DOM.

Maybe re-triggering the attached() method is not the way to go, but a way to add a handler so when data is updated and the binding triggers, updating the view component and attaching new/updated elements, we can know when they're "ready".

@EisenbergEffect
Copy link
Contributor

@emzero You can build such a handler yourself very easily using the technique above. Simply create a custom attribute that dispatches a custom bubbling dom event in its attached callback. Then just put that custom attribute on any element you care to be notified about. Finally, add an event listener inside any element you need to know about that event.

@empz
Copy link

empz commented Mar 21, 2016

@EisenbergEffect Your example works well if I needed to execute something for every element that has the custom attribute. I just need to run something once after everything have been "re-attached".
I still cannot figure out how to do it.

Is it really impossible to attach a handler whenever a view is updated because of its view-model data changed (i.e. one or more items have been added to an array that's being used in a repeat.for). Isn't that what attached() does? Why not an updated() callback too?

In your example I only know when each "item" has been attached, not when all of them. I just need to know when that update is done and it's all attached to the DOM (don't care if it's done "painting').

Sorry to be annoying with it but I can't figure out the Aurelia wayt to implement a simple ajax polling system that gets data => render the update to the DOM => once it's all attached, run some code

@EisenbergEffect
Copy link
Contributor

Attached fires whenever the current component that implements the attached callback is added to the DOM...not whenever any component inside it's content is added to the DOM.

Why do you need to depend on the DOM here at all? If you have an array of data, can't you just execute your code whenever your data changes? That would be the correct way to handle this.

@empz
Copy link

empz commented Mar 21, 2016

@EisenbergEffect I have an array of data that gets updated in a timer. New data (posts) are pushed to this array which is used in a repeat.for in the VM.
I need the DOM because I'm using the desandro/masonry plugin and I need to call the appended(posts) method with the posts (the DOM elements, not the data objects) that have been added. Of course I can't do this when I get the data because the elements are not in the DOM yet.

If I do what you suggest, I end up appending one element at a time and calling appended() several times. I want to do it only once, with the collection of new posts.

Is there a way to programatically get the output HTML that a Custom Element would render?
Something like...

In a view-model

import CustomElement;

export class MyViewModel() {
  someMethod() {
    let html = CustomElement
               .withProperty('bindableProperty', this.something)
               .withProperty('anotherBindableProperty', this.somethingElse)
               .getHtml();
  }
}

This would assign html to what the following code would render in a view

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

  <custom-element bindableProperty.bind="something" anotherBIndableProperty="somethingElse"></custom-element>
</template>

This view would have its view-model with this.something and this.somethingElse assigned of course.

@charlespockert
Copy link

@emzero you can watch the DOM for mutation events using a mutation observer. Even better you could just use the @children decorator to be notified of mutations by Aurelia. (This is assuming your DOM elements are direct child elements of the template, at the moment there's an outstanding issue for this: #250)

This would give you a collection of DOM elements that had been added and you could pass these as a collection in one go to the masonry plugin

@empz
Copy link

empz commented Mar 21, 2016

@charlespockert That sounds just what I need. Unfortunately I can't find anything on this @children decorator.
Any documentation/article on it?

@charlespockert
Copy link

@emzero search for @children here: http://aurelia.io/docs#/aurelia/framework/1.0.0-beta.1.1.4/doc/article/cheat-sheet

There are some examples of use: http://plnkr.co/edit/FauNC4JgvG1X6ugbOzYo?p=preview (I forked someone elses Plunk but look at cs-tabs.html and cs-tabs.js)

The alternative to all this is to inspect the DOM yourself using a mutation observer (but you may have to deal with browsers that don't support that) or to take advantage of your "data fetch" method... this should work assuming you know a bit about how Aurelia batches/renders the DOM

All updates to the DOM are batched by a task queue. This means several data updates that result in bindings changing in quick succession only end up as a single big DOM batch update, improving performance.

You can actually put your code on the back of this queue manually to ensure that any outstanding bindings have taken effect (and likely your DOM elements are present). An example is here:

http://blog.durandal.io/2015/06/05/building-aurelias-focus-attribute/

Just search for TaskQueue

This will probably work for you, quite a few people in the gitter channel have had jQuery plugins that needed to wait for a DOM update before they could take effect and this seems to solve the issue most of the time

@EisenbergEffect
Copy link
Contributor

Yes to what @charlespockert has said :)

@mreiche
Copy link

mreiche commented Apr 6, 2017

It is possible to delegate/trigger an event in a custom attribute?

import {inject, bindable,customAttribute} from 'aurelia-framework';
@inject(Element)
@customAttribute('render')
export class RenderCustomAttribute {
	constructor(element) {
		this.element = element;
	}
	bind() {
		console.log('bind',this.value);
	}
}
<li repeat.for="item of items" render.delegate="elementAppeared($event,item)">
</li>
elementAppeared(event,item) {
    console.log('rendered', event.target);
}

@zewa666
Copy link
Member

zewa666 commented Apr 6, 2017

@mreiche this feels more like a question rather than part of this issue. I guess what you're looking for is the EventAggregator. If you have another issue please either open a new issue or visit our official gitter channel if you need help with the EventAggregator

@mreiche
Copy link

mreiche commented Apr 7, 2017

It's on topic, because it referes to aurelia/binding#132

@EisenbergEffect wrote:

You could easily create a custom attribute that you could place on the repeat item template that would fire the event you want.

This is my solution I'm not so happy with:

import {inject,customAttribute} from 'aurelia-framework';
import {DOM} from 'aurelia-pal';

@inject(Element)
@customAttribute('element')
export class ElementCustomAttribute {
	constructor(element) {
		this.element = element;
	}
	attached() {
		let ev = DOM.createCustomEvent('attached');
		this.element.dispatchEvent(ev);
	}
	detached() {
		let ev = DOM.createCustomEvent('detached');
		this.element.dispatchEvent(ev);
	}
}
<require from="element-attribute.js"/>
<li repeat.for="item of items" element attached.trigger="elementAppeared($event,item)">
</li>

@qraynaud
Copy link

qraynaud commented Jul 3, 2019

@mreiche I'm adding this a bit late, but I have a simpler codebase for the same purpose :

import {customAttribute} from 'aurelia-framework';

@customAttribute('attached')
export class AttachedCustomAttribute {
  attached() {
    this.value();
  }
}
<require from="attached.js"/>
<li repeat.for="item of items" attached.call="elementAppeared(item)"/>

You can also add some bit of code on valueChanged to ensure value is a function and create a DetachedCustomAttribute if you also want to handle detached « events ».

This issue was closed.
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

8 participants