I have come up with a methodology for making editable text in my Meteor app. However, it does not follow the DRY paradigm and I'd like to change that but I am not too good with Javascript yet...
Suppose I have a table cell with some text and I'd like to double click it to edit it. I created a template variable to handle this:
<td class="itemName">
{{#unless editItemName}}
{{name}}
{{else}}
<input class="editItemName" type="text" value="{{name}}" style="width:100px;">
{{/unless}}
</td>
I then create an event to execute this transition on a double-click:
Template.inventoryItemDetail.events = {
'dblclick td.itemName': function (evt) {
Session.set("editItemName",true);
},
'blur input.editItemName': function () {
Session.set("editItemName",null);
},};
I also reused the ok_cancel code from the ToDo's example app (but that's sort of irrelevant):
// Returns an event_map key for attaching "ok/cancel" events to
// a text input (given by selector)
var okcancel_events = function (selector) {
return 'keyup '+selector+', keydown '+selector+', focusout '+selector;
};
// Creates an event handler for interpreting "escape", "return", and "blur"
// on a text field and calling "ok" or "cancel" callbacks.
var make_okcancel_handler = function (options) {
var ok = options.ok || function () {};
var cancel = options.cancel || function () {};
return function (evt) {
if (evt.type === "keydown" && evt.which === 27) {
// escape = cancel
cancel.call(this, evt);
evt.currentTarget.blur();
} else if (evt.type === "keyup" && evt.which === 13) {
// blur/return/enter = ok/submit if non-empty
var value = String(evt.target.value || "");
if (value) {
ok.call(this, value, evt);
evt.currentTarget.blur();
}
else {
cancel.call(this, evt);
evt.currentTarget.blur();
}
}
};
};
Template.inventoryItemDetail.events[ okcancel_events('input.editItemName') ] = make_okcancel_handler({
ok: function (value) {
Items.update(this._id, {$set: {name: value}});
}
});
Finally, I have to tie this Session variable to the template variable:
Template.inventoryItemDetail.editItemName = function () {
return Session.get("editItemName");
};
So right now, I have repeated all of this again and again for each editable text field and it all works, but it seems like terribly programming practice. I have found various editable text utilities on Github but I don't entirely understand them and none of them are for Meteor!
I'd really like to expand my knowledge of Meteor and Javascript by creating a tool that allows me to have editable text without repeating myself this ridiculous amount for each editable text field.
Thanks,
Chet
https://github.com/nate-strauser/meteor-x-editable-bootstrap for the package.
http://vitalets.github.io/x-editable/docs.html for the docs.
I just implemented this in my project and I won't ever go back to contenteditable.
Related
Using Fullcalendar 4, I am trying to show/hide my resources using a select menu. When the user selects one of the providers from a menu, I want to only show that one resourc's events.
Above my fullcalendar I have my select menu:
<select id="toggle_providers_calendar" class="form-control" >
<option value="1" selected>Screech Powers</option>
<option value="2">Slater</option>
</select>
I am gathering the resources I need using an ajax call on my included fullcalendar.php page. I am storing them in an object and then trying to control which resources are shown onscreen:
document.addEventListener('DOMContentLoaded', function() {
var resourceData = [];
$.getJSON('ajax_get_json.php?what=schedule_providers_at_location',
function(data) {
$.each(data, function(index) {
resourceData.push({
id: data[index].value,
title: data[index].text
});
});
console.log(resourceData);
});
//below, set the visible resources to whatever is selected in the menu
//using 1 in order for that to show at start
var visibleResourceIds = ["1"];
//below, get the selected id when the the menu is changed and use that in the toggle resource function
$('#toggle_providers_calendar').change(function() {
toggleResource($('#toggle_providers_calendar').val());
});
var calendar_full = document.getElementById('calendar_full');
var calendar = new FullCalendar.Calendar(calendar_full, {
events: {
url: 'ajax_get_json.php?what=location_appointments'
},
height: 700,
resources: function(fetchInfo, successCallback, failureCallback) {
// below, I am trying to filter resources by whether their id is in visibleResourceIds.
var filteredResources = [];
filteredResources = resourceData.filter(function(x) {
return visibleResourceIds.indexOf(x.id) !== -1;
});
successCallback(filteredResources);
},
...
});
// below, my toggle_providers_calendar will trigger this function. Feed it resourceId.
function toggleResource(resourceId) {
var index = visibleResourceIds.indexOf(resourceId);
if (index !== -1) {
visibleResourceIds.splice(index, 1);
} else {
visibleResourceIds.push(resourceId);
}
calendar.refetchResources();
}
To make sure the getJSON is working, I have console.log(resourceData). The information in the console once it's gathered is:
[{id: '1', title: 'Screech Powers'}, {id: '2', title: 'Slater}]
... the above are the correct resources that can be chosen/rendered. So that seems to be okay.
On page load, no resources show at all, when resource id of '1' (Screech Powers) should be shown per my code. Well, at least, that's what I am trying to do right now.
When the menu changes, resources will show/hide, but not based on what's selected; the logic of only showing what is selected in the menu doesn't seem to be working.
I used to use a URL request for my resources: 'ajax_get_json.php?what=schedule_providers_at_location', and it worked fine! All resources show then their events properly. I am just trying to modify it by using a menu to show/hide the resources as needed.
Here's what I'm doing to make it happen so far! In case someone comes across this post ever, this will help.
Here's my code before my fullcalendar code.
var resourceData = [];
var visibleResourceIds = [];
$.getJSON('ajax_get_json.php?what=schedule_providers_at_location',
function(data) {
$.each(data, function(index) {
resourceData.push({
id: data[index].value,
title: data[index].text
});
});
});
$('#toggle_providers_calendar').change(function() {
toggleResource($('#toggle_providers_calendar').val());
});
My select menu with id 'toggle_providers_calendar' is the same as my original post. My fullcalendar resources as a function is the same too.
After the calendar is rendered, here are the changes I made to my toggle resources function:
// menu button/dropdown will trigger this function. Feed it resourceId.
function toggleResource(resourceId) {
visibleResourceIds = [];
//if select all... see if undefined from loading on initial load = true
if ((resourceId == '') || (resourceId === undefined)) {
$.map( resourceData, function( value, index ) {
visibleResourceIds.push(value.id);
});
}
var index = visibleResourceIds.indexOf(resourceId);
if (index !== -1) {
visibleResourceIds.splice(index, 1);
} else {
visibleResourceIds.push(resourceId);
}
calendar.refetchResources();
}
This causes the resources to show and hide properly. If the user selects "Show All" that works too!
In order to have a default resource show on load, I add this to my fullcalendar script:
loading: function(bool) {
if (bool) {
//insert code if still loading
$('.loader').show();
} else {
$('.loader').hide();
if (initial_load) {
initial_load = false;
//code here once done loading and initial_load = true
var default_resource_to_show = "<?php echo $default_provider; ?>";
if (default_resource_to_show) {
//set the menu to that provider and trigger the change event to toggleresrource()
$('#toggle_providers_calendar').val(default_provider).change();
} else {
//pass in nothing meaning 'select all' providers for scheduler to see
toggleResource();
}
}
}
},
I am using a bool variable of initial_load to see if the page was just loaded (basically not loading data without a page refresh). The bool of initial_load = true is set outside of DOMContentLoaded
<script>
//show selected date in title box
var initial_load = true;
document.addEventListener('DOMContentLoaded', function() {
My only current problem is that when toggleResource function is called, the all day vertical time block boundaries don't line up with the rest of the scheduler. Once I start navigating, they do, but I don't understand why it looks like this on initial load or when toggleResource() is called:
Any thoughts on how to correct the alignment of the allday vertical blocks?
I have a selectbox in template A which selects an item "name and id".
I also have an "item" template that needs an ID as parameter to load its data from its database. I am using a session variable for the id and I pass the id to the "item" template using return Session.get . This only works for on load. When the session variable change the "item" template is not updated. How do I get the "item" template to re-initialize
Some code:
Template.selectBox.helpers({
selectList: function () {
return Templates.find({}, {fields: {'_id': 1, 'name': 1}});
},
selectedId: function() {
return Session.get("selectedId");
}
});
Template.selectBox.events({
'change #item-chooser': function (event) {
var selectedId = $(event.currentTarget).find(":selected").val();
if (typeof(selectedId) === 'undefined'
|| selectedId === "new") {
Session.set("selectedId", "new");
}
else {
Session.set("selectedId", selectedId);
}
}
});
The items template is called using
{{> item selectedId}}
Template.item.onCreated(function() {
var selectedId = this.data.selectedId;
this.selectedItem = new ReactiveVar;
if (typeof(selectedId) === 'undefined'
|| selectedId === "new") {
this.selectedItem.set(emptyItem);
}
else {
var selectedItemData = Templates.findOne({_id: selectedId});
this.selectedItem.set(selectedItemData );
}
});
It's important to note that the Template.onCreated method is not reactive so if you have reactive variables, this method does not automatically re-run when a reactive data source changes unlike Template.helpers
The easiest way to fix your problem would be to use autorun
Template.item.onCreated(function(){
var self = this;
self.autorun(function(){
// some code that has a reactive data source (e.g. Session Var, Collection, or Reactive Var
// NOTE: you can access template instance data using self.data
});
});
However, based on your description, I think there could be a better way to handle your problem using Template.helpers
Template.item.helpers({
selectedItem: function(){
return Templates.findOne({_id: Session.get("selectedId")});
}
});
I'm new to Meteor and I want to create a slideshow with items from a collection, in this case simple words. The slideshow should be controlled by back and forward buttons and replace the current word.
In JavaScript/jQuery I would create an array of objects and a control index, with limits via if-statements, so the index never can drop below zero or overflow the length of the array.
See fiddle for working example:
http://jsfiddle.net/j0pqd26w/8/
$(document).ready(function() {
var wordArray = ["hello", "yes", "no", "maybe"];
var arrayIndex = 0;
$('#word').html(wordArray[arrayIndex]);
$("#previous").click(function(){
if (arrayIndex > 0) {
arrayIndex -= 1;
}
$('#word').html(wordArray[arrayIndex]);
});
$("#next").click(function(){
if (arrayIndex < wordArray.length) {
arrayIndex += 1;
}
$('#word').html(wordArray[arrayIndex]);
});
});
Meteor
I'm curious how to implement this in regards to best practice in meteor and abide to the reactive pattern as I'm still trying to wrap my head around this interesting framework. My first hurdle is to translate the
if (arrayIndex < wordArray.length)
// to
if (Session.get("wordIndex") < ( (((length of collection))) )
According to the docs I should do a find on the collection, but I have only manage to return an empty array later with fetch. Sorry if this got long, but any input would be appreciated to help me figure this out.
collection.find([selector], [options])
cursor.fetch()
This is the code I have so far:
Words = new Mongo.Collection("words");
if (Meteor.isClient) {
// word index starts at 0
Session.setDefault("wordIndex", 0);
Template.body.helpers({
words: function () {
return Words.find({});
},
wordIndex: function () {
return Session.get("wordIndex");
}
});
Template.body.events({
"submit .new-word": function (event) {
// This function is called when the word form is submitted
var text = event.target.text.value;
Words.insert({
text: text,
createdAt: new Date() //current time
});
// Clear form
event.target.text.value = "";
// Prevent default form submit
return false;
},
'click #previous': function () {
// decrement the word index when button is clicked
if (Session.get("wordIndex") > 0) {
Session.set("wordIndex", Session.get("wordIndex") - 1);
}
},
'click #next': function () {
// increment the word index when button is clicked
if (Session.get("wordIndex") < 10 ) {
Session.set("wordIndex", Session.get("wordIndex") + 1);
}
}
});
}
if (Meteor.isServer) {
Meteor.startup(function () {
});
}
.count() will return the number of documents in a collection.
`db.collection.count()`
There is something called Collection helpers, which works similar to other helpers (eg., template, etc.,). More elaborate explanation is covered here: https://medium.com/space-camp/meteor-doesnt-need-an-orm-2ed0edc51bc5
UPDATED
NOW I try to do this in my app (thx to Akshat)
//common
LANG = 'ru';
Dictionary = new Meteor.Collection("dictionary");
//if server
Meteor.startup(function () {
if (Dictionary.find().count() === 0) {
// code to fill the Dictionary
}
});
Meteor.publish('dictionary', function () {
return Dictionary.find();
});
//endif
//client
t = function(text) {
if (typeof Dictionary === 'undefined') return text;
var res = Dictionary.find({o:text}).fetch()[0];
return res && res.t;
}
Meteor.subscribe('dictionary', function(){
document.title = t('Let the game starts!');
});
Template.help.text = t('How to play');
//html
<body>
{{> help}}
</body>
<template name="help">
{{text}}
</template>
Still doesn't work as we wanted: when templates are rendered Dictionary was undefined. Butt('How to play') in console works perfectly )
Javascript variables aren't shared between the client and server reactively. You have to use a Meteor Collection to store your data e.g
if (Meteor.isServer) {
var Dictionary = new Meteor.Collection("dictionary");
if(Dictionary.find().count() == 0) {
//If the 'dictionary collection is empty (count ==0) then add stuff in
_.each(Assets.getText(LANG+".txt").split(/\r?\n/), function (line) {
// Skip comment lines
if (line.indexOf("//") !== 0) {
var split = line.split(/ = /);
DICTIONARY.insert({o: split[0], t:split[1]});
}
});
}
}
if (Meteor.isClient) {
var Dictionary = new Meteor.Collection("dictionary");
Template.help.text = function() {
return Dictionary.find({o:'Let the game starts!'});
}
}
In the above i'm assuming you have the autopublish package in (its in by default when you create a package so this shouldn't really bother you, but just in case you removed)
With your document title you would have to use a slightly different implementation because remember the data wouldn't be downloaded at the time Meteor.startup is run, since the html and javascript are downloaded first & the data is empty, then the data comes in slowly (and then reactively fills the data up)
I want to capture the event of a user pressing enter on an input of type="text" when they are filling out a form. This is done all over the web, yet the answer eludes me.
This is what I have so far:
In the html file, I have a text input like so:
<input type="text" size=50 class="newlink">
In the Javascript file, I am trying to capture the the user pressing enter to effectively submit the form. I am then grabbing the text from the input and going to stash it in the database:
Template.newLink.events = {
'submit input.newLink': function () {
var url = template.find(".newLink").value;
// add to database
}
};
The submit event is emitted from forms, not single input elements.
The built in event map for meteor is documented here: http://docs.meteor.com/#eventmaps.
You'll have to listen for a keyboard event (keydown, keypress, keyup). Within the event handler, check, if it's the return/enter key (Keycode 13), and proceed on success.
Template.newLink.events = {
'keypress input.newLink': function (evt, template) {
if (evt.which === 13) {
var url = template.find(".newLink").value;
// add to database
}
}
};
You could look into how this is achieved in the todos example (client/todos.js).
It uses a generic event handler for input fields (as seen below). You can browse the rest of the code for usage.
////////// Helpers for in-place editing //////////
// Returns an event map that handles the "escape" and "return" keys and
// "blur" events on a text input (given by selector) and interprets them
// as "ok" or "cancel".
var okCancelEvents = function (selector, callbacks) {
var ok = callbacks.ok || function () {};
var cancel = callbacks.cancel || function () {};
var events = {};
events['keyup '+selector+', keydown '+selector+', focusout '+selector] =
function (evt) {
if (evt.type === "keydown" && evt.which === 27) {
// escape = cancel
cancel.call(this, evt);
} else if (evt.type === "keyup" && evt.which === 13 ||
evt.type === "focusout") {
// blur/return/enter = ok/submit if non-empty
var value = String(evt.target.value || "");
if (value)
ok.call(this, value, evt);
else
cancel.call(this, evt);
}
};
return events;
};
I used this js function once to suppress the user using the return key in the text field to submit the form data. Perhaps you could modify it to suit the capture?
function stopRKey(evt) { // Stop return key functioning in text field.
var evt = (evt) ? evt : ((event) ? event : null);
var node = (evt.target) ? evt.target : ((evt.srcElement) ? evt.srcElement : null);
if ((evt.keyCode == 13) && (node.type=="text")) { return false; }
}
document.onkeypress = stopRKey;
You can also use event.currentTarget.value
Template.newLink.events = {
'keypress input.newLink': function (evt) {
if (evt.which === 13) {
var url = event.currentTarget.value;
// add to database
}
}
};