Created
November 13, 2019 12:09
-
-
Save rubypirate/ec813be97e686eb8037abe535cab0e91 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### | |
Can't share whole app, but this is a snippet of recent work I did using Rails 6 and React to render nested categories in the CMS to allow users grasp hierarchy easily. | |
### | |
class Category < ApplicationRecord | |
resourcify | |
has_ancestry | |
before_save :default_values | |
attribute :depth | |
def default_values | |
write_attribute(:slug, Faker::Internet.domain_word) | |
end | |
def self.tree | |
categories = Category.order('created_at asc').arrange | |
Category.json_tree(categories) | |
end | |
def depth | |
parent ? (parent.depth+1) : 0 | |
end | |
def self.json_tree(nodes) | |
nodes.map do |node, sub_nodes| | |
{ | |
id: node.id, | |
depth: node.depth, | |
title: node.title, | |
children: Category.json_tree(sub_nodes).compact | |
} | |
end | |
end | |
end | |
#### | |
Controller | |
#### | |
module Api | |
class CategoriesController < ApplicationController | |
before_action :set_category, only: [:show, :update, :destroy] | |
# GET /categories | |
def index | |
render json: Category.tree | |
end | |
# GET /categories/1 | |
def show | |
render json: @category | |
end | |
# POST /categories | |
def create | |
@category = Category.new(category_params) | |
if @category.save | |
render json: Category.tree, status: :created | |
else | |
render json: Category.tree, status: :unprocessable_entity | |
end | |
end | |
# PATCH/PUT /categories/1 | |
def update | |
if @category.update(category_params) | |
render json: @category | |
else | |
render json: @category.errors, status: :unprocessable_entity | |
end | |
end | |
# DELETE /categories/1 | |
def destroy | |
@category.destroy | |
render json: Category.tree | |
end | |
def collection_for_select | |
categories = nested_dropdown(Category.order('created_at asc').arrange) | |
render json: categories | |
end | |
private | |
# Use callbacks to share common setup or constraints between actions. | |
def set_category | |
@category = Category.find(params[:id]) | |
end | |
# Only allow a trusted parameter "white list" through. | |
def category_params | |
params.require(:category).permit(:slug, :title, :description, :kind, :ancestry, :options, :language) | |
end | |
end | |
end | |
#### | |
React component embbeded in rails 6 | |
#### | |
import React from "react"; | |
import ReactDOM from "react-dom"; | |
import { Formik, Form, Field, ErrorMessage } from "formik"; | |
import axios from "axios"; | |
import TextInput from "./form/TextInput"; | |
import Spinner from "./spinner"; | |
import CategoryTable from "./category_table"; | |
class CreateCategory extends React.Component { | |
constructor(props) { | |
super(props); | |
this.deleteCategory = this.deleteCategory.bind(this); | |
this.state = { | |
showSpinner: false, | |
categories: [], | |
collection_for_select: [], | |
ancestry: null | |
}; | |
} | |
componentDidMount() { | |
// CALL API | |
axios | |
.get("/api/categories") | |
.then(res => { | |
this.setState({ categories: res.data }); | |
}) | |
.catch(err => { | |
console.log("AXIOS ERROR: ", err); | |
}); | |
// CALL API | |
axios | |
.get("/api/collection_for_select") | |
.then(res => { | |
this.setState({ collection_for_select: res.data }); | |
}) | |
.catch(err => { | |
console.log("AXIOS ERROR: ", err); | |
}); | |
} | |
deleteCategory(id) { | |
axios.delete(`/api/categories/${id}`).then(res => { | |
this.setState({categories: res.data}) | |
}); | |
}; | |
render() { | |
return ( | |
<div> | |
<Formik | |
initialValues={{ | |
showSpinner: false, | |
title: "", | |
ancestry: null | |
}} | |
// validate={values => { | |
// JAMAL, todo: fix regex here. | |
// let errors = {}; | |
// if (!values.title) { | |
// errors.title = "Required"; | |
// } else if ( | |
// !/^[A-Za-z0-9]?$/i.test( | |
// values.title | |
// ) | |
// ) { | |
// errors.title = "Invalid title"; | |
// } | |
// return errors; | |
// }} | |
onSubmit={(values, { setSubmitting }) => { | |
this.setState({ showSpinner: true }); | |
const token = document.querySelector("[name=csrf-token]").content; | |
axios.defaults.headers.common["X-CSRF-TOKEN"] = token; | |
var categoryData = { | |
category: { | |
title: values.title, | |
ancestry: this.state.ancestry | |
} | |
}; | |
// CALL API | |
axios | |
.post("/api/categories", categoryData) | |
.then(res => { | |
this.setState({ showSpinner: false, categories: res.data }); | |
}) | |
.catch(err => { | |
console.log("AXIOS ERROR: ", err); | |
}); | |
}} | |
> | |
{({ isSubmitting, values, errors }) => ( | |
<div className="row"> | |
<div className="col-md-8"> | |
<div className="card card-info"> | |
<div className="card-header"> | |
<h3 className="card-title">Categories</h3> | |
<div className="card-tools"></div> | |
</div> | |
<div className="card-body table-responsive p-0"> | |
<CategoryTable | |
categories={this.state.categories} | |
deleteCategory={this.deleteCategory} | |
/> | |
</div> | |
</div> | |
</div> | |
<div className="col-md-4"> | |
<div className="card card-info"> | |
<div className="card-header"> | |
<h3 className="card-title">Create new category</h3> | |
</div> | |
<Form className="form-horizontal"> | |
<div className="card-body"> | |
<ErrorMessage name="subdomain" component="span" /> | |
<div className="form-group row"> | |
<label | |
htmlFor="title" | |
className="col-sm-4 col-form-label" | |
> | |
Title | |
</label> | |
<div className="col-sm-8"> | |
<TextInput | |
type="title" | |
name="title" | |
label="Category title" | |
/> | |
</div> | |
</div> | |
<div className="form-group row"> | |
<label | |
htmlFor="ancestry" | |
className="col-sm-4 col-form-label" | |
> | |
Parent | |
</label> | |
<div className="col-sm-8"> | |
<select | |
className="form-control" | |
onChange={() => { | |
this.setState({ ancestry: event.target.value }); | |
}} | |
> | |
<option>None</option> | |
{this.state.collection_for_select.map( | |
(value, index) => { | |
return ( | |
<option key={value[1]} value={value[1]}> | |
{value[0]} | |
</option> | |
); | |
} | |
)} | |
</select> | |
</div> | |
</div> | |
</div> | |
<div className="card-footer"> | |
<button | |
type="submit" | |
className="btn btn-info" | |
disabled={isSubmitting} | |
> | |
Create | |
</button> | |
</div> | |
</Form> | |
</div> | |
</div> | |
</div> | |
)} | |
</Formik> | |
</div> | |
); | |
} | |
} | |
export default CreateCategory; | |
document.addEventListener("DOMContentLoaded", () => { | |
ReactDOM.render( | |
<CreateCategory />, | |
document.getElementById("comp-categories") | |
); | |
}); | |
### | |
child component | |
### | |
import React from "react"; | |
const CategoryTable = props => { | |
return ( | |
<ul className="list-group"> | |
{props.categories.map((category, index) => { | |
return ( | |
<div key={category.id}> | |
<li className={`list-group-item depth-${category.depth}`}> | |
{category.title} | |
<button | |
className="btn btn-danger btn-sm" | |
onClick={() => props.deleteCategory(category.id)} | |
> | |
Delete | |
</button> | |
</li> | |
<CategoryTable | |
categories={category.children} | |
deleteCategory={props.deleteCategory} | |
/> | |
</div> | |
); | |
})} | |
</ul> | |
); | |
}; | |
export default CategoryTable; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment