Rails: direct upload and create has_one_attached parent - ruby-on-rails

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)
})
},

Related

Typing indicator with action cable in rails- 7

I am unable to add typing indicator in my rails app with action cable I have created app in rails 7 and I user trubo stream tag and broadcast in it so I did't used channel for live chat , I tried to find tutorial and video but there is not any
I want to add typing indicator so I writtern js for the same on input it will called and it will go to controller
On input I am calling controller "rtm"
room controller
def rtm
#typing = "hhhhhhhhhhhhhhhhhhh"
# ActionCable.server.broadcast "typing_channel",{ message: "helloo"}
# #typings.broadcast_append_to "typing"
Turbo::StreamsChannel.broadcast_append_to "typing", target: 'typing', partial: 'rooms/typing', locals: { message: "#typing" }
end
here I have issue how can I broadcast the typing message to my room page
Room.rb
class Room < ApplicationRecord
scope :public_rooms, -> { where(is_private: false) }
has_many :messages
after_create_commit {broadcast_append_to "rooms"}
end
message.rb
class Message < ApplicationRecord
belongs_to :user
belongs_to :room
after_create_commit { broadcast_append_to self.room }
end
rooms/index
<script>
$(document).ready(function(){
var tmo = null;
$("#msg").on("input", function(){
$.ajax({
type: 'GET',
url: '/rooms/rtm',
data: {data: ''}
});
document.getElementById("typing").innerHTML = "Typing...";
if (tmo) {
clearTimeout(tmo);
}
tmo = setTimeout(function () {
$.ajax({
type: 'GET',
url: '/rooms/rmm',
data: {data: ''}
});
document.getElementById("typing").innerHTML = "";
}, 1000);
});
});
</script>
<div class="container">
<h5> Hi <%= current_user&.firstname %> </h5>
<%= debug(params) if Rails.env.development? %>
<br> <h4> Rooms </h4>
<%= render partial: 'layouts/new_room_form' %>
<%= turbo_stream_from "rooms" %>
<div id="rooms">
<%= render #rooms %>
</div>
</div>
<% if #single_room.present? %>
<%= link_to #single_room.name,#single_room, class: "btn btn-primary" %>
<%= turbo_stream_from #single_room %>
<div id="messages">
<%= render #messages %>
</div>
<%= render partial: 'layouts/new_message_form' %>
<%= #typing %>
<%= turbo_stream_from #typing %>
<div id="typing">
</div>
<%= render partial: 'rooms/typing' %>
<span id="typing"></span><br>
<% end %>
To get the typing indicator you need to use action cable and create a channel for it. You can use turbo stream to render the typing indicator. Example:
app/channels/typing_channel.rb
class TypingChannel < ApplicationCable::Channel
def subscribed
stream_from "typing_channel"
end
end
app/javascript/channels/typing_channel.js
import consumer from "./consumer"
consumer.subscriptions.create("TypingChannel", {
connected() {
// Called when the subscription is ready for use on the server
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received(data) {
// Called when there's incoming data on the websocket for this channel
}
});
app/views/rooms/index.html.erb
<div id="typing">
<%= turbo_stream_from "typing_channel" %>
</div>
app/views/rooms/_typing.html.erb
<p><%= message %></p>
app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
def rtm
ActionCable.server.broadcast "typing_channel", { message: "helloo" }
end
end
app/javascript/controllers/rooms_controller.js
import { Controller } from "stimulus"
import consumer from "../channels/consumer"
export default class extends Controller {
static targets = [ "input" ]
connect() {
this.subscription = consumer.subscriptions.create("TypingChannel", {
received: (data) => {
this.renderTyping(data)
}
})
}
renderTyping(data) {
const typing = document.getElementById("typing")
typing.innerHTML = data.message
}
disconnect() {
this.subscription.unsubscribe()
}
}
Is not possible to use turbo stream with action cable. You need to use action cable to get the typing indicator. You can use turbo stream to render the typing indicator.
typing_channel.js
import consumer from "channels/consumer"
consumer.subscriptions.create("TypingChannel", {
connected() {
console.log("connected")
// Called when the subscription is ready for use on the server
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received(data) {
console.log(data)
const box = document.getElementById('typing');
if (box.textContent.includes(data.body)) {
} else {
$('#typing').append('<p>'+ data.body + data.message +'</p>');
}
}
});
I am using below js for indicator
<script>
$(document).ready(function(){
var tmo = null;
$("#chat").on("input", function(){
$.ajax({
type: 'GET',
url: '/rooms/rtm',
});
if (tmo) {
clearTimeout(tmo);
}
tmo = setTimeout(function () {
$.ajax({
type: 'GET',
url: '/rooms/rmm',
});
}, 1000);
});
});
</script>
in controller
ActionCable.server.broadcast "typing_channel", {message: 'Typing', body: "#{current_user.email}"}

How to limit upload files in dropzone.js?

I have ruby on rails application with stimulus.js and dropzone.js for uploading attachment. There is now a limit on uploading one file, but this allows you to upload more than one file and just shows an error message on them. I need that it is not possible to upload more than one file and if after that the user tries to upload another one, a replacement occurs.
dropzone_controller.js
import Dropzone from "dropzone";
import { Controller } from "stimulus";
import { DirectUpload } from "#rails/activestorage";
import {
getMetaValue,
toArray,
findElement,
removeElement,
insertAfter
} from "helpers";
export default class extends Controller {
static targets = ["input"];
connect() {
this.dropZone = createDropZone(this);
this.hideFileInput();
this.bindEvents();
Dropzone.autoDiscover = false; // necessary quirk for Dropzone error in console
}
// Private
hideFileInput() {
this.inputTarget.disabled = true;
this.inputTarget.style.display = "none";
}
bindEvents() {
this.dropZone.on("addedfile", file => {
setTimeout(() => {
file.accepted && createDirectUploadController(this, file).start();
}, 500);
});
this.dropZone.on("removedfile", file => {
file.controller && removeElement(file.controller.hiddenInput);
});
this.dropZone.on("canceled", file => {
file.controller && file.controller.xhr.abort();
});
}
get headers() {
return { "X-CSRF-Token": getMetaValue("csrf-token") };
}
get url() {
return this.inputTarget.getAttribute("data-direct-upload-url");
}
get maxFiles() {
return this.data.get("maxFiles") || 1;
}
get maxFileSize() {
return this.data.get("maxFileSize") || 256;
}
get acceptedFiles() {
return this.data.get("acceptedFiles");
}
get addRemoveLinks() {
return this.data.get("addRemoveLinks") || true;
}
}
class DirectUploadController {
constructor(source, file) {
this.directUpload = createDirectUpload(file, source.url, this);
this.source = source;
this.file = file;
}
start() {
this.file.controller = this;
this.hiddenInput = this.createHiddenInput();
this.directUpload.create((error, attributes) => {
if (error) {
removeElement(this.hiddenInput);
this.emitDropzoneError(error);
} else {
this.hiddenInput.value = attributes.signed_id;
this.emitDropzoneSuccess();
}
});
}
createHiddenInput() {
const input = document.createElement("input");
input.type = "hidden";
input.name = this.source.inputTarget.name;
insertAfter(input, this.source.inputTarget);
return input;
}
directUploadWillStoreFileWithXHR(xhr) {
this.bindProgressEvent(xhr);
this.emitDropzoneUploading();
}
bindProgressEvent(xhr) {
this.xhr = xhr;
this.xhr.upload.addEventListener("progress", event =>
this.uploadRequestDidProgress(event)
);
}
uploadRequestDidProgress(event) {
const element = this.source.element;
const progress = (event.loaded / event.total) * 100;
findElement(
this.file.previewTemplate,
".dz-upload"
).style.width = `${progress}%`;
}
emitDropzoneUploading() {
this.file.status = Dropzone.UPLOADING;
this.source.dropZone.emit("processing", this.file);
}
emitDropzoneError(error) {
this.file.status = Dropzone.ERROR;
this.source.dropZone.emit("error", this.file, error);
this.source.dropZone.emit("complete", this.file);
}
emitDropzoneSuccess() {
this.file.status = Dropzone.SUCCESS;
this.source.dropZone.emit("success", this.file);
this.source.dropZone.emit("complete", this.file);
}
}
function createDirectUploadController(source, file) {
return new DirectUploadController(source, file);
}
function createDirectUpload(file, url, controller) {
return new DirectUpload(file, url, controller);
}
function createDropZone(controller) {
return new Dropzone(controller.element, {
url: controller.url,
headers: controller.headers,
maxFiles: controller.maxFiles,
maxFilesize: controller.maxFileSize,
acceptedFiles: controller.acceptedFiles,
addRemoveLinks: controller.addRemoveLinks,
autoQueue: false
});
}
_form.html.erb
<div data-lite-visibility-target="dynamic" class="space-y-8 <%= #automate_task_report.attachment.present? ? '' : "hidden" %>" >
<div class="form-group inverted">
<%= form.label :attachment, "Upload test execution results", class: "form-label" %>
<button type="button" class="dropzone dropzone-default dz-clickable form-control form-file form-file-btn" data-controller="dropzone" data-dropzone-max-file-size="10" data-dropzone-max-files="1" data-dropzone-accepted-files=".xml,.html,.jpg,.jpeg,.png,.gif">
<%= form.file_field :attachment, direct_upload: true, data: { target: 'dropzone.input' } %>
<div class="dropzone-msg dz-message needsclick flex m-0">
<% if #automate_task_report.attachment.attached? %>
<%= form.hidden_field :attachment, value: #automate_task_report.attachment.signed_id %>
<div class="mx-5 attachment vertical">
<%= link_to #automate_task_report.attachment, target: "_blank", class:"attachment-thumb" do %>
<%= image_tag(#automate_task_report.attachment) %>
<% end %>
<%= link_to #automate_task_report.attachment.filename.to_s, #automate_task_report.attachment, target: "_blank", class:"attachment-name" %>
<%= link_to #automate_task_report.attachment, download: #automate_task_report.attachment, class:"btn btn-primary attachment-btn" do %>
<span class="icon text-icon-default icon-download"></span>
<% end %>
</div>
<% end %>
<span class="icon text-icon-lg icon-file-image-plus-lg mr-3"></span>
<div class="text-left mt-0">
<p>Upload a file or drag and drop</p>
<p class="text-xs">XML, HTML, PNG, JPG, GIF up to 10MB</p>
</div>
</div>
</button>
</div>
</div>
</div>
</div>
I'm already tried things like
= {
maxFiles: 1
};
but it didn't work for me. How can I achive expected result?
Use the "accept" callback to check for the max amount of files:
accept: function(file, done) {
if (/* do a check here */) {
done(error message);
// File is cancel.
file.status = Dropzone.CANCELED;
}
}
key point here is change the file status to cancelled to let dropzone know about it

Creating a Review Rails Backend/React Frontend

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!

Stripe SCA one-time payments Rails

I'm integrating Stripe SCA with payment intents into my rails 5.2.3 (ruby 2.5.1) app. I successfully had one-time payments working properly, however after I integrated subscriptions (successfully working), the one-time payments are receiving an incomplete status on Stripe "The customer has not entered their payment method". Looking at the JSON I can see my payment intent with ID successfully being created, however my charges, data is showing up as null. I can't figure out why the data is not being passed to stripe. Here are the corresponding files:
purchases.show.html.erb
<div class="container">
<h1>Purchasing <%= #recipe.title %> for <%= number_to_currency(#recipe.price) %></h1>
<%= form_with url: recipe_purchase_path(#recipe.id), local: true, id: "payment-form", data: { payment_intent_id: #payment_intent.client_secret } do |form| %>
<div class="form-group">
<label for="card-element">
Credit or debit card
</label>
<div id="card-element" class="form-control">
</div>
<div id="card-errors" role="alert">
</div>
</div>
<div class="form-group">
<label>Name on Card</label>
<%= form.text_field :name_on_card, placeholder: "Full name", class: "form-control" %>
</div>
<div class="form-group">
<%= form.hidden_field :payment_intent_id, value: #payment_intent.id %>
<button class="btn btn-outline-primary buy-recipe">Submit Payment</button>
</div>
<% end %>
</div>
purchases_controller.rb
class PurchasesController < ApplicationController
before_action :authenticate_user!
before_action :set_recipe, only:[:show, :create]
def receipt
#purchase = Purchase.find_by_uuid(params[:id])
#recipe = Recipe.find(#purchase.recipe_id)
end
def show
#payment_intent = Stripe::PaymentIntent.create(
amount: #recipe.price_in_cents,
currency: 'usd',
payment_method_types: params['card'],
metadata: {integration_check: 'accept_a_payment'},
)
end
def create
#payment_intent = Stripe::PaymentIntent.retrieve(params[:payment_intent_id])
if #payment_intent.status == "succeeded"
charge = #payment_intent.charges.data.first
card = charge.payment_method_details.card
purchase = Purchase.create(
customer_id: charge.id,
user_id: current_user.id,
recipe_id: #recipe.id,
uuid: SecureRandom.uuid,
amount: #recipe.price
)
current_user.favorites << #recipe
redirect_to recipe_path(#recipe.slug), notice: "#{#recipe.title} has been added to your Cookbook, thanks for purchasing!"
else
flash[:alert] = "Your order was unsuccessful. Please try again."
redirect_to recipe_purchase_path(#recipe.id)
end
end
private
def set_recipe
#recipe = Recipe.find(params[:recipe_id])
end
end
purchases.index.js
document.addEventListener("turbolinks:load", () => {
const form = document.querySelector("#payment-form")
if (form == null) { return }
const public_key = document.querySelector("meta[name='stripe-key']").getAttribute("content")
const stripe = Stripe(public_key)
const elements = stripe.elements()
const card = elements.create('card')
card.mount('#card-element')
card.addEventListener("change", (event) => {
var displayError = document.getElementById('card-errors')
if (event.error) {
displayError.textContent = event.error.message
} else {
displayError.textContent = ''
}
})
form.addEventListener("submit", (event) => {
event.preventDefault()
let data = {
payment_method: {
card: card,
billing_details: {
name: form.querySelector("#name_on_card").value
}
}
}
stripe.confirmCardPayment(form.dataset.paymentIntentId, data).then((result) => {
if (result.error) {
var errorElement = document.getElementById('card-errors')
errorElement.textContent = result.error.message
} else {
//
//
form.submit()
}
})
})
})
and a screenshot of JSON
here is my subscriptions.js file
document.addEventListener("turbolinks:load", () => {
let cardElement = document.querySelector("#card-element")
if (cardElement !== null) { setupStripe() }
})
function setupStripe() {
const stripe_key = document.querySelector("meta[name='stripe-key']").getAttribute("content")
const stripe = Stripe(stripe_key)
const elements = stripe.elements()
const card = elements.create('card')
card.mount('#card-element')
var displayError = document.getElementById('card-errors')
card.addEventListener('change', (event) => {
if (event.error) {
displayError.textContent = event.error.message
} else {
displayError.textContent = ''
}
})
const form = document.querySelector("#payment-form")
let paymentIntentId = form.dataset.paymentIntent
let setupIntentId = form.dataset.setupIntent
if (paymentIntentId) {
if (form.dataset.status == "requires_action") {
stripe.confirmCardPayment(paymentIntentId, { setup_future_usage: 'off_session' }).then((result) => {
if (result.error) {
displayError.textContent = result.error.message
form.querySelector("#card-details").classList.remove("d-none")
} else {
form.submit()
}
})
}
}
form.addEventListener('submit', (event) => {
event.preventDefault()
let name = form.querySelector("#name_on_card").value
let data = {
payment_method_data: {
card: card,
billing_details: {
name: name,
}
}
}
// Complete a payment intent
if (paymentIntentId) {
stripe.confirmCardPayment(paymentIntentId, {
payment_method: data.payment_method_data,
setup_future_usage: 'off_session',
save_payment_method: true,
}).then((result) => {
if (result.error) {
displayError.textContent = result.error.message
form.querySelector("#card-details").classList.remove("d-none")
} else {
form.submit()
}
})
// Updating a card or subscribing with a trial (using a SetupIntent)
} else if (setupIntentId) {
stripe.confirmCardSetup(setupIntentId, {
payment_method: data.payment_method_data
}).then((result) => {
if (result.error) {
displayError.textContent = result.error.message
} else {
addHiddenField(form, "payment_method_id", result.setupIntent.payment_method)
form.submit()
}
})
} else {
//subscribing w no trial
data.payment_method_data.type = 'card'
stripe.createPaymentMethod(data.payment_method_data).then((result) => {
if (result.error) {
displayError.textContent = result.error.message
} else {
addHiddenField(form, "payment_method_id", result.paymentMethod.id)
form.submit()
}
})
}
})
}
function addHiddenField(form, name, value) {
let input = document.createElement("input")
input.setAttribute("type", "hidden")
input.setAttribute("name", name)
input.setAttribute("value", value)
form.appendChild(input)
}
So special thanks to Chris Oliver on this...but what needed to be done was on the show.html.erb I had to change the payment_intent_id to :payment_intent in the form data.
<%= form_with url: recipe_purchase_path(#recipe.id), local: true, id: "payment-form", data: { payment_intent: #payment_intent.client_secret } do |form| %>
then in my show action in purchases_controller.rb I needed to add customer
def show
#payment_intent = Stripe::PaymentIntent.create(
amount: #recipe.price_in_cents,
currency: 'usd',
payment_method_types: ['card'],
customer: current_user.stripe_id || current_user.stripe_customer.id
)
end
Then I completely removed my purchases.js since the one-time payments are being handled in the subscription.js

Nested Form Fields in AngularJS

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?

Resources