https://github.com/fzingg/hyperloop-showcase
A simple Rails 5.0.1 app showcasing HyperReact, HyperMesh, Opal and associated technologies
https://github.com/fzingg/hyperloop-showcase
hyper-rails hyperloop isomorphic opal rails-application react-components webpack
Last synced: 21 days ago
JSON representation
A simple Rails 5.0.1 app showcasing HyperReact, HyperMesh, Opal and associated technologies
- Host: GitHub
- URL: https://github.com/fzingg/hyperloop-showcase
- Owner: fzingg
- Created: 2017-02-09T13:26:27.000Z (about 9 years ago)
- Default Branch: master
- Last Pushed: 2017-02-23T10:36:50.000Z (about 9 years ago)
- Last Synced: 2026-01-20T15:14:53.354Z (about 2 months ago)
- Topics: hyper-rails, hyperloop, isomorphic, opal, rails-application, react-components, webpack
- Language: JavaScript
- Homepage:
- Size: 1.1 MB
- Stars: 4
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.html
Awesome Lists containing this project
README
body {
width: 45em;
border: 1px solid #ddd;
outline: 1300px solid #fff;
margin: 16px auto;
}
body .markdown-body
{
padding: 30px;
}
@font-face {
font-family: fontawesome-mini;
src: url(data:font/woff;charset=utf-8;base64,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) format('woff');
}
@font-face {
font-family: octicons-anchor;
src: url(data:font/woff;charset=utf-8;base64,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) format('woff');
}
.markdown-body {
font-family: sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
color: #333333;
overflow: hidden;
font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif;
font-size: 16px;
line-height: 1.6;
word-wrap: break-word;
}
.markdown-body a {
background: transparent;
}
.markdown-body a:active,
.markdown-body a:hover {
outline: 0;
}
.markdown-body b,
.markdown-body strong {
font-weight: bold;
}
.markdown-body mark {
background: #ff0;
color: #000;
font-style: italic;
font-weight: bold;
}
.markdown-body sub,
.markdown-body sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.markdown-body sup {
top: -0.5em;
}
.markdown-body sub {
bottom: -0.25em;
}
.markdown-body h1 {
font-size: 2em;
margin: 0.67em 0;
}
.markdown-body img {
border: 0;
}
.markdown-body hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
.markdown-body pre {
overflow: auto;
}
.markdown-body code,
.markdown-body kbd,
.markdown-body pre,
.markdown-body samp {
font-family: monospace, monospace;
font-size: 1em;
}
.markdown-body input {
color: inherit;
font: inherit;
margin: 0;
}
.markdown-body html input[disabled] {
cursor: default;
}
.markdown-body input {
line-height: normal;
}
.markdown-body input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
.markdown-body table {
border-collapse: collapse;
border-spacing: 0;
}
.markdown-body td,
.markdown-body th {
padding: 0;
}
.markdown-body .codehilitetable {
border: 0;
border-spacing: 0;
}
.markdown-body .codehilitetable tr {
border: 0;
}
.markdown-body .codehilitetable pre,
.markdown-body .codehilitetable div.codehilite {
margin: 0;
}
.markdown-body .linenos,
.markdown-body .code,
.markdown-body .codehilitetable td {
border: 0;
padding: 0;
}
.markdown-body td:not(.linenos) .linenodiv {
padding: 0 !important;
}
.markdown-body .code {
width: 100%;
}
.markdown-body .linenos div pre,
.markdown-body .linenodiv pre,
.markdown-body .linenodiv {
border: 0;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
-webkit-border-top-left-radius: 3px;
-webkit-border-bottom-left-radius: 3px;
-moz-border-radius-topleft: 3px;
-moz-border-radius-bottomleft: 3px;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
.markdown-body .code div pre,
.markdown-body .code div {
border: 0;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
-webkit-border-top-right-radius: 3px;
-webkit-border-bottom-right-radius: 3px;
-moz-border-radius-topright: 3px;
-moz-border-radius-bottomright: 3px;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
.markdown-body * {
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.markdown-body input {
font: 13px Helvetica, arial, freesans, clean, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol";
line-height: 1.4;
}
.markdown-body a {
color: #4183c4;
text-decoration: none;
}
.markdown-body a:hover,
.markdown-body a:focus,
.markdown-body a:active {
text-decoration: underline;
}
.markdown-body hr {
height: 0;
margin: 15px 0;
overflow: hidden;
background: transparent;
border: 0;
border-bottom: 1px solid #ddd;
}
.markdown-body hr:before,
.markdown-body hr:after {
display: table;
content: " ";
}
.markdown-body hr:after {
clear: both;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 15px;
margin-bottom: 15px;
line-height: 1.1;
}
.markdown-body h1 {
font-size: 30px;
}
.markdown-body h2 {
font-size: 21px;
}
.markdown-body h3 {
font-size: 16px;
}
.markdown-body h4 {
font-size: 14px;
}
.markdown-body h5 {
font-size: 12px;
}
.markdown-body h6 {
font-size: 11px;
}
.markdown-body blockquote {
margin: 0;
}
.markdown-body ul,
.markdown-body ol {
padding: 0;
margin-top: 0;
margin-bottom: 0;
}
.markdown-body ol ol,
.markdown-body ul ol {
list-style-type: lower-roman;
}
.markdown-body ul ul ol,
.markdown-body ul ol ol,
.markdown-body ol ul ol,
.markdown-body ol ol ol {
list-style-type: lower-alpha;
}
.markdown-body dd {
margin-left: 0;
}
.markdown-body code,
.markdown-body pre,
.markdown-body samp {
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 12px;
}
.markdown-body pre {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body kbd {
background-color: #e7e7e7;
background-image: -moz-linear-gradient(#fefefe, #e7e7e7);
background-image: -webkit-linear-gradient(#fefefe, #e7e7e7);
background-image: linear-gradient(#fefefe, #e7e7e7);
background-repeat: repeat-x;
border-radius: 2px;
border: 1px solid #cfcfcf;
color: #000;
padding: 3px 5px;
line-height: 10px;
font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
display: inline-block;
}
.markdown-body>*:first-child {
margin-top: 0 !important;
}
.markdown-body>*:last-child {
margin-bottom: 0 !important;
}
.markdown-body .headeranchor-link {
position: absolute;
top: 0;
bottom: 0;
left: 0;
display: block;
padding-right: 6px;
padding-left: 30px;
margin-left: -30px;
}
.markdown-body .headeranchor-link:focus {
outline: none;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
position: relative;
margin-top: 1em;
margin-bottom: 16px;
font-weight: bold;
line-height: 1.4;
}
.markdown-body h1 .headeranchor,
.markdown-body h2 .headeranchor,
.markdown-body h3 .headeranchor,
.markdown-body h4 .headeranchor,
.markdown-body h5 .headeranchor,
.markdown-body h6 .headeranchor {
display: none;
color: #000;
vertical-align: middle;
}
.markdown-body h1:hover .headeranchor-link,
.markdown-body h2:hover .headeranchor-link,
.markdown-body h3:hover .headeranchor-link,
.markdown-body h4:hover .headeranchor-link,
.markdown-body h5:hover .headeranchor-link,
.markdown-body h6:hover .headeranchor-link {
height: 1em;
padding-left: 8px;
margin-left: -30px;
line-height: 1;
text-decoration: none;
}
.markdown-body h1:hover .headeranchor-link .headeranchor,
.markdown-body h2:hover .headeranchor-link .headeranchor,
.markdown-body h3:hover .headeranchor-link .headeranchor,
.markdown-body h4:hover .headeranchor-link .headeranchor,
.markdown-body h5:hover .headeranchor-link .headeranchor,
.markdown-body h6:hover .headeranchor-link .headeranchor {
display: inline-block;
}
.markdown-body h1 {
padding-bottom: 0.3em;
font-size: 2.25em;
line-height: 1.2;
border-bottom: 1px solid #eee;
}
.markdown-body h2 {
padding-bottom: 0.3em;
font-size: 1.75em;
line-height: 1.225;
border-bottom: 1px solid #eee;
}
.markdown-body h3 {
font-size: 1.5em;
line-height: 1.43;
}
.markdown-body h4 {
font-size: 1.25em;
}
.markdown-body h5 {
font-size: 1em;
}
.markdown-body h6 {
font-size: 1em;
color: #777;
}
.markdown-body p,
.markdown-body blockquote,
.markdown-body ul,
.markdown-body ol,
.markdown-body dl,
.markdown-body table,
.markdown-body pre,
.markdown-body .admonition {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body hr {
height: 4px;
padding: 0;
margin: 16px 0;
background-color: #e7e7e7;
border: 0 none;
}
.markdown-body ul,
.markdown-body ol {
padding-left: 2em;
}
.markdown-body ul ul,
.markdown-body ul ol,
.markdown-body ol ol,
.markdown-body ol ul {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body li>p {
margin-top: 16px;
}
.markdown-body dl {
padding: 0;
}
.markdown-body dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: bold;
}
.markdown-body dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.markdown-body blockquote {
padding: 0 15px;
color: #777;
border-left: 4px solid #ddd;
}
.markdown-body blockquote>:first-child {
margin-top: 0;
}
.markdown-body blockquote>:last-child {
margin-bottom: 0;
}
.markdown-body table {
display: block;
width: 100%;
overflow: auto;
word-break: normal;
word-break: keep-all;
}
.markdown-body table th {
font-weight: bold;
}
.markdown-body table th,
.markdown-body table td {
padding: 6px 13px;
border: 1px solid #ddd;
}
.markdown-body table tr {
background-color: #fff;
border-top: 1px solid #ccc;
}
.markdown-body table tr:nth-child(2n) {
background-color: #f8f8f8;
}
.markdown-body img {
max-width: 100%;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.markdown-body code,
.markdown-body samp {
padding: 0;
padding-top: 0.2em;
padding-bottom: 0.2em;
margin: 0;
font-size: 85%;
background-color: rgba(0,0,0,0.04);
border-radius: 3px;
}
.markdown-body code:before,
.markdown-body code:after {
letter-spacing: -0.2em;
content: "\00a0";
}
.markdown-body pre>code {
padding: 0;
margin: 0;
font-size: 100%;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.markdown-body .codehilite {
margin-bottom: 16px;
}
.markdown-body .codehilite pre,
.markdown-body pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f7f7f7;
border-radius: 3px;
}
.markdown-body .codehilite pre {
margin-bottom: 0;
word-break: normal;
}
.markdown-body pre {
word-wrap: normal;
}
.markdown-body pre code {
display: inline;
max-width: initial;
padding: 0;
margin: 0;
overflow: initial;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.markdown-body pre code:before,
.markdown-body pre code:after {
content: normal;
}
/* Admonition */
.markdown-body .admonition {
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
position: relative;
border-radius: 3px;
border: 1px solid #e0e0e0;
border-left: 6px solid #333;
padding: 10px 10px 10px 30px;
}
.markdown-body .admonition table {
color: #333;
}
.markdown-body .admonition p {
padding: 0;
}
.markdown-body .admonition-title {
font-weight: bold;
margin: 0;
}
.markdown-body .admonition>.admonition-title {
color: #333;
}
.markdown-body .attention>.admonition-title {
color: #a6d796;
}
.markdown-body .caution>.admonition-title {
color: #d7a796;
}
.markdown-body .hint>.admonition-title {
color: #96c6d7;
}
.markdown-body .danger>.admonition-title {
color: #c25f77;
}
.markdown-body .question>.admonition-title {
color: #96a6d7;
}
.markdown-body .note>.admonition-title {
color: #d7c896;
}
.markdown-body .admonition:before,
.markdown-body .attention:before,
.markdown-body .caution:before,
.markdown-body .hint:before,
.markdown-body .danger:before,
.markdown-body .question:before,
.markdown-body .note:before {
font: normal normal 16px fontawesome-mini;
-moz-osx-font-smoothing: grayscale;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
line-height: 1.5;
color: #333;
position: absolute;
left: 0;
top: 0;
padding-top: 10px;
padding-left: 10px;
}
.markdown-body .admonition:before {
content: "\f056\00a0";
color: 333;
}
.markdown-body .attention:before {
content: "\f058\00a0";
color: #a6d796;
}
.markdown-body .caution:before {
content: "\f06a\00a0";
color: #d7a796;
}
.markdown-body .hint:before {
content: "\f05a\00a0";
color: #96c6d7;
}
.markdown-body .danger:before {
content: "\f057\00a0";
color: #c25f77;
}
.markdown-body .question:before {
content: "\f059\00a0";
color: #96a6d7;
}
.markdown-body .note:before {
content: "\f040\00a0";
color: #d7c896;
}
.markdown-body .admonition::after {
content: normal;
}
.markdown-body .attention {
border-left: 6px solid #a6d796;
}
.markdown-body .caution {
border-left: 6px solid #d7a796;
}
.markdown-body .hint {
border-left: 6px solid #96c6d7;
}
.markdown-body .danger {
border-left: 6px solid #c25f77;
}
.markdown-body .question {
border-left: 6px solid #96a6d7;
}
.markdown-body .note {
border-left: 6px solid #d7c896;
}
.markdown-body .admonition>*:first-child {
margin-top: 0 !important;
}
.markdown-body .admonition>*:last-child {
margin-bottom: 0 !important;
}
/* progress bar*/
.markdown-body .progress {
display: block;
width: 300px;
margin: 10px 0;
height: 24px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
background-color: #ededed;
position: relative;
box-shadow: inset -1px 1px 3px rgba(0, 0, 0, .1);
}
.markdown-body .progress-label {
position: absolute;
text-align: center;
font-weight: bold;
width: 100%; margin: 0;
line-height: 24px;
color: #333;
text-shadow: 1px 1px 0 #fefefe, -1px -1px 0 #fefefe, -1px 1px 0 #fefefe, 1px -1px 0 #fefefe, 0 1px 0 #fefefe, 0 -1px 0 #fefefe, 1px 0 0 #fefefe, -1px 0 0 #fefefe, 1px 1px 2px #000;
-webkit-font-smoothing: antialiased !important;
white-space: nowrap;
overflow: hidden;
}
.markdown-body .progress-bar {
height: 24px;
float: left;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
background-color: #96c6d7;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5), inset 0 -1px 0 rgba(0, 0, 0, .1);
background-size: 30px 30px;
background-image: -webkit-linear-gradient(
135deg, rgba(255, 255, 255, .4) 27%,
transparent 27%,
transparent 52%, rgba(255, 255, 255, .4) 52%,
rgba(255, 255, 255, .4) 77%,
transparent 77%, transparent
);
background-image: -moz-linear-gradient(
135deg,
rgba(255, 255, 255, .4) 27%, transparent 27%,
transparent 52%, rgba(255, 255, 255, .4) 52%,
rgba(255, 255, 255, .4) 77%, transparent 77%,
transparent
);
background-image: -ms-linear-gradient(
135deg,
rgba(255, 255, 255, .4) 27%, transparent 27%,
transparent 52%, rgba(255, 255, 255, .4) 52%,
rgba(255, 255, 255, .4) 77%, transparent 77%,
transparent
);
background-image: -o-linear-gradient(
135deg,
rgba(255, 255, 255, .4) 27%, transparent 27%,
transparent 52%, rgba(255, 255, 255, .4) 52%,
rgba(255, 255, 255, .4) 77%, transparent 77%,
transparent
);
background-image: linear-gradient(
135deg,
rgba(255, 255, 255, .4) 27%, transparent 27%,
transparent 52%, rgba(255, 255, 255, .4) 52%,
rgba(255, 255, 255, .4) 77%, transparent 77%,
transparent
);
}
.markdown-body .progress-100plus .progress-bar {
background-color: #a6d796;
}
.markdown-body .progress-80plus .progress-bar {
background-color: #c6d796;
}
.markdown-body .progress-60plus .progress-bar {
background-color: #d7c896;
}
.markdown-body .progress-40plus .progress-bar {
background-color: #d7a796;
}
.markdown-body .progress-20plus .progress-bar {
background-color: #d796a6;
}
.markdown-body .progress-0plus .progress-bar {
background-color: #c25f77;
}
.markdown-body .candystripe-animate .progress-bar{
-webkit-animation: animate-stripes 3s linear infinite;
-moz-animation: animate-stripes 3s linear infinite;
animation: animate-stripes 3s linear infinite;
}
@-webkit-keyframes animate-stripes {
0% {
background-position: 0 0;
}
100% {
background-position: 60px 0;
}
}
@-moz-keyframes animate-stripes {
0% {
background-position: 0 0;
}
100% {
background-position: 60px 0;
}
}
@keyframes animate-stripes {
0% {
background-position: 0 0;
}
100% {
background-position: 60px 0;
}
}
.markdown-body .gloss .progress-bar {
box-shadow:
inset 0 4px 12px rgba(255, 255, 255, .7),
inset 0 -12px 0 rgba(0, 0, 0, .05);
}
/* Multimarkdown Critic Blocks */
.markdown-body .critic_mark {
background: #ff0;
}
.markdown-body .critic_delete {
color: #c82829;
text-decoration: line-through;
}
.markdown-body .critic_insert {
color: #718c00 ;
text-decoration: underline;
}
.markdown-body .critic_comment {
color: #8e908c;
font-style: italic;
}
.markdown-body .headeranchor {
font: normal normal 16px octicons-anchor;
line-height: 1;
display: inline-block;
text-decoration: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.headeranchor:before {
content: '\f05c';
}
.markdown-body .task-list-item {
list-style-type: none;
}
.markdown-body .task-list-item+.task-list-item {
margin-top: 3px;
}
.markdown-body .task-list-item input {
margin: 0 4px 0.25em -20px;
vertical-align: middle;
}
/* Media */
@media only screen and (min-width: 480px) {
.markdown-body {
font-size:14px;
}
}
@media only screen and (min-width: 768px) {
.markdown-body {
font-size:16px;
}
}
@media print {
.markdown-body * {
background: transparent !important;
color: black !important;
filter:none !important;
-ms-filter: none !important;
}
.markdown-body {
font-size:12pt;
max-width:100%;
outline:none;
border: 0;
}
.markdown-body a,
.markdown-body a:visited {
text-decoration: underline;
}
.markdown-body .headeranchor-link {
display: none;
}
.markdown-body a[href]:after {
content: " (" attr(href) ")";
}
.markdown-body abbr[title]:after {
content: " (" attr(title) ")";
}
.markdown-body .ir a:after,
.markdown-body a[href^="javascript:"]:after,
.markdown-body a[href^="#"]:after {
content: "";
}
.markdown-body pre {
white-space: pre;
white-space: pre-wrap;
word-wrap: break-word;
}
.markdown-body pre,
.markdown-body blockquote {
border: 1px solid #999;
padding-right: 1em;
page-break-inside: avoid;
}
.markdown-body .progress,
.markdown-body .progress-bar {
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
}
.markdown-body .progress {
border: 1px solid #ddd;
}
.markdown-body .progress-bar {
height: 22px;
border-right: 1px solid #ddd;
}
.markdown-body tr,
.markdown-body img {
page-break-inside: avoid;
}
.markdown-body img {
max-width: 100% !important;
}
.markdown-body p,
.markdown-body h2,
.markdown-body h3 {
orphans: 3;
widows: 3;
}
.markdown-body h2,
.markdown-body h3 {
page-break-after: avoid;
}
}
README
HyperLoop Showcase
This is a simple Rails application showcasing HyperReact (wrapper for the React.js library for creating user interfaces), Opal, NPM, Webpack, React Bootstrap, HyperMesh (gives your HyperReact components CRUD access to your server side ActiveRecord models and implements push notifications) and other associated technologies.
This Showcase application will mix native React and HyperReact components, be styled by Bootstrap CSS (using ReactBootstrap), display a video (using a native React component) and use HyperMesh to handle data for an Events feed app.
The Showcase application will look like this:

Technologies highlighted in this Showcase application
- For the backend we are using Rails 5.0.1 with Ruby 2.3.1
-
NPM and Webpack to manage front end assets -
HypeRails to install HyperReact and Opal in Rails 4.x or 5.x -
HyperReact to use React with Rails and to write reactive UI components with Ruby’s elegance -
React Bootstrap to show how to use native React components in HyperReact -
HyperMesh between Rails models and the front end and to magically push
changed data between all connected clients -
HyperReact Hot-Reloader and Opal IRB for programmer joy and hot-loading with developing
Introduction
Introductions to HyperReact
- An overview of HyperReact (formerly Reactrb) by Mitch VanDuyn
- Power of React-js with the joy of Ruby by Forrest Chang
HyperReact Help and Questions
-
Gitter.im for general questions, discussion, and interactive help. -
Stack Overflow tagreactrbfor specific problems. -
Github Issues for bugs, feature enhancements, etc. -
Further reading at the end of this tutorial
Using NPM and Webpack alongside Rails
Ruby libraries are distributed as gems, and are managed in your Rails app using the Gemfile and bundler.
In the Javascript world things are still evolving but I have found that the easiest way to manage Javascript libraries is using NPM (Node Package Manager) and Webpack. Pretty much every front end library is packaged with NPM these days so it is easy to get help and most things just work.
Happily NPM, Webpack, Rails, and HyperReact can all play together very nicely.
This tutorial requires that Ruby, Rails, NPM and Webpack are installed. Please see their websites for installation instructions.
Setup
Step 1: Creating a new Rails application
rails new hyperloop-showcase
cd hyperloop-showcase
bundle install
Update your Database config file for accessing your Database properly : config/database.yml
Then execute
rails db:create
You should have a empty Rails application
bundle exec rails s
And in your browser
http://localhost:3000/
You should be seeing the Rails Welcome aboard page. Great, Rails is now installed. Lets get started with the interesting stuff.
Step 2: Adding HyperReact and HyperMesh
There are 2 ways do achieve that :
- Using HyperLoop installation generator
OR - Manually.
In this tutorial we advise you to follow the manual way, so you can see what’s happen and you can be sure to have all gems versions identical to those used in this tutorial.
if you want to see the automatic way for the future you can go to the official HyperLoop web page : HyperLoop installation with Rails
Manual installation of HyperReact and HyperMesh
Step 2.1: Add the gems
in your Gemfile
gem 'react-rails', '1.4.2'
gem 'hyper-rails', '0.4.1'
gem 'opal-rails', '0.9.1'
gem 'opal-browser', '0.2.0'
gem 'hyper-react', '0.11.0'
gem 'hyper-mesh', '0.5.3'
gem 'hyper-router', '2.4.0'
gem 'therubyracer', platforms: :ruby
Then run
bundle update
Step 3: Webpack for managing front-end assets
There are three parts to this step:
- Setting up NPM (node package manager) for the project
- Setting up Webpack
- Updating the rails asset pipeline to use the bundles generated by Webpack
This is just a matter of adding 4 boiler plate files, and updating two of your rails files.
First add a package.json file to your root directory (same place as your Gemfile) like this:
// package.json
{
"name": "reactrb-showcase",
"version": "0.0.1",
"dependencies": {
"bootstrap": "^3.3.6",
"react": "^0.14.2",
"react-dom": "^0.14.2",
"react-bootstrap": "^0.29.5",
"webpack": "^1.13.1"
},
"devDependencies": {
}
}
Notice how similar this is to your Gemfile.
Now run npm install which will make sure you have all these packages.
So that we can run Webpack from the command line do a npm install webpack -g
Now that we have Webpack, we need to add 3 boiler plate files to configure it. As you add more javascript packages you will be updating these files. Again this is similar to updating your Gemfile when you add new gems to a project.
Add webpack.config.js to the root of your project:
var path = require("path");
module.exports = {
context: __dirname,
entry: {
client_only: "./webpack/client_only.js",
client_and_server: "./webpack/client_and_server.js"
},
output: {
path: path.join(__dirname, 'app', 'assets', 'javascripts', 'webpack'),
filename: "[name].js",
publicPath: "/webpack/"
},
module: {
loaders: [
// add any loaders here
]
},
resolve: {
root: path.join(__dirname, '..', 'webpack')
},
};
and create a folder called
webpack and add the following two files:
// webpack/client_only.js
// any packages that depend specifically on the DOM go here
// for example the webpack css loader generates code that will break prerendering
console.log('client_only.js loaded');
// webpack/client_and_server.js
// all other packages that you can run on both server (prerendering) and client go here
// most well behaved packages can be required here
ReactDOM = require('react-dom')
React = require('react')
console.log('client_and_server.js loaded')
Now run webpack from the command line. This will grab all necessary dependencies and package them up into the client_and_server.js and client_only.js bundles. If you look in the app/assets/javascripts/webpack directory you should see the two files there.
Finally we need to require these two bundles into our rails asset pipeline.
Edit app/assets/javascript/application.js and add
//= require 'webpack/client_only'
just above the line that reads
Opal.load('components');. This will pull in any webpack assets that can only run on the client.
Then edit app/views/components.rb and replace the require 'react' line with
require 'webpack/client_and_server.js'
In otherwords instead of pulling in react from the react-rails gem, we are going to pull in react and any other javascript packages we want from our webpack bundle.
Reactrb can automatically access our components loaded by Webpack, but we have to opt in to this behavior. Edit app/views/components.rb and add
require 'reactrb/auto-import'
immediately after require 'reactrb' (which is right near the top of the file.) Auto-import will now search the javascript name space, and import into ruby any components that are referenced by your Reactrb components.
Now run bundle exec rails s and refresh the browser. Look at the console and you should see something like this:
client_and_server.js loaded
client_only.js loaded
client_and_server.js loaded
************************ React Prerendering Context Initialized Show ***********************
************************ React Browser Context Initialized ****************************
Reactive record prerendered data being loaded: [Object]
Congratulations you are setup and ready to begin adding javascript packages to your application.
Working with native React components
It is time to reap some of the rewards from all the hard work above. We have everything setup so we can easily add front end components and work with them in Reactrb. Lets jump in and add a native React component that plays a video.
We are going to use Pete Cook’s React rplayr
First let’s install the component via NPM:
npm install react-player --save
Next we need to require it in webpack/client_and_server.js
ReactPlayer = require('react-player')
Next run webpack so it can be bundled
webpack
And then finally let’s add it to our Show component:
def render
div do
ReactPlayer(url: 'https://www.youtube.com/embed/FzCsDVfPQqk',
playing: true
)
end
end
Refresh your browser and you should have a video. How simple was that!
Working with React Bootstrap
We will be using React Bootstrap which is a native React library
The main purpose for React Bootstrap is that it abstracts away verbose HTML & CSS code into React components which makes it a lot cleaner for React JSX developers. One of the very lovely things about Reactrb is that we already work in beautiful Ruby. To emphasise this point, consider the following:
Sample 1 - In HTML (without React Bootstrap):
<button id="something-btn" type="button" class="btn btn-success btn-sm">
Something
</button>
$('#something-btn').click(someCallback);
Sample 2 - In JSX (with React Bootstrap components):
<Button bsStyle="success" bsSize="small" onClick={someCallback}>
Something
</Button>
Sample 3 - In Reactrb (without React Bootstrap):
button.btn_success.btn_sm {'Something'}.on(:click) do
someMethod
end
Sample 4 - In Reactrb (with React Bootstrap):
Bs.Button(bsStyle: 'success' bsSize: "small") {'Something'}.on(:click) do
someMethod
end
As you can see, sample 3 & 4 are not that different and as a Reactrb developer, I actually prefer sample 3. If I were a JavaScript or JSX developer I would completely understand the advantage of abstracting Bootstrap CSS into React Components so I don’t have to work directly with CSS and JavaScript but this is not the case with Reactrb as CSS classes are added to HTML elements with simple dot notation:
span.pull_right {}
compiles to (note the conversion from _ to -)
<span class='pull-right'></span>
So I hear you ask: why if I prefer the non-React Bootstrap syntax why am worrying about React Bootstrap? For one very simple reason: components like Navbar and Modal that requires bootstrap.js will not work with React on it’s own so without the React Bootstrap project you would need to implement all that functionality yourself. The React Bootstrap project has re-implemented all this functionality as React components.
Lets implement a Navbar in this project using React Bootstrap in Reactrb. First, we need to install Bootstrap and React Bootstrap:
npm install bootstrap react-bootstrap --save
Note: The --save option will update the package.json file.
And then we need to require it in webpack/client_and_server.js by adding this line:
ReactBootstrap = require('react-bootstrap')
Run the
webpack command again, and restart your rails server.
If you refresh your browser now and open the JavaScript console we will be able to interact with React Bootstrap by typing:
In the JavaScript console type: ReactBootstrap
and you will see the ReactBootstrap object with all its components like Accordion, Alert, Badge, Breadcrumb, etc. This is great news, React Bootstrap is installed and ready to use. Accessing the JavaScript object in this way is a really great way to see what you have to work with. Sometimes the documentation of a component is not as accurate as actually seeing what you have in the component itself.
To make sure everything is working lets add a Button to our our Show component like this:
module Components
module Home
class Show < React::Component::Base
def render
ReactBootstrap::Button(bsStyle: 'success', bsSize: "small") do
'Success'
end.on(:click) do
alert('you clicked me!')
end
end
end
end
end
Notice that we reference
ReactBoostrap in ruby using the same identifer that was in the require statement in our client_and_server.js webpack bundle. The first time Reactrb hits the ReactBootstrap constant it will not be defined. This triggers a search of the javascript name space for something that looks either like a component or library of components. It then defines the appropriate module or component class wrapper in ruby.
Visit your page and if all is well you will see a clickable button. However it will not have any styles. This is because ReactBootstrap does not automatically depend on any particular style sheet, so we will have to supply one. An easy way to do this is to just copy the css file from the bootstrap repo, and stuff it our rails assets directory, however with a little upfront work we can setup webpack to do it all for us.
First lets add four webpack loaders using npm:
npm install css-loader file-loader style-loader url-loader --save-dev
Notice we use
--save-dev instead of just --save as these packages are only used in the development process.
Now edit your webpack.config.js file, and update the loaders section so it looks like this:
var path = require("path");
module.exports = {
...
module: {
loaders: [
{ test: /\.css$/,
loader: "style-loader!css-loader"
},
{ test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url?limit=10000&mimetype=application/font-woff'
},
{ test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url?limit=10000&mimetype=application/octet-stream'
},
{ test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
loader: 'file'
},
{ test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url?limit=10000&mimetype=image/svg+xml'
}
]
},
...
};
We have set webpack up so that when a css file is required it uses the style loader to process the file. Because the bootstrap css file will require font face files, we also have 4 font loaders. All this will package up everything when we require any css file.
Now we are ready to require CSS files, and have webpack build a complete bundle including the css and any fonts referenced.
To bundle in the bootstrap css file add this line to webpack/client_only.js
require('bootstrap/dist/css/bootstrap.css');
And install the bootstrap package
npm install bootstrap --save
Now run webpack to update our bundles, and restart your server. Now our button is properly styled you should be rewarded with a nice Bootstrap styled green Success Button.
Now that everything is loaded, lets update our component to use a few more of the Bootstrap components. Update your Show component so that it looks like this:
module Components
module Home
class Show < React::Component::Base
def say_hello(i)
alert "Hello from number #{i}"
end
def render
div do
ReactBootstrap::Navbar(bsStyle: :inverse) do
ReactBootstrap::Nav() do
ReactBootstrap::NavbarBrand() do
a(href: '#') { 'Reactrb Showcase' }
end
ReactBootstrap::NavDropdown(
eventKey: 1,
title: 'Things',
id: :drop_down
) do
(1..5).each do |n|
ReactBootstrap::MenuItem(href: '#',
key: n,
eventKey: "1.#{n}"
) do
"Number #{n}"
end.on(:click) { say_hello(n) }
end
end
end
end
div.container do
ReactPlayer(url: 'https://www.youtube.com/embed/FzCsDVfPQqk',
playing: true
)
end
end
end
end
end
end
A few things to notice in the code above:
We add React Bootstrap components simply by ReactBootstrap::Name where Name is the JavaScriot component you want to render. All the components are documented in the React Bootstrap documentation
See with div.container we are mixing in CSS style which will compile into <div class='container'>
Also notice how I have added an .on(:click) event handler to the MenuItem component while setting href: '#' as this will allow us to handle the event instead of navigating to a new page.
So far we have a very basic application which is looking OK and showing a video. Time to do something a little more interesting. How about if we add Post and Comment functionality which will let us explore Reactive Record!
Using Reactrb Reactive Record
We will be using the Reactive Record gem
Reactive Record compiles your Active Record models so they are accessible to the front-end and implements an API based on your models and their associations. Lazy loads just the data that is needed to render a component and is fully integrated with Reactrb and paired with Synchromesh to push database changes to all connected clients. ReactiveRecord and Synchromesh give you Relay + GraphQL like functionality with a fraction of the effort and complexity (the original idea for Reactive Record is credited to Volt and not Relay).
Installing Reactive Record
Installing Reactive Record is straight forward.
First add this line to your application’s Gemfile:
gem 'reactive-record'
And then execute:
$ bundle install
Finally you need to add a line to your routes.rb:
mount ReactiveRecord::Engine => '/rr'
Creating the models
We are going to need a few models to work with so let’s go ahead and create those now.
rails g model Post
rails g model Comment post:references
And then before you run the migrations, lets flesh them out a little so they look like this:
# db/migrate/..create_posts.rb
class CreatePosts < ActiveRecord::Migration
def change
create_table :posts do |t|
t.string :body
t.timestamps null: false
end
end
end
# db/migrate/..create_comments.rb
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.references :post, index: true, foreign_key: true
t.string :body
t.timestamps null: false
end
end
end
Now would be a good time to run the migrations:
rake db:migrate
Making your models accessible to Reactive Record
Reactive Record needs to ‘see’ your models as a representation of them get compiled into JavaScript along with your Reactrb components so they are accessible in your client side code.
The convention (though this is choice and you can change this if you prefer) is to create a public folder under models and then provide a linkage file which will require_tree your models when compiling components.rb.
Create a new folder:
models/public
Then move post.rb and comment.rb to models/public
$ mv app/models/post.rb app/models/public
$ mv app/models/comment.rb app/models/public
Next create _react_public_models.rb in your models folder:
# models/_react_public_models.rb
require_tree './public'
Finally add a line to your views/components.rb file:
# views/components.rb
...
require '_react_public_models'
Model Associations
Reactive Record is particular about both sides of an association being specified. If you forget to do this you will see warnings to this effect.
# models/public/post.rb
class Post < ActiveRecord::Base
has_many :comments
end
# models/public/comment.rb
class Comment < ActiveRecord::Base
belongs_to :post
end
Accessing your models in Reactrb components
To get started, lets create a new component which will display a list of Posts and Comments under the video:
# views/components/show.rb
...
div.container do
ReactPlayer(url: 'https://www.youtube.com/embed/FzCsDVfPQqk', playing: true)
br # line break
PostsList()
end
...
Note that to place a Reactrb component you either need to include ( ) or { }, so PostsList() or PostsList { } would be valid but just PostsList would not.
Next lets create the PostsList component:
module Components
module Home
class PostsList < React::Component::Base
define_state :new_post, ""
before_mount do
# note that this will lazy load posts
# and only the fields that are needed will be requested
@posts = Post.all
end
def render
div do
new_post
ul.list_unstyled do
@posts.reverse.each do |post|
PostListItem(post: post)
CommentsList(comments: post.comments)
end
end
end
end
def new_post
ReactBootstrap::FormGroup() do
ReactBootstrap::FormControl(
value: state.new_post,
type: :text,
).on(:change) { |e|
state.new_post! e.target.value
}
end
ReactBootstrap::Button(bsStyle: :primary) do
"Post"
end.on(:click) { save_new_post }
end
def save_new_post
post = Post.new(body: state.new_post)
post.save do |result|
# note that save is a promise so this code will only run after the save
# yet react will move onto the code after this (before the save happens)
alert "unable to save" unless result == true
end
state.new_post! ""
end
end
class PostListItem < React::Component::Base
param :post
def render
li do
# note how you access post.body just like with Active Record
h4 { params.post.body }
end
end
end
end
end
Things to note in the code above:
See how we fetch the Reactive Record Post collection in before_mount. Setting this here instead of in after_mount means that we do not need to worry about @posts being nil as the collection will always contain at least one entry with the actual records being lazy loaded when needed.
Note how we are binding the state variable new_post to the FormControl and then setting its value based on the value being passed to the .on(:change) block. This is a standard React pattern.
Also see how we are saving the new post where Reactive Record’s save returns a promise which means that the block after save is only evaluated when it returns yet React would have moved on to the rest of the code.
Finally note that there is no code which checks to see if there are new posts yet when you run this, the list of posts remains magically up-to-date.
Welcome to the wonderful of Reactive Record and React!
Synchromesh
We will be using the Synchromesh gem
Reactive Record is the data layer between one client and its server and Synchromesh uses push notifications to push changed records to all connected Reactive Record clients.
Synchromesh is incredibly simple to setup. Add this line to your Gemfile:
gem 'synchromesh', git: "https://github.com/reactrb/synchromesh.git"
And then execute:
$ bundle install
Next add this line to your components.rb:
require 'synchromesh'
Finally, you need to add an initialiser config/initializers/synchromesh.rb
# config/initializers/synchromesh.rb
Synchromesh.configuration do |config|
# this is the initialiser for polling, see the synchromesh
# documentation for using pusher.com
config.transport = :simple_poller
config.channel_prefix = "synchromesh"
config.opts = {
seconds_between_poll: 1.second,
seconds_polled_data_will_be_retained: 1.hour
}
end
Restart your server, open two browser windows and be amazed to see any new posts added to one session magically appearing in the other!
Todo:
+ Reactrb Router
Reactrb Hot-reloader and Opal IRB
Before we go any further, let’s install too fantastic tools written by Forrest Chang:
Opal Hot Loader is for pure programmer joy (not having to reload the page to compile your source) and the Opal console is incredibly useful to test how Ruby code compiles to JavaScript.
We are also going to add the Foreman gem to run our Rails server and the Hot Loader service for us.
Add the following lines to your gemfile and run bundle:
gem 'opal_hot_reloader', git: 'https://github.com/fkchang/opal-hot-reloader.git'
gem 'foreman'
bundle install
Modify your components.rb, adding the following lines inside the if statement so they only run on the client and not as part of the server pre-rendering process:
require 'opal_hot_reloader'
OpalHotReloader.listen(25222, true)
Then modify your procfile so that the Hot Loader service will start whenever you start your server:
rails: bundle exec rails server
hotloader: opal-hot-reloader -p 25222 -d app/views/components
To start both servers:
foreman start
Refresh your browser for the last time and try modifying your show.rb component and you should see your changes appearing magically in your browser as you save. Pure joy.
Further reading
Other Reactrb tutorials and examples
- Getting started with Reactrb and Rails
- ChatRB Demo App
- Reactive Record sample ToDo app
- Flux pattern in Reactrb
- Getting with Reactrb, React Bootstrap and Webpack
Other Reactrb resources
Reactrb is powered by React
Reactrb and friends are in most cases simple DSL Ruby wrappers to the underlying native JavaScript libraries and React Components. It is really important to have a solid grip on how these technologies work to complement your understanding of Reactrb. Most searches for help on Google will take you to examples written in JSX or ES6 JavaScript but you will learn over time to transalte this to Reactrb equivalents. To make headway with Reactrb you do need a solid understanding of the underlying philosophy of React and its component based architecture. The ‘Thinking in React’ tutorial below is an excellent place to start. (Make sure you see the Flux pattern in Reactrb above for an example of how to communicate between grandparent and child components).
Opal under the covers
Reactrb is a DSL wrapper of React which uses Opal to compile Ruby code to ES5 native JavaScript. If you have not used Opal before then you should at a minimum read the excellent guides as they will teach you enough to get you started with Reactrb.