Skip to content

Instantly share code, notes, and snippets.

@rubypirate
Created November 13, 2019 12:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rubypirate/ec813be97e686eb8037abe535cab0e91 to your computer and use it in GitHub Desktop.
Save rubypirate/ec813be97e686eb8037abe535cab0e91 to your computer and use it in GitHub Desktop.
###
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