I'm trying to make a basic CRUD store app with Rails and React, but I'm stuck on displaying the author (user) association of the post. The post itself shows just fine. I'm trying to avoid using jbuilder so I can understand the problem I'm having.
The current show method in the controller, which works:
controllers/post_controller.rb
def show
if post
render json: post
else
render json: post.errors
end
end
The current React view, which works:
app/javascript/components/Post.js
import React from "react";
import { Link } from "react-router-dom";
class Post extends React.Component {
constructor(props) {
super(props);
this.state = { post: { description : '' } };
}
componentDidMount() {
const {
match: {
params: { id }
}
} = this.props;
const url = `/api/v1/show/${id}`;
fetch(url)
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(response => this.setState({ post: response }))
.catch(() => this.props.history.push("/posts"));
}
render() {
const { post } = this.state;
let descriptionList = "No descriptions present";
if (post.description.length > 0) {
descriptionList = post.description
.split(",")
.map((description, index) => (
<li key={index} className="list-group-item">
{description}
</li>
));
}
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={post.image}
alt={`${post.description} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Description</h5>
{descriptionList}
<div>{post.title}</div>
<div>${(post.price * .01).toLocaleString()}</div>
</ul>
</div>
</div>
<Link to="/posts" className="btn btn-link">
Back to all posts
</Link>
</div>
</div>
);
}
}
export default Post;
When I add render json: post, include: :user to the controller and {post.user.email} and render() { const { post, user } = this.state;
to the view, the error message in the console is cannot read property 'email' of undefined. When I try to define the user in the controller method user = post.user.email and in the view {user}, the terminal error is:
NoMethodError (undefined method 'oswaldo#daugherty.info' for #<Post id: 5, title: "Post 5", description: "You can't synthesize the bandwidth without compres...", image: "https://loremflickr.com/300/300/cats 5", price: 883105, rating: nil, review: nil, created_at: "2021-01-31 23:26:03", updated_at: "2021-01-31 23:26:03", user_id: 5>):
I've checked my database and all the associations display correct there. In short, I don't know how to send the post's user association correctly to the view. What am I missing? Any help appreciated because I'm really spinning my wheels on this one.
you might be facing the bug reported bug.
If you are only looking email from related user record, you can use following
# in Post model
delegate :email, to: :user, prefix: true, allow_nil: true
# and while rendering
use Post.as_json(methods: [:user_email])
Related
I'm making an image library type thing in Rails and Vue, and I'm using DirectUpload to manage attachments.
# photo.rb
class Photo < ApplicationRecord
has_one_attached :file
# ...
end
# photos_controller.rb
class PhotosController < ApplicationController
load_and_authorize_resource
before_action :set_photo, only: %i[show update destroy]
protect_from_forgery unless: -> { request.format.json? }
def index
#photo = current_user.photos.new
render action: 'index'
end
def create
#photo = current_user.photos.create(photo_params)
render json: PhotoBlueprint.render(#photo, root: :photo)
end
# ...
def photo_params
params.require(:photo).permit(:id, :file)
end
end
# photos/index.html.erb
<%= simple_form_for(#photo, url: photos_path(#photo), html: { multipart: true }) do |f| %>
<%= f.file_field :file, multiple: true, direct_upload: true, style: 'display: none;' %>
<% end %>
<div id='photos-app'></div>
// UserFileLib.vue
<script>
import { mapState, mapActions } from 'pinia'
import { usePhotoStore } from '#stores/photo'
import { DirectUpload } from '#rails/activestorage'
export default {
name: 'UserFileLib',
computed: {
...mapState(usePhotoStore, [
'photos'
]),
url () {
return document.getElementById('photo_file').dataset.directUploadUrl
},
input () {
return document.getElementById('file-input')
},
},
mounted () {
this.getPhotos()
},
methods: {
...mapActions(usePhotoStore, [
'addPhoto',
'getPhotos',
]),
activestorageURL (blob) {
return `/rails/active_storage/blobs/redirect/${blob.signed_id}/${blob.filename}`
},
uploadToActiveStorage () {
const file = this.input.files[0]
const upload = new DirectUpload(file, this.url)
upload.create((error, blob) => {
if (error) {
console.log(error)
} else {
const url = this.activestorageURL(blob)
console.log(url)
this.getPhotos()
}
})
},
openFileBrowser () {
this.input.click()
},
formatSize (bytes) {
return Math.round(bytes / 1000)
}
}
}
</script>
<template>
<div
#click="openFileBrowser"
class="card p-3">
Click or drop files here
</div>
<input
type="file"
:multiple="true"
#change="uploadToActiveStorage"
id="file-input" />
<div class="grid is-inline-grid mt-2">
<div
class="image-container"
v-for="image in photos"
:key="image.id">
<img :src="image.url" :alt="image.label" />
<div class="filename">
<strong>{{ image.label }}</strong>
<br />
{{ formatSize(image.size) }} kb
</div>
<div class="close">
×
</div>
</div>
</div>
</template>
Now, the uploads work fine, the blob is stored correctly.
My issue is that a new Photo object is not created to wrap the attachment, meaning the uploads are lost in the system and have no parent record.
What am I doing wrong?
I've solved this for anyone else looking for help. The logic is to create or update the parent record after the upload is done. I missed this in the official documentation.
upload.create((error, blob) => {
if (error) {
// Handle the error
} else {
// ** This is the way **
// Add an appropriately-named hidden input to the form with a
// value of blob.signed_id so that the blob ids will be
// transmitted in the normal upload flow
// ** End of **
//
const hiddenField = document.createElement('input')
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("value", blob.signed_id);
hiddenField.name = input.name
document.querySelector('form').appendChild(hiddenField)
}
})
Since I'm using Vue and Pinia I made a solution in keeping with that logic:
// UserImageLib.vue
uploadToActiveStorage (input, file) {
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
console.log(error)
} else {
const params = { [input.name]: blob.signed_id }
this.createPhoto(params)
}
})
},
// stores/photo.js
addPhoto (payload) {
this.photos.unshift(payload)
},
createPhoto (payload) {
http.post(`/photos`, payload).then(res => {
const photo = res.data.photo
this.addPhoto(photo)
})
},
I am attempting to allow a user that is logged in to create a review for a game. I am having a couple issues that keep popping up in my console.
HTTP Origin header (http://localhost:3001) didn't match request.base_url (http://localhost:3000)
I attempted to remedy this with putting config.force_ssl = true in my production file from what I read up on, but still hitting this issue.
Im also hitting
NoMethodError (undefined method `id' for nil:NilClass):
which is ref my review_controller in create
app/controllers/reviews_controller.rb:19:in `create'
Below is my ReviewController and my ReviewContainer and ReviewForm
class ReviewsController < ApplicationController
# before_action :authorized, only:[:create]
before_action :authenticate_with_http_digest, only: [:new, :create]
def index
reviews = Review.all
render json: reviews
end
def show
review = Review.find_by(params[:id])
render json: review
end
def create
game = Game.find_or_create_by(name: params[:review][:game_name])
review = Review.new(review_params)
review.game_id = game.id
review.user_id = #user.id
review.save
render json: review
end
def update
review = Review.find(params[:id])
review.update(review_params)
review.save
render json: review
end
def destroy
review = Review.find(params[:id])
review.destroy
render json: {error: "Review Removed"}
end
private
def review_params
params.require(:review).permit(:user_id, :game_id, :user_username, :reviewed_game, :rating, :game_name)
end
end
import React, { Component } from 'react'
import Review from './Review'
import ReviewForm from './ReviewForm'
export default class ReviewsContainer extends Component {
state = {
reviews: [],
}
componentDidMount(){
fetch('http://localhost:3000/reviews')
.then(res => res.json())
.then(reviews => this.setState({ reviews }))
}
addReview = (review) => {
fetch('http://localhost:3000/reviews',{
method: "POST",
headers: {
"Content-Type" : "application/json",
Accept: "application/json",
Authorization: `bearer ${localStorage.token}`
},
body: JSON.stringify({ review: review }
),
})
.then(res => res.json())
.then(( json => {
this.setState(prevState => ({
reviews: [...prevState.reviews, json ]
}))
}))
}
// handleShowForm = () => {
// this.setState({edit: false})
// }
render() {
return (
<div className="review-grid">
<ReviewForm addReview={this.addReview} review={this.handleSubmit} />
<h1 className="review-content">REVIEWS!</h1>
<ul className="review-cards">
{
this.state.reviews.map(review => <Review key={review.id} review={review}/>)
}
</ul>
</div>
)
}
}
import React, { Component } from 'react'
class ReviewForm extends React.Component {
state = {
reviewed_game: '',
rating: '',
user_username: '',
}
handleReviewedGame = (event) => {
this.setState ({
reviewed_game: event.target.value
})
}
handleRating = (event) => {
this.setState ({
rating: event.target.value
})
}
handleUser = (event) => {
this.setState ({
user_username: event.target.value
})
}
handleForm = (e) => {
e.preventDefault()
// console.log(e)
const review = {
reviewed_game: this.state.reviewed_game,
rating: this.state.rating,
}
this.props.addReview(review)
}
render() {
return (
<div className="form-container">
<form onSubmit={(e) => {this.handleForm(e)}}>
<div>
<label>Review</label>
<br></br>
<textarea type="text" placeholder="Drop Your Review" rows={10} cols={50} value={this.state.reviewed_game} onChange={this.handleReviewedGame} className="form"/>
<div>
<label>Stars</label>
<br></br>
<input type="number" max="5" min="0" value={this.state.rating} onChange={this.handleRating} />
</div>
</div>
<button type="submit" className="sub-review">Create Review!</button>
</form>
</div>
)
}
}
export default ReviewForm;
Any advise on how to correct the issue is appreciated! thanks!
I am having an issue with submitting my data from form on the front end. Everytime I submit the form I get a Unpermitted parameter: :recipe
I was told to make sure my attributes on my frontend matched what was on the backend strong params.
Here is what is in my controller for my create action and my strong params
class RecipesController < ApplicationController
def create
recipe = Recipe.create(recipe_params)
if recipe.save
render json: recipe
else
render json: { error: "Couldn't save" }
end
end
private
def recipe_params
params.permit(:category_id,:name,:ingredients,:chef_name,:origin,category_attribute:[:category])
end
end
And here is my React frontend where I am inputting the info in the form
Side note I took out the event handlers for this code snippet but left the submit handler just to keep this explanation shorter
import React, { Component } from 'react'
import Categories from './Categories.js'
class RecipeInput extends Component{
constructor(props){
super(props)
this.state = {
category: [],
categoryId: '',
name:'',
ingredients: '',
chef_name: '',
origin: ''
}
this.handleNameChange.bind(this)
this.handleOriginChange.bind(this)
this.handleChefChange.bind(this)
this.handleIngChange.bind(this)
}
componentDidMount(){
let initialCats = [];
const BASE_URL = `http://localhost:10524`
const CATEGOREIS_URL =`${BASE_URL}/categories`
fetch(CATEGOREIS_URL)
.then(resp => resp.json())
.then(data => {
initialCats = data.map((category) => {
return category
})
console.log(initialCats)
this.setState({
category: initialCats,
})
});
}
handleSubmit = (event) =>{
event.preventDefault();
this.props.postRecipes(this.state)
this.setState({
categoryId: '',
name:'',
ingredients: '',
chef_name: '',
origin: ''
})
}
render(){
return(
<div>
<form onSubmit={this.handleSubmit}>
<Categories category={this.state.category}/>
<div>
<label for='name'>Recipe Name:</label>
<input type='text' value={this.state.name} onChange={this.handleNameChange} />
</div>
<div>
<label for='name'>Country Origin:</label>
<input type='text' value={this.state.origin} onChange={this.handleOriginChange} />
</div>
<div>
<label for='name'>Chef Name:</label>
<input type='text' value={this.state.chef_name} onChange={this.handleChefChange} />
</div>
<div>
<label for='name'>Ingredients:</label>
<textarea value={this.state.ingredients} onChange={this.handleIngChange} />
</div>
<input value='submit' type='submit'/>
</form>
</div>
)
}
}
export default RecipeInput
I am just a little clueless on where to go to from here. Am I matching the attributes correctly?
Edit
I forgot to include my postRecipes function with my dispatches in place
export const postRecipes = (recipe)=>{
const BASE_URL = `http://localhost:10524`
const RECIPES_URL =`${BASE_URL}/recipes`
const config = {
method: "POST",
body:JSON.stringify(recipe),
headers: {
"Accept": "application/json",
"Content-type": "application/json"
}
}
//category field
return(dispatch)=>{
fetch(RECIPES_URL,config)
.then(response =>
response.json())
.then(resp => {
dispatch({
type: 'Add_Recipe',
payload:{
category:resp.category,
name: resp.name,
ingredients: resp.ingredients,
chef_name: resp.chef_name,
origin: resp.origin,
categoryId: resp.categoryId
}
})
})
//.then(response => <Recipe />)
.catch((error) => console.log.error(error))
}
}
Edit
Here is my reducer that defines my Add-Recipe action for payload.
export default function manageRecipes(state={
recipes:[],
category:[],
}, action){
switch(action.type){
case 'Add_Recipe':
const recipe = {
name: action.name,
ingredients: action.ingredients,
chef_name: action.chef_name,
origin: action.origin,
categoryId: action.categoryId,
category: action.category,
// id: cuidFn()
}
return{
...state,
recipes: [...state.recipes, recipe],
}
case 'Delete_Recipe':
const recipes = state.recipes.filter(recipe => recipe.id !== action.id)
return {...state, recipes}
case 'Add_Catagory':
const cat = {
name: action.name
}
return{
...state,
category: [...state.category, cat],
}
default:
return state
}
}
try this in your backend
def recipe_params
params.require(:recipe).permit(
:category_id, :name,:ingredients, :chef_name,
:origin,category_attribute:[:category]
)
end
I'm trying to upload images to a rails 5 api app with carrierwave and react but for some reason it doesn't work.
Here're some parts of the app.
migration
class AddImagesToTopics < ActiveRecord::Migration[5.1]
def change
add_column :topics, :images, :json
end
end
topic model
class Topic < ApplicationRecord
mount_uploader :images, ImageUploader
...
end
topics controller
def topic_params
params.require(:topic).permit(:name, :options, :subject, :body, {images: []})
end
submitted form logs
#topic = #board.topics.new(topic_params)
p "-----------"
p params[:topic][:images]
p #topic
p "-----------"
gives the following:
Unpermitted parameter: :images
"-----------"
<ActionController::Parameters {"0"=>{}} permitted: false>
#<Topic id: nil, board_id: 5, body: "dsf", created_at: nil, updated_at: nil, name: "", options: "", subject: "", images: nil>
"-----------"
form
class Form extends Component {
constructor(props) {
super(props)
this.state = {
topic: {
name: "",
options: "",
subject: "",
body: "",
images: null
}
}
this.handleSubmit = this.handleSubmit.bind(this)
this.handleChange = this.handleChange.bind(this)
this.handleImagesSelect = this.handleImagesSelect.bind(this)
}
handleSubmit(e) {
e.preventDefault();
this.props.add_topic(this.props.perm, this.state);
}
handleChange(e) {
this.setState({ topic: { ...this.state.topic, [e.target.name]: e.target.value } });
}
handleImagesSelect(e) {
this.setState({ topic: { ...this.state.topic, images: e.target.files } });
console.log(e.target.files);
console.log(this.state);
}
render() {
const { name, options, subject, body, images } = this.state.topic
return(
<div>
<div className="form_wrapper">
<form id="form" onSubmit={this.handleSubmit } encType="multipart/form-data">
<table>
<tbody>
....
<tr>
<td>body</td>
<td>
<textarea name="body" id="body" rows="10" cols="50"
value={body}
onChange={this.handleChange}/>
</td>
</tr>
<tr>
<td>file</td>
<td>
<input name="images" type="file" id="images" multiple="true"
onChange={this.handleImagesSelect}/>
</td>
</tr>
</tbody>
</table>
</form>
</div>
<hr/>
</div>
)
}
}
add topic action
export function add_topic(perm, topic) {
return dispatch => {
axios.post(api + perm + "/topics/", topic)
.then((res) => {
dispatch({ type: ADD_TOPIC, payload: res.data })
})
.catch((err) => console.log(err))
}
}
I am working on an AngularJS app with a remote Rails backend for registrations for a sports event. Besides registering as a single runner, users should also be able so submit registrations for relay teams with 4 runners.
Rails backend structure:
class Relay < ActiveRecord::Base
has_many :registrations
accepts_nested_attributes_for :registrations
end
class Registration < ActiveRecord::Base
belongs_to :relay
end
class API::RelaysController < API::BaseController
def create
#relay = Relay.new(relay_params)
if #relay.save
render json: #relay, status: :created#, location: #relay
else
render json: #relay.errors, status: :unprocessable_entity
end
end
end
Nested form in Rails frontend:
My Angular relay create controller:
appControllers.controller('RelayCreateCtrl', ['$scope', '$routeParams', 'Relay', 'Registration', '$location', function($scope, $routeParams, Relay, Registration, $location) {
$scope.errors = {}
$scope.relay = new Relay();
$scope.registrations_attributes = [];
for (var i=0; i<4; ++i ){
registration = new Registration();
registration.run_id = $routeParams.run_id;
$scope.registrations_attributes.push(registration);
}
$scope.relay.registrations_attributes = $scope.registrations_attributes;
$scope.submit = function() {
function success(response) {
console.log("success", response)
$location.path("/registrations");
}
function failure(response) {
console.log("failure", response);
$scope.errors = response.data;
console.log($scope.errors);
}
Relay.create($scope.relay, success, failure)
};
}]);
ngResource Services:
rilaServices.factory('Registration', ['$resource',
function($resource){
return $resource('http://localhost:3000/api/registrations/:id', { id: "#id" }, {
'create': { method: 'POST' }
});
}
]);
rilaServices.factory('Relay', ['$resource',
function($resource){
return $resource('http://localhost:3000/api/relays/:id', { id: "#id" }, {
'create': { method: 'POST' }
});
}
]);
relay-create.html:
<form name="form" ng-submit="submit()" class="form-horizontal" novalidate>
<div ng-repeat="registration in relay.registrations_attributes" ng-form="registration_form" class="well">
...
<div class="form-group">
<label class="control-label col-sm-4">
Firstname
</label>
<div class="input-group col-sm-7">
<input class="form-control" ng-model="registrations_attributes.runner_firstname" type="text">
</div>
</div>
...
</div>
</form>
What steps do I need to follow to get a form with nested fields running in AngularJS like the one I created in Rails?