Let's say, I would like to call an Ember Handlebars helper from an emblem.js template.
I have declared the helper through (CoffeeScript syntax):
Ember.Handlebars.registerHelper 't', (key, value, context) ->
...
When attempting to invoke the helper from emblem using
= t "my.i18n.key", "val", count: 42
key is assigned correctly, all but the first arguments are dropped and the second argument is replaced by some options hash as would be the case for a bound helper (the third argument is undefined).
Is there any way to invoke a helper in emblem.js with more than a single argument?
I think you can only pass one value, but you can use the options hash with many bound/computed properties like this:
Ember.Handlebars.registerBoundHelper('truncatedQuery',
function truncatedQueryHelper(value, options) {
console.log('value', value, 'options:',
options.hash['key1'], options.hash['key2'], options.hash['key3']);
// do work...
return somethingUseful;
});
And in your template use optionsHashKeyBinding=propertyOnControllerName like this:
<div class='truncated-query'>
{{truncatedQuery 'value' key1Binding=prop1 key2Binding=prop2 key3=42}}
</div>
Where prop1 and prop2 are on the controller:
App.IndexController = Ember.Controller.extend({
prop1: 'foo',
prop2: function() {
return this.get('prop1') + 'bar';
}.property('prop1'),
});
Related
I currently use a {{link-to}} helper that was written by someone else to explicitly state the query params to pass to the next route and strip out others that are not stated. It looks like this:
//link-to example
{{#link-to 'route' (explicit-query-params fromDate=thisDate toDate=thisDate)} Link Text {{/link-to}}
//the helper
import {helper} from '#ember/component/helper';
import Object from '#ember/object';
import {assign} from '#ember/polyfills';
export function explicitQueryParams(params, hash) {
let values = assign({}, hash);
values._explicitQueryParams = true;
return Object.create({
isQueryParams: true,
values,
});
}
export default helper(explicitQueryParams);
// supporting method in router.js
const Router = EmberRouter.extend({
_hydrateUnsuppliedQueryParams(state, queryParams) {
if (queryParams._explicitQueryParams) {
delete queryParams._explicitQueryParams;
return queryParams;
}
return this._super(state, queryParams);
},
});
I've recently had a use case where I need to apply the same logic to a transitionTo() that is being used to redirect users from a route based on their access:
beforeModel() {
if (auth) {
this.transitionTo('route')
} else {
this.transitionTo('access-denied-route')
}
},
I am really struggling to see how I can refactor what I have in the handlebars helper to a re-usable function in the transitionTo() segment. I'm even unsure if transitionTo() forwards the same arguments as {{link-to}} or if I will have to fetch the queryParams somehow from a different location.
Any insight would be greatly appreciated.
Well first off, tapping into private methods like _hydrateUnsuppliedQueryParams is risky. It will make upgrading more difficult. Most people use resetController to clear stick query params. You could also explicitly clear the default values by passing empty values on the transition.
But, ill bite because this can be fun to figure out :) Check this ember-twiddle that does what you're wanting.
If you work from the beginning in the transitionTo case, we can see that in the router.js implementation:
transitionTo(...args) {
let queryParams;
let arg = args[0];
if (resemblesURL(arg)) {
return this._doURLTransition('transitionTo', arg);
}
let possibleQueryParams = args[args.length - 1];
if (possibleQueryParams && possibleQueryParams.hasOwnProperty('queryParams')) {
queryParams = args.pop().queryParams;
} else {
queryParams = {};
}
let targetRouteName = args.shift();
return this._doTransition(targetRouteName, args, queryParams);
}
So, if the last argument is an object with a query params obj, that's going directly into _doTransition, which ultimately calls:
_prepareQueryParams(targetRouteName, models, queryParams, _fromRouterService) {
let state = calculatePostTransitionState(this, targetRouteName, models);
this._hydrateUnsuppliedQueryParams(state, queryParams, _fromRouterService);
this._serializeQueryParams(state.handlerInfos, queryParams);
if (!_fromRouterService) {
this._pruneDefaultQueryParamValues(state.handlerInfos, queryParams);
}
}
which has the _hydrateUnsuppliedQueryParams function. So, to make this all work, you can't share the function directly from the helper you've created. Rather, just add _explicitQueryParams: true to your query params. Job done :)
The link-to version is different. The query params use
let queryParams = get(this, 'queryParams.values');
since the link-to component can take a variable number of dynamic segments and there needs to be some way to distinguish between the passed dynamic segments, a passed model, and query params.
When Blaze makes a call to a function defined inside a Template.xxx.helper, it passes one or more arguments. The first argument is an object which appears to be empty. What is it? What can it be used for?
Here's how to recreate my barebones test, using a Terminal window:
meteor create test
cd test
cat > test.html << EOF
<body>
{{> test}}
</body>
<template name="test">
<p>{{test1 "data"}}</p>
<p>{{test2 key="value"}}</p>
</template>
EOF
cat > test.js << EOF
if (Meteor.isClient) {
Template.test.helpers({
test1: function (argument) {
console.log(this, argument)
return "test1 helper: " + argument
}
, test2: function (argument) {
console.log(this, argument)
return "test2 helper: " + argument.hash.key
}
});
}
EOF
meteor run
Here's what I see in the browser console, after expanding the hash object:
Object {} "data"
Object {} S…s.kw {hash: Object}
hash: Object
key: "value"
__proto__: Object__proto__: Spacebars.kw
What is the this Object {}?
And in particular, is there a way I can use it to discover which HTML element triggered the call?
Inside a template helper, this is the data context of your template instance.
In your example the data context is not set, so it is returning an empty object. But that is not always the case. Imagine the following example:
<template name='parent'>
{{#with currentUser}}
{{> child}}
{{/with}}
</template>
In this case Meteor.user() has been set as the data context for the instance of Template.child, so Meteor.user() is bound to this in Template.child.helpers(). Which allows you to do the following:
Template.child.helpers({
greeting: function(){
console.log(this); // logs Meteor.user() || undefined
return 'Welcome back ' + this.username;
}
});
The data context can be set explicitly, via each or with blocks, or via parent template contexts. As in the example above you'll generally want to check against undefined when using this in helpers.
The short answer to your question whether this in a template helper can identify the DOM node(s) that invoke it is no. You may be able to dig it out of the helper's parameters via the prototype (I haven't checked), but I would consider this an antipattern. If you care where the helper is coming from, just include a parameter. Continuing the prior example:
<template name='child'>
<p>{{greeting}}</p>
<p>{{greeting 'special'}}</p>
</template>
And:
Template.child.helpers({
greeting: function(str){
if (str === 'special'){
return 'Welcome to first class Ambassador ' + this.username;
}
return 'Please take your seat in coach ' + this.username;
}
});
I have a {{render 'B' model}} helper in template A, so B/BView/BController are essentially children of A/AView/AController. BController even has AController as its parentController.
Is there a way to (easily) reference BController from AController? I'd prefer not to set something to B's parentController because it's not always A.
Ember lets you use a needs property for this purpose: http://emberjs.com/guides/controllers/dependencies-between-controllers/
App.AController = Ember.ObjectController.extend({
needs: ['b']
actions: {
somethingWithA: function() {
var bController= this.get('controllers.b');
// ...
}
}
});
For instance, is there a way to nest my "i18n" helper inside another helper's hash variable?
{{view "SearchView" placeholder="{{t 'search.root'}}" ref="search" url="/pages/search" className='home-search' polyfill=true}}
Update: Handlebars now supports subexpressions, so you can just do:
{{view "SearchView" (t 'search.root')}}
Your scenario is not directly supported, but there a couple of workarounds you can use. The handlebars helpers are just javascript code, so you can execute them from within the helper code itself:
function translateHelper() {
//...
}
function viewHelper = function(viewName, options) {
var hash = options.hash;
if(hash.placeholder) {
hash.placeholder = translateHelper(hash.placeholder);
}
};
Handlebars.registerHelper('view', viewHelper);
Handlebars.registerHelper('t', translateHelper);
And just pass the i18n key to as the argument:
{{view placeholder="search.root"}}
This is nice, as long as your helper knows which arguments should be localized, and which not. If that is not possible, you can try running all the helper arguments through Handlebars, if they contain a handlebars expression:
function resolveNestedTemplates(hash) {
_.each(hash, function(val, key) {
if(_.isString(val) && val.indexOf('{{' >= 0)) {
hash[key] = Handlebars.compile(val)();
}
});
return hash;
}
function view(viewName, options) {
var hash = resolveNestedTemplates(options.hash, this);
}
And use the nested template syntax you described:
{{view placeholder="{{t 'search.root'}}" }}
I realize neither of these options are perfect, but they're the best I could think of.
I have a problem using knockoutjs with custom template bindings.
Suppose I have a HTML body like this:
<div id="1">
<div data-bind="template:{name: '2', data: data}"></div>
</div>
<div id="2">
<h3 data-bind="text: caption"></h3>
</div>
JS code looks like this:
var ViewModel2 = function () {
this.caption = ko.observable("Caption");
}
var ViewModel1 = function () {
this.data = new ViewModel2();
}
ko.applyBindings(new ViewModel1(), document.getElementById("1"));
If we test this code, everything will work just fine;
See JSFiddle example: http://jsfiddle.net/4eTWW/33/
Now suppose we want to make our custom template binding. We'll use 'templatex' binding instead of 'template'.
In HTML we need to change just one line:
<div data-bind="templatex:{name: '2', data: data}"></div>
Next, let's add custom template binding to JS:
/*Custom binding*/
ko.bindingHandlers.templatex = {
init: function (element) {
ko.bindingHandlers.template.init.apply(this, arguments);
},
update: ko.bindingHandlers.template.update
}
See: http://jsfiddle.net/4eTWW/35/
But in this case we have an error, saying that it can't find 'caption' in the model.
Now let's add template {} to html bindings:
<div data-bind="template: {}, templatex:{name: '2', data: data}"></div>
See: http://jsfiddle.net/4eTWW/36/
And now everything works just fine.
It seems that while binding parent div it can't determine that child div is a template.
So how can I mark it as a template in my custom template binder?
Thanks.
You have wrong update handler, change to this:
ko.bindingHandlers.templatex= {
init: function(element) {
// do things
return ko.bindingHandlers.template.init.apply(this, arguments);
},
update: function(element) {
return ko.bindingHandlers.template.update.apply(this, arguments);
}
}
Here is working fiddle: http://jsfiddle.net/vyshniakov/4eTWW/39/
I don't think you can use a custom binding to create a new template engine. You need to register your custom engine with ko.setTemplateEngine().
From the knockoutjs source:
If you want to make a custom template engine,
[1] Inherit from the ko.templateEngine class (like ko.nativeTemplateEngine does)
[2] Override 'renderTemplateSource', supplying a function with this signature:
function (templateSource, bindingContext, options) {
// - templateSource.text() is the text of the template you should render
// - bindingContext.$data is the data you should pass into the template
// - you might also want to make bindingContext.$parent, bindingContext.$parents,
// and bindingContext.$root available in the template too
// - options gives you access to any other properties set on "data-bind: { template: options }"
//
// Return value: an array of DOM nodes
}
[3] Override 'createJavaScriptEvaluatorBlock', supplying a function with this signature:
function (script) {
// Return value: Whatever syntax means "Evaluate the JavaScript statement 'script' and output the result"
// For example, the jquery.tmpl template engine converts 'someScript' to '${ someScript }'
}
This is only necessary if you want to allow data-bind attributes to reference arbitrary template variables.
If you don't want to allow that, you can set the property 'allowTemplateRewriting' to false (like ko.nativeTemplateEngine does)
and then you don't need to override 'createJavaScriptEvaluatorBlock'.
Example: http://jsfiddle.net/6pStz/ (see Note 7 on this page)