How to call functions between templates?


#1

I have two templates: mapControls and map. The first has buttons. The second has a map and lots of functions that manipulate the map. When I have a click event in mapControls, what’s a clean way to call functions in the map template to change the map?

Reading these:


it sounds like I could have a Session variable which I change to trigger a Tracker.autorun in the other template. But it feels wrong to use something like Session.get(‘buttonSetLineToolClickCounter’), and just increase that var each time I click the button. Session feels appropriate to store data, but not to trigger events.

ps. The templates are not parent/child.


#2

I think I’d use a globally available object that stores Tracker dependencies and autorun upon them

coincidentally, Session is exactly that :wink:

(for the beginner readers of this post, refrain from using Session within a single template… your scope is well defined so you shouldn’t breach it)


#3

One of my concerns is that I have buttons like “deleteShape”. If the autorun would run for some reason when reloading the page, data would get deleted.

If relaying on autorun is the only way to do this, how can I make sure the user clicked the button? Maybe storing the mouse event in the Session variable, so the “receiver” can make sure it reacts only to expected events?

Ok, this does not sound so ugly:

Template.mapControls.events({
  'click .remove': function(e) {
    Session.set('mapControlsEvent', e);
  }
});

But it gives an error: Uncaught InvalidStateError: Failed to read the ‘selectionDirection’ property from ‘HTMLInputElement’: The input element’s type (‘button’) does not support selection.

I’ll try not sending the whole event, but just the button name.


#4

Hold on, does the button have any significance other than initiating a
signal?
If not, avoid coupling it with the action and rely solely on the message
itself (i.e. The session variable)
This allows better modularity


#5

The buttons only initiate signals.

If I do something like this:

Template.mapControls.events({
  'click .send-event': function(e) {
    Session.set('mapControlsEvent', $(e.target).data());
  }
});

Then I can place on each button a data value, and have only one

Tracker.autorun(function () {
  var e = Session.get("mapControlsEvent");
  if(e && mapReady) {  
    // now look at e.command

If I don’t couple if with the action, won’t I need many different Session variables, and a bunch of Tracker.autoruns? And I still need to check that the Session variable comes with some kind of value, because when I load the page the autorun runs, even no one clicked anything.


#6

what I meant was that the button or any aspects of it besides a required parameter (i.e. value) shouldn’t be part of the message (i.e. your Session variable)

I’m not familiar with your complete code, but in general as long as you keep the publisher & consumer isolated and communicating through “an agnostic” messenger, I’d approve

your revision of sending just the data() portion seems to be right in this manner


#7

I don’t know how open you are to trying something new but with ViewModel this whole allowing templates talk to each other issue becomes trivially easy. In your case you would have a view model object for the mapControls template and another for map. You would then be able to call any map function/property from mapControls with ViewModel.byId("map").doSomething()

No need for session variables, autoruns, or raising events. If you’re interested see Communication Between View Models


#8

@chenroth It’s working now. This is how it looks like:

mapControls.html

<input type="button" value="remove" data-action="removeLastRegion" data-type="mapControl">

mapControls.js

Template.mapControls.events({
  'click input[data-type=mapControl]': function(e) {
    Session.set('mapControlsEvent', $(e.eventTarget).data());
  }
});

map.js

Tracker.autorun(function () {
  var e = Session.get('mapControlsEvent');
  if(e) {  
    switch(e.action) {
      case 'removeLastRegion':
        doSomething();
        break;
    }
  }
  // need this to allow repeated clicks on the same button
  Session.set('mapControlsEvent', false);
});

@manuel ViewModel looks great, but I can’t implement such changes at this stage. It’s the 4th month developing this project, near the end, and I find it hard how every month there’s a new better way to do things, packages that change, packages that might save time but sometimes don’t work well, lots of packages that do the same kind of thing, new versions of meteor… Too much change.


#9

I know the feeling…


#10

cool, I’d make 2 changes though:

  1. register a click event on [data-action=removeLastRegion] instead of the class name.
    class names are conventionally for styling, not binding template events, which may lead to someone (or even you) unintentionally breaking your code, whereas data-action is more self descriptive and isn’t likely to be coupled with styling

Also, if you’re consistent with the naming convention, you may get an extra DRY benefit by binding all data-action=* elements (within a certain category, of course) using a single template event definition

  1. In my experience, I stick to e.currentTarget cause e.target might point to a parent element (caused by bubbling) and not the triggering element in same cases

just one queston though - doesn’t resetting the Session variable within autorun cause an extra run, resulting always in a false state?


#11

Thanks for the tips! I updated the example above. Depending on the class felt wrong, but was too tired to find out how to do a css selector on [data-action]. Much better now. I don’t match removeLastRegion specifically because I have several buttons.

Note: I could send $(e.eventTarget).data('action') instead, but in the real program I have two kinds of data, data-action and data-util, so I send whatever data I find and look at the data object on the receiver side.

And yes, it does probably retrigger the autorun, but since I set it to false, the if(e) makes it innocuous.


#12

perhaps you should still have some common attribute to all these buttons, otherwise you might trigger the event with an unrelated element. maybe add something along data-category=“mapControl” and bind event to this selector


#13

I’m doing something similar with leaflet. I created a namespace for the application/package. Each map component (map, layers, markers) and their associated variables/reactivevar or functions are accessible by this namespace.

  1. From control to map, for example I can call from the control template a leaflet function embeded in the namespace that will pan to a marker and open a popup.
    From map to control, if I double click on a marker, this will trigger some leaflet functions like draggable/popup… then it will route to the marker edit form on the control template.

  2. To synchronize data display between the map and the control, I’m using the ReactiveVar package instead of sessionvar, observe on the cursor associated with the collection of markers. If the map is zoomed in/out the map boundaries changed and triggers a new selector on the collection then modify the displayed list of markers in the control… Likewise in control a search input modifies the selector on the collection and the same reactvar triggers observe and filters the markers displayed in map. You can check this example for some ideas: http://meteorcapture.com/how-to-create-a-reactive-google-map/


#14

I like to use the mediator pattern, or my version of it, although I call it an event broker. The idea is that you create a custom listener in the sub/child template, then you can trigger that event from anywhere else in the code. The listener is registered in the global EventBroker class and it simply runs the callback functions you associated with the listener when you trigger it. You simply use strings to associate triggers and listeners, and if need to get more specific I simply add ids to the strings to make them unique.

Create a listener in the map template:
EventBroker.listen(‘zoomin’, {}, function (listenerArgs, triggerArgs) {
map code goes here to make map zoomin, or just call another function
});

Trigger the event in the mapControls (or anywhere):
EventBroker.trigger(‘zoomin’, {});

The string Zoomin is used to link the two together. The two empty objects become the listenerArgs and triggerArgs, so you can pass information as well.

Here is the event Broker Class;
EventBroker = new (function () {
this.events = [];

this.listen = function (events, listenerArgs, callback) {
    if(!JSLib.isString(events)){
        log.error("The first paramater (events) must be a string that represents the event to listen too")
    }
    
    if(JSLib.isUndefined(listenerArgs)){
        log.error("The second paramater must be an object (listenerArgs) or the call back function")
    }
    
    if(JSLib.isFunction(listenerArgs)){
        callback = listenerArgs;
        listenerArgs = {};
    }
    
    if($.isArray(events)){
        var eventCounter = 0;
        for(eventCounter = 0;eventCounter<events.length ;eventCounter++){
            addListener(events[eventCounter], listenerArgs, callback);
        }
    }else{
        addListener(events, listenerArgs, callback);
    }
}
var addListener= function (event, listenerArgs, callback) {
    if (JSLib.isUndefined(EventBroker.events[event])) {
        EventBroker.events[event] = [];
    }
    EventBroker.events[event].push({event:event, listenerArgs: listenerArgs, callback: callback})
}
this.trigger = function (event, triggerArgs) {
    triggerArgs = JSLib.setDefaultParameter(triggerArgs, {});
    
    if(JSLib.isUndefined(EventBroker.events[event])){
        return false;
    }
    
    var listenerCounter = 0;

    for (listenerCounter = 0; listenerCounter < EventBroker.events[event].length; listenerCounter++) {
        var listener = EventBroker.events[event][listenerCounter]
        listener.callback(listener.listenerArgs, triggerArgs);
    }
}
this.remove = function (listener) {
    JSLib.remove(EventBroker.events[listener.event], listener)
}

})();