React Components In Lucky With Laravel Mix and lucky-react

07/30/20183 Min Read — In React, Lucky, Crystal, Laravel

I just started learning React after 2 years of Angular and Vue. I'm surprised at how fun React is to use and how amazing the community and supporting packages are. I'm also a huge fan of Crystal and the Lucky framework, so what could be more awesome than using these tools together?

In this post I'm going to create a React component and drop it into my Lucky app for some interactivity. Now although Laravel Mix has some support for React by transpiling your jsx to javascript for you, I'm not going to be writing my React components in jsx. Instead I'll be writing them in Lucky components and have them transpiled by Babel using babel-standalone.

Babel Standalone and React

In src/components/shared/layout.cr let's import the scripts for babel-standalone, react and react-dom in the shared_layout_head method which will include it in the <head></head> tags of every page.

# src/components/shared/layout.cr
module Shared::Layout
...
def shared_layout_head
head do
utf8_charset
title "React in Lucky - #{page_title}"
css_link asset("css/app.css")
js_link asset("js/app.js")
js_link "https://unpkg.com/babel-standalone@6/babel.min.js"
js_link "https://unpkg.com/react@16/umd/react.development.js"
js_link "https://unpkg.com/react-dom@16/umd/react-dom.development.js"
csrf_meta_tags
responsive_meta_tag
end
end
...
end

Now Lucky will be able to recognize and process React components in our templates.

React Components

Since we're using babel-standalone we can render React components with something as simple as this in our template:

def react_component
tag "react-component"
script do
<<-JS
class ReactComponent extends React.Component {
render() {
return (
<div>
<p>I'm a React component!</p>
</div>
)
}
}
const target = document.getElementsByTagName('react_component')[0];
ReactDOM.render(
<ReactComponent />,
target
)
JS
end
end

But that's pretty boring. We can't pass in props from our Lucky app to the component, and we're just writing the React component in a script tag. We can do better using Lucky's components.

React Component Module

I've already built a Base module to make the process easy. It's pretty big so I'll go over an example and then explain how it works.

Let's say we want to have a react component that wraps an input element, captures our keystrokes and sends them somewhere to do something. ajax requests for a search bar or autocomplete. We would want to use it in Lucky like this:

class SearchPage
include React::Search
def content
h2 "Search"
auto_complete placeholder: "Search something..."
end
end
require "./component_base.cr"
module React::AutoComplete
include React::ComponentBase
def component_tag
tag @tag
end
private def component_definition(**props)
# the content of the React class goes here
end
end

React::ComponentBase

To make this work I've made this module that generates helper methods, wraps your React component in a script tag that supports babel and renders the component on every html tag that matches the name.

Details for what each method does can be found in the class.

# React::ComponentBase is used to create Lucky component modules that
# render React components.
# Requires react, react-dom and babel-standalone scripts.
#
# Usage
# - Create a module named `React::<ComponentNameInPascalCase>`
# - Define a `component_definition` method wich becomes the content
# of the React component
#
# Example
#
# module React::BlockQuote
# include React::ComponentBase
#
# def block_quote(**props)
# render_component **props
# end
#
# def block_quote_tag
# tag @tag
# end
#
# private def component_definition(**props)
# <<-JS
# render() {
# console.log('props: ', this.props);
# return (
# <blockquote>{this.props.quote}</blockquote>
# )
# }
# JS
# end
# end
#
module React::ComponentBase
macro included
{% name = @type.id.gsub(/React::/, "") %}
@name : String = "{{ name }}"
@tag : String = "{{ name.underscore }}"
# React::AutoComplete will generate an `#auto_complete` method
def {{ name.underscore }} (**props)
render_component **props
end
private def render_tag
tag @tag
end
# Allows rendering the tag only.
# For example:
# React::AutoComplete will have an `#auto_complete_tag` method
def {{name.underscore}}_tag
render_tag
end
end
# Renders a string inside the React component class and must include
# at least a render function.
abstract def component_definition : String
private def render_component(**props)
render_tag
component = definition(**props)
render_in_dom component, at_tag: @tag
end
# We'll render our component on every element with the right tag.
# This lets us render multiple instances using the `#render_tag` method.
private def render_in_dom(component : String, at_tag : String)
babel_script do
<<-JS
#{component}
const tags = document.getElementsByTagName('#{at_tag}')
console.log('tags: ', tags);
for (let i = 0; i < tags.length; i++) {
ReactDOM.render(
<#{@name} />,
tags[i]
)
}
JS
end
end
# Since our component will be mounted by `ReactDOM` we can't pass props into
# it directly. Instead we'll create a wrapper component, nest our component
# in that and pass our props to the wrapper.
private def definition(**props)
child = @name + "Child"
name = @name
properties = props.empty? ? "" : format_props(**props)
<<-JS
class #{child} extends React.Component {
#{component_definition}
}
class #{name} extends React.Component {
render() {
return (
<#{child} #{properties}/>
)
}
}
JS
end
# We render the script using `babel-standalone` to handle transpilation
private def babel_script
raw %(
<script
type="text/babel"
data-plugins="transform-class-properties">
#{yield}
</script>
)
end
# Map the named tuple into component props
private def format_props(**props)
props.map { |key| tuple_to_prop(key, props[key]) }.join(" ")
end
# Here we use overloads to serialize tuple values so they can be consumed by React
private def tuple_to_prop(key : Symbol, prop : String)
"#{key}={\"#{prop}\"}"
end
# These values can be passed in directly and will be converted to their javascript counterparts
private def tuple_to_prop(key : Symbol, prop : Bool | Int32 | Int64 | Float)
"#{key}={#{prop}}"
end
end

And here's an example AutoComplete React component;

module React::AutoComplete
include React::ComponentBase
private def component_definition(**props)
<<-JS
state = {
query: '',
items: [
"Man in the High Castle",
"The Expanse",
"Silicon Valley",
"Daredevil",
"The Punisher",
"Stranger Things",
],
filtered: [],
showList: false
}
handleInput = (event) => {
const query = event.target.value;
this.setState({
filtered: this.state.items.filter(item =>
item.toLowerCase().indexOf(query.toLowerCase()) !== -1,
),
showList: this.shouldShowList()
});
}
handleFocus = () => {
this.setState({
showList: this.shouldShowList()
})
}
shouldShowList = () => {
return this.state.filtered.length > 0;
}
handleBlur = () => {
this.setState({
showList: false
})
}
render() {
const list = <ul style={{
margin: 0,
listStyle: 'none',
border: '1px solid',
padding: '5px',
boxSizing: 'border-box'}}>
{ this.state.filtered.map(item => <li key={item}>{item}</li>) }
</ul>
return (
<div
style={{width: 200, padding: '15px 15px 0'}}>
<input
onChange={this.handleInput}
placeholder={this.props.placeholder}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
style={{width: '100%', boxSizing: 'border-box'}}
/>
{this.state.showList ? list : null}
</div>
)
}
JS
end
end

`,


` Sometimes we need to get a couple columns from our database, or make complex queries and return many columns that don't fit into our models. In these cases we want the framework we use to be flexible enough to allow such queries and make it easy to use the results in our app. Crystal and Lucky let us do just that.

In this post we'll look at how to use crystal-db's DB.mapping macro to map database queries to generic Crystal classes. Then we'll quickly look at how Lucky uses DB.mapping internally.

In this article we'll be using Lucky to make the database queries, but remember that crystal-db can be used alone or with any framework.

Setup

If you want to test this out yourself you can use my demo app, just clone the repo and checkout the db-mapping-0 to follow along, or db-mapping-1-complete to see the finished code.

git clone git@github.com:mikeeus/lucky_api_demo.git
cd lucky_api_demo
bin/setup
git checkout db-mapping-0

The Query

For this example we'll map this fairly simple query which fetches posts, joins users on user_id and return the user's name and email as a JSON object. Since Lucky uses the crystal-pg Postgresql driver, we can use DB.mapping to easily parse json objects from our query into JSON::Any.

SELECT
posts.id,
posts.title,
('PREFIX: ' || posts.content) as custom_key, -- custom key for fun
json_build_object(
'name', users.name,
'email', users.email
) as author
FROM posts
JOIN users
ON users.id = posts.user_id;

The Class

crystal-db returns the results of the query as DB::ResultSet which isn't directly useful for us. So lets create the class that the result will be mapped to, and we can use the DB.mapping to handle the dirty work.

class CustomPost
DB.mapping({
id: Int32,
title: String,
content: {
type: String,
nilable: false,
key: "custom_key"
},
author: JSON::Any
})
end

Essentially the mapping macro will create a constructor that accepts a DB::ResultSet and initializes this class for us, as well as a from_rs class method for intializing multiple results. It would expand to something like this.

class CustomPost
def initialize(%rs : ::DB::ResultSet)
# ...lots of stuff here
end
def self.from_rs(rs : ::DB::ResultSet)
objs = Array(self).new
rs.each do
objs << self.new(rs)
end
objs
ensure
rs.close
end
end

Hooking It All Up

Now let's write a spec to ensure everything is working as planned.

# spec/mapping_spec.cr
require "./spec_helper"
describe App do
describe "CustomPost" do
it "maps query to class" do
user = UserBox.new.name("Mikias").create
post = PostBox.new
.user_id(user.id)
.title("DB mapping")
.content("Post content")
.create
sql = <<-SQL
SELECT
posts.id,
posts.title,
('PREFIX: ' || posts.content) as custom_key,
json_build_object(
'name', users.name,
'email', users.email
) as author
FROM posts
JOIN users
ON users.id = posts.user_id;
SQL
posts = LuckyRecord::Repo.run do |db|
db.query_all sql, as: CustomPost
end
posts.size.should eq 1
posts.first.title.should eq post.title
posts.first.content.should eq "PREFIX: " + post.content
posts.first.author["name"].should eq user.name
end
end
end
class CustomPost
DB.mapping({
id: Int32,
title: String,
content: {
type: String,
nilable: false,
key: "custom_key"
},
author: JSON::Any
})
end

We can run the tests with lucky spec spec/mapping_spec and... green! Nice.

Lucky Models

This is actually very similar to how LuckyRecord sets up it's database mapping. For example if you have a User model like this.

class User < BaseModel
table :users do
column name : String
column email : String
column encrypted_password : String
end
end

Calls to the column method will add the name and type of each column to a FIELDS constant.

macro column(type_declaration, autogenerated = false)
... # check type_declaration's data_type and if it is nilable
{% FIELDS << {name: type_declaration.var, type: data_type, nilable: nilable.id, autogenerated: autogenerated} %}
end

The table macro will setup the model, including calling the setup_db_mapping macro which will call DB::mapping by iterating over the FIELDS.

macro setup_db_mapping
DB.mapping({
{% for field in FIELDS %}
{{field[:name]}}: {
{% if field[:type] == Float64.id %}
type: PG::Numeric,
convertor: Float64Convertor,
{% else %}
type: {{field[:type]}}::Lucky::ColumnType,
{% end %}
nilable: {{field[:nilable]}},
},
{% end %}
})
end

Just like that each of your Lucky models can now be instantiated from DB::ResultSet and have a from_rs method that can be called by your queries. Pretty simple right?

Join Us

I hope you enjoyed this tutorial and found it useful. Join us on the Lucky gitter channel to stay up to date on the framework or checkout the docs for more information on how to bring your app idea to life with Lucky.