Template deletion not updating properly


#1

Hey All,

So I am building a shopping cart package. I have two templates:

Shopping Cart:

//import Cart from 'meteor/bearly:cart'
import {Random} from'meteor/random'

Template.ShoppingCart.onCreated(function(){
    if(!Meteor.userId() && !Session.get('user'))
        Session.set('user', Random.id())
    this.user = Meteor.userId() ? Meteor.userId() : Session.get('user');
    this.products = new ReactiveVar(false)    
    this.subscribe('cart', this.user) 
    Tracker.autorun(()=>{
        if(this.subscriptionsReady()){
            this.products.set(Cart.findOne({active:true}).products)
            $('#cart-product-list').empty()
            Blaze.renderWithData(Template.cartList, {products:this.products.get()}, document.getElementById('cart-product-list'), this.view)            
            
        }
    })     
})

Template.ShoppingCart.helpers({
    'products': ()=>{
        return Template.instance().products.get()
    },
    
})

Template.ShoppingCart.events({
    'click .remove-product': (e,t)=>{
        let usr = Meteor.userId()  ? Meteor.userId() : Session.get('user')
        Meteor.call('removeProductFromCart', e.currentTarget.id, usr)
      
    },
    'click .close-sidebar':()=>{
        $('.shopping-sidebar').animate({right:'-100%'}, 'slow')
    },
    'click .shopping-cart-logo':()=>{
        $('.shopping-sidebar').animate({right:0}, 'slow')
    }

})

and cartProduct:

Template.cartProduct.onCreated(function(){
    this.id  = this.data.id;
    this.product = new ReactiveVar(false)
    Tracker.autorun(()=>{
        if(this.id != this.product.get()._id){
            Meteor.call('getProduct', this.id, (err,res)=>{
                console.log(res) 
                !err ? this.product.set(res) : console.log(err)
            })
        }
    })
})

Template.cartProduct.helpers({
    'product': ()=>{
        return Template.instance().product.get()
    },
    'total' : ()=>{
        return Template.instance().data.qty * Template.instance().product.get().price
    },

})

HTML::

<template name="ShoppingCart">
    <div class="shopping-cart-logo"><i class="fa fa-shopping-cart"></i><div class="products-qty badge badge-primary">{{products.length}}</div></div>
    <div class="shopping-sidebar">
        <div class="container">
                <div class="row">
                    <div class="col-sm-12 d-flex header">
                        <h1 clas="mx-auto">Shopping Cart</h1><i class="fa fa-times ml-auto close-sidebar"></i>
                    </div>
                </div>
                <div class="row">
                    <div class="col-sm-12 products-list" id="cart-product-list">

                        {{>cartList products=products}}


                    </div>
                </div>
        </div>
    </div>
</template>

<template name="cartList">
        <ul>
        {{#each products}}
            {{> cartProduct}}
        {{/each}}
        <li>
            </li>
            <li class="d-flex w-100">
                <button class="btn btn-success ml-auto">Checkout!</button>
            </li>
        </ul>
</template>

<template name="cartProduct">
    <li>
        <div class="w-100 d-flex cart-product">            
            <div class="qty">{{qty}}  </div>
            <div class="product">{{product.title}}<br>{{product.content.description}}</div>
            <div class="trash ml-auto"><i class="fa fa-trash remove-product" id="{{id}}"></i></div>
            <div class="total">{{total}}</div>

        </div>
    </li>
</template>

I am loading the shopping cart from a Cart collection that only contains the productID and QTY, then I was hopping to just grab each products info from a meteor call as they load in to the template. Which seemed to work fine, but when I delete the templates do not update properly. For instance if three products load and I delete the first product, the correct ID is removed but the product info remains on screen for the deleted product and the last item in the array is removed. If the template refreshes it will display the correct data, as in with a hot code reload.

Any ideas on how to fix this?


#3

I was able to solve as such but it seems messy and you can see the reload…

Template.ShoppingCart.onCreated(function(){
    if(!Meteor.userId() && !Session.get('user'))
        Session.set('user', Random.id())
    this.user = Meteor.userId() ? Meteor.userId() : Session.get('user');
    this.products = new ReactiveVar(false)    
    this.subscribe('cart', this.user) 
    Tracker.autorun(()=>{
        if(this.subscriptionsReady()){
            this.products.set(Cart.findOne({active:true}).products)
            $('#cart-product-list').empty()
            Blaze.renderWithData(Template.cartList, {products:this.products.get()}, document.getElementById('cart-product-list'), this.view)            
            
        }
    })     
})

Template.ShoppingCart.helpers({
    'products': ()=>{
        return Template.instance().products.get()
    },
    
})

#4

We’ll need to see your HTML too - this for sure doesn’t look right. You should almost never need to call Blaze.renderWithData


#5

updated with html in the main post and put in my current working solution with the manual re render on auto tracker.


#6

What happens if you remove these lines:

$('#cart-product-list').empty()
Blaze.renderWithData(Template.cartList, {products:this.products.get()}, document.getElementById('cart-product-list'), this.view)                    

and do you get any console errors with/without those lines in place?


#7

Ok it looks like its working correctly now without that. I think I was forgetting the if(this.subscriptionsReady()) and having it set to a reactive var at first.

thank you.


#8

Actually no its still doing it.

So what happenss is, if I have three products lets say

1 apple
3 oranges
6 cats

if I delte the oranges it will then display as:

1 apple
6 oranges

I am supplying the qty and ID only to the template cartProduct and then it is doing a meteor.call to get the product details. It appears that when the collection updates it removes the correct item from the array but on the template it will display the old products information. Its as if template 2 isnt removed but that the data from 3 is shifted to 2 and thus not re rendered? If this doesnt make sense I can do a screen grab. The manual render with data seems to fix the issue but thats weird. the ID changes but the product date stored in the cartProduct doesn’t change… I even tried putting a tracker on the cartProduct template to update when the Id didn’t match the product ID but that did not work.


#9

your problem is with the this.id in your Tracker.autorun - it’s not reactive so it doesn’t trigger a change. Instead use Template.currentData().id. A different (better) solution would be to use one meteor call to load all the products at once rather than waiting on the round trip time for each method call, and if you use _id instead of id to pass into the template, blaze has some efficiency to remove item’s rather than rerendering them. Consider you have 100 items in your cart, as you currently do it, if you remove the first item, you re-render 99 items (1-99) and remove the last. If you use _id, it will destroy the first item, and not re-render any of the others.

You should also use this.autorun as it is bound to the lifetime of the template it is declared in - Tracker.autorun will exist forever


#10

Awesome thank you man. Below is the final code, I removed the forced rerender and only had to make changes to the cartProduct.onCreated()

Template.cartProduct.onCreated(function(){
    this._id  = new ReactiveVar(this.data._id)
    this.product = new ReactiveVar(false)
    this.loadProduct = ()=>{
        Meteor.call('getProduct', this._id.get(), (err,res)=>{
            !err ? this.product.set(res) : console.log(err)
        })
    }
    this.loadProduct()

})

I find it odd that this works like this, I would have thought that the template would have just shifted similar to removing a html element regardless of the _id and setting to a Reactive Var, I also am confused why the id needs to be a reactive far since I would think the template would just be moving.

Thanks so much for the help.


#11

The only way blaze knows that an item is removed vs changed is using the _id - it assumes that your _id’s are unique, so if one is not there, or they are reorderd, or one is added it can behave accordingly, if items are removed AND changed, and have no ids it doesn’t know which item was removed. Consider:

["a", "b", "c"] changes to ["a", "c"] - did I remove b or did I remove c AND change b to c - there is no way of knowing. However:
[{_id: "a"}, {_id: "b"}, {_id: "c"}] changes to [{_id: "a"}, {_id: "c"}] - blaze knows that _id: "b" has been removed because _id’s are immutable and unique.

You’ve fixed the issue using _id - you DONT need internal reactivity in this case because when blaze detects that one _id is missing, it removes that template.

As such, you could simplify your code slightly:

Template.cartProduct.onCreated(function(){
    this.product = new ReactiveVar(false)
    this.loadProduct = ()=>{
        Meteor.call('getProduct', this.data._id,  (err,res)=>{
            !err ? this.product.set(res) : console.log(err)
        })
    }
    this.loadProduct()
})

This is for two reasons

  1. you don’t ever call this._id.set
  2. your this.loadProduct is not reactive - only helpers and this.autorun blocks are reactive.

Remember that onCreated get’s called exactly once when a new template instance is created - not when the data changes.

In the future - if you do need to make a method call when the data changes (which I almost garauntee you will) remember that you must wrap your Meteor.call in an autorun block AND reference the reactive context which is changing - like so:

Template.someTemplate.onCreated(function onCreated() {
  this.autorun(() => {
    const data = Template.currentData();
    Meteor.call("someMethod", data.someKey, (err, res) => {...});
  });
});

I’ve written a package to help new developers on our team understand (or at least work around) some of these blaze bits: https://atmospherejs.com/znewsham/blaze-component


#12

Thank you, the _id note I guess was my lesson of the day. Awesome link I wish I would have seen this a while ago.