Skip to content

Instantly share code, notes, and snippets.

@filipenevola
Created November 7, 2023 16:01
Show Gist options
  • Save filipenevola/fe9036b7c78e6112fa4cac00ca9045b5 to your computer and use it in GitHub Desktop.
Save filipenevola/fe9036b7c78e6112fa4cac00ca9045b5 to your computer and use it in GitHub Desktop.

Sharing State

Manage the state in the parent and provide to children templates is very often something that needs to be handled in frontend applications.

In React there is a standard called “Lift up the state” and this approach is also adopted in other frontend libraries/frameworks.

To make the code easier we can use some utilities:

// utility code
Blaze.TemplateInstance.prototype.parentTemplate = function (levels) {
  var view = Blaze.currentView;
  if (typeof levels === "undefined") {
   levels = 1;
  }
  while (view) {
   if (view.name.substring(0, 9) === "Template." && !(levels--)) {
    return view.templateInstance();
   }
   view = view.parentView;
  }
};

const getFromParent = (template, propName) => {
  if(template.parentTemplate()[propName]) {
    return template.parentTemplate()[propName];
  }
  return getFromParent(template.parentTemplate(), propName)
}

Then in the parent component you can create a ReactiveDict as source of truth for your state:

const STATE_NAME = 'statePropName';
Template.parent.onCreated(function () {
  this[STATE_NAME] = new ReactiveDict();
});

In the children you can access like:

const STATE_NAME = 'statePropName';

Template.children.helpers({
  myHelper: function () {
    return getFromParent(Template.instance(), STATE_NAME);
  },
});

Let's see an actual example, imagine that you need to create the Notification options like Google Calendar

reminders

Take a look in the code below as the state is well defined and centralized in the parent componente, there is also a updateState utility function to help updating the state only when necessary.

import { Random } from "meteor/random";

import { CalendarAutomatedReminderNotificationType } from "./CalendarAutomatedReminderNotificationType";
import { CalendarAutomatedReminderUnit } from "./CalendarAutomatedReminderUnit";

const DEFAULT_CALENDAR_AUTOMATED_REMINDERS = [
  {
    type: CalendarAutomatedReminderNotificationType.EMAIL.name,
    value: 1,
    unit: CalendarAutomatedReminderUnit.DAYS.name,
  },
  {
    type: CalendarAutomatedReminderNotificationType.EMAIL.name,
    value: 60,
    unit: CalendarAutomatedReminderUnit.MINUTES.name,
  },
];

const updateState = ({ state, automatedReminders }) => {
  if (
    JSON.stringify(state.get("automatedReminders")) ===
    JSON.stringify(automatedReminders)
  ) {
    return;
  }
  state.set("automatedReminders", automatedReminders);
  Users.update(Meteor.userId(), {
    $set: { "profile.automatedReminders": automatedReminders },
  });
};
Template.calendarAutomatedReminders.onCreated(function () {
  this.state = new ReactiveDict();
  this.state.setDefault({
    automatedReminders: Tracker.nonreactive(function () {
      const user = Users.findOne(
        { _id: Meteor.userId() },
        { fields: { "profile.automatedReminders": 1 } }
      );
      const automatedReminders =
        user && user.profile && user.profile.automatedReminders;
      return (
        automatedReminders ||
        DEFAULT_CALENDAR_AUTOMATED_REMINDERS.map((item) => ({
          _id: Random.id(),
          ...item,
        }))
      );
    }),
  });
});
Template.calendarAutomatedReminders.helpers({
  automatedReminders: function () {
    return Template.instance().state.get("automatedReminders");
  },
});

Template.calendarAutomatedReminders.events({
  'click [data-action="addAutomatedReminder"]': function (event, template) {
    event.stopPropagation();
    event.preventDefault();
    updateState({
      state: template.state,
      automatedReminders: [
        ...template.state.get("automatedReminders"),
        { _id: Random.id(), ...DEFAULT_CALENDAR_AUTOMATED_REMINDERS[0] },
      ],
    });
  },
});

Template.automatedReminder.helpers({
  types: function () {
    return Object.values(CalendarAutomatedReminderNotificationType);
  },
  units: function () {
    return Object.values(CalendarAutomatedReminderUnit);
  },
  selectedType: function () {
    return Template.instance().data.type === this.name ? "selected" : "";
  },
  selectedUnit: function () {
    return Template.instance().data.unit === this.name ? "selected" : "";
  },
});

Template.automatedReminder.events({
  'click [data-action="removeAutomatedReminder"]': function (event, template) {
    event.stopPropagation();
    event.preventDefault();
    const parent = template.parentTemplate();
    updateState({
      state: parent.state,
      automatedReminders: parent.state
        .get("automatedReminders")
        .filter(({ _id }) => _id !== template.data._id),
    });
  },
  'change [data-action="trackAutomatedReminder"]': function (event, template) {
    event.stopPropagation();
    event.preventDefault();

    const parent = template.parentTemplate();
    const { name, value } = event.target;
    updateState({
      state: parent.state,
      automatedReminders: parent.state
        .get("automatedReminders")
        .map((ar) =>
          ar._id !== template.data._id
            ? ar
            : { ...ar, [name]: name === "value" ? +value : value }
        ),
    });
  },
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment