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

Need way to reactively depend on data context / template arguments #2010

Closed
dgreensp opened this issue Apr 4, 2014 · 54 comments
Closed

Need way to reactively depend on data context / template arguments #2010

dgreensp opened this issue Apr 4, 2014 · 54 comments
Labels
confirmed We want to fix or implement it

Comments

@dgreensp
Copy link
Contributor

dgreensp commented Apr 4, 2014

As part of a template, you often want to respond reactively to changes in the input (i.e. the arguments / the data context of the template). For example, you want to kick off a CSS animation when some text changes, or you want to feed an argument into a jQuery plugin.

Currently, compiled templates get under-the-hood reactive access to the data context, and lifecycle callbacks get non-reactive access (this.data). There should be a way to get reactive access to the data context in cases where you can't just hard-code the source (e.g. a Session variable).

Based on discussion in #2001.

@mizzao
Copy link
Contributor

mizzao commented Apr 4, 2014

Very important. If this were possible, we wouldn't need to write no-op helpers to carry out certain actions using reactivity as we are forced to now. Such helpers are problematic because 1) they are a hack that has no purpose being in the template, and 2) can't access the template instance anyway.

@ai10
Copy link

ai10 commented Apr 4, 2014

Consider more distinction between a Template class, a Template instance,
the Data Context, using an Angular like 'dirty' re-evaluation on a reactive Instance.

First, instead of 'this' in a Template instance helper being simply Data Context perhaps we could initialize the instance 'this' properties in the Template.created / instantiated / init function.
A default init instance = dataContext
we could extend this with an object array like helpers

   Template.myUI.init (
      { max: 100 }
      { things_cursor:  Things.find { family: @data.family } }
      { highlightClass: ->
            if @value > @max then return 'highlight'
            'lowlight'
       })
       ... // and then 'this' on the instance is extended with each of the computed init objects.

   Template.myUI._tidy = ->
        //re-compute the dirty instance properties perhaps by registering the Deps.dependency in init.
       // so if data.family changes rerun 'thing_cursor' 

   Template.myUI.filteredThingsA = ->
      _.filter @things_cursor.fetch(), (thing) -> return thing.A

   Template.myUI.filteredThingsB
     _.filter @things_cursor.fetch(), (thing) -> return thing.B

@artpolikarpov
Copy link

I just have no idea how to initialize/reinitialize a custom widget .

Let say I have uploader.html:

<template name="uploader">
    <input id="{{_id}}"
           type="hidden" />
</template>

...and I want to initialize it like this:

whateverWidget("#" + _id);

I want to reinitialize the widget when _id changes. Where to do this?

It doesn’t work in Template.uploader.rendered callback. And also doesn’t work in Template.uploader._id helper, because when helper is called input is not updated yet. It is possible to initialize with timeout:

Template.uploader._id = function () {
  var _id = this._id;
  setTimeout(function () {
    whateverWidget("#" + _id)
  }, 10);
  return _id;
}

...but I think it’s not a good idea.

Does this issue cover my case?

@ghost
Copy link

ghost commented Apr 6, 2014

as a workaround maybe this will work

var mutationsObserver = function (targetNodes, callback){
    var MutationObserver    = window.MutationObserver || window.WebKitMutationObserver;
    var myObserver          = new MutationObserver (mutationHandler);
    var obsConfig           = { childList: true, characterData: true, attributes: true, subtree: true };

    targetNodes.each ( function () {
        myObserver.observe (this, obsConfig);
    } );

    function mutationHandler (mutationRecords) {
        console.info ("mutationHandler:");
        callback();

    }
}

Template.someTemplate.rendered = function (){
    mutationsObserver($("#target"), function (){
        doSomthing();
    })
}

@mizzao
Copy link
Contributor

mizzao commented Apr 7, 2014

@artpolikarpov good point, I actually use Meteor.defer to do that right now and it's also not too pretty.

@timtch although a reasonable workaround, that is going way outside of the Meteor API and probably introducing unnecessary complexity; it probably would not make sense to depend on this in core Meteor code - I'm guessing we will still need to come up with a good way to do this in a more straightforward way.

@ghost
Copy link

ghost commented Apr 7, 2014

@mizzao the observer should be disconnected() on destroyed. In my case it works

@ai10
Copy link

ai10 commented Apr 7, 2014

+1 @timtch ; maybe some sugar could make it more palatable.
@rafaelw - https://github.com/rafaelw/mutation-summary

@davidworkman9
Copy link
Contributor

Some suggestions:

Emit an event on an element when it's updated. Example:

Template.someTempl.rendered = function () {
  this.$('.item').on('blaze-update', function (e) {
     // maybe e has some information about what exactly happened, if there's multiple deps on a single element
  });
};

In a rendered give this a property named Deps that will automatically be cleaned up on destroyed. Example:

Template.someTmpl.rendered = function () {
    this.Deps.autorun(func);
};

Make accessing the template data reactive, like it's own local session. Example:

Template.someTmpl.rendered = function () {
  var self = this;
  Deps.autorun(function () {
    var id = self.data._id(); // note it's now a function.. either this or maybe self.data._id.get()
    doSomething(id);
  });
};

I ran into a problem related to the above, I had a template like so:

<template name="tmpl">
   {{> showDetail _id=selectedId }}
</template>

JS:

Template.showDetail.rendered = function () {
    var id = self.data._id;
    Deps.autorun(function () {
        self.$('.edit').editable({
            success: function (newValue) {
                myColl.update({ _id: id }. { $set: { val: newValue} });
            }
        });
    });
};

I don't know if you can guess, but when selectedId changes, and showDetail gets passed the new ID, the rendered event never changes, so any success callbacks update the old selectedId. HUGE problem. Glad our tests found it because it was not immediately obvious to me. I'd really like to know how I should approach this..

@tmitch
Copy link

tmitch commented Apr 7, 2014

@charlesjshort typo i guess

@mitar
Copy link
Contributor

mitar commented Apr 8, 2014

I think this could be easily solved by simply making data reactive. You could use defineProperty to make getter a function which records a dependency. Because this is on the server side you do not have to worry of defineProperty not being supported on all browsers. So this would be a backwards compatible way to achieve this.

Other approach would be to simply change API from templateInstance.data to templateInstance.data().

BTW, if we are changing this, why would not this always point to data? In callbacks, event handlers and template helpers? Unifying this would make this really easy and easy to reuse code.

@mizzao
Copy link
Contributor

mizzao commented Apr 8, 2014

@mitar Not sure what you are talking about about the server side; I think this is all client side.

I think it would actually be really helpful to be able to access template instances (somehow) from helpers, especially given some of the current limitations in how we can use inputs reactively. Of course, if we are able to get fully reactive inputs in rendered, then this would not be necessary.

@mitar
Copy link
Contributor

mitar commented Apr 8, 2014

Oh, my bad. I do not know how I confused this. :-(

Yes, also accessing template instance in helpers is needed. There is a ticket #1529 for this.

@tmitch
Copy link

tmitch commented Apr 8, 2014

dat spam, please write the right person ...

2014-04-08 20:09 GMT+02:00 Mitar notifications@github.com:

Oh, my bad. I do not know how I confused this. :-(

Yes, also accessing template instance in helpers is needed. There is a
ticket #1529 #1529 for this.

Reply to this email directly or view it on GitHubhttps://github.com//issues/2010#issuecomment-39881844
.

@yankeyhotel
Copy link

Ok, I am not going to pretend to know as much as everyone on here, but I have been trying to solve this issue for over a week since updating to Meteor 0.8.0. I have put together a small little repo as best I know how to describe the problem that I am having related to this issue. Basically outside of using a setTimeout I am unable to use jQuery to affect the data being loaded into the DOM.

https://github.com/yankeyhotel/meteor-rendered-test2

In my mind I do think something like a callback for when the data is accessible in the Template would be good. I started noticing this issue when I had the same x-editable problem @davidworkman9 had on Issue #2001.

Hopefully this helps someone else and informs the team on how people are using it. If I've got anything wrong just let me know. Still kinda new at all this and just trying to keep up. Thanks again.

@TimHeckel
Copy link

+1 - would welcome a Template.updated callback that fired reactively based on @dgreensp's initial comment:

There should be a way to get reactive access to the data context in cases where you can't just hard-code the source

Therein, this.data would be the reactive data context, with some mechanism letting me know what just changed...

AND while I say that...I realize it's not ideal: if I have an each helper in my template and simply wanted to push all of the rendered nodes into a jquery widget, I would be back to square one with rendered pre blaze, where updated is called n-number of times, and the jquery widget ought to only be instantiated once after it's all done.

@timtch's use of the MutationObserver is really interesting too...

@DenisGorbachev
Copy link
Contributor

+1 for Template.updated.

We have a lot of code which is based on old Template.rendered semantics. Porting it to 0.8 would be a real pain without Template.updated.

@aaronjudd
Copy link

Personally, I think I would be happy with a Template.destroy() method, that caused the whole rendered process to happen again the next time the template was instantiated.

@fletchtj
Copy link

+1 for Template.updated.

I'm running into problems much like @yankeyhotel and @DenisGorbachev, and would prefer not to depend on workarounds such as @timtch has put together...only to have to replace when the proper "Meteor way" is finally introduced. Hopefully, that didn't come across as whiney. I plan to try the MutationObserver workaround on one or two of my current issues, so appreciate the comment @timtch.

@mitar
Copy link
Contributor

mitar commented Apr 14, 2014

I think the easiest workaround for now would be to send a custom jQuery object whenever data changes. So you have a dummy template helper which sends an event you listen to in a listener you attach in created and detach in destroyed.

@davidworkman9
Copy link
Contributor

@mitar one of the solutions I gave above was pretty much that:

Template.someTempl.rendered = function () {
  this.$('.item').on('blaze-update', function (e) { });
};

Except you don't need to turn off the event on template.destroyed, when the element is removed it's automatically cleaned up.

@davidworkman9
Copy link
Contributor

If we went that way, it'd be nice if there was some extra info that came along with it, for example:

$('.item').on('blaze-update', function (event, template, changedData) { });

@mitar
Copy link
Contributor

mitar commented Apr 14, 2014

@davidworkman9: Yes, but I am proposing that one can implement this for a workaround in meantime. I do not think that official solution should involve events because Meteor does not really use them anywhere. But for a workaround is good enough.

@davidworkman9
Copy link
Contributor

@mitar, I see. The more I think about this I like the idea of an emitted event, you could even define it in the event map. A Template.updated event doesn't really make a whole lot of sense. Why have the same function execute every time any of the templates dependencies change when you could listen to individual DOM nodes.

@mitar
Copy link
Contributor

mitar commented Apr 14, 2014

The good question is why we would need both rendered and updated then?

@awatson1978
Copy link
Contributor

Simple: rendered happens on the first instance; updated happens on all subsequent instances (2 through N). Basic number theory.

@mitar
Copy link
Contributor

mitar commented Apr 14, 2014

But does this difference warrant a new callback? You can know yourself it you are called first time or nth time. If you need to know that, you keep a counter.

@awatson1978
Copy link
Contributor

Correct me if I don't understand the inner workings of Blaze, but the whole ideas is that rendered only gets called once in Blaze. So a counter doesn't really help in that situation. If rendered were firing like it used to, then sure... a counter would be an easy fix. But people specifically wanted rendered to fire only once. Which leaves the people who were depending on the old behavior in the lurch.

@mitar
Copy link
Contributor

mitar commented Apr 14, 2014

What I am suggesting is that maybe rendered should start firing multiple times again, every time data changes.

@mitar
Copy link
Contributor

mitar commented Apr 14, 2014

(With different semantics, that it is not called when rerendering happens, which now does not happen anymore, but when data changes. So more like: rerender if you have something custom, here is new data.)

@avital
Copy link
Contributor

avital commented Apr 14, 2014

@mitar Yes, we definitely need to let you know when the data context changes. That is what this issue is about.

For everyone else, please refrain from talking about an .updated() callback here -- it's off-topic. We can have that discussion on meteor-core. In that discussion, please discuss specific use-cases. Simply the fact that it existed before isn't a use-case.

@mizzao
Copy link
Contributor

mizzao commented Apr 15, 2014

Another issue, related to this,, seems to be that the this object given in the rendered callback never updates; this wasn't a problem before because the rendered callback would run each time whenever the data changed. So If the rendered callback sets up a Bootstrap popover, for example, the popover will draw whatever data was given to the template initially, not after it updated. This sucks for a number of reasons.

I'm really looking forward to the next iteration of blaze.

EDIT: Workaround for this is to grab the updated data directly from the element.

Template.foo.rendered = ->  
  $(@firstNode).popover
    html: true
    placement: "auto right"
    trigger: "hover"
    container: @firstNode
    content: =>
      # FIXME: Workaround as popover doesn't update with changed data
      # https://github.com/meteor/meteor/issues/2010#issuecomment-40532280
      UI.toHTML Template.bar.extend data: UI.getElementData(@firstNode)

@kcarnold
Copy link

The folks at React have thought through a lot of the issues in making a reactive DOM extensible for third-party components. I'd be surprised if Blaze ends up needing significantly fewer events than theirs: http://facebook.github.io/react/docs/component-specs.html

@davidworkman9
Copy link
Contributor

@mizzao I mentioned that exact problem above where I suggested that this.data be a method that's reactive. Thanks for the tip about UI.getElementData as a workaround, didn't know that existed.

@mizzao
Copy link
Contributor

mizzao commented Apr 16, 2014

@davidworkman9 it's the replacement for the old Spark.getDataContext.

I think a reactive this.data() combined with UI.emboxValue would allow a lot of use cases to depend on exactly the changed fields that they would need.

@mitar
Copy link
Contributor

mitar commented Jul 28, 2014

Is there any update on this?

@mizzao
Copy link
Contributor

mizzao commented Jul 28, 2014

@mitar there is a ton of stuff in Meteor 0.8.3. Specifically, Blaze.View#autorun and Blaze.ReactiveVar are probably going to be your tools of choice.

@gadicc
Copy link
Contributor

gadicc commented Aug 17, 2014

and Blaze.getCurrentData(), which is also reactive.

@dgreensp, we can close this issue now, no? Or is there still something not addressed? I know all my needs have been met :)

@mitar
Copy link
Contributor

mitar commented Aug 17, 2014

Hm, what is difference between UI.* and Blaze.*?

@gadicc
Copy link
Contributor

gadicc commented Aug 17, 2014

MDG will correct me if I'm wrong, but it looks like it's being phased out. The Blaze-like functions seem to only be in the UI namespace for backwards compatibility (i.e. ui.js#L36-38), and similar for some Handlebars stuff in handlebars_backcompat.js file. Beyond that, it offers global helpers (which are still very important) and some escaping functions.

@dgreensp
Copy link
Contributor Author

I'm currently working on merging UI.* and Blaze.*, so that UI is just an alias for Blaze. (If you were using Blaze outside of Meteor, you might want to call it Blaze instead of UI.)

@awatson1978
Copy link
Contributor

+1 for UI being an alias of Blaze and not being hardcoded as the API.

@Sewdn
Copy link

Sewdn commented Sep 1, 2014

Great new about the new reactive context Blaze.getCurrentData() in combination with the autorun of view! Thanks a lot!

One more question: Do you need to stop the autorun on views or are they automatically stopped when the view is being destroyed?

@dgreensp
Copy link
Contributor Author

dgreensp commented Sep 1, 2014

Autoruns on Views are automatically stopped when the View is destroyed.

On Monday, September 1, 2014, Pieter Soudan notifications@github.com
wrote:

Great new about the new reactive context Blaze.getCurrentData() in
combination with the autorun of view! Thanks a lot!

One more question: Do you need to stop the autorun on views or are they
automatically stopped when the view is being destroyed?


Reply to this email directly or view it on GitHub
#2010 (comment).

@Sewdn
Copy link

Sewdn commented Sep 1, 2014

ok, nice.
And does the API provide access to the computations these autoruns return?

@dgreensp
Copy link
Contributor Author

dgreensp commented Sep 1, 2014

Yup, the computation is returned.

On Monday, September 1, 2014, Pieter Soudan notifications@github.com
wrote:

ok, nice.
And does the API provide access to the computations these autoruns return?


Reply to this email directly or view it on GitHub
#2010 (comment).

@Sewdn
Copy link

Sewdn commented Sep 1, 2014

Sorry, I wasn't clear. I know they are being returned (as with every autorun), but I wondered if you could access all the view computation instances from other lifecycle callbacks (without manually registering them).

Something like this.computations from within a view lifecyclecallback, or UI._templateInstance().computations from a helper

This can come in handy when you want to stop all view computations manually and rerun them later on (when the view is being hidden, but not destroyed for instance)

@dgreensp
Copy link
Contributor Author

dgreensp commented Sep 1, 2014

No, you'll need to keep track of the Computation objects yourself in this case.

@mcbain
Copy link

mcbain commented Sep 7, 2014

Does getCurrentData allow any filtering on certain attributes somehow?

@rijk
Copy link

rijk commented Sep 9, 2014

Thanks! Using Template.currentData() inside of a template.autorun function did the trick for me (using a Wistia video widget which was not cleared from the DOM when switching to a different video):

Template.watchVideo.rendered = ->
    @autorun( () ->
        video = Template.currentData();
        $( "wistia_#{video.wistia_id}" ).empty()
        wistiaEmbed = Wistia.embed( "#{video.wistia_id}", videoFoam: true )
    )

IMO the docs could be a little more verbose on the utility of these functions.

@dgreensp
Copy link
Contributor Author

Closing now that we have Template.currentData().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
confirmed We want to fix or implement it
Projects
None yet
Development

No branches or pull requests