-
-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Refactor the has_many form #2645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor the has_many form #2645
Conversation
|
||
@has_many_index ||= $(@).siblings('fieldset').length | ||
regex = new RegExp $(@).data('placeholder'), 'g' | ||
$(@).before unescape_html $(@).data('html').replace regex, ++@has_many_index |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This relies on a really weird (but useful) thing: you can set properties on DOM elements, and they'll store that information for you. So if you come back to the same element, that data will still be there. I'm doing that here to store the current index, because people could potentially have multiple has_many forms inside the same parent form.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if this is the correct, I think you will end up w. duplicate indices if a fieldset is removed. Also I don't quite understand what you mean about storing the index and how it helps with multiple has_many forms
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if this is the correct, I think you will end up w. duplicate indices if a fieldset is removed.
If a fieldset is removed, the number will just keep counting up. So you might have some numbers not used at all, but at least they won't conflict.
Also I don't quite understand what you mean about storing the index and how it helps with multiple has_many forms
If I had a global variable that stored the current index, that wouldn't act sanely if you have mutliple has_many forms. Whereas this solution binds the index onto the add button, so it's unique to every has_many form.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, that makes sense
On Sun, Nov 3, 2013 at 1:57 PM, Sean Linsley notifications@github.comwrote:
In app/assets/javascripts/active_admin/components/has_many.js.coffee:
@@ -0,0 +1,17 @@
+unescape_html = (html)->
- String(html).replace(/&/g, '&').replace(/</g, '<').replace(/>/g,'>').replace(/"/g, '"').replace(/'/g, "'")
+$ ->
- $(document).on 'click', 'a.button.has_many_remove', (e)->
- e.preventDefault()
- $(@).closest('.has_many_fields').remove()
- $('a.button.has_many_add').click (e)->
- e.preventDefault()
- $(@).trigger 'has_many_add:before'
- @has_many_index ||= $(@).siblings('fieldset').length
- regex = new RegExp $(@).data('placeholder'), 'g'
$(@).before unescape_html $ (@).data('html').replace regex, ++@has_many_indexI'm not sure if this is the correct, I think you will end up w.
duplicate indices if a fieldset is removed.If a fieldset is removed, the number will just keep counting up. So you
might have some numbers not used at all, but at least they won't conflict.Also I don't quite understand what you mean about storing the index and
how it helps with multiple has_many formsIf I had a global variable that stored the current index, that wouldn't
act sanely if you have mutliple has_many forms. Whereas this solution binds
the index onto the add button, so it's unique to every has_many form.—
Reply to this email directly or view it on GitHubhttps://github.com//pull/2645/files#r7391569
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As for the actual way I'm storing that index, it's apparently a bit controversial.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'@' refers to a DOMNode and @hasManyIndex will be an expando on that dom object so updating @hasManyIndex will by its nature update the DOM.
jQuery data uses a single expando for all the data you store for that node, avoiding dom updates for future writes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I say "update the DOM", I'm really saying "redraw the canvas". The current approach is just updating the DOM's JavaScript runtime object for that HTML, which as far as I can tell doesn't require a redraw.
I have a working replacement but because of the way JavaScript is built, it's unfortunately hard to read:
unless index = parent.data 'has_many_index'
parent.data 'has_many_index', parent.find('fieldset').length
index = parent.data 'has_many_index'
regex = new RegExp elem.data('placeholder'), 'g'
html = elem.data('html').replace regex, index++
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another implementation would be:
if parent.data 'has_many_index'
parent.data has_many_index: parent.data('has_many_index') + 1
else
parent.data has_many_index: parent.find('fieldset').length
regex = new RegExp elem.data('placeholder'), 'g'
html = elem.data('html').replace regex, parent.data('has_many_index')
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
index = parent.data('hasManyIndex') || parent.find('fieldset').length
parent.data('hasManyIndex', ++index);
@@ -141,13 +140,10 @@ def js_for_has_many(assoc, form_block, template, new_record) | |||
:class => "inputs has_many_fields", | |||
:for_options => { child_index: placeholder } | |||
} | |||
js = with_new_form_buffer{ inputs_for_nested_attributes opts, &form_block } | |||
js = template.escape_javascript js | |||
html = CGI.escapeHTML with_new_form_buffer{ inputs_for_nested_attributes opts, &form_block } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will cause the html to be double escaped (which I'm guessing is reason why you added a manual unescape_html function
The reason that happens is because:
- with_new_form_buffer returns unescaped html thats marked as html_safe
- CGI.escapeHTML returns escaped html thats not marked html_safe
- link_to ... {data: html} re-escape the html because its not marked save
the most succinct way to accomplish what you want is to taint the return value from with_new_form_buffer:
'' + with_new_form_buffer {... }
but to be clearer:
html = CGI.escapeHTML(...).html_safe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yeah that was the problem. Updating the PR now.
would you have any interest in including sortable functionality in your pull request? or should I open up a new PR once you merge this? |
Not in this pull request, no. I'm really just scratching an itch here, because I couldn't stand leaving the has_many JS embedded in the HTML like that, and figured it'd then be simple to add hooks once that was done. Feel free to create a PR for that after this. 🐶 |
Now to actually update the tests 😅 |
parent = elem.closest '.has_many' | ||
parent.trigger 'has_many_add:before' | ||
|
||
index = parent.data('has_many_index') || parent.find('fieldset').length - 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to account for @shekibobo nested use case, this should be
index = parent.data('has_many_index') || parent.children('fieldset').length - 1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused, isn't that exactly what I already have?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parent.find('fieldset') vs parent.children('fieldset')
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah. Updated.
Coverage increased (+0.05%) when pulling fdd7d4aaeee904541bcdef052e2a6b6d2549494e on seanlinsley:refactor/has_many-form into ef166a0 on gregbell:master. |
First and foremost, this commit moves the JS `onclick` calls out into their own dedicated file. This also adds before & after hooks so you can more easily update dynamic parts of the form, for example if you're using a plugin like Chosen for interactive dropdowns: ```coffee $ -> $(document).on 'has_many_add:after', '.has_many', (e, fieldset)-> fieldset.find('select').chosen() ``` There's one other change here: I switched around the text used for has_many deletion because it didn't make sense to me that delete was applied to something that hadn't yet been saved, while remove was used for already-saved records.
First and foremost, this PR moves the JS
onclick
calls out into their own dedicated file. This also adds before & after hooks so you can more easily update dynamic parts of the form, for example if you're using a plugin like Chosen for interactive dropdowns:There's one other change here: I switched around the text used for has_many deletion because it didn't make sense to me that delete was applied to something that hadn't yet been saved, while remove was used for saved records.
Update:
After talking with @igorbernstein, I updated the jQuery hooks to be bound to the parent div, and now the after-add hook returns the just-added fieldset:
Another update:
You can now short-circuit creation of new has_many fieldsets:
Finally:
#2679 updated the CSS classes so now you use
has_many_container
instead ofhas_many