• Stars
    star
    393
  • Rank 109,518 (Top 3 %)
  • Language
    Ruby
  • License
    MIT License
  • Created almost 4 years ago
  • Updated 12 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A jsx-inspired way to render view components in Ruby.

rux

Unit Tests

Rux is a JSX-inspired way to write HTML tags in your Ruby code. It can be used to render view components in Rails via the rux-rails gem. This repo however contains only the rux parser itself.

Introduction

A bit of background before we dive into how to use rux.

React and JSX

React mainstreamed the idea of composing websites from a series of components. To make it conceptually easier to transition from HTML templates to Javascript components, React also introduced an HTML-based syntax called JSX that allows developers to embed HTML into their Javascript code.

Rails View Components

For a long time, Rails didn't really have any support for components, preferring to rely on HTML template languages like ERB and HAML. The fine folks at Github however decided components could work well in Rails and released their view_component framework. There was even some talk about merging view_component into Rails core as ActionView::Component, but unfortunately it looks like that won't be happening.

NOTE: I'm going to be focusing on Rails examples here using the view_component gem, but rendering views from a series of components is a framework-agnostic idea.

View Component Example

A view component is just a class. The actual view portion is usually stored in a secondary template file that the component renders in the context of an instance of that class. For example, here's a very basic view component that displays a person's name on the page:

# app/components/name_component.rb
class NameComponent < ViewComponent::Base
  def initialize(first_name:, last_name:)
    @first_name = first_name
    @last_name = last_name
  end
end
<%# app/components/name_component.html.erb %>
<span><%= @first_name %> <%= last_name %></span>

View components have a number of very nice properties. Read about them on viewcomponent.org or watch Joel Hawksley's excellent 2019 Railsconf talk.

HTML in Your Ruby

Rux does one thing: it lets you write HTML in your Ruby code. Here's the name component example from earlier rewritten in rux (sorry about the syntax highlighting, Github doesn't know about rux yet).

# app/components/name_component.rux
class NameComponent < ViewComponent::Base
  def initialize(first_name:, last_name:)
    @first_name = first_name
    @last_name = last_name
  end

  def call
    <span>
      {@first_name} {@last_name}
    </span>
  end
end

NOTE: The example above takes advantage of a feature of the view_component gem that lets you define a call method instead of creating a separate template file.

Next, we'll run the ruxc tool to translate the rux code into Ruby code, eg. ruxc app/components/name_component.rux. Here's the result:

class NameComponent < ViewComponent::Base
  def initialize(first_name:, last_name:)
    @first_name = first_name
    @last_name = last_name
  end

  def call
    Rux.tag("span") {
      Rux.create_buffer.tap { |_rux_buf_,|
        _rux_buf_ << @first_name
        _rux_buf_ << " "
        _rux_buf_ << @last_name
      }.to_s
    }
  end
end

As you can see, the span tag was converted to a Rux.tag call. The instance variables containing the first and last names are concatenated together and rendered inside the span.

Composing Components

Things get even more interesting when it comes to rendering components inside other components. Let's create a greeting component that makes use of the name component:

# app/components/greeting_component.rux
class GreetingComponent < ViewComponent::Base
  def call
    <div>
      Hey there <NameComponent first-name="Homer" last-name="Simpson" />!
    </div>
  end
end

The ruxc tool produces:

class GreetingComponent < ViewComponent::Base
  def call
    Rux.tag("div") {
      Rux.create_buffer.tap { |_rux_buf_,|
        _rux_buf_ << " Hey there "
        _rux_buf_ << render(NameComponent.new(first_name: "Homer", last_name: "Simpson"))
        _rux_buf_ << "! "
      }.to_s
    }
  end
end

The <NameComponent> tag was translated into an instance of the NameComponent class and the attributes into its keyword arguments.

NOTE: The render method is provided by ViewComponent::Base.

Embedding Ruby

Since rux code is translated into Ruby code, anything goes. You're free to put any valid Ruby statements inside the curly braces.

For example, let's say we want to change our greeting component to greet a variable number of people:

# app/components/greeting_component.rux
class GreetingComponent < ViewComponent::Base
  def initialize(people:)
    # people is an array of hashes containing :first_name and :last_name keys
    @people = people
  end

  def call
    <div>
      {@people.map do |person|
        <NameComponent
          first-name={person[:first_name]}
          last-name={person[:last_name]}
        />
      end}
    </div>
  end
end

Notice we were able to embed Ruby within rux within Ruby within rux. Within Ruby. The rux parser supports unlimited levels of nesting, although you'll probably not want to go too crazy.

Slots

Rux fully supports the view_component gem's slots feature, which allows a component to expose specific points in the rendered output where the caller can provide their own content. Let's look at a table component that exposes rows and columns via slots:

class TableComponent < ViewComponent::Base
  renders_many :rows, RowComponent

  def call
    <table>
      {rows.each do |row|
        <>{row}</>
      end}
    </table>
  end
end

class RowComponent < ViewComponent::Base
  renders_many :columns, ColumnComponent

  def call
    <tr>
      {columns.each do |column|
        <>{column}</>
      end}
    </tr>
  end
end

class ColumnComponent < ViewComponent::Base
  def call
    <td>{content}</td>
  end
end

Notice the use of rux fragments (analogous to JSX fragments) via the <></> syntax. This allows emitting a slot by dropping back to ruby via rux.

The TableComponent might be rendered in an ERB template like so:

<%= render(TableComponent.new) do |table| %>
  <% table.with_row do |row| %>
    <% row.with_column { "Row 1, Col 1" } %>
    <% row.with_column { "Row 1, Col 2" } %>
  <% end %>
  <% table.with_row do |row| %>
    <% row.with_column { "Row 2, Col 1" } %>
    <% row.with_column { "Row 2, Col 2" } %>
  <% end %>
<% end %>

Notice the slots are "filled in" using the #with_row and #with_column methods. In rux, these methods become components:

<TableComponent>
  <WithRow>
    <WithColumn>Row 1, Col 1</WithColumn>
    <WithColumn>Row 1, Col 2</WithColumn>
  </WithRow>
  <WithRow>
    <WithColumn>Row 2, Col 1</WithColumn>
    <WithColumn>Row 2, Col 2</WithColumn>
  </WithRow>
</TableComponent>

The as: argument

In ViewComponent, component instances are yielded to the block on #render, eg:

<%= render(MyComponent.new) do |component| %>
  <%# 'component' is the instance of MyComponent passed to #render above %>
<% end %>

Most of the time in rux, a reference to the component instance isn't necessary (see the section on slots above). Occasionally however it can be useful to, for example, call methods on the component instance to query its state, etc. Use the as: argument to assign the component instance to a local variable that's available inside the tag body:

<TableComponent something={value} as={table}>
  {if table.something
    # your code here
  end}
</TableComponent>

Keyword Arguments Only

Any view component that will be rendered by rux must only accept keyword arguments in its constructor. For example:

class MyComponent < ViewComponent::Base
  # GOOD
  def initialize(first_name:, last_name:)
  end

  # BAD
  def initialize(first_name, last_name)
  end

  # BAD
  def initialize(first_name, last_name = 'Simpson')
  end
end

In other words, positional arguments are not allowed. This is because there's no such thing as a positional HTML attribute - all HTML attributes are key/value pairs. So, in order to match up with HTML, rux components are written with keyword arguments.

Note also that the rux parser will replace dashes with underscores in component tag attributes to adhere to both HTML and Ruby syntax conventions, since HTML attributes use dashes while Ruby keyword arguments use underscores. For example, here's how to write a rux tag for MyComponent above:

<MyComponent first-name="Homer" last-name="Simpson" />

Notice that the rux attribute "first-name" is passed to MyComponent#initialize as "first_name".

Attributes on regular tags, i.e. non-component tags like <div> and <span>, are not modified. In other words, <div data-foo="foo"> does not become <div data_foo="foo"> because that would be very annoying.

How it Works

Translating rux code (Ruby + HTML tags) into Ruby code happens in three phases: lexing, parsing, and emitting. The lexer phase is implemented as a wrapper around the lexer from the Parser gem that looks for specific patterns in the token stream. When it finds an opening HTML tag, it hands off lexing to the rux lexer. When the tag ends, the lexer continues emitting Ruby tokens, and so on.

In the parsing phase, the token stream is transformed into an intermediate representation of the code known as an abstract syntax tree, or AST. It's the parser's job to work out which tags are children of other tags, associate attributes with tags, etc.

Finally it's time to generate Ruby code in the emitting phase. The rux gem makes use of the visitor pattern and the excellent unparser gem to walk over all the nodes in the AST and generate a big string of Ruby code. This big string is the final product that can be written to a file and executed by the Ruby interpreter.

Transpiling Rux to Ruby

While the ruxc tool is a convenient way to transpile rux to Ruby via the command line, it's also possible to do so programmatically.

Transpiling Strings

Let's say you have a string containing a bunch of rux code. You can transpile it to Ruby like so:

require 'rux'

str = 'some rux code'
Rux.to_ruby(str)

NOTE: The to_ruby method accepts a visitor instance as its second argument (see below for more information about creating custom visitors). It uses the default visitor if no second argument is provided.

Transpiling Files

Rux comes with a handy File class to make transpiling files easier:

require 'rux'

f = Rux::File.new('path/to/some/file.rux')

# get result as a string, same as calling Rux.to_ruby
f.to_ruby

# write result to path/to/some/file.rb
f.write

# write result to the given file
f.write('somewhere/else/file.rb')

# the default file the result will be written, i.e. the location
# #write will write to
f.default_outfile

Custom Visitors

Rux comes with a default visitor capable of emitting Ruby code that is mostly compatible with the view_component gem discussed earlier. A little bit of extra work is required to render rux components in Rails, which is why the rux-rails gem uses a modified version of the default visitor to emit Ruby code that will render correctly in Rails views. It's likely other frameworks that want to render rux components will need a custom visitor as well.

Visitors should inherit from the Rux::Visitor class and implement the various methods. See lib/rux/visitor.rb for details. If you're looking to tweak the default visitor, inherit from Rux::DefaultVisitor instead, and see lib/rux/default_visitor.rb for details.

Custom Tag Builders

The Rux.tag method emits HTML tags via the configured tag builder. You can configure a custom tag builder by setting Rux.tag_builder to any object that responds to the call method (and accepts three arguments). For example:

class MyTagBuilder
  def call(tag_name, attributes = {}, &block)
    # Should return a string, eg. '<div foo="bar"></div>'.
    # When called, the block should return the tag's body contents.
  end
end

Rux.tag_builder = MyTagBuilder.new

Or, since the only requirement is that the tag builder respond to #call, you could pass a lambda:

Rux.tag_builder = -> (tag_name, attributes = {}, &block) do
  # Should return a string, eg. '<div foo="bar"></div>'.
  # When called, the block should return the tag's body contents.
end

Custom Buffers

You may have noticed calls to Rux.create_buffer in the code examples above. Rux comes with a default buffer implementation, but you can configure a custom one as well. The rux-rails gem for example configures rux to use ActiveSupport::SafeBuffer in order to be compatible with Rails view rendering. Buffer implementations only need to define two methods: #>> and #to_s:

class MyBuffer
  def initialize
    @buffer = ''
  end

  def <<(thing)
    # it's important to handle nils here
    @buffer << (thing || '')
  end

  def to_s
    @buffer
  end
end

Rux.buffer = MyBuffer

The Library Path

It is my hope that, in the future, Ruby and Rails devs will publish collections of view components in gem form that other devs can use in their own projects. Maybe some of those view component libraries will even be written in rux. Accordingly, I wanted a way of adding rux components to Rails' eager load system, but without actually depending on Rails.

The rux library path is a way for libraries written in rux to register themselves. The rux-rails gem automatically appends every entry in the library path to the Rails eager load and autoload paths so .rux files are automatically reloaded in development mode. Hopefully the library path enables other frameworks to do something similar.

Adding a path is done like so:

Rux.library_paths << 'path/to/dir/with/rux/files'

Editor Support

Sublime Text: https://github.com/camertron/rux-SublimeText

Atom: https://github.com/camertron/rux-atom

VSCode: https://github.com/camertron/rux-vscode

Running Tests

bundle exec rspec should do the trick.

License

Licensed under the MIT license. See LICENSE for details.

Authors

More Repositories

1

arel-helpers

Useful tools to help construct database queries with ActiveRecord and Arel.
Ruby
390
star
2

scuttle-server

Server behind scuttle.io, a SQL editor and Arel converter.
Ruby
145
star
3

scuttle-rb

A library for transforming raw SQL statements into ActiveRecord/Arel queries. Ruby wrapper and tests for scuttle-java.
Ruby
86
star
4

rux-rails

Rux view components on Rails.
Ruby
83
star
5

gelauto

Automatically annotate your code with Sorbet type definitions.
Ruby
53
star
6

cldr-segmentation.js

CLDR text segmentation for JavaScript
JavaScript
38
star
7

active_nutrition

An ActiveRecord-backed collection of models for storing and retrieving nutritional information from the USDA's Nutrient Database.
Ruby
33
star
8

utfstring

UTF-safe string operations in JavaScript.
TypeScript
25
star
9

garnet-js

An implementation of the YARV virtual machine in TypeScript.
Ruby
23
star
10

onload

A preprocessor system for Ruby.
Ruby
22
star
11

cskit-strongs-rb

Strong's concordance resources for CSKit.
Ruby
19
star
12

turbo-sprockets-rails4

Speed up asset precompliation by compiling assets in parallel.
Ruby
19
star
13

json-write-stream

An easy, streaming way to generate JSON.
Ruby
17
star
14

SQLParser

ANTLR4-based SQL Parser extracted from Apache Tajo
Java
15
star
15

antlr4-native-rb

Create native Ruby extensions from (almost) any ANTLR4 grammar.
Ruby
14
star
16

mosaico-rails

The Mosaico email editor on Rails.
Ruby
12
star
17

prebundler

Experimental. Speed up gem installation by prebuilding gems and storing them in S3.
Ruby
11
star
18

trie-file

Memory-efficient cached trie and trie storage.
Ruby
9
star
19

viewcat

A faster ActionView::OutputBuffer written in C.
C
9
star
20

esprima-rb

Ruby wrapper around the Esprima static code analyzer for JavaScript.
Ruby
8
star
21

net-smtp-proxy

Proxy support for Ruby's Net::SMTP.
Ruby
8
star
22

llama.rb

llama.cpp for Ruby
C++
7
star
23

antlr-gemerator

Generate a complete Rubygem from (almost) any ANTLR4 grammar.
ANTLR
6
star
24

tmx-parser

Parser for the Translation Memory eXchange (.tmx) file format.
Ruby
6
star
25

Pongo

A modular, multi-threaded web application deployment framework written in PHP.
PHP
4
star
26

grape-client-generator

Automatically generate clients for your Grape APIs.
Ruby
4
star
27

twitter-windows

A (eventually) full-fledged Twitter client built to have the same look and feel as Twitter for Mac.
C#
4
star
28

jvectormap-rails

jVectorMap for the Rails asset pipeline
Ruby
4
star
29

i18n-js-assets

Compile your Javascript translations with the asset pipeline instead of a rake task.
Ruby
4
star
30

turbo-sprockets-rails5

Speed up asset precompliation by compiling assets in parallel.
Ruby
3
star
31

rails-middleware-extensions

Adds several additional operations useful for customizing your Rails middleware stack.
Ruby
3
star
32

simple-graph

A simple, no-frills graph implementation.
Ruby
3
star
33

generated-assets

Programmatically generate assets for the Rails asset pipeline.
Ruby
3
star
34

rux-vscode

Rux syntax highlighting for VSCode.
3
star
35

cldr-plurals

Tokenizes and parses CLDR plural rules and provides a mechanism for emitting them as source code
JavaScript
3
star
36

rux-bootstrap

A collection of Rux view components for building webpages with Bootstrap.
Ruby
3
star
37

myrb

Inline types for Ruby
Ruby
3
star
38

ohm-stateful-model

Integrate state machines (from the state_machine gem) into your Ohm models.
Ruby
3
star
39

scuttle-java

A library for transforming raw SQL statements into ActiveRecord/Arel queries.
Java
3
star
40

mosaico-example

Example Rails 5 app showing how to integrate the mosaico-rails gem.
Ruby
2
star
41

storybuilder

Drag-and-drop editor for Primer view components.
Ruby
2
star
42

cldr-plurals-runtime-rb

Ruby runtime methods for CLDR plural rules (see camertron/cldr-plurals).
Ruby
2
star
43

avoidance

Manipulate ActiveRecord models and their associations naturally without persisting them to the database.
Ruby
2
star
44

xml-write-stream

An easy, streaming way to generate XML.
Ruby
2
star
45

ohey

A rewrite of the platform detection logic in ohai, but with fewer dependencies and 100% less metaprogramming.
Ruby
2
star
46

popforms

jQuery plugin that can show any form as a modal dialog.
JavaScript
1
star
47

pet-detector

Automatic solver for Lumosity's Pet Detective game.
Ruby
1
star
48

erb2rux

An easy way to convert your ERB templates to rux.
Ruby
1
star
49

rtl-string

Easier string manipulation for people who come from LTR backgrounds.
Ruby
1
star
50

range-set

An efficient set implementation that treats runs of sequential elements as ranges.
Ruby
1
star
51

abroad

A set of parsers and serializers for dealing with localization file formats.
Ruby
1
star
52

jotto-client

Jotto word game iPhone app written in Ruby with RubyMotion.
Ruby
1
star
53

curdle

Programmatically remove Sorbet type annotations from Ruby code.
Ruby
1
star
54

fragmont

Font subsetting for the Rails asset pipeline.
Ruby
1
star
55

yaml-write-stream

An easy, streaming way to generate YAML.
Ruby
1
star
56

binascii

A Ruby version of Python's binascii module
Ruby
1
star
57

chopstick

An example Rails app demonstrating how to use twitter-cldr-rails to internationalize content.
Ruby
1
star
58

SmoothControls

Smooth-looking controls for Windows forms (.NET)
C#
1
star
59

camertron-rails-assets-codemirror

In-browser code editor (with a fix for PhantomJS) http://codemirror.net/
Ruby
1
star
60

rsc.js

A javascript implementation of the RSC (Reasonably Simple Computer)
JavaScript
1
star
61

rux-atom

Rux syntax highlighting for Atom.
CoffeeScript
1
star
62

commonrb

A (probably stupid) attempt at bringing CommonJs-style modules to Ruby
Ruby
1
star
63

any2tmx-web

A web front-end for the any2tmx converter that converts certain data formats to TMX (Translation Memory eXchange)
Ruby
1
star
64

any2tmx

A command-line tool to convert Rails locale-specific yaml files to the standard TMX format for translation memories.
Ruby
1
star
65

commonjs-rhino

CommonJs support for Rhino, in Ruby.
Ruby
1
star
66

bundler-tools

Ruby
1
star
67

escodegen-rb

Ruby wrapper around the escodegen JavaScript generator that generates ECMA script from an abstract syntax tree.
Ruby
1
star