In Meteor v0.8.2, it appears that helpers must be created for the individual templates (Template.story_en, Template.story_ne) called by the dynamic template.
Is it possible to create helpers for just the dynamic template (Template.story) and avoid repeating it for all possible templates that the dynamic templates can use, such as in the example below? It appears that the method I'm using requires a lot of repeated code.
story.html
<template name="story">
{{> UI.dynamic template=storyTemplate}}
</template>
story.js
Template.story.storyTemplate = function() {
return "story_" + Session.get('lang')
}
// This does not work
Template.story.color = function() {
return '#f00'
}
// This works
Template.story_en.color = function() {
return '#f00'
}
// This works (but seems to be unnecessary code)
Template.story_ne.color = function() {
return '#f00'
}
You could use global helpers, or pass the helpers in as data
Using Global Helpers (work on every template you have)
UI.registerHelper("color", function() {
return '#f00'
});
Or Passing in the helpers as data (does not work under current version of iron router - open bug).
Template.story.helpers({
dataHelpers: function() {
var data = UI._templateInstance().data || {};
//Add the helpers onto the existing data (if any)
_(data).extend({
color: function() {
return "#f00";
}
});
return data;
});
});
Then the html:
<template name="story">
{{> UI.dynamic template=storyTemplate data=dataHelpers}}
</template>
Then in the subtemplates you can use {{color}} without having the helpers in them.
You could also try your luck with using this instead of UI._remplateInstance.data if you have iron-router issues.
Related
Can I have a script only component in Polymer to hold all my helper functions used across application? I am not sure what is the recommended way of having reusable functions, constants that can be imported across components ?
<dom-module id="helper-functions">
<script>
(function() {
var helper1 = function() { ... };
var helper2 = function() { ... };
Polymer({
is : 'helper-functions'
});
})();
</script>
</dom-module>
You "could" do this, but it depends on what those helper functions are doing and whether they need any "Polymer" features.
One way to package up this sort of thing is as a "behavior", and seems to be the way that the Polymer Elements themselves are doing things. Split your helpers into functional areas and make each a separate behavior, and then include the behavior in those elements that need it. Here's an example to show how its done (I am including all my behaviors in the PAS namespace.
<link rel="import" href="../polymer/polymer.html">
<script>
window.PAS = window.PAS || {};
(function() {
'use strict';
var dialogs = [];
PAS.DialogBehaviour = {
attached: function() {
dialogs.push(this); //Capture all dialogs as they start up
},
_requestClose: function() {
this.async(function() { //Wait a second to allow inflight ajax to have a chance to finish
dialogs.forEach(function(dialog) {
if (dialog !== this) {
dialog._forceClose();
}
},this);
},1000);
},
_forceClose: function() {
//abstract
}
};
})();
</script>
I then include it in my elements like ...
Polymer({
is: 'pas-standin',
behaviors: [AKC.Route,PAS.DialogBehaviour,Polymer.NeonAnimatableBehavior],
listeners: {
'pas-error': '_closeDialog'
},
But for pure javascript functions, I have added my helper functions in my app.js file. I don't have that many at the moment, and I suspect if I did it would be a sign that I haven't designed the right elements.
I'm using I18n localization package to take care of the switching language part of my app. It uses a global variable to set the language wanted and a json file to store the translations.
As the switching of a language is just a change in a global variable ember doesn't detect it and doesn't render the templates automatically. I forced it via an action in the application controller :
Extranet.ApplicationController = Ember.ObjectController.extend(
{
actions:
{
localToFr: function()
{
this.localisation('fr'); // this changes the global variable
this.get('target.router').refresh(); // this is what refresh the template
},
localToEn: function()
{
this.localisation('en');
this.get('target.router').refresh();
}
},
localisation: function(lg)
{
I18n.locale = lg;
}
})
I have two problems with that solution :
1) The application template isn't rerendered via my
this.get('target.router').refresh();
2) And my other problem, it doesn't work on templates which don't request a server access ( e.g. : the nest of routes 'authSession' )
Extranet.Router.map(function()
{
this.resource(
'parkings', {path:'/'}, function ()
{
this.route('parking', {path:'/parking/:parking_id'});
this.route('historique', {path:'/parking/:parking_id/historique'});
this.route('sessAct', {path:'/parking/:parking_id/sessAct'});
this.route('rapport', {path:'/parking/:parking_id/rapport'});
}
);
this.resource(
'authSession', function ()
{
this.route('login');
this.route('logout');
}
);
}
);
I was having a similar issue. I just went with View.rerender() on the main view, which was a form in my case.
I am creating a handlebars helper, which takes the following form:
define(['Handlebars'], function (Handlebars) {
Handlebars.registerHelper("myHelper", function (options) {
console.log('myHelper');
if (*condition*) {
console.log('myHelper False');
return options.inverse(this);
} else {
console.log('myHelper True');
return options.fn(this);
}
});
});
As you can see, I'm using require.js. I'm also using this as part of a Backbone.js application. In the template, the helper is called like so:
{{#myHelper}}
<!-- Some HTML -->
{{else}}
<!-- Some HTML -->
{{/myHelper}}
However, the helper always returns false because it is not recognized. I know this because the console.log is never called. I have other custom helpers in the application that work, but they all take in arguments. If I add a dummy argument, the helper works fine:
define(['Handlebars'], function (Handlebars) {
Handlebars.registerHelper("myHelper", function (dummy, options) {
console.log('myHelper');
if (*condition*) {
console.log('myHelper False');
return options.inverse(this);
} else {
console.log('myHelper True');
return options.fn(this);
}
});
});
Template:
{{#myHelper "string"}}
<!-- Some HTML -->
{{else}}
<!-- Some HTML -->
{{/myHelper}}
I'm using handlebars v1.0.0. Is this something that is addressed in 2.0.0? This isn't blocker, but I clearly would prefer not to use a dummy argument if possible.
Here is fiddle with helper you need. handlebars-1.0.rc.1 used. Also tried with handlebars-1.3.0 - works fine.
HTML
<script id="topLevel" type="text/x-handlebars-template">
{{#myHelper}}
it's truthy
{{else}}
it's falsy
{{/myHelper}}
</script>
JS
Handlebars.registerHelper('myHelper', function (options) {
if (true) {
console.log("It's true");
return options.fn(this);
}
console.log("It's false");
return options.inverse(this);
});
var _template = Handlebars.compile($('#topLevel').html());
$('body').append(_template());
So your issue could occur:
Outdated library or you are trying to use helper before it has been registered. Require.js loads libraries/files asynchronously, call handlebars as dependency. Example:
define(function(require){
var yourObj = function() {
require(['handlebars'], function (Handlebars) {
// use Handlebars here
});
};
return yourObj;
});
Hope it's help.
I'm learning about Session and reactive data sources in Meteor JS. They work great for setting global UI states. However, I can't figure out how to scope them to a specific instance of a template.
Here's what I'm trying to do
I have multiple contenteditable elements on a page. Below each is an "Edit" button. When the user clicks on the Edit button, it should focus on the element and also show "Save" and "Cancel" buttons.
If the user clicks "Cancel", then any changes are eliminated, and the template instance should rerender with the original content.
Here's the code I have so far
// Helper
Template.form.helpers({
editState: function() {
return Session.get("editState");
}
});
// Rendered
Template.form.rendered = function(e){
var $this = $(this.firstNode);
var formField = this.find('.form-field');
if (Session.get("editState")) formField.focus();
};
// Event map
Template.form.events({
'click .edit-btn' : function (e, template) {
e.preventDefault();
Session.set("editState", "is-editing");
},
'click .cancel-btn' : function (e, template) {
e.preventDefault();
Session.set("editState", null);
},
});
// Template
<template name="form">
<div class="{{editState}}">
<p class="form-field" contenteditable>
{{descriptionText}}
</p>
</div>
Edit
Save
Cancel
</template>
// CSS
.edit-btn
.cancel-btn,
.save-btn {
display: inline-block;
}
.cancel-btn,
.save-btn {
display: none;
}
.is-editing .cancel-btn,
.is-editing .save-btn {
display: inline-block;
}
The problem
If I have more than one instance of the Form template, then .form-field gets focused for each one, instead of just the one being edited. How do I make so that only the one being edited gets focused?
You can render a template with data, which is basically just an object passed to it when inserted in to a page.
The data could simply be the key to use in the Session for editState.
eg, render the template with Template.form({editStateKey:'editState-topForm'})
you could make a handlebars helper eg,
Handlebars.registerHelper('formWithOptions',
function(editStateKey){
return Template.form({editStateKey:editStateKey})
});
then insert it in your template with
{{{formWithOptions 'editState-topForm'}}} (note the triple {, })
Next, change references from Session.x('editState') to Session.x(this.editStateKey)/ Session.x(this.data.editStateKey)
Template.form.helpers({
editState: function() {
return Session.get(this.editStateKey);
}
});
// Rendered
Template.form.rendered = function(e){
var $this = $(this.firstNode);
var formField = this.find('.form-field');
if (Session.get(this.data.editStateKey)) formField.focus();
};
// Event map
Template.form.events({
'click .edit-btn' : function (e, template) {
e.preventDefault();
Session.set(this.editStateKey, "is-editing");
},
'click .cancel-btn' : function (e, template) {
e.preventDefault();
Session.set(this.editStateKey, null);
},
});
Note: if you are using iron-router it has additional api's for passing data to templates.
Note2: In meteor 1.0 there is supposed to be better support for writing your own widgets. Which should allow better control over this sort of thing.
As a matter of policy I avoid Session in almost all cases. I feel their global scope leads to bad habits and lack of good discipline regarding separation-of-concerns as your application grows. Also because of their global scope, Session can lead to trouble when rendering multiple instances of a template. For those reasons I feel other approaches are more scalable.
Alternative approaches
1 addClass/removeClass
Instead of setting a state then reacting to it elsewhere, can you perform the needed action directly. Here classes display and hide blocks as needed:
'click .js-edit-action': function(event, t) {
var $this = $(event.currentTarget),
container = $this.parents('.phenom-comment');
// open and focus
container.addClass('editing');
container.find('textarea').focus();
},
'click .js-confirm-delete-action': function(event, t) {
CardComments.remove(this._id);
},
2 ReactiveVar scoped to template instance
if (Meteor.isClient) {
Template.hello.created = function () {
// counter starts at 0
this.counter = new ReactiveVar(0);
};
Template.hello.helpers({
counter: function () {
return Template.instance().counter.get();
}
});
Template.hello.events({
'click button': function (event, template) {
// increment the counter when button is clicked
template.counter.set(template.counter.get() + 1);
}
});
}
http://meteorcapture.com/a-look-at-local-template-state/
3 Iron-Router's state variables
Get
Router.route('/posts/:_id', {name: 'post'});
PostController = RouteController.extend({
action: function () {
// set the reactive state variable "postId" with a value
// of the id from our url
this.state.set('postId', this.params._id);
this.render();
}
});
Set
Template.Post.helpers({
postId: function () {
var controller = Iron.controller();
// reactively return the value of postId
return controller.state.get('postId');
}
});
https://github.com/iron-meteor/iron-router/blob/devel/Guide.md#setting-reactive-state-variables
4 Collection data
Another approach is to simply state by updating data in your collection. Sometimes this makes perfect sense.
5 update the data context
Session is often the worse choice in my opinion. Also I don't personally use #3 as I feel like being less tied to iron-router is better incase we ever want to switch to another router package such as "Flow".
In my app, the <body> tag contains just a single <script type="text/x-handlebars> tag which contains all my views. Sproutcore 2.0 nicely adds a jQuery on-document-ready handler that parses those templates and renders them back into the DOM.
I'd like to call a function on one of the views as soon as it's rendered. The problem is that the re-insertion happens asynchronously, so I don't know when the view is available.
Example
Page
<body>
<script type="text/x-handlebars">
...
{{view "MyApp.TweetInputView"}}
...
</script>
</body>
View:
MyApp.TweetInputView = SC.View.extend({
init: function() {
// act like a singleton
MyApp.TweetInputView.instance = this;
return this._super();
},
focus: function() {
...
this.$().focus();
}
});
Initializer
// if the URL is /tweets/new, focus on the tweet input view
$(function() {
if (window.location.pathname === '/tweets/new') {
// doesn't work, because the view hasn't been created yet:
MyApp.TweetInputView.instance.focus();
}
});
I've also tried SC.run.schedule('render', function() { MyApp.TweetInputView.instance.focus(); }, 'call'); in the hopes that Sproutcore would run that after all the view rendering and insertion, but that does not seem to be the case.
Try this:
MyApp.TweetInputView = SC.View.extend({
didInsertElement: function() {
console.log("I've been rendered!");
}
});