React Components In Lucky With Laravel Mix and lucky-react
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.crmodule Shared::Layout...def shared_layout_headhead doutf8_charsettitle "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_tagsresponsive_meta_tagendend...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_componenttag "react-component"script do<<-JSclass 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)JSendend
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 SearchPageinclude React::Searchdef contenth2 "Search"auto_complete placeholder: "Search something..."endend
require "./component_base.cr"module React::AutoCompleteinclude React::ComponentBasedef component_tagtag @tagendprivate def component_definition(**props)# the content of the React class goes hereendend
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::ComponentBasemacro included{% name = @type.id.gsub(/React::/, "") %}@name : String = "{{ name }}"@tag : String = "{{ name.underscore }}"# React::AutoComplete will generate an `#auto_complete` methoddef {{ name.underscore }} (**props)render_component **propsendprivate def render_tagtag @tagend# Allows rendering the tag only.# For example:# React::AutoComplete will have an `#auto_complete_tag` methoddef {{name.underscore}}_tagrender_tagendend# Renders a string inside the React component class and must include# at least a render function.abstract def component_definition : Stringprivate def render_component(**props)render_tagcomponent = definition(**props)render_in_dom component, at_tag: @tagend# 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])}JSendend# 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 = @nameproperties = props.empty? ? "" : format_props(**props)<<-JSclass #{child} extends React.Component {#{component_definition}}class #{name} extends React.Component {render() {return (<#{child} #{properties}/>)}}JSend# We render the script using `babel-standalone` to handle transpilationprivate def babel_scriptraw %(<scripttype="text/babel"data-plugins="transform-class-properties">#{yield}</script>)end# Map the named tuple into component propsprivate 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 Reactprivate 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 counterpartsprivate def tuple_to_prop(key : Symbol, prop : Bool | Int32 | Int64 | Float)"#{key}={#{prop}}"endend
And here's an example AutoComplete
React component;
module React::AutoCompleteinclude React::ComponentBaseprivate def component_definition(**props)<<-JSstate = {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 (<divstyle={{width: 200, padding: '15px 15px 0'}}><inputonChange={this.handleInput}placeholder={this.props.placeholder}onFocus={this.handleFocus}onBlur={this.handleBlur}style={{width: '100%', boxSizing: 'border-box'}}/>{this.state.showList ? list : null}</div>)}JSendend
`,
` 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.gitcd lucky_api_demobin/setupgit 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
.
SELECTposts.id,posts.title,('PREFIX: ' || posts.content) as custom_key, -- custom key for funjson_build_object('name', users.name,'email', users.email) as authorFROM postsJOIN usersON 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 CustomPostDB.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 CustomPostdef initialize(%rs : ::DB::ResultSet)# ...lots of stuff hereenddef self.from_rs(rs : ::DB::ResultSet)objs = Array(self).newrs.each doobjs << self.new(rs)endobjsensurers.closeendend
Hooking It All Up
Now let's write a spec to ensure everything is working as planned.
# spec/mapping_spec.crrequire "./spec_helper"describe App dodescribe "CustomPost" doit "maps query to class" douser = UserBox.new.name("Mikias").createpost = PostBox.new.user_id(user.id).title("DB mapping").content("Post content").createsql = <<-SQLSELECTposts.id,posts.title,('PREFIX: ' || posts.content) as custom_key,json_build_object('name', users.name,'email', users.email) as authorFROM postsJOIN usersON users.id = posts.user_id;SQLposts = LuckyRecord::Repo.run do |db|db.query_all sql, as: CustomPostendposts.size.should eq 1posts.first.title.should eq post.titleposts.first.content.should eq "PREFIX: " + post.contentposts.first.author["name"].should eq user.nameendendendclass CustomPostDB.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 < BaseModeltable :users docolumn name : Stringcolumn email : Stringcolumn encrypted_password : Stringendend
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_mappingDB.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.