Design Pattern to Avoid Overworking Helpers


#1

I have the following helper, which I think is a bit overworked. What this basically does, is loop through a bunch of SVGs that I have, and if they have corresponding data in the DB, then the fill color of the SVG should change (visual representation of the data).

Template.patientContentTreatment.helpers({

    treatments: function () {

        var completedTeeth = $('.svg-tooth-completed > .tooth'); // get all the svgs
        
        var currentNumber, currentPart;

        for ( var i = 0; i < completedTeeth.length; i++ ) {
            // get a related data-id
            currentNumber = $(completedTeeth[i]).closest('.svg-tooth-completed').data('id');

            // get the related data-title
            currentPart = $(completedTeeth[i]).data('title');

            if ( Treatments.find({ patient_id: Session.get('currentPatient'), tooth_number: currentNumber, tooth_part: currentPart }).fetch().length ) {
                // loop through each SVG and change the fill color if correspoding data is founf
                $(completedTeeth[i]).css({'fill': '#54a6f8', 'fillOpacity': .8});
            }
        }

        return Treatments.find({patient_id: Session.get('currentPatient')});
    }
});

I’m still learning how to use Meteor. Before, I had implemented this with autopublish still installed, so it was attached to a Template.rendered . But after I moved to using pubs/subs, I found that I couldn’t quite implement it the same way as I had before because:

  1. There was no way to return a helper from .rendered
  2. The subscription data isn’t yet ready when the template is rendered anyway. So no data was being displayed.

Here’s my previous implementation:

    Template.single_patient_treatment_plan.rendered = function () {
        /*
         loop through all teeth
         find if any of them have any findings or treatments attached to them in the database
         if so, change the fill color
        */

    var completedTeeth = $('.svg-tooth-completed > .tooth'),
        currentNumber, currentPart;

    Tracker.autorun(function() {
   
        for ( var j = 0; j < completedTeeth.length; j++ ) {
            currentNumber = $(completedTeeth[j]).closest('.svg-tooth-completed').data("id");
            currentPart = $(completedTeeth[j]).data("title");

            if (Treatments.find({patient_id: Session.get("current_patient"), tooth_number: currentNumber, tooth_part: currentPart}).fetch().length) {
                $(completedTeeth[j]).css({"fill": "#54A6F8", fillOpacity: .8});
            }
        }
    });

So now I’ve attached the functionality to the helper, but it feels very wrong and bloated. Is there a better way to achieve the same result without using a helper?


#2

It feels like this is really business logic. In that case I would write one or more ES6 classes which also become testable. Then the helper only asks the class something like:

treatments: function() {
  return Teeth.getSVG();
}

All logic moves into one or more objects. In your case something like this basic structure (no real code):

class Teeth {
  // contains array with all Teeth 
  getSVG() {
    forEach(this.teethArray) {this.isInTreatment();}
    return the svg
  }

}

class Tooth {
  isInTreatment() {return true / false }
}

#3

EDIT: see later post for pure Blaze alternative (maybe better)

This is how I’d do it:

I’m using d3 because it’s designed for this sort of thing (dynamically linking dom elements to data) but the relevant part should apply to your manual SVG manipulation too.

Look at client/diagram.js specifically:

Template.diagram.onCreated(function diagramOnCreated() {
    this.subscribe('teeth');
});

Template.diagram.onRendered(function diagramOnRendered(){
  this.autorun(() => {
    if (this.subscriptionsReady()){
      renderSvg();
    }
  });
});


#4

This is the classic and generic headache when you want to have more control over Blaze. And it’s not SVG specific.

The solution is to checkout viewmodel.org, the Blaze version. You shall be able to reduce code size to 1/2 or 1/3 with much simpler wiring.


#5

How are the SVGs getting generated? Is it possible to do the Treatments lookup and apply the styling as they’re getting generated?


#6

I rewrote the above demo to be pure-Blaze. It’s actually a lot simpler. I use D3 a lot so fell into the habit of “If all you’ve got is a hammer, then every problem looks like a nail”.

New repo:

Entire blaze diagram template:

<template name="diagram">
  <h1>Diagram</h1>
  <svg id="diagram" width="1000px" height="75px">
    {{#each teeth}}
      <path d="{{toothPath}}" transform="{{toothTransform}}" fill="{{color}}"/>
    {{/each}}
  </svg>
</template>

Entire javascript for the diagram:

import { Template } from 'meteor/templating';
import { Teeth } from '/imports/teeth';

import './diagram.html';

const TOOTH = "M315.204,32.786c-42.129-49.124-113.851-10.811-124.018-4.996C158.503,5.47,112.842-14.194,76.081,13.835 C49.654,33.953,34.451,59.927,30.843,91.013c-5.426,46.829,17.823,92.293,29.924,106.968c6.141,7.436,8.267,43.639,9.83,70.084 c3.317,56.28,6.187,104.904,34.966,111.004c7.349,1.58,14.384,0.279,20.356-3.694c15.499-10.341,20.379-36.239,26.02-66.227 c7.256-38.597,13.919-68.027,31.121-66.831c5.188,0.36,7.378,2.254,8.696,3.857c8.737,10.585,4.95,42.037,2.167,65.007 c-4.008,33.334-7.482,62.125,14.46,68.526c1.998,0.559,4.415,1.012,7.191,1.012c4.427,0,9.726-1.127,15.534-4.637 c33.706-20.285,60.22-107.785,70.468-155.062c26.815-20.855,44.313-51.354,48.252-84.328 C352.849,111.252,349.306,72.585,315.204,32.786z M326.138,133.868c-3.322,27.902-18.74,53.644-42.28,70.63 c-2.429,1.754-4.113,4.346-4.718,7.273c-13.953,67.713-41.014,132.267-60.335,143.885c-0.86,0.512-1.952,1.07-2.894,1.197 c-2.719-6.297,0.035-29.012,1.696-42.838c3.835-31.742,7.785-64.564-7.459-83.027c-6.111-7.424-14.901-11.734-25.41-12.467 c-1.068-0.081-2.126-0.115-3.166-0.115c-36.843,0-45.83,47.764-53.086,86.338c-3.654,19.414-8.661,46.021-15.801,50.785 c-0.354,0.244-0.761,0.523-2.173,0.209c-10.985-2.312-14.576-63.113-16.109-89.068c-2.573-43.641-4.688-71.049-15.244-83.841 c-8.174-9.922-29.209-49.472-24.626-89.057c2.83-24.411,14.93-44.923,35.989-60.975c46.556-35.513,124.819,46.114,125.609,46.951 c4.531,4.77,12.084,4.985,16.859,0.465c4.787-4.525,4.996-12.066,0.477-16.858c-1.069-1.133-8.947-9.365-21.146-19.549 c19.09-8.76,60.975-23.26,84.77,4.485C319.632,74.607,329.67,104.194,326.138,133.868z";


Template.diagram.onCreated(function diagramOnCreated() {
    this.subscribe('teeth');
});

Template.diagram.helpers({
  teeth() {
    return Teeth.find();
  },

  toothPath(){
    return TOOTH;
  },

  toothTransform(){
    return `translate(${this._id *100},10),scale(.1)`;
  }
});

Functionally, 100% identical to the original demo:

Unless you’ve got a compelling reason for it, I’d jettison all of your manual DOM manipulation code and do something like the above.


#7

Excellent example. Couldn’t have done it better myself. Mind if I include it in the Clinical Meteor SDK’s examples?


#8

Not at all, and thanks.