Turning off reactivity for contenteditable areas (Meteor) - meteor

I'm aware that you can turn off reactivity with reactive: false while fetching collections. How to accomplish the same with collection fields within, say, a contenteditable area? Example:
Template.documentPage.events({
'input .document-input': (function(e) {
e.preventDefault();
content = $(e.target).html();
Documents.update(this._id, {
$set: {
content: content
}
});
}
});
<template name="documentPage">
<div class='document-input' contenteditable='true'>{{{content}}}</div>
</template>

to turn of reactivity of e.g. helpers or other reactive functions wrap your function with Tracker.nonreactive(fn). See: http://docs.meteor.com/#/full/tracker_nonreactive

you could set the reactive data to an attribute and move it to the html in onRendered
<template name="documentPage">
<div class='document-input' contenteditable='true' data-content='{{content}}'></div>
</template>
Template.documentPage.onRendered(function () {
var doc = this.find(".document-input");
doc.innerHTML = doc.dataset.content;
});
That way the data-content attribute will be updated by blaze on input not the text of the element.
SECURITY NOTE: I changed the template to use {{content}} instead of {{{content}}} so it can be escaped by meteor but used doc.innerHTML so the content will be "unescaped" in the element. Any time you are rendering html from user input you should make sure you are sanitizing it so you don't open an xss vulnerability in your site.
The alternative would be to use a library like https://github.com/UziTech/jquery.toTextarea.js to change the editable content of a div to text and use doc.textContent in place of doc.innerHTML to remove any chance of xss

Related

How make a dynamic value in MeteorJS

I need to set a variable or array in meteor JS which whenever changes. Wherever it is used on the page changes.
So far I have tried to set values in session.
let in = Session.get("in");
and this in HTML. The following works fine. This changes whenever it detects a change in array
{{#each in}}
<span class="selectable-tags"> {{this}} </span>
{{/each}}
But whenever I try to add these tags to other div using following. Then the ones added to other div does not change.
"click .selectable-tags"(e, t) {
// sets the tags inside the Method box on click
var el = e.target.cloneNode(true);
el.setAttribute('contenteditable', false);
document.querySelector('[contenteditable]').appendChild(el); },
using meteor with blaze UI. This can be used as reference link
Edit: Session.get or set is given for reference . This was to tell that these values are changing as they on any event triggered wherever set.
You need to add a helper that returns the session variable and show the helper in your html code :
Template.yourtemplate.helpers({
inHelper: ()=> {
return Session.get('in');
}
)}
And in html :
<div>{{inHelper}}</div>
Every modification on in by Session.set('in', newValue) will change your HTML.

How to expose wrapped <input> in Vue?

I'm trying to create a reusable styled input field in Vue. To make it styled (e.g. with an icon inside) I need to wrap it in another html-element.
Lets call the example below StyledInput
<div class="hasIcon">
<input />
<i class="someIcon"></i>
<div>
If I want to use StyledInput it might look like so:
<styled-input #keyup.enter="doSomething">
</styled-input>
But this would not work, due to the event listener being attached to the <div> instead of the <input>.
A workaround to that could be to emit all key-events from the input field:
<div class="hasIcon">
<input #keyup="$emit('keyup', $event) />
<i class="someIcon"></i>
<div>
But this will not scale well since it would have to be rewritten every time a developer uses an unmapped prop or event.
Is there a way to only make the inner element exposed to whomever uses it?
I'm not sure there is a Vue way to achieve this, because, as far as I'm aware there is no way to bind vue events dynamically, it is however possible to do this using vanilla javascript by passing all events as a prop then mapping them using addEventListener() to add your custom events:
Vue.component('my-input', {
template: "#my-input",
props: ['events'],
mounted() {
// get the input element
let input = document.getElementById('styled-input');
// map events
this.events.forEach((event) => {
let key = Object.keys(event);
input.addEventListener(key, event[key]);
});
}
})
Then you can just pass through all events as a prop like so:
<my-input :events="events"></my-input>
View Model:
var app = new Vue({
el: "#app",
data: {
events: [{
focus: () => {
console.log('focus')
}
}, {
keyup: (e) => {
console.log(e.which)
}
}]
}
})
Heres the JSFiddle: https://jsfiddle.net/h1dnk40v/
Of course, this means any developer would have to do things like map key codes etc, so you will lose some of the convenience Vue provides.
One thing I will just mention is that Vue components aren't necessarily intended to be infinitely reusable, they are supposed to provide specific functionality and encapsulate complex logic, so you would probably do better to implement the most likely use cases, and if the component doesn't fit you can extend it or write a new one for that particular event.
You can also use $attrs to pass props and events onto children elements:
<template>
<div>
<input v-bind="$attrs">
</div>
</template>
In Vue 3, you can specify a second script tag:
<script setup>
</script>
<script>
export default {
inheritAttrs: false,
};
</script>
https://vuejs.org/guide/components/attrs.html#disabling-attribute-inheritance
You could use slots to achieve this. If your <styled-input> template looks like this:
<div class="hasIcon">
<slot><input></slot>
<i class="someIcon"></i>
<div>
Then you can use it like this:
<styled-input>
<input #keyup.enter="doTheThing">
</styled-input>
Or, in cases where you don't care about the input events, like this:
<styled-input></styled-input>
and the default slot content (a bare <input>) will be used. You can use CSS to style the <input> inside the component, but you can't add custom properties or classes to it, so this approach may or may not fit your requirements.

how to retain (state of) non-reactive DOM elements in meteorjs?

I'm trying to understand options for using a stateful / non-reactive DOM component in a Meteor template, in a way that allows the component to retain its state as Meteor updates the DOM.
One specific example involves Leaflet.js: I have an application that includes a Leaftlet map, and I want the user to be able to switch between a display of the map, and some other content. The map is interactive --- the user can pan and zoom in the map --- and I'd like the current zoom/pan state of the map to be retained if/when the user switches away from the map to other content, and then back to the map.
My first attempt at doing this is to put the map in one template, and the other content in another template, and use conditional logic in the containing template to determine which template is rendered:
HTML:
<body>
<div>
<input type="submit" id="mapbutton" value="Display Map">
<input type="submit" id="otherbutton" value="Display Other Stuff">
</div>
{{#if showmap}}
{{> map}}
{{else}}
{{> otherstuff}}
{{/if}}
</body>
<template name="map">
<div id="map"></div>
</template>
<template name="otherstuff">
<p>Here is some other stuff</p>
</template>
JS:
Template.map.rendered = function() {
var map = L.map('map', {
doubleClickZoom: false
}).setView([38.0, -98.0], 5);
L.tileLayer('https://{s}.tiles.mapbox.com/v3/{id}/{z}/{x}/{y}.png', {
maxZoom: 18,
id: 'examples.map-i875mjb7'
}).addTo(map);
};
Session.setDefault("showmap", true);
Template.body.helpers({
"showmap" : function() {
return Session.get("showmap");
}
});
Template.body.events({
"click input#mapbutton": function() {
Session.set("showmap", true);
},
"click input#otherbutton": function() {
Session.set("showmap", false);
}
});
The problem with this approach is that every time the user switches to the map display, Meteor re-renders the map template, creating a new Leaflet map (and associated DOM component), which is initialized from scratch. This means that whatever pan and/or zoom settings the user had previously made in the map are lost. It also involves a short delay in the display while the Leaflet map is constructed. I'd like the Leaflet map to get created one time only, the first time it is displayed, and then saved somewhere off-screen when the user swiches to other content, so that it can be immediately swapped back in later, without incurring the construction delay, and retaining its previous pan/zoom state.
I know that one way to accomplish this would be to design my HTML templates to keep the map div in the DOM when switching displays,
and to use CSS to hide it when necessary. Something like the following:
HTML:
<body>
<div>
<input type="submit" id="mapbutton" value="Map">
<input type="submit" id="otherbutton" value="Other Stuff">
</div>
<div id="map" class="{{#if showmap}}visible{{else}}hidden{{/if}}"></div>
{{#if showother}}
{{> otherstuff}}
{{/if}}
</body>
<template name="otherstuff">
<p>Here is some other stuff</p>
</template>
JS:
Template.body.rendered = function() {
var map = L.map('map', {
doubleClickZoom: false
}).setView([38.0, -98.0], 5);
L.tileLayer('https://{s}.tiles.mapbox.com/v3/{id}/{z}/{x}/{y}.png', {
maxZoom: 18,
id: 'examples.map-i875mjb7'
}).addTo(map);
};
Session.setDefault("showmap", true);
Template.body.helpers({
"showmap" : function() {
return Session.get("showmap");
},
"showother" : function() {
return !Session.get("showmap");
}
});
Template.body.events({
"click input#mapbutton": function() {
Session.set("showmap", true);
},
"click input#otherbutton": function() {
Session.set("showmap", false);
}
});
CSS:
#map.visible {
display: block;
}
#map.hidden {
display: none;
}
This works fine for this simple example, but in reality my application (and the associated templates and resulting DOM) is much more complex.
What I REALLY want is to be able to move the map component around arbitrarily in the DOM. For example, depending on the context, the
map might appear inside a table, or full-screen, or not at all, and I'd like to retain the map's internal state between all of these contexts. Using a Meteor template for the map with conditional logic that determines where it is included seems like a natural way to structure this kind of thing, but that returns to the above problem that every time the map template is rendered, the map is rebuilt from
scratch and reset to its initial state.
Is there a way to tell Meteor to "cache" its rendering of a particular template, and to hang on to the associated DOM element, so that subsequent times when that template is used in the rendering of other content, the previously constructed DOM element is used? I realize this goes against the grain of the reactive approach, but this is a situation where I'm trying to use a complex non-reactive component, and it seems like support for such things could be useful in many contexts.
This issue isn't specific to Leaftlet.js, by the way. I have other non-reactive, stateful components that I would like to use in my Meteor application, and I'd love to find a graceful way to solve this problem for all of them.
Does anyone know if there is a way to do this, or have ideas for a better approach?
Thanks!
I don't think you can keep a rendered item ready for hiding/displaying without any re-rendering, except if you use CSS.
Blaze (the component taking care of rendering templates) can't do that (yet). Have a look at this topic where they basically say the same, but it comes from a meteor dev: https://github.com/meteor/meteor/issues/4351
Either you rely on CSS, either you keep the values you need in for example a reactive dictionary and use them when you render your map template.
Thanks #Billybobbonnet. Your comment to keep the values you need and re-use them when rendering the template gave me the idea to try this:
HTML:
<body>
<div>
<input type="submit" id="mapbutton" value="Map">
<input type="submit" id="otherbutton" value="Other Stuff">
</div>
{{#if showmap}}
{{> map}}
{{else}}
{{> otherstuff}}
{{/if}}
</body>
<template name="map">
<div id="mapcontainer">
<div id="map"></div>
</div>
</template>
<template name="otherstuff">
<p>Here is some other stuff</p>
</template>
JS:
var $mapdiv = undefined;
Template.map.rendered = function() {
if ($mapdiv === undefined) {
// if this is the first time the map has been rendered, create it
var map = L.map('map', {
doubleClickZoom: false
}).setView([38.0, -98.0], 5);
L.tileLayer('https://{s}.tiles.mapbox.com/v3/{id}/{z}/{x}/{y}.png', {
maxZoom: 18,
id: 'examples.map-i875mjb7'
}).addTo(map);
// and hang on to the map's div element for re-use later
$mapdiv = $("#map");
} else {
// map has already been created, so just empty out the container
// and re-insert it
$("#mapcontainer").empty();
$("#mapcontainer").append($mapdiv);
}
};
Session.setDefault("showmap", true);
Template.body.helpers({
"showmap" : function() {
return Session.get("showmap");
}
});
Template.body.events({
"click input#mapbutton": function() {
Session.set("showmap", true);
},
"click input#otherbutton": function() {
Session.set("showmap", false);
}
});
This seems to be working well. It feels a little kludgy, but I like the fact that it lets me put the map in a template which I can use anywhere, just like any other template, and yet the map is only created once.

how to attach events to generated html of a template in Meteor 0.8 with Blaze

I'm using Meteor 0.8 with Blaze and I want to attach events dynamically to HTML contents generated using UI.toHTML of a template. The functionality I am looking for is the alternative to Spark.attachEvents in Blaze.
What I have done so far is that I have created the following template to be used like a widget/component.
<template name="postLinks">
<div id="link-popover-wrapper" >
<ul class="link-popover">
{{#each linkOptions}}
<li><a tabindex="-1" class="link-action" id="link-{{value}}" href="#">{{label}}</a>
</li>
{{/each}}
</ul>
</div>
</template>
And the template is used in Helper of the myPostItem template.
Template.myPostItem.events({
'click .post-item-link-picker': function (evt, tmpl) {
var tempData = {linkOptions:[{label:'Favorite', value : 'favorite'}, ...]};
// Get the HTML content of the template passing data
var linkContent = UI.toHTML(Template['postLinks'].extend({data: function () { return tempData; }}));
// Attach events to the linkContent like in Spark
/*Spark.attachEvents({
'click link-action': function (e, tmpl) {
alert("Component item click");
}
}, linkContent);*/
// Popover the content using Bootstrap popover function
}
});
So my requirement is to attach events to a dynamically generated HTML contents.in the linkContent like Spark.attachEvents after the following line as mentioned in above code.
var linkContent = UI.toHTML(Template['postLinks'].extend({data: function () { return tempData; }}));
Hope somebody can help to find a way to do this in Meteor 0.8 with Blaze.
The reason that Spark-generated HTML could be directly inserted into the DOM is because it had "landmarks" - annotations that could be processed into events and handlers when the DOM nodes were materialized.
Blaze works differently - it inserts a UI component into the DOM directly and attaches events, using the UI.render function. It cannot directly attach template events to the DOM if you use UI.toHTML because there are none of the annotations that Spark had for doing this.
I'm also using Bootstrap popovers in my app, and as far as I know there's no clean way to insert reactive content into a popover. However, you can approximate it with a hack in the following way:
In the content callback of the popover, render the template with UI.toHTML - a nonreactive version of the content. This is necessary because otherwise the popover won't be sized and positioned properly.
Using a Meteor.defer call, replace the popover contents with reactive content, so they'll continue updating while the popover is open.
Because Bootstrap uses jQuery, you should be fine with removing reactive logic properly, for now. Future versions of Meteor will probably have easier ways to do this.

Meteor, Form validation and Bootstrap accordion

I have a form in a Meteor application that contains a list of radio buttons that is made accessible by a Bootstrap accordion. I have implemented a validation routing in meteor that inserts classes into the input elements to mark them as valid/invalid.
Problem is: Everytime a validation state of one input element changes, the template is redrawn and the currently open accordion closes.
I solved this by doing this clumsy approach:
Meteor.startup(function() {
// track collapse state of accordion....
$('.collapse').on('shown', function() {
Session.set(addBookingCollapse, $(this).attr('id'));
});
});
Template.addrecord.helpers({
openAccordion: function(accordion) {
if (Session.get(addBookingCollapse) == accordion) {
return 'in'
};
}
});
<div id="collapseOne" class="accordion-body
collapse {{openAccordion 'collapseOne'}}">
...and so on for the other four collapsible elements
But for whoever's sake, there must be a more elegant solution? I do not want to waste a session variable for this....
It may help to put the input elements in {{#isolate}}{{\isolate}} blocks to avoid re-rendering the entire template. See: Meteor Isolates
I haven't really looked super close at your code/problem, but why do you use id's to target each of them on their own? Why not use a class for all of them like this:
$('.a-class-that-you-name').on('shown', function() {
var thisSpecificId = $(this).attr('id');
Session.set('addBookingCollapse', thisSpecificId);
});
Would that work?

Resources