Skip to content

Commit 6c51c75

Browse files
authored
register_helper: accept javascript function as string (#7)
This updates `register_helper` to accept a string of JavaScript source code which will be interpreted as the helper function.
1 parent c4174a5 commit 6c51c75

3 files changed

Lines changed: 139 additions & 19 deletions

File tree

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,32 @@ See https://handlebarsjs.com/guide/#custom-helpers.
8282

8383
### Block Helpers
8484

85+
Block helpers make it possible to define custom iterators and other
86+
functionality that can invoke the passed block with a new context.
87+
88+
Currently, there is a limitation with the underlying JavaScript engine: it does
89+
not allow for reentrant calls from within attached Ruby functions: see
90+
[MiniRacer#225](https://github.com/rubyjs/mini_racer/issues/225). Thus, the
91+
block function returned to the helper (in `options.fn`) cannot be invoked.
92+
93+
Thus, for block helpers, a string of JavaScript must define the helper function:
94+
```ruby
95+
handlebars = Handlebars::Engine.new
96+
handlebars.register_helper(map: <<~JS)
97+
function(...args) {
98+
const ctx = this;
99+
const opts = args.pop();
100+
const items = args[0];
101+
const separator = args[1];
102+
const mapped = items.map((item) => opts.fn(item));
103+
return mapped.join(separator);
104+
}
105+
JS
106+
template = handlebars.compile("{{#map items '|'}}'{{this}}'{{/map}}")
107+
template.call({ items: [1, 2, 3] })
108+
# => "'1'|2'|'3'"
109+
```
110+
85111
See https://handlebarsjs.com/guide/#block-helpers.
86112

87113
### Partials

lib/handlebars/engine.rb

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,28 @@ def template(*args)
6363

6464
# Registers helpers accessible by any template in the environment.
6565
#
66+
# The function can be either a proc or a string:
67+
# * When the function is a proc, it can be either passed in as a normal
68+
# parameter or as a block.
69+
# * When the function is a string, it is interpreted as a JavaScript
70+
# function.
71+
#
6672
# @param name [String, Symbol] the name of the helper
73+
# @param function [Proc, String] the helper function
6774
# @yieldparam context [Hash] the current context
6875
# @yieldparam arguments [Object] the arguments (optional)
6976
# @yieldparam options [Hash] the options hash (optional)
7077
# @see https://handlebarsjs.com/api-reference/runtime.html#handlebars-registerhelper-name-helper
71-
def register_helper(name = nil, **helpers, &block)
72-
helpers[name] = block if name
78+
def register_helper(name = nil, function = nil, **helpers, &block)
79+
helpers[name] = block || function if name
7380
helpers.each do |n, f|
74-
attach(n, &f)
75-
call(:registerHelper, [n.to_s, n.to_sym], eval: true)
81+
case f
82+
when Proc
83+
attach(n, &f)
84+
evaluate("registerHelper('#{n}', #{n})")
85+
when String, Symbol
86+
evaluate("Handlebars.registerHelper('#{n}', #{f})")
87+
end
7688
end
7789
end
7890

@@ -217,14 +229,7 @@ def init!
217229
end
218230

219231
def js_args(args)
220-
args.map { |arg|
221-
case arg
222-
when Symbol
223-
arg
224-
else
225-
JSON.generate(arg)
226-
end
227-
}
232+
args.map { |arg| JSON.generate(arg) }
228233
end
229234
end
230235
end

spec/handlebars/engine_spec.rb

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,21 +150,32 @@
150150
let(:template) { "{{#{name} name name=name}}" }
151151

152152
before do
153-
allow(function).to receive(:call).with(any_args).and_call_original
154-
engine.register_helper(name, &function)
153+
engine.register_helper(name, function)
155154
end
156155

157156
it "is defined" do
158157
expect(engine).to respond_to(:register_helper)
159158
end
160159

161160
context "with positional parameters" do
162-
before do
163-
engine.register_helper(name, &function)
161+
context "when function is argument" do
162+
before do
163+
engine.register_helper(name, function)
164+
end
165+
166+
describe "rendering" do
167+
include_examples "rendering"
168+
end
164169
end
165170

166-
describe "rendering" do
167-
include_examples "rendering"
171+
context "when function is block" do
172+
before do
173+
engine.register_helper(name, &function)
174+
end
175+
176+
describe "rendering" do
177+
include_examples "rendering"
178+
end
168179
end
169180
end
170181

@@ -178,7 +189,11 @@
178189
end
179190
end
180191

181-
describe "parameters" do
192+
context "with a Ruby function" do
193+
before do
194+
allow(function).to receive(:call).with(any_args).and_call_original
195+
end
196+
182197
describe "the first parameter" do
183198
it "is the context" do
184199
render_context.transform_keys!(&:to_s)
@@ -233,6 +248,80 @@
233248
end
234249
end
235250
end
251+
252+
context "with a JavaScript function" do
253+
let(:function) {
254+
<<~JS
255+
function (...args) {
256+
args.unshift(this);
257+
return tester(...args);
258+
}
259+
JS
260+
}
261+
let(:tester) { ->(_ctx, *_args, _opts) { rendered } }
262+
263+
before do
264+
allow(tester).to receive(:call).with(any_args).and_call_original
265+
engine_context.attach("tester", tester)
266+
end
267+
268+
describe "rendering" do
269+
include_examples "rendering"
270+
end
271+
272+
describe "`this`" do
273+
it "is the context" do
274+
render_context.transform_keys!(&:to_s)
275+
args = [render_context, any_args, anything]
276+
render
277+
expect(tester).to have_received(:call).with(*args)
278+
end
279+
end
280+
281+
describe "the first parameter(s)" do
282+
it "is the positional argument(s)" do
283+
args = [anything, *render_context.values_at(:name), anything]
284+
render
285+
expect(tester).to have_received(:call).with(*args)
286+
end
287+
end
288+
289+
describe "the last parameter" do
290+
it "is the options" do
291+
opts = include(
292+
"data" => kind_of(Hash),
293+
"hash" => { "name" => render_context[:name] },
294+
"name" => name.to_s,
295+
)
296+
args = [anything, any_args, opts]
297+
render
298+
expect(tester).to have_received(:call).with(*args)
299+
end
300+
end
301+
302+
context "with a block helper" do
303+
let(:template) { "{{##{name} age}}function{{else}}inverse{{/#{name}}}" }
304+
let(:function) {
305+
<<~JS
306+
function (age, opts) {
307+
return age > 0 ? opts.fn() : opts.inverse();
308+
}
309+
JS
310+
}
311+
312+
describe "the options" do
313+
it "includes the main block function" do
314+
render_context[:age] = 30
315+
expect(render).to eq("function")
316+
end
317+
318+
it "includes the else block function" do
319+
render_context[:age] = 0
320+
expect(render).to eq("inverse")
321+
end
322+
end
323+
end
324+
end
236325
end
237326

238327
describe "#unregister_helper" do

0 commit comments

Comments
 (0)