Passing models to components - ruby-on-rails

Using the hyperstack.org framework, how can I reduce the rendering cycles when mutating models that are being rendered?
When passing a Model which is being rendered to a Component which mutates that Model, all Components rendering that Model get re-rendered on any mutation. This is fine unless the mutation is per key press as this means that all Components get re-rendered per key press.
For example, if we have this table:
class UserIndex < HyperComponent
render(DIV) do
puts "UserIndex render"
BridgeAppBar()
UserDialog(user: User.new)
Table do
TableHead do
TableRow do
TableCell { 'Name' }
TableCell { 'Gender' }
TableCell { 'Edit' }
end
end
TableBody do
user_rows
end
end
end
def user_rows
User.each do |user|
TableRow do
TableCell { "#{user.first_name} #{user.last_name}" }
TableCell { user.is_female ? 'Female' : 'Male' }
TableCell { UserDialog(user: user) }
end
end
end
end
And this Compnent (which is used for edit and new):
class UserDialog < HyperComponent
param :user
before_mount do
#open = false
end
render do
puts "UserDialog render"
if #open
render_dialog
else
edit_or_new_button.on(:click) { mutate #open = true }
end
end
def render_dialog
Dialog(open: #open, fullWidth: false) do
DialogTitle do
'User'
end
DialogContent do
content
error_messages if #User.errors.any?
end
DialogActions do
actions
end
end
end
def edit_or_new_button
if #User.new?
Fab(size: :small, color: :primary) { Icon { 'add' } }
else
Fab(size: :small, color: :secondary) { Icon { 'settings' } }
end
end
def content
FormGroup(row: true) do
TextField(label: 'First Name', defaultValue: #User.first_name.to_s).on(:change) do |e|
#User.first_name = e.target.value
end
TextField(label: 'Last Name', defaultValue: #User.last_name.to_s).on(:change) do |e|
#User.last_name = e.target.value
end
end
BR()
FormLabel(component: 'legend') { 'Gender' }
RadioGroup(row: true) do
FormControlLabel(label: 'Male',
control: Radio(value: false, checked: !#User.is_female).as_node.to_n)
FormControlLabel(label: 'Female',
control: Radio(value: true, checked: #User.is_female).as_node.to_n)
end.on(:change) do |e|
#User.is_female = e.target.value
end
end
def actions
Button { 'Cancel' }.on(:click) { cancel }
if #User.changed? && validate_content
Button(color: :primary, variant: :contained, disabled: (#User.saving? ? true : false)) do
'Save'
end.on(:click) { save }
end
end
def save
#User.save(validate: true).then do |result|
mutate #open = false if result[:success]
end
end
def cancel
#User.revert
mutate #open = false
end
def error_messages
#User.errors.full_messages.each do |message|
Typography(variant: :h6, color: :secondary) { message }
end
end
def validate_content
return false if #User.first_name.to_s.empty?
return false if #User.last_name.to_s.empty?
return false if #User.is_female.nil?
true
end
end
The underlying table (from the first code example) is re-rendered on every keypress, caused by:
TextField(label: 'First Name', defaultValue: #User.first_name.to_s)
.on(:change) do |e|
#User.first_name = e.target.value
end
This is causing typing to appear sluggish due to the amount of re-rendering.
Should I be keeping a local state variable for each field then only mutating the model fields on save?

Looks like you are using Material UI which will dynamically size tables to be best fit the content. So I suspect what is happening is that you are displaying the value of first_name and last_name in the MUI table, while you editing the values in the Dialog box.
So MUI is constantly recalculating the size of the MUI table columns as each character is typed.
Not only is this going to slow things down, but its also going to be disconcerting to the human user. It will give the impression that the changes they are making all already taking effect even before you have saved them.
So yes I think the best approach is to not directly update the state of the record while the user is typing but rather update a local state variable. Then only when the user saves, do you update the actual record.
I do notice that you have defaultValue which indicates an "uncontrolled" input. But you are reacting to every change in the input, which is the "controlled" behavior. I think you can change defaultValue to value.
class UserDialog < HyperComponent
param :user
before_mount do
#open = false
#first_name = #User.first_name
#last_name = #User.last_name
#is_female = #User.is_female
end
render do
puts "UserDialog render"
if #open
render_dialog
else
edit_or_new_button.on(:click) { mutate #open = true }
end
end
def render_dialog
Dialog(open: #open, fullWidth: false) do
DialogTitle do
'User'
end
DialogContent do
content
error_messages if #User.errors.any?
end
DialogActions do
actions
end
end
end
def edit_or_new_button
if #User.new?
Fab(size: :small, color: :primary) { Icon { 'add' } }
else
Fab(size: :small, color: :secondary) { Icon { 'settings' } }
end
end
def content
FormGroup(row: true) do
TextField(label: 'First Name', value: #first_name).on(:change) do |e|
mutate #first_name = e.target.value
end
TextField(label: 'Last Name', value: #last_name).on(:change) do |e|
mutate #last_name = e.target.value
end
end
BR()
FormLabel(component: 'legend') { 'Gender' }
RadioGroup(row: true) do
FormControlLabel(label: 'Male',
control: Radio(value: false, checked: !#is_female).as_node.to_n)
FormControlLabel(label: 'Female',
control: Radio(value: true, checked: #is_female).as_node.to_n)
end.on(:change) do |e|
mutate #is_female = e.target.value
end
end
def actions
Button { 'Cancel' }.on(:click) { cancel }
return unless ready_to_save?
Button(color: :primary, variant: :contained, disabled: (#User.saving? ? true : false)) do
'Save'
end.on(:click, &:save)
end
def save
#User.update(first_name: #first_name, last_name: #last_name, is_female: #is_female).then do |result|
mutate #open = false if result[:success]
end
end
def cancel
mutate #open = false
end
def error_messages
#User.errors.full_messages.each do |message|
Typography(variant: :h6, color: :secondary) { message }
end
end
def ready_to_save?
return false if #first_name.empty?
return false if #last_name.empty?
return false if #is_female.nil?
return true if #first_name != #User.first_name
return true if #last_name != #User.last_name
return true if #is_female != #User.is_female
end
end

As it turned out the things that were causing the performance problem was that I was not passing a unique key to the items in the list. React is very particular about this yet this is not something you get warnings about.
All I had to change was:
User.each do |user|
TableRow do
...
TableCell { UserDialog(user: user) }
end
end
To:
User.each do |user|
TableRow do
...
# this passes a unique key to each Component
TableCell { UserDialog(user: user, key: user) }
end
end
With the above change everything works perfectly in both examples (the first being where the underlying table is updated as the user types and the second, provided by #catmando, where the changes are only applied on save.

Related

Ruby return unless block

I've got a field from github webhook - webhook.repository.private - which checks if created repository was private (boolean). I want use return if block to handle scenario:
check if webhook.repository.private is true and if not call new class PublicRepositoryCreated but if this is true - return and execute fields_hash
code below:
def required_fields
PublicRepositoryCreated.new(webhook).call unless webhook.repository.private
fields_hash
end
private
def fields_hash
{
'fields' => {
'summary' => 'summary',
'description' => 'description',
'project' => '123'
}
}
end
Right now it seems that fields_hash is still executed even when webhook.repository.private is false
You have multiple ways of solving your problem.
You can either :
call your function and return
def required_fields
PublicRepositoryCreated.new(webhook).call && return unless webhook.repository.private
fields_hash
end
return your function
def required_fields
return PublicRepositoryCreated.new(webhook).call unless webhook.repository.private
fields_hash
end
use a ternary
def required_fields
webhook.repository.private ? fields_hash : PublicRepositoryCreated.new(webhook).call
end

Update array with strong parameters Rails 4

I'm receiving a JSON object and nested array via a Rails 4 api with params like so:
{
"token" => "123"
"lessons" => [
{
"token_id" => "j12l3n123",
"attr_1" => "hello",
"attr_2" = "is it me you're looking for"
},
{
"token_id" => "j12l",
"attr_1" => "Nope",
"attr_3" = "You're not."
}
]
}
And I have a controller like so:
def update_all
#fetch collection with one db hit
token_ids = params[:lessons].map{|l| l[:token_id]}
#lessons = Lesson.where(token_id: token_ids)
params[:lessons].each do |l|
lesson = #lessons.detect { |lesson| lesson.token_id == l[:token_id] }
# How do I update the record with strong params?
lesson.update_attributes(lesson_params)
end
end
private
def lesson_params
params.permit(
:attr_1,
:attr_2,
:attr_3
)
end
How do i update each record with the right object in the array, and use strong parameters to do so?
def update_all
lesson_params.each do |l|
lesson = Lesson.where(token_id: l[:token_id]).first
lesson.update_attributes(l)
end
end
private
def lesson_params
params.require(:lessons).map do |l|
ActionController::Parameters.new(l.to_hash).permit(
:attr_1,
:attr_2,
:attr_3
)
end
end
def lesson_params
params.permit(:token, lessons: [:token_id, :attr_1, :attr_2, :attr_3 ])
end
in Controller something like following
def update_all
lesson_params[:lessons].each do |lesson_param|
lesson = Lesson.find(lesson_param[:token_id])
lesson.update_attributes(lesson_param)
end
end

Update value with first_or_create in rails

I have a table 'Likes' with columns business_id, user_id and liked(0,1) and a function 'change_like_status'.
Now on every function call, If the value is 1 then set it to 0 (or vice versa) and if record doesn't exists then create one with value 1.
The first_or_create method is working just fine but how can i toggle value of column 'liked' while using this method?
Here is my function:
def change_like_status
if current_user.present?
status = Like.where("business_id = ? AND user_id = ?",params['id'],current_user.id).first_or_create(:business_id => params['id'],:user_id => current_user.id,:liked => '1')
abort status.inspect
else
return render :json => {:status => false,:msg=>"You need to sign in before performing this action."}
end
end
In you controller, make the changes
def change_like_status
if current_user
status = Like.create_or_change_status(params[:id], current_user.id)
else
return render json: { status: false, msg: "You need to sign in before performing this action." }
end
end
In your model like.rb file, add a method
def self.create_or_change_status(business_id, user_id)
status = where(business_id: business_id, user_id: user_id).first
if status.nil?
status = create({business_id: business_id, user_id: user_id, liked: 1})
else
status.update_attributes(liked: !status.liked)
end
status
end
def change_like_status
if current_user
current_user.likes.find_by(business_id: params[:id]).switch_status!
else
return render json: { status: false, msg: "You need to sign in before performing this action." }
end
end
class Like
def switch_status!
self.update_column :liked, !liked
end
end
other approach should be something like that
class Like
def switch_status!
self.update_column :liked, !liked
end
end
class User
def likes id
likes_for_business id
end
def likes_for_business(id)
likes.find_by(business_id: id) || likes.create(:business_id: id, liked: true)
end
end
# controller
current_user.likes(params[:id]).switch_status!

How to access an instance variable from a do block?

The following code does not work. It says undefined method 'table_name' for nil:NilClass
#members = Members.all
table member_list_rows do
if #members.table_name == members
row(0).background_color = '3498db'
end
end
Full code
class MemberPdf < Prawn::Document
def initialize(members, view, allcount)
super(top_margin: 50)
if members.size != allcount
#warn = " (Not all members)"
else
#all = " All"
end
text "Showing#{#all} #{members.size} Members", size: 18, style: :bold, align: :center, color: "636363"
text "#{#warn}", size: 11, align: :center, color: "858585"
#members = members
#view = view
member_list
end
def member_list
move_down 20
table member_list_rows do
self.row(0).align = :center
if #members.table_name == "members"
row(0).background_color = '3498db'
else
end
row(0).text_color = "FFFFFF"
self.row_colors = ["DDDDDD", "FFFFFF"]
self.header = true
#self.cell.text_color = "B3B3B3"
row(0).columns(0).style size: 20
end
end
def member_list_rows
[["Name", "Awardunit", "Address", "Contact", "Level of Entry", "Current Award", "Disabled?" ]] +
#members.map do |member|
[member.name, member.awardunit.name, member.address, member.name, member.entrylvl, member.currentaward, #view.yesno(member.disabled)]
end
end
end
Members controller
if params[:commit] == "Clear"
params[:q] = nil
end
respond_to do |format|
format.html
format.pdf do
pdf = MemberPdf.new(Member.search(params[:q]).result.order( 'name ASC' ), view_context, Member.all.size)
send_data pdf.render, filename: "Members_List.pdf", type: "application/pdf", disposition: "inline"
end
end
It is due to #members is nil.You are doing it wrong.
Change this
#members = Members.all #Wrong
to
#members = Member.all #Right
Always remember,the Model name should be singular.
Those are called Naming Conventions. For more information,read these Style guides(Ruby and Rails)
Most likely table method is changing context, in which you don't have access to the #members instance variable anymore. This can be achieved easily by this sample code:
def do_stuff(&block)
cls = Class.new
cls.instance_eval(&block)
end
#test_var = "test_var"
do_stuff { puts #test_var }
You will receive nothing, because #test_var does not exist in the cls.
Am not sure what you are doing with "table member_list_rows". Didn't get that.
In the third line though, it should be
if #members.table_name == "members"

Really having difficulty with New tokens on token input (from different model)

Im using jquery-tokeninput, but a fork of it which allows the User to add new custom tokens (Tag) for each Resource.
Example here (Scroll down to the tag box and type a few letters. you can type ones that dont exist): http://www.tomsguide.fr/solutions/nouveau_sujet.htm
The current return value from the fork I'm using is this (new value in quotes):
16,42,'Subway',37,'McDonald\'s',734
I'm having extreme difficulty trying to handle this in Rails. This sums it up perfectly.
This is what I have so far, and its not working, probably for a lot of reasons I'm not seeing, but the main reason is that I need to create new Tag instances but not save them, that way I can somehow pass them back into the token input, and save the new Tags along with the new Resource when they submit the form. When you use Tag.new though, it doesnt create an ID.
resource.rb
attr_accessor :tokens_list
# CUSTOM TOKENS
def tag_tokens=(tokens)
self.tokens_list = tokens.split(",")
if new_custom_tokens?
self.tokens_list.each do |token|
tokens_list << token if token.include? "'"
end
end
self.tag_ids = self.tokens_list
end
def new_custom_tokens?
self.tokens_list.each do |token|
return true if token.include? "'"
end
false
end
resources_controller.rb
def create
#title = "Submit Resource"
#resource = Resource.new(params[:resource])
assign_to_global_user?
# CUSTOM TOKENS
if #resource.new_custom_tokens?
custom_token_time_restriction
# Create Tag.new
end
if #resource.valid?
#resource.save
flash[:notice] = "Your link has been successfully submitted."
redirect_to root_url
else
render :action => :new
end
end
def assign_to_global_user?
if user_signed_in?
#resource.user_id = current_user.id
else
#resource.user_id = User.find_by_username("Global_User").id
end
end
private
# CUSTOM TOKENS
def custom_token_time_restriction
limit = 7 # days
if (#resource.user_id != User.global_user_id) and (Time.now - limit.days > User.find(#resource.user_id).created_at)
# TODO: Check if they are anonymous or their account is newer than 7 days
else
flash[:notice] = "You be Logged in to add new tags, and your account must be older than #{limit} days."
render :action => :new
end
end
new.html.erb (for resource#new)
<div class="field">
<%= f.label :tags %>
<%= f.text_field :tag_tokens, "data-pre" => #resource.tags.to_json(:only => [:id, :name]), :class => :tagbox %>
</div>
I had the same problem. This is what I have done:
This is the function where I return tokens of search in json format.
tags = TagMaster.where("name LIKE ?", "%#{params[:q]}%").limit(10)
if tags == []
list << {"id" => "0","name"=>new_tag.rstrip}
else
tags.each { |tag| list << {"id" => tag.id.to_s, "name" => tag.name }}
end
respond_to do |format|
format.json { render :json => list.to_json, :layout => false }
end
Now this will allow show you whatever you type in auto complete dropdown and on clicked it will show as a token.
Now you can't add any more custom tokens because any token that is not in database will return id 0 so only one custom token is allowed at this point of time.
For that problem I did following.
var k = jQuery.noConflict();
k("#project_tags").tokenInput("tag_list", {
hintText: "Enter Tags for your Project",
noResultsText: "No Such Tags",
searchingText: "Looking for your Tags",
preventDuplicates: true,
theme: "facebook",
onAdd: function (item) {
if (item.id == '0') {
k.ajax({
url: '/add_project_tag',
data: { name: item.name },
success:function(data) {
k("#project_tags").tokenInput("add", {id: data, name: item.name});
k("#project_tags").tokenInput("remove", {id: '0' });
}
});
}
}
});
As you can see here i call add_project_tag where I store that custom tag into database and it returns id of that inserted tag. So now you simply add the same token with it's id and remove token with 0.
So now there won't be any token with 0 and you can add as many new token as you want.
Hope this helps. Throw your questions if any more complications.

Resources