CircleCI codecov Maintainability Gem Version

Jsrb

A Rails view handler providing simple Ruby DSL to generate JavaScript code.

Getting Started

Jsrb handler works in .jsrb view files. All ruby syntax is available and js is provided in it. You can construct JavaScript code via js.

name = js.var!(:name) { 'foo' }
ary = js.var! :ary
obj = js.var! :obj
result = js.var!
# var name = 'foo';
# var ary;
# var obj;
# var _v1; // auto generated

js.set! ary, [1, 2, 3]
# ary = [1, 2, 3];

js.set! obj, {
  name: name,
  profile: {
    age: 20,
    sex: 'm'
  }
}
# obj = {
#   name: name,
#   profile: {
#     age: 20,
#     sex: 'm'
#   }
# };

js.set! result, (obj.name + "(" + obj.profile.age + ")")
# _v1 = obj.name + "(" + obj.profile.age + ")";

js.set! ary, ary.map { |x| x * 2 }
# ary = ary.map(function(x) {
#  return x * 2;
# });

js.if!(ary[1] === 4) {
  js.set! result, 'four'
}.elsif(ary[1] === 2) {
  js.set! result, 'two'
}.else {
  js.set! result, 'other'
}
# // The actual output will have certain immediate functions
# // that preserve variable scope for each case.
# if (ary[1] === 4) {
#   _v1 = 'four'
# } else if (ary[1] === 2) {
#   _v1 = 'two'
# } else {
#   _v1 = 'other'
# }

js.set! result, js.expr.Date.new
# _v1 = new Date;

js.set! js.expr.console.log('hello')
# console.log('hello');

Usage

In contrast to Ruby, statements and expressions are specifically distinguished as different elements in JavaScript. And the program is composed of a list of statements. This means that the jsrb file will have a series of statement pushing expression in it.

To make clear whether a method is pushing statement or not, jsrb adopted the rule that the name of method pushing statement should be #..!.

Statements

Variable declaration

js.var! pushes a VariableDeclaration into current context.

# with variable name and initializer
js.var!('varname') { 100 }
# var varname = 100;

# without initializer
js.var!('varname')
# var varname;

# variable name is auto-generated if not specified
js.var!
# var _v1;

# var! returns Jsrb::ExprChain instance, so that you can
# assign value with `.set!` method.
a = js.var!
js.set! a, 100
# var _v1;
# v1 = 100;

If statement, and conditional expression

js.if! pushes an IfStatement into current context, js.if to build a conditional expression.

# start with `#if!`
# and chain `#elsif` to add next case.
# Note that this is a statement, not expression.
js.if!(v === 1) {
  # ..
}.elsif(v === 2) {
  # ..
}.else {
  # ..
}

# if you don't need else clause, close with `#end`
js.if!(v === 1) {
  # ..
}.end

# if you want to regard this as an expression, use `#if` without exclamation.
js.set! v, js.if(v === 1) {
  # ..
}.else {
  # ..
}

Expression statement

js.do! pushes an ExpressionStatement of a given expression.

get_elements = js.expr[:getElements]
js.do! get_elements.('.foo').forEach { |n| n.delete.() }
# getElements('.foo').forEach(function(n) { return n.delete(); });

Expressions

Expression chain (ExprChain class) is an utility class to construct JavaScript expressions.

Initialize with wrapping ruby values

js.expr create a new ExprChain instance, taking an initial value optionally. Some methods in js automatically convert ruby value to ExprChain.

x = js.var! :x

js.set! x, js.expr(100)
# x = 100;
# set! automatically wraps argument with ExprChain.
js.set! x, 100
# x = 100;
#
# Note that if you need to compare a ruby value by operator with another one,
# you have to wrap it.
js.set! x, (js.expr(100) < js.expr.y) # (100 < js.expr.y) will fail.

See the conversion section to check mappings from ruby value to JavaScript one.

Chains

Member expression

ExprChain#[], #.. constructs MemberExpression. #[] and #member! is safe. #.. can be used only if the name has no conflict.

x = js.var! :x

obj = js.expr[:someObj]
# js.expr with no argument constructs empty chain,
# in which every member chain will be an identifier.
js.set! x, obj.field
# x = someObj['field'];
js.set! x, obj[:field]
# x = someObj['field'];

js.set! x, obj.send # NOTE that this is interpreted as a ruby Object's method, and causes an error.

Assignment

ExprChain#set constructs an AssignmentExpression.

a = js.var! :a
# var a;

js.do! a.set 100
# a = 100;

# js.set!(a, b) is short hand of js.do!(a.set(b))
js.set! a, 100
# a = 100;

Function Call

ExprChain#call, so #.(), #.. with argument or block constructs CallExpression.

console = js.expr[:console]

# using call method
js.do! console.log.('foo')
# console.log('foo')
js.do! console.log.call('foo')
# console.log('foo')

# using dynamic method
# if #..() has at least one argument or block, it will be a call expression.
js.do! console.log('foo')
# console.log('foo')

js.do! js.expr(:ary).forEach { |item| item.execute.() }
# ary.forEach(function(item) { return item.execute(); });

Operators

Any ruby-overridable and JS-existing operators are overridden for chaining. Supported operators are: ** + - * / % >> << & ^ | <= < > >= == === != ! && ||.

x = js.var! :x
a = js.expr[:a]

js.set! x, (a === 1)
# x = a === 1;

js.set! x, (a * a)
# x = a * a;

js.set! x, (3 * a) # raises an error because the method :* of Fixnum does not accept ExprChain as RHS.

New

ExprChain#new constructs NewExpression.

x = js.var! :x

js.set! x, js.expr[:Date].new
# x = new Date;

Function expression

ExprChain#forall constructs FunctionExpression. You can also construct it directly from Proc or passing a block.

ary = js.var! :ary, [1, 2, 3]

js.do! ary.map((js.expr[:x] * 2).forall('x'))
# ary.map(function(x) { return x * 2; });

js.do! ary.map { |x| x * 2 }
# ary.map(function(x) { return x * 2; });

js.do! ary.map(->(x) { x * 2 })
# ary.map(function(x) { return x * 2; });

Conversion

Every Ruby's generic value will be converted to Javascript value by following rule:

Ruby value JavaScript
finite 100 100
NaN Float::NAN NaN
+Infinity Float::INFINITY Number.POSITIVE_INFINITY
-Infinity Float::INFINITY Number.NEGATIVE_INFINITY
true true
false false
:foo "foo"
"foo" "foo"
nil null
[1, 2, 3] [1, 2, 3]
{ foo: 'bar' } { "foo": "bar" }
->(x, y, z) { x + y + z } function (x, y, z) { return x + y + z; }

Customize Chain

You can add custom chain methods in ExprChain via Jsrb::ExprChain.#add_custom_chain.

Jsrb::ExprChain.add_custom_chain('log_here', '__tap_log__')

js.do! js.expr[:foo][:bar].log_here
# __tap_log__(foo['bar']);

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/effective-spa/jsrb.

License

The gem is available as open source under the terms of the MIT License.