FullCalendar: show reversed list views - fullcalendar

How can I reverse the events in the list views, so that the event with the most futuristic date appears at the beginning (top)?

#F.Mora your solution is almost perfect but in our case we add some custom classNames and have multiple items under each headline.
Here is our enhanced version :
eventAfterAllRender: function(view) {
var renderedEvents = $('.fc-list-table tr');
var reorderedEvents = [];
var blockEvents = null;
renderedEvents.map(function(key, event) {
if ($(event).hasClass('fc-list-heading')) {
if (blockEvents) {
reorderedEvents.unshift(blockEvents.children());
}
blockEvents = $('<tbody></tbody>');
}
blockEvents.append(event);
});
reorderedEvents.unshift(blockEvents.children());
$('.fc-list-table tbody').html(reorderedEvents);
}

#CarComp,
As ADyson commented in the comment to the OP, your best bet if you want do not want to deal with the dom after the html has been rendered is to download the source and make the modification there in the ListView renderSegList function.
Reverse the order of iteration through the list that it is being created and then you will have what you are looking for.
This will, of course, apply to all ListView implementations of the calendar. There would need to be an option added to toggle back and forth, which would be a bit more involved.

For anyone still looking for this, inverted event lists using jquery:
eventAfterAllRender: function(view) {
var eventosRendered = $('#timeline tr');
var eventosInversa = [];
var headingPendiente = null;
eventosRendered.map(function(key, evento) {
switch(evento.className) {
case 'fc-list-heading':
if (headingPendiente) {
eventosInversa.unshift(headingPendiente);
}
headingPendiente = evento;
break;
case 'fc-list-item':
eventosInversa.unshift(evento);
break;
}
});
eventosInversa.unshift(headingPendiente);
$('#timeline tbody').append(eventosInversa);
}

Here's the version I use (fullCalendar v4):
datesRender: function(info) {
var list = $(info.el).find('.fc-list-table tbody');
list.find('.fc-list-heading').each((i,heading) => {
var children = $(heading).nextUntil('.fc-list-heading')
list.prepend(children)
list.prepend(heading)
})
},

I used this for fullCalendar v5. It´s based on #Yo1 answer
eventsSet: function(dateInfo){
var renderedEvents = $('.fc-list-table tr');
var reorderedEvents = [];
var blockEvents = null;
renderedEvents.map(function(key, event) {
if ($(event).hasClass('fc-list-day')) {
if (blockEvents) {
reorderedEvents.unshift(blockEvents.children());
}
blockEvents = $('<tbody></tbody>');
}
blockEvents.append(event);
});
if (blockEvents){
reorderedEvents.unshift(blockEvents.children());
$('.fc-list-table tbody').html(reorderedEvents);
}
},

Related

How to persist scrolls with turbolinks?

Sometimes it is desirable to persist scroll positions between page visits.
Turbolinks resets scroll position after loading the data.
How can I disable it for particular elements?
My solution in ES6:
const turbolinksPersistScroll = () => {
const persistScrollDataAttribute = 'turbolinks-persist-scroll'
let scrollPosition = null
let enabled = false
document.addEventListener('turbolinks:before-visit', (event) => {
if (enabled)
scrollPosition = window.scrollY
else
scrollPosition = null
enabled = false
})
document.addEventListener('turbolinks:load', (event) => {
const elements = document.querySelectorAll(`[data-${persistScrollDataAttribute}="true"]`)
for (let i = 0; i < elements.length; i++) {
elements[i].addEventListener('click', () => {
enabled = true
})
}
if (scrollPosition)
window.scrollTo(0, scrollPosition)
})
}
turbolinksPersistScroll()
And add on your links data-turbolinks-persist-scroll=true on links you want persist scrollbar position.
<a href="..." data-turbolinks-persist-scroll=true>Link</a>
This works for me, also with link_to remote: true.
Use the following javascript to persist scrolls. I have created a selector that matches all elements with class turbolinks-disable-scroll. Before loading,the script saves the scroll position and after loading it loads the saved positions.
// persist scrolls
// pirated from https://github.com/turbolinks/turbolinks-classic/issues/205
var elementsWithPersistentScrolls, persistentScrollsPositions;
elementsWithPersistentScrolls = ['.turbolinks-disable-scroll'];
persistentScrollsPositions = {};
$(document).on('turbolinks:before-visit', function() {
var i, len, results, selector;
persistentScrollsPositions = {};
results = [];
for (i = 0, len = elementsWithPersistentScrolls.length; i < len; i++) {
selector = elementsWithPersistentScrolls[i];
results.push(persistentScrollsPositions[selector] = $(selector).scrollTop());
}
return results;
});
$(document).on('turbolinks:load', function() {
var results, scrollTop, selector;
results = [];
for (selector in persistentScrollsPositions) {
scrollTop = persistentScrollsPositions[selector];
results.push($(selector).scrollTop(scrollTop));
}
return results;
});
It seems like there are two approaches to this problem.
Preserve flagged elements (#vedant1811's answer)
Preserve body scroll for flagged links
The second approach is the one that I've been looking for and couldn't find anywhere, so I'll provide my answer to that here.
The solution here is very similar to that of the first approach, but perhaps a little simpler. The idea is to grab the current scroll position of the body when an element is clicked, and then scroll to that position after the page is loaded:
Javascript
Turbolinks.scroll = {}
$(document).on('click', '[data-turbolinks-scroll=false]', function(e){
Turbolinks.scroll['top'] = $('body').scrollTop();
})
$(document).on('page:load', function() {
if (Turbolinks.scroll['top']) {
$('body').scrollTop(Turbolinks.scroll['top']);
}
Turbolinks.scroll = {};
});
Markup
<a href='/' data-turbolinks-scroll='false'>Scroll preserving link</a>
I use a scroll attribute on the Turbolinks object to store my scroll position when a [data-turbolinks-scroll=false] link is clicked, then after I scroll the page I clear this attribute.
It is important that you clear the attribute (Turbolinks.scroll = {}) otherwise, subsequent clicks on non-flagged anchor links will continue to scroll you to the same position.
Note: depending on the specific styling of html and body you may need to use the scroll offset from both. An example of how this might be accomplished is:
Turbolinks.scroll = {};
$(document).on('click', '[data-turbolinks-scroll=false]', function (e) {
Turbolinks.scroll['top'] = {
html: $("html").scrollTop(),
body: $("body").scrollTop()
}
});
$(document).on('turbolinks:load', function() {
if (Turbolinks.scroll['top']) {
$('html').scrollTop(Turbolinks.scroll['top']['html']);
$('body').scrollTop(Turbolinks.scroll['top']['body']);
}
Turbolinks.scroll = {};
});
I noticed that sometimes scroll is going up and then only down. This version prevents such behaviour:
const persistScrollDataAttribute = 'turbolinks-persist-scroll';
let scrollPosition = null;
const turbolinksPersistScroll = () => {
if (scrollPosition) {
window.scrollTo(0, scrollPosition);
scrollPosition = null;
}
const elements = document.querySelectorAll(`[data-${persistScrollDataAttribute}="true"]`)
for (let i = 0; i < elements.length; i++) {
elements[i].addEventListener('click', () => {
document.addEventListener("turbolinks:before-render", () => {
scrollPosition = window.scrollY;
}, {once: true})
})
}
}
document.addEventListener('turbolinks:load', turbolinksPersistScroll);
document.addEventListener('turbolinks:render', turbolinksPersistScroll);

Reactive cursor without updating UI for added record

I am trying to make a newsfeed similar to twitter, where new records are not added to the UI (a button appears with new records count), but updates, change reactively the UI.
I have a collection called NewsItems and I a use a basic reactive cursor (NewsItems.find({})) for my feed. UI is a Blaze template with a each loop.
Subscription is done on a route level (iron router).
Any idea how to implement this kind of behavior using meteor reactivity ?
Thanks,
The trick is to have one more attribute on the NewsItem Collection Say show which is a boolean. NewsItem should have default value of show as false
The Each Loop Should display only Feeds with show == true and button should show the count of all the items with show == false
On Button click update all the elements in the Collection with show == false to show = true
this will make sure that all your feeds are shown .
As and when a new feed comes the Button count will also increase reactively .
Hope this Helps
The idea is to update the local collection (yourCollectionArticles._collection): all articles are {show: false} by default except the first data list (in order not to have a white page).
You detect first collection load using :
Meteor.subscribe("articles", {
onReady: function () {
articlesReady = true;
}
});
Then you observe new added data using
newsItems = NewsItems.find({})
newsItems.observeChanges({
addedBefore: (id, article, before)=> {
if (articlesReady) {
article.show = false;
NewsItems._collection.update({_id: id}, article);
}
else {
article.show = true;
NewsItems._collection.update({_id: id}, article);
}
}
});
Here is a working example: https://gist.github.com/mounibec/9bc90953eb9f3e04a2b3.
Finally I managed it using a session variable for the current date /time:
Template.newsFeed.onCreated(function () {
var tpl = this;
tpl.loaded = new ReactiveVar(0);
tpl.limit = new ReactiveVar(30);
Session.set('newsfeedTime', new Date());
tpl.autorun(function () {
var limit = tpl.limit.get();
var time = Session.get('newsfeedTime');
var subscription = tpl.subscribe('lazyload-newsfeed', time, limit);
var subscriptionCount = tpl.subscribe('news-count', time);
if (subscription.ready()) {
tpl.loaded.set(limit);
}
});
tpl.news = function() {
return NewsItems.find({creationTime: {$lt: Session.get('newsfeedTime')}},
{sort: {relevancy: -1 }},
{limit: tpl.loaded.get()});
},
tpl.countRecent = function() {
return Counts.get('recentCount');
},
tpl.displayCount = function() {
return Counts.get('displayCount');
}
});
Template.newsFeed.events({
'click .load-new': function (evt, tpl) {
evt.preventDefault();
var time = new Date();
var limit = tpl.limit.get();
var countNewsToAdd = tpl.countRecent();
limit += countNewsToAdd;
tpl.limit.set(limit);
Session.set('newsfeedTime', new Date());
}
});

Filter and search using Textbox

This question is a follow-up of My Previous Question on Filtering DropdownList
I am adding an extra feature and for that I want to Filter values using a textbox. Here is the code for filter
var filterAndLimitResults = function (cursor) {
if (!cursor) {
return [];
}
var raw = cursor.fetch();
var currentSearchTitle = searchTitle.get();
if(!(!currentSearchTitle || currentSearchTitle == "")) {
filtered = _.filter(filtered, function (item) {
return item.title === currentSearchTitle ;
console.log(item.title === currentSearchTitle);
});
}
var currentLimit =limit.get();
//enforce the limit
if (currentLimit ) {
filtered = _.first(filtered, currentLimit );
}
return filtered;
};
and this is the keyup event i am doing on the search textbox
"keyup #search-title":function(e,t){
if(e.which === 27){
searchTitle.set("");
}
else {
var text = $(e.target.val);
searchTitle.set(text)
console.log(searchTitle.set(text));
}
}
With this i am able to return total collection objects on each keypress in the console but it is not filtering the values in the listing and it vanishies all the listings from the UI. Please help
You are almost right. Your keyUp event is ok but you could also avoid to use jQuery like this:
"keyup .textInput": function(e, t) {
var searchString = e.currentTarget.value;
switch (e.which) {
case 27:
e.currentTarget.value = "";
searchTitle.set("");
break;
default:
searchTitle.set(searchString);
}
}
Note that I use a switch in case you would want to add shortcuts for specific searches, like cmd+maj+c to search only for cities (it might be a little overkill)
Concerning the search function, I assume you want to search among the titles of your items, within the current dropdown filtering.
You then need to add an extra step to do this. You also need to set it before the other filters. See my example below, you insert it after var filtered = [];:
var filtered = [];
var currentSearchTitle = searchTitle.get();
if(!currentSearchTitle || currentSearchTitle == "") {
filtered = raw;
} else {
currentSearchTitle = currentSearchTitle .replace(".", "\\.");//for regex
var regEx = new RegExp(currentSearchTitle , "i");
filtered = _.filter(raw, function(item) {
return (item && item.title && item.title.match(regEx));
});
}
// your other filtering tasks
return filtered;
Also, take the time to understand what the code does, you must not just copy paste it.
This code is strongly inspired by the work of meteorkitchen author, #Perak. I adapted it but I have not tested it "as-is".

Filter results from Google Autocomplete

Is there a way to get the results from Google Autocomplete API before it's displayed below the input? I want to show results from any country except U.S.A.
I found this question: Google Maps API V3 - Anyway to retrieve Autocomplete results instead of dropdown rendering it? but it's not useful, because the method getQueryPredictions only returns 5 elements.
This is an example with UK and US Results: http://jsfiddle.net/LVdBK/
Is it possible?
I used the jquery autocomplete widget and called the google methods manually.
For our case, we only wanted to show addresses in Michigan, US.
Since Google doesn't allow filtering out responses to that degree you have to do it manually.
Override the source function of the jquery autocomplete
Call the google autocompleteService.getQueryPredictions method
Filter out the results you want and return them as the "response" callback of the jquery autocomplete.
Optionally, if you need more detail about the selected item from Google, override the select function of the jquery autocomplete and make a call to Google's PlacesService.getDetails method.
The below assumes you have the Google api reference with the "places" library.
<script src="https://maps.googleapis.com/maps/api/js?key=[yourKeyHere]&libraries=places&v=weekly" defer></script>
var _autoCompleteService; // defined globally in script
var _placesService; // defined globally in script
//...
// setup autocomplete wrapper for google places
// starting point in our city
var defaultBounds = new google.maps.LatLngBounds(
new google.maps.LatLng('42.9655426','-85.6769166'),
new google.maps.LatLng('42.9655426','-85.6769166'));
if (_autoCompleteService == null) {
_autoCompleteService = new google.maps.places.AutocompleteService();
}
$("#CustomerAddress_Street").autocomplete({
minLength: 2,
source: function (request, response) {
if (request.term != '') {
var googleRequest = {
input: request.term,
bounds: defaultBounds,
types: ["geocode"],
componentRestrictions: { 'country': ['us'] },
fields: ['geometry', 'formatted_address']
}
_autoCompleteService.getQueryPredictions(googleRequest, function (predictions) {
var michiganOnly = new Array(); // array to hold only addresses in Michigan
for (var i = 0; i < predictions.length; i++) {
if (predictions[i].terms.length > 0) {
// find the State term. Could probably assume it's predictions[4], but not sure if it is guaranteed.
for (var j = 0; j < predictions[i].terms.length; j++) {
if (predictions[i].terms[j].value.length == 2) {
if (predictions[i].terms[j].value.toUpperCase() == 'MI') {
michiganOnly.push(predictions[i]);
}
}
}
}
}
response(michiganOnly);
});
}
},
select: function (event, ui) {
if (ui != null) {
var item = ui.item;
var request = {
placeId: ui.item.place_id
}
if (_placesService == null) {
$("body").append("<div id='GoogleAttribution'></div>"); // PlacesService() requires a field to put it's attribution image in. For now, just put on on the body
_placesService = new google.maps.places.PlacesService(document.getElementById('GoogleAttribution'));
}
_placesService.getDetails(request, function (result, status) {
if (result != null) {
const place = result;
if (!place.geometry) {
// User entered the name of a Place that was not suggested and
// pressed the Enter key, or the Place Details request failed.
//window.alert("No details available for input: '" + place.name + "'");
return;
}
else {
var latitude = place.geometry.location.lat();
var longitude = place.geometry.location.lng();
// do something with Lat/Lng
}
}
});
}
}
}).autocomplete("instance")._renderItem = function (ul, item) {
// item is the prediction object returned from our call to getQueryPredictions
// return the prediction object's "description" property or do something else
return $("<li>")
.append("<div>" + item.description + "</div>")
.appendTo(ul);
};
$("#CustomerAddress_Street").autocomplete("instance")._renderMenu = function (ul, items) {
// Google's terms require attribution, so when building the menu, append an item pointing to their image
var that = this;
$.each(items, function (index, item) {
that._renderItemData(ul, item);
});
$(ul).append("<li class='ui-menu-item'><div style='display:flex;justify-content:flex-end;'><img src='https://maps.gstatic.com/mapfiles/api-3/images/powered-by-google-on-white3.png' /></div></li>")
}

Meteor - How can I pass data between helpers and events for a template?

I'm a bit new to Meteor and something I'm having trouble with is reactive data -- particularly in instances where I need to change the data shown based on a mouse or keyboard event. Doing this kind of stuff the normal js way seems to give me trouble in meteor since everything I change gets re-rendered and reset constantly.
So, I thought I'd see if this would be a case in which I could use Meteor's Deps object, however I can't quite grasp it. Here's the code I'm using:
(function(){
var tenants = [];
var selectedTenant = 0;
var tenantsDep = new Deps.Dependency;
Template.tenantsBlock.tenantsList = function()
{
tenants = [];
var property = $properties.findOne({userId: Meteor.userId(), propertyId: Session.get('property')});
var tenancies = _Utils.resolveTenancies(property, true, null, true);
for(var i = 0; i < tenancies.length; i++)
{
if(tenancies[i].tenancyId == Session.get('tenancy'))
{
tenants = tenants.concat(tenancies[i].otherTenants, tenancies[i].primaryTenant);
}
}
tenants[selectedTenant].selected = 'Selected';
tenantsDep.changed();
return tenants;
};
Template.tenantsBlock.onlyOneTenant = function()
{
tenantsDep.depend();
return tenants.length > 1 ? '' : 'OneChild';
};
Template.tenantsBlock.phoneNumber = function()
{
tenantsDep.depend();
for(var i = 0; i < tenants[selectedTenant].details.length; i++)
if(_Utils.getDynamicContactIconClass(tenants[selectedTenant].details[i].key) == 'Phone')
return tenants[selectedTenant].details[i].value;
return null;
};
Template.tenantsBlock.emailAddress = function()
{
tenantsDep.depend();
for(var i = 0; i < tenants[selectedTenant].details.length; i++)
if(_Utils.getDynamicContactIconClass(tenants[selectedTenant].details[i].key) == 'Email')
return tenants[selectedTenant].details[i].value;
return null;
};
Template.tenantsBlock.addedDate = function()
{
tenantsDep.depend();
return _Utils.timeToDateString(tenants[selectedTenant].created);
};
Template.tenantsBlock.events({
'click .Name': function(e, template)
{
tenantsDep.depend();
var _this = e.currentTarget;
var tenantName = _this.innerHTML;
$(_this).addClass('Selected');
$(_this).siblings().removeClass('Selected');
for(var i = 0; i < tenants.length; i++)
{
if(tenants[i].name == tenantName)
tenants[i].selected = "Selected";
else
tenants[i].selected = '';
}
}
})
})();
^This seemed to be what they were getting at in the meteor documentation (http://docs.meteor.com/#deps_dependency) for dependency.changed() and dependency.depend(), but all this does is give me an infinite loop.
So can I modify the way I declare deps to get this to make data reactive? Is there a better way to do this all together?
UPDATE:
Although I was skeptical to do so, I've been inclined to try to use Session.set/Session.get in a localized way. So, the next time I have to do this, I'll just do
Session.set('tenantsBlock' {tenants: [], selectedTenant: 0});
and then just access this variable from within helpers and event maps related to Template.tenantsBlock. That way they all have real time access to the data and they all get re-run when the data changes. Here's what I converted this script into (sorry these are both so large):
(function()
{
Template.tenantsBlock.created = Template.tenantsBlock.destroyed =function()
{
_Utils.setSession('tenantsBlock', {
tenants: [],
selectedTenant: 0
})
};
Template.tenantsBlock.tenantsList = function()
{
var localContext = Session.get('tenantsBlock');
localContext.tenants = [];
var property = $properties.findOne({userId: Meteor.userId(), propertyId: Session.get('property')});
var tenancies = _Utils.resolveTenancies(property, true, null, true);
for(var i = 0; i < tenancies.length; i++)
{
if(tenancies[i].tenancyId == Session.get('tenancy'))
{
localContext.tenants = localContext.tenants.concat(tenancies[i].otherTenants, tenancies[i].primaryTenant);
break;
}
}
localContext.tenants[localContext.selectedTenant].selected = 'Selected';
Session.set('tenantsBlock', localContext);
return localContext.tenants;
};
Template.tenantsBlock.onlyOneTenant = function()
{
var localContext = Session.get('tenantsBlock');
return localContext.tenants.length > 1 ? '' : 'OneChild';
};
Template.tenantsBlock.phoneNumber = function()
{
var localContext = Session.get('tenantsBlock');
for(var i = 0; i < localContext.tenants[localContext.selectedTenant].details.length; i++)
if(_Utils.getDynamicContactIconClass(localContext.tenants[localContext.selectedTenant].details[i].key) == 'Phone')
return localContext.tenants[localContext.selectedTenant].details[i].value;
return null;
};
Template.tenantsBlock.emailAddress = function()
{
var localContext = Session.get('tenantsBlock');
var selectedTenantDetails = localContext.tenants[localContext.selectedTenant].details;
for(var i = 0; i < selectedTenantDetails.length; i++)
if(_Utils.getDynamicContactIconClass(selectedTenantDetails[i].key) == 'Mail')
return selectedTenantDetails[i].value;
return null;
};
Template.tenantsBlock.addedDate = function()
{
var localContext = Session.get('tenantsBlock');
return _Utils.timeToDateString(localContext.tenants[localContext.selectedTenant].created);
};
Template.tenantsBlock.events({
'click .Name': function(e, template)
{
var localContext = Session.get('tenantsBlock');
var _this = e.currentTarget;
var tenantName = _this.innerHTML;
for(var i = 0; i < localContext.tenants.length; i++)
{
if(localContext.tenants[i].name == tenantName)
{
localContext.tenants[i].selected = 'Selected';
localContext.selectedTenant = i;
}
else
{
localContext.tenants[i].selected = '';
}
}
Session.set('tenantsBlock', localContext);
}
})
})();
You'll have to overcome the old-school way of doing it :) Meteor is a lot simpler than you think. A good rule of thumb is that if you're using jQuery to manipulate any DOM elements, you're probably doing it wrong. Additionally, if you're accessing any data without using the collection API, you'd better have good reason to do so.
In your case, you don't need to code up any manual dependencies at all. Manual dependencies are rarely needed in most Meteor applications.
The first thing you need to do is put all your tenants inside a Meteor.Collection, which will make them easier to work with.
Tenants = new Meteor.Collection("tenants");
Your tenantsBlock template should look something like this (modulo some different html elements):
<template name="tenantsBlock">
<ol>
{{#each tenants}}
<li class="name {{selected}}">
<span>Primary Tenant: {{primaryTenant}}</span>
<span>Other Tenants: {{otherTenants}}</span>
<span>Phone Number: {{phoneNumber}}</span>
<span>Email Address: {{emailAddress}}</span>
<span>Added Date: {{addedDate}}</span>
</li>
{{/each}}
</ol>
</template>
Each document in Tenants should look something like the following:
{
primaryTenant: "Joe Blow",
otherTenants: "Mickey Mouse, Minnie Mouse",
phoneNumber: "555-234-5623",
emailAddress: "joe.blow#foo.com",
addedDate: "2005-10-30T10:45Z"
}
Then, all the code you would need is just for the selection/deselection, and you can delete everything else:
Template.tenantsBlock.tenants = function() {
return Tenants.find();
};
Template.tenantsBlock.selected = function() {
return Session.equals("selectedTenant", this._id);
};
Template.tenantsBlock.events({
'click .name': function(e) {
Session.set("selectedTenant", this._id);
}
});
Once again, I reiterate that you should never be doing DOM manipulations with Javascript when using Meteor. You just update your data and your templates will reactively update if everything is done correctly. Declare how you want your data to look, then change the data and watch the magic.
Meteor has really evolved since I posted this back in 2013. I thought
I should post a modern, superior method.
For a while now you've been able to create a ReactiveVar and now you can append those directly to templates. A ReactiveVar, similar to Session, is a reactive data store. ReactiveVar, however, holds only a single value (of any type).
You can add ReactiveVar to the client side of your project by running this in your terminal from your app's root directory:
$meteor add reactive-var
This javascript shows how you can pass the variable between the template's onCreated, onRendered, onDestroyed, events and helpers.
Template.myTemplate.onCreated = function() {
// Appends a reactive variable to the template instance
this.reactiveData = new ReactiveVar('Default Value');
};
Template.myTemplate.events({
'click .someButton': (e, template) => {
// Changes the value of the reactive variable for only this template instance
template.reactiveData.set('New Value');
},
});
Template.myTemplate.helpers({
theData: () => {
// Automatically updates view when reactive variable changes
return Template.instance().reactiveData.get();
},
});
This is superior for a few reasons:
It scopes the variable only to a single template instance. Particularly useful in cases where you might have a dozen instances of a template on a page, all requiring independent states.
It goes away when the template goes away. Using ReactiveVar or Session variables you will have to clear the variable when the template is destroyed (if it is even destroyed predictably).
It's just cleaner code.
Bonus Points: See ReactiveDict for cases in which you have many instances of a template on a page at once, but need to manage a handful of reactive variables and have those variables persist during the session.

Resources