Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/ntamvl/rails7withreacttailwindcssbootstrapexample

Rails 7 with React, TailwindCSS and Bootstrap 5 Example
https://github.com/ntamvl/rails7withreacttailwindcssbootstrapexample

bootstrap5 rails rails7 react tailwindcss

Last synced: about 5 hours ago
JSON representation

Rails 7 with React, TailwindCSS and Bootstrap 5 Example

Awesome Lists containing this project

README

        

# Rails 7 with React, TailwindCSS and Bootstrap 5 Example

Rails 7 with React, TailwindCSS and Bootstrap 5 Example
- Ruby 3.0.3
- Rails 7.0.0
- SQLite
- Node v14.15.0
- NPM 6.14.8
- Yarn 1.22.17
- TailwindCSS 3
- Bootstrap 5
- React 17.0.2

## Setup Rails 7 project
**Create Rails 7 project**
```bash
rails _7.0.0_ new Rails7WithReactTailwindCSSBootstrapExample -j esbuild -c tailwind
```

**Install node packages**
```bash
yarn add @tailwindcss/forms @tailwindcss/typography bootstrap @popperjs/core jquery postcss-flexbugs-fixes postcss-import postcss-nested postcss-preset-env react react-dom prop-types
```

**Create PostCSS config file `postcss.config.js`**
```js
module.exports = {
plugins: [
require("autoprefixer"),
require("postcss-import"),
require("tailwindcss"),
require("postcss-nested"),
require("postcss-flexbugs-fixes"),
require("postcss-preset-env")({
autoprefixer: {
flexbox: "no-2009",
},
stage: 3,
}),
],
};

```

**Create esbuild plugin to load css file `esbuild.style.loader.plugin.js`**
```js
// esbuild.style.loader.plugin.js

const fs = require('fs');

const styleLoaderPlugin = {
name: 'styleLoader',
setup: build => {
// replace CSS imports with synthetic 'loadStyle' imports
build.onLoad({ filter: /\.css$/ }, async args => {
return {
contents: `
import {loadStyle} from 'loadStyle';
loadStyle(${JSON.stringify(args.path)});
`,
loader: 'js',
};
});

// resolve 'loadStyle' imports to the virtual loadStyleShim namespace which is this plugin
build.onResolve({ filter: /^loadStyle$/ }, args => {
return { path: `loadStyle(${JSON.stringify(args.importer)})`, namespace: 'loadStyleShim' };
});

// define the loadStyle() function that injects CSS as a style tag
build.onLoad({ filter: /^loadStyle\(.*\)$/, namespace: 'loadStyleShim' }, async args => {
const match = /^loadStyle\(\"(.*)"\)$/.exec(args.path);
const cssFilePath = match[1];
const cssFileContents = String(fs.readFileSync(cssFilePath));
return {
contents: `
export function loadStyle() {
const style = document.createElement('style');
style.innerText = \`${cssFileContents}\`;
document.querySelector('head').appendChild(style);
}
`,
};
});
},
};

module.exports = {
styleLoaderPlugin
};

```

**Create file `esbuild.config.js`**
```js
// esbuild.config.js

const path = require('path')
const { styleLoaderPlugin } = require("./esbuild.style.loader.plugin");

require("esbuild").build({
entryPoints: [
'application.js',
'react/hello_react.js',
'styles/index.css'
],
bundle: true,
logLevel: 'info',
outdir: path.join(process.cwd(), "app/assets/builds"),
absWorkingDir: path.join(process.cwd(), "app/javascript"),
watch: process.argv.includes("--watch"),
publicPath: '/assets',
loader: {
'.js': 'jsx',
'.png': 'file'
},
plugins: [
styleLoaderPlugin
],
}).catch(() => process.exit(1))

```

**Add build script to `package.json`**
```json
// ...
"scripts": {
"build": "node esbuild.config.js",
"build:css": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css"
},
// ...
```

**Generate tailwindcss config**
```
rm tailwind.config.js
npx tailwindcss init --full
```

**Update Tailwind config `tailwind.config.js`**
```js
// ...
content: [
'./app/views/**/*.html.erb',
'./app/helpers/**/*.rb',
'./app/javascript/**/*.js'
],
prefix: 'tw-',
// ...
```

**Create custom styles `app/assets/stylesheets/styles.css`**
```css
/* app/assets/stylesheets/styles.css */
/* Custom Styles */

.my-styles {
font-weight: 600;
color: green;
}

```

**Update file `application.tailwind.css`**
```css
/* this line is used for the case if prioritizes Bootstrap first */
@import "bootstrap/dist/css/bootstrap.css";

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

/* this line is used for the case if prioritizes Bootstrap later */
/* @import "bootstrap/dist/css/bootstrap.css"; */
@import "./styles";

```

Copy images to `app/assets/images/` base on this project
- app/assets/images/beams.jpeg
- app/assets/images/grid.svg
- app/assets/images/logo.svg

## Generate home, hello_react pages
```
./bin/rails g controller pages home hello_react

```

## Update `route.rb`
```rb
Rails.application.routes.draw do
root 'pages#home'
get '/hello_react' => 'pages#hello_react'
end
```

## Setup Bootstrap and jQuery
```
mkdir app/javascript/libs
touch app/javascript/libs/bootstrap.js
touch app/javascript/libs/jquery.js
touch app/javascript/libs/index.js
```

**Create bootstrap config `app/javascript/libs/bootstrap.js`**
```js
// app/javascript/libs/bootstrap.js

// import "bootstrap/dist/css/bootstrap.css"
// import "bootstrap/dist/js/bootstrap.bundle.js"

const bootstrap = require("bootstrap/dist/js/bootstrap.bundle.js")
const popoverElements = document.querySelector('[data-bs-toggle="popover"]')
if (popoverElements) new bootstrap.Popover(popoverElements, { trigger: 'hover' })

```

**Create jQuery config `app/javascript/libs/jquery.js`**
```js
// app/javascript/libs/jquery.js
import jquery from 'jquery';
window.jQuery = jquery;
window.$ = jquery;

```

Create file index.js to include bootstrap and jquery `app/javascript/libs/index.js`
```js
// app/javascript/libs/index.js
import "./jquery";
import "./bootstrap";
```

Update `app/javascript/application.js`
```js
// Entry point for the build script in your package.json
import "@hotwired/turbo-rails"
import "./controllers"
import "./libs"

```

## Create Tailwind components

I use tailwind prefix `tw-` for this example. I have created a simple tool to generate tailwind prefix, link: http://tailwind-prefix-generator.ntam.me/

----
app/views/pages/_content_home_tw.html.erb
```html









An advanced online playground for Tailwind CSS, including support for things like:









  • Customizing your
    tailwind.config.js file









  • Extracting classes with
    @apply








  • Code completion with instant preview




Perfect for learning how the framework works, prototyping a new idea, or creating a demo to share online.




Want to dig deeper into Tailwind?



Read the docs →




My Styles






```

app/views/pages/_content_tailwind_1.html.erb
```html







shadow-cyan-500/50


Subscribe


shadow-blue-500/50


Subscribe


shadow-indigo-500/50


Subscribe





```

app/views/pages/_content_tailwind_2.html.erb
```html




snap point






























```

app/views/pages/_content_tailwind_3.html.erb
```html



Scroll in the tw-grid of images to see the expected behaviour








snap point

































```

## Create Bootstrap components
app/views/pages/_nav_bootstrap.html.erb
```html

```

## Create React component
app/javascript/react/components/MyClock/styles.css
```css
.Clock {
padding: 5px;
margin-top: 15px;
margin-left: auto;
margin-right: auto;
}
```

app/javascript/react/components/MyClock/MyClock.js
```js
import React, { Component } from 'react'
import PropTypes from 'prop-types';

export class MyClock extends Component {
render() {
return (








);
}
}

export default MyClock

export class Clock extends Component {
constructor(props) {
super(props);

this.state = { time: new Date() };
this.radius = this.props.size / 2;
this.drawingContext = null;
this.draw24hour = this.props.timeFormat.toLowerCase().trim() === "24hour";
this.drawRoman = !this.draw24hour && this.props.hourFormat.toLowerCase().trim() === "roman";

}

componentDidMount() {
this.getDrawingContext();
this.timerId = setInterval(() => this.tick(), 1000);
}

componentWillUnmount() {
clearInterval(this.timerId);
}

getDrawingContext() {
this.drawingContext = this.refs.clockCanvas.getContext('2d');
this.drawingContext.translate(this.radius, this.radius);
this.radius *= 0.9;
}

tick() {
this.setState({ time: new Date() });
const radius = this.radius;
let ctx = this.drawingContext;
this.drawFace(ctx, radius);
this.drawNumbers(ctx, radius);
this.drawTicks(ctx, radius);
this.drawTime(ctx, radius);
}

drawFace(ctx, radius) {
ctx.beginPath();
ctx.arc(0, 0, radius, 0, 2 * Math.PI);
ctx.fillStyle = "white";
ctx.fill();

const grad = ctx.createRadialGradient(0, 0, radius * 0.95, 0, 0, radius * 1.05);
grad.addColorStop(0, "#333");
grad.addColorStop(0.5, "white");
grad.addColorStop(1, "#333");
ctx.strokeStyle = grad;
ctx.lineWidth = radius * 0.1;
ctx.stroke();

ctx.beginPath();
ctx.arc(0, 0, radius * 0.05, 0, 2 * Math.PI);
ctx.fillStyle = "#333";
ctx.fill();
}

drawNumbers(ctx, radius) {
const romans = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"];
const fontBig = radius * 0.15 + "px Arial";
const fontSmall = radius * 0.075 + "px Arial";
let ang, num;

ctx.textBaseline = "middle";
ctx.textAlign = "center";
for (num = 1; num < 13; num++) {
ang = num * Math.PI / 6;
ctx.rotate(ang);
ctx.translate(0, -radius * 0.78);
ctx.rotate(-ang);
ctx.font = fontBig;
ctx.fillStyle = "black";
ctx.fillText(this.drawRoman ? romans[num - 1] : num.toString(), 0, 0);
ctx.rotate(ang);
ctx.translate(0, radius * 0.78);
ctx.rotate(-ang);

// Draw inner numerals for 24 hour time format
if (this.draw24hour) {
ctx.rotate(ang);
ctx.translate(0, -radius * 0.60);
ctx.rotate(-ang);
ctx.font = fontSmall;
ctx.fillStyle = "red";
ctx.fillText((num + 12).toString(), 0, 0);
ctx.rotate(ang);
ctx.translate(0, radius * 0.60);
ctx.rotate(-ang);
}
}

// Write author text
ctx.font = fontSmall;
ctx.fillStyle = "#3D3B3D";
ctx.translate(0, radius * 0.30);
ctx.fillText("React Clock", 0, 0);
ctx.translate(0, -radius * 0.30);
}

drawTicks(ctx, radius) {
let numTicks, tickAng, tickX, tickY;

for (numTicks = 0; numTicks < 60; numTicks++) {

tickAng = (numTicks * Math.PI / 30);
tickX = radius * Math.sin(tickAng);
tickY = -radius * Math.cos(tickAng);

ctx.beginPath();
ctx.lineWidth = radius * 0.010;
ctx.moveTo(tickX, tickY);
if (numTicks % 5 === 0) {
ctx.lineTo(tickX * 0.88, tickY * 0.88);
} else {
ctx.lineTo(tickX * 0.92, tickY * 0.92);
}
ctx.stroke();
}
}

drawTime(ctx, radius) {
const now = this.state.time;
let hour = now.getHours();
let minute = now.getMinutes();
let second = now.getSeconds();

// hour
hour %= 12;
hour = (hour * Math.PI / 6) + (minute * Math.PI / (6 * 60)) + (second * Math.PI / (360 * 60));
this.drawHand(ctx, hour, radius * 0.5, radius * 0.05);
// minute
minute = (minute * Math.PI / 30) + (second * Math.PI / (30 * 60));
this.drawHand(ctx, minute, radius * 0.8, radius * 0.05);
// second
second = (second * Math.PI / 30);
this.drawHand(ctx, second, radius * 0.9, radius * 0.02, "red");
}

drawHand(ctx, position, length, width, color) {
color = color || "black";
ctx.beginPath();
ctx.lineWidth = width;
ctx.lineCap = "round";
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.moveTo(0, 0);
ctx.rotate(position);
ctx.lineTo(0, -length);
ctx.stroke();
ctx.rotate(-position);
}

render() {
return (




);
}
}

Clock.defaultProps = {
size: 400, // size in pixels => size is length & width
timeFormat: "24hour", // {standard | 24hour} => if '24hour', hourFormat must be 'standard'
hourFormat: "standard" // {standard | roman}
};

Clock.propTypes = {
size: PropTypes.number,
timeFormat: PropTypes.string,
hourFormat: PropTypes.string
};

```

app/javascript/react/components/App/styles.css
```css
.my_styles_3 {
font-family: 600;
font-size: 2rem;
color: pink;
text-align: center;
}
```

app/javascript/react/components/App/index.js
```js
import React from 'react'
import MyClock from '../MyClock/MyClock'
import "./styles.css";

export const App = () => {
return (



Hello React!!!




)
}

export default App

```

app/javascript/react/hello_react.js
```js
import React from "react";
import { render } from "react-dom";
import App from "./components/App";

document.addEventListener("DOMContentLoaded", () => {
render(, document.getElementById('react-components'));
});

```

## Render Tailwind and Bootstrap 5 components
Update file `app/views/pages/home.html.erb`
```html
<%= render 'pages/nav_bootstrap' %>
<%= render 'pages/content_home_tw' %>
```

## Render React components
Update file `app/views/pages/hello_react.html.erb`
```html
<%= render 'pages/nav_bootstrap' %>
<%= render 'pages/popover_bs' %>


React Components




<%= javascript_include_tag "react/hello_react" %>


Tailwind


<%= render 'pages/content_tailwind_1' %>
<%= render 'pages/content_tailwind_2' %>
<%= render 'pages/content_tailwind_3' %>

```

app/javascript/styles/index.css
```css
.my-styles-2 {
font-weight: 600;
color: red;
}
```

## If you want to use SCSS/SASS
Add package `esbuild-plugin-sass`
```bash
yarn add esbuild-plugin-sass
```

Update `esbuild.config.js`
```js
// ....
const sassPlugin = require('esbuild-plugin-sass')

// ...
plugins: [
styleLoaderPlugin,
sassPlugin()
],

// ...

```

And then you can create file like `styles.scss` and import it
Example:

```scss
// styles2.scss
.my_styles_4 {
font-family: 600;
font-size: 2rem;
color: red;
text-align: center;
cursor: pointer;
&:hover {
color: green;
}
}
```

```js
import React from 'react'
import MyClock from '../MyClock/MyClock'
import "./styles2.scss";

export const App = () => {
return (



Hello React!!! 000

Styles SCSS




)
}
```

## Create Article
```bash
./bin/rails g scaffold Article title:string body:text
./bin/rails db:create db:migrate
```

*Use Tailwind and Bootstrap to update styles for article pages*

## Run app
```bash
./bin/dev
```

Then go to http://localhost:3000/

OR clone source and run to see the result:
```bash
cd
git clone https://github.com/ntamvl/Rails7WithReactTailwindCSSBootstrapExample.git
cd Rails7WithReactTailwindCSSBootstrapExample
bundle install
yarn install
./bin/dev
```

Screenshots:
![Rails 7 with React, TailwindCSS and Bootstrap 5 Example](https://raw.githubusercontent.com/ntamvl/Rails7WithReactTailwindCSSBootstrapExample/main/screenshot_1.png)
![Rails 7 with React, TailwindCSS and Bootstrap 5 Example](https://raw.githubusercontent.com/ntamvl/Rails7WithReactTailwindCSSBootstrapExample/main/screenshot_2.png)

Enjoy ^_^ :))

---
References:
- https://rubyonrails.org/2021/12/15/Rails-7-fulfilling-a-vision
- https://getbootstrap.com/docs/5.1/getting-started/introduction/
- https://tailwindcss.com/docs/installation