Arthur
Arthur

Reputation: 5158

Meteor: How to get Reactive Parent/Child dependency templates for Object attributes

I'm working on a reactive dependence tree on Meteor (with Blaze), but I didn't found the best way to do it.

First, the problem

On this example I have container who set an item Object, who have an attribute elements (Array).

The elements attribute is loop, and a child template is loaded for every instance of it.

The element can be update on the child template, but the update isn't propagate to the container.

Also, I added an Add button to update the item ReactiveVar (from the container). Each field rendering is updated after item is updated on container, so the object as been updated but not re-rendered.

HTML

<template name="container">
  {{#let item=getItem}}
    <h1>Update {{item._id}}</h1>
    {{#each element in item.elements}}
      <div>
        {{> elementForm element}}
        <small>{{element.name}}</small>
      </div>
    {{/each}}
  {{/let}}
  <button name="add">Add</button>
</template>


<template name="elementForm">
  {{#let el=getEl}}
    <input value="{{el.name}}"/>
  {{/let}}
</template>

Parent Template

Template.container.onCreated(function onContainerCreated() {
  // From data
  let item = {
    _id: '123abc',
    elements: [
      {name: "El #1"},
      {name: "El #2"},
      {name: "El #3"},
    ]
  }
  // Create master ReactiveVar
  this.item = new ReactiveVar(item);
});

Template.container.helpers({
  getItem() {
    return Template.instance().item.get();
  },
});
Template.container.events({
  // Add a new element (Will refresh all elements render)
  'click button[name="add"]'(e) {
    let instance = Template.instance()
    let item = instance.item.get()
    item.elements.push({name: "new El"})
    instance.item.set(item)
  },
})

Child template

/* Element */
Template.elementForm.onCreated(function onElementCreated() {
  this.el = new ReactiveVar(this.data)
});
Template.elementForm.helpers({
  getEl() {
    return Template.instance().el.get()
  }
})
Template.elementForm.events({
  'change input'(e) {
    let instance = Template.instance()
    let el = instance.el.get()
    el.name = e.currentTarget.value
    instance.el.set(el)
  }
})

Preview

As meteor can't be integrate on Embed code, I make some images.

Default page

enter image description here

Update input didn't affect container render

enter image description here

Add a new item, will re render all the container and get new values

enter image description here

Solution 1 ? Send item ReactiveVar to the child

I know than sending the entire ReactiveVar to a child can be a solution to use the .set() method on the child.

But if I use this hack, I will have to send the entire item to each elementForm (not the best)

Exemple

/* Html */
<Template name="parentTemplate">
  <small>{{getFoo}}</small>
  {{> childTemplate getItem}}
</Template>
<Template name="childTemplate">
  <input value="{{getFoo}}" />
</Template>

/* Parent */
Template.parentTemplate.onCreated(function onParentCreated() {
  this.item = new ReactiveVar({ foo: bar})
});
Template.parentTemplate.helpers({
  getFoo() {
    return Template.instance().item.get().foo    
  },
  getItem() {
    return Template.instance().item // Send reactive var, net value (.get)
  }
})
/* Child */
Template.childTemplate.onCreated(function onChildCreated() {
  this.item = this.data.item // Use Parent Reactive var
});
Template.childTemplate.helpers({
  getFoo() {
    return Template.instance().item.get().foo    
  },
})
 Template.childTemplate.events({
  'change input'(e) {
    let item = Template.instance().item.get()   
    item.foo = e.currentTarget.value
    Template.instance().item.set(item) // Will update parent   
  },
})

Solution 2 ? Callback

An other solution is to send to the child template, in addition to element data, a callback for each action (like change) to inform the parent template to update the master ReactiveVar for a specific element.

Example

/* Html */
<Template name="parentTemplate">
  <small>{{getFoo}}</small>
  {{> childTemplate getData}}
</Template>
<Template name="childTemplate">
  <input value="{{getFoo}}" />
</Template>

/* Parent */
Template.parentTemplate.onCreated(function onParentCreated() {
  this.item = new ReactiveVar({ foo: bar})
});
Template.parentTemplate.helpers({
  getFoo() {
    return Template.instance().item.get().foo    
  },
  getData() {
    let instance = Template.instance()
    return {
      item: instance.item.get(),
      onChange: function(val) {
        // Update item here
        let item = instance.item.get()
        item.foo = val
        instance.item.set(item)
      }
    }
  }
})
/* Child */
Template.childTemplate.onCreated(function onChildCreated() {
});
Template.childTemplate.helpers({
  getFoo() {
    return Template.instance().data.item.foo    
  },
})
 Template.childTemplate.events({
  'change input'(e) {
    // Call parent callback
    Template.instance().data.onChange(e.currentTarget.value)  
  },
})

Final Question

What's the best way to handle Parent/Child Template to handle a list of element from a item object ?

Upvotes: 1

Views: 963

Answers (1)

Julien Leray
Julien Leray

Reputation: 1687

Actually a good question.

IMHO depending on the case I mostly do kind of your solution 1.

Something like:

Template.container.onCreated(function(){
  //each elements are ReactiveVar
  let item = {
    _id: '123abc',
    elements: [
      {name: new ReactiveVar("El #1")},
      {name: new ReactiveVar("El #2")},
      {name: new ReactiveVar("El #3")},
    ]
  }
  // I don't wrap the wole item on a ReactiveVar if I don't need it.
});

Then, when you iterate on it with your #each, you will inject only the current element data into the datacontext of your child. From here, you can easily do:

Template.childTemplate.helpers({
  getName() {
    return this.name.get();    
  },
})
 Template.childTemplate.events({
  'change input'(e, tp) {
    newVal = e.currentTarget.value
    tp.currentData().name.set(newVal); 
  }
});

I used some "shortcut" as Template.currentData() and direct datacontext access on helper with this.name.get().

You also can have direct access to the template instance as second parameter of an event:

'change input'(event, templateInstance) {}

I did not test it, I only give you the idea.

Hope this will help you.

Upvotes: 1

Related Questions