Nested form for domain

This commit is contained in:
Martin Lensment 2014-09-19 17:49:02 +03:00
parent d6543f2148
commit 4fef7e94c8
8 changed files with 382 additions and 26 deletions

View file

@ -6,6 +6,7 @@
#= require nprogress-turbolinks
#= require typeahead.bundle.min
#= require autocomplete
#= require jquery.nested_attributes
#= require app
NProgress.configure

View file

@ -6,6 +6,8 @@ class Admin::DomainsController < ApplicationController
owner_contact = Contact.find(params[:owner_contact_id]) if params[:owner_contact_id]
@domain = Domain.new(owner_contact: owner_contact)
params[:domain_owner_contact] = owner_contact
@domain.nameservers.build
end
def create
@ -59,7 +61,7 @@ class Admin::DomainsController < ApplicationController
end
def domain_params
params.require(:domain).permit(:name, :period, :period_unit, :registrar_id, :owner_contact_id)
params.require(:domain).permit(:name, :period, :period_unit, :registrar_id, :owner_contact_id, nameservers_attributes: [:id, :hostname, :ipv4, :ipv6, :_destroy])
end
def verify_deletion

View file

@ -14,7 +14,8 @@ class Domain < ActiveRecord::Base
where(domain_contacts: { contact_type: DomainContact::ADMIN })
end, through: :domain_contacts, source: :contact
has_many :nameservers, dependent: :delete_all, autosave: true
has_many :nameservers, dependent: :delete_all
accepts_nested_attributes_for :nameservers, allow_destroy: true
has_many :domain_statuses, dependent: :delete_all

View file

@ -7,30 +7,33 @@
%hr
.row
.col-md-6
.form-group
= f.label :name
= f.text_field(:name, class: 'form-control')
.form-group
= f.label :period
.row
.col-md-6
= f.text_field(:period, class: 'form-control')
.col-md-6
= f.select :period_unit, options_for_select(['y', 'm', 'd'], @domain.period_unit), {}, {class: 'form-control'}
.col-md-6
.form-group.has-feedback
= f.label :registrar
= text_field_tag(:domain_registrar, params[:registrar], class: 'form-control js-registrar-typeahead', placeholder: t('shared.registrar_name'), autocomplete: 'off')
%span.glyphicon.glyphicon-ok.form-control-feedback.js-typeahead-ok.hidden
%span.glyphicon.glyphicon-remove.form-control-feedback.js-typeahead-remove
= f.hidden_field(:registrar_id, class: 'js-registrar-id')
.form-group.has-feedback
= f.label :owner_contact
= text_field_tag(:domain_owner_contact, params[:domain_owner_contact], class: 'form-control js-contact-typeahead', placeholder: t('shared.contact_code'), autocomplete: 'off')
%span.glyphicon.glyphicon-ok.form-control-feedback.js-typeahead-ok.hidden
%span.glyphicon.glyphicon-remove.form-control-feedback.js-typeahead-remove
= f.hidden_field(:owner_contact_id, class: 'js-contact-id')
.col-md-12
/ Nav tabs
%ul.nav.nav-tabs{:role => "tablist", id: 'tabs'}
%li.active
%a{"data-toggle" => "tab", :href => "#general-tab", :role => "tab"} general-tab
%li
%a{"data-toggle" => "tab", :href => "#nameservers-tab", :role => "tab"} nameservers-tab
%li
%a{"data-toggle" => "tab", :href => "#tech-contacts-tab", :role => "tab"} tech-contacts-tab
%li
%a{"data-toggle" => "tab", :href => "#admin-contacts", :role => "tab"} admin-contacts-tab
%li
%a{"data-toggle" => "tab", :href => "#statuses-tab", :role => "tab"} statuses-tab
/ Tab panes
.tab-content{style:'margin-top: 20px;'}
#general-tab.tab-pane.active
= render 'admin/domains/form_partials/general', f: f
#nameservers-tab.tab-pane
= render 'admin/domains/form_partials/nameservers', f: f
#tech-contacts-tab.tab-pane
#admin-contacts-tab.tab-pane ...
#statuses-tab.tab-pane ...
.row
.col-md-12.text-right
= button_tag(t('shared.save'), class: 'btn btn-primary')
:javascript
$(function () {
$('#tabs a:first').tab('show')
})

View file

@ -0,0 +1,25 @@
.row
.col-md-6
.form-group
= f.label :name
= f.text_field(:name, class: 'form-control')
.form-group
= f.label :period
.row
.col-md-6
= f.text_field(:period, class: 'form-control')
.col-md-6
= f.select :period_unit, options_for_select(['y', 'm', 'd'], @domain.period_unit), {}, {class: 'form-control'}
.col-md-6
.form-group.has-feedback
= f.label :registrar
= text_field_tag(:domain_registrar, params[:registrar], class: 'form-control js-registrar-typeahead', placeholder: t('shared.registrar_name'), autocomplete: 'off')
%span.glyphicon.glyphicon-ok.form-control-feedback.js-typeahead-ok.hidden
%span.glyphicon.glyphicon-remove.form-control-feedback.js-typeahead-remove
= f.hidden_field(:registrar_id, class: 'js-registrar-id')
.form-group.has-feedback
= f.label :owner_contact
= text_field_tag(:domain_owner_contact, params[:domain_owner_contact], class: 'form-control js-contact-typeahead', placeholder: t('shared.contact_code'), autocomplete: 'off')
%span.glyphicon.glyphicon-ok.form-control-feedback.js-typeahead-ok.hidden
%span.glyphicon.glyphicon-remove.form-control-feedback.js-typeahead-remove
= f.hidden_field(:owner_contact_id, class: 'js-contact-id')

View file

@ -0,0 +1,30 @@
#nameservers
= f.fields_for :nameservers do |ns_fields|
.panel.panel-default
.panel-heading.text-right
= link_to(t('shared.add_another'), '#', class: 'btn btn-primary btn-xs add-nameserver')
= link_to(t('shared.delete'), '#', class: 'btn btn-danger btn-xs destroy')
.panel-body
.errors
= render 'admin/shared/errors', object: ns_fields.object
.row
.col-md-12
.form-group
= ns_fields.label :hostname
= ns_fields.text_field :hostname, class: 'form-control'
.row
.col-md-6
.form-group
= ns_fields.label :ipv4
= ns_fields.text_field :ipv4, class: 'form-control'
.col-md-6
.form-group
= ns_fields.label :ipv6
= ns_fields.text_field :ipv6, class: 'form-control'
:javascript
$("#nameservers").nestedAttributes({
bindAddTo: $(".add-nameserver"),
afterAdd: function(item) {
item.find('.errors').html('');
}
});

View file

@ -259,3 +259,4 @@ en:
failed_to_add_domain: 'Failed to add domain!'
domain_added: 'Domain added!'
new_contact: 'New contact'
add_another: 'Add another'

View file

@ -0,0 +1,293 @@
###
Authors: Nick Giancola (@patbenatar), Brendan Loudermilk (@bloudermilk)
Homepage: https://github.com/patbenatar/jquery-nested_attributes
###
$ = jQuery
methods =
init: (options) ->
$el = $(@)
throw "Can't initialize more than one item at a time" if $el.length > 1
if $el.data("nestedAttributes")
throw "Can't initialize on this element more than once"
instance = new NestedAttributes($el, options)
$el.data("nestedAttributes", instance)
return $el
add: ->
$el = $(@)
unless $el.data("nestedAttributes")?
throw "You are trying to call instance methods without initializing first"
$el.data("nestedAttributes").addItem()
return $el
$.fn.nestedAttributes = (method) ->
if methods[method]?
return methods[method].apply @, Array.prototype.slice.call(arguments, 1)
else if typeof method == 'object' || !method
return methods.init.apply(@, arguments)
else
$.error("Method #{method} does not exist on jQuery.nestedAttributes")
class NestedAttributes
RELEVANT_INPUTS_SELECTOR: ":input[name][name!=\"\"]"
settings:
collectionName: false # If not provided, we will autodetect
bindAddTo: false # Required
removeOnLoadIf: false
collectIdAttributes: true
beforeAdd: false
afterAdd: false
beforeMove: false
afterMove: false
beforeDestroy: false
afterDestroy: false
destroySelector: '.destroy'
deepClone: true
$clone: null
######################
## ##
## Initialization ##
## ##
######################
constructor: ($el, options) ->
# This plugin gets called on the container
@$container = $el
# Merge default options
@options = $.extend({}, @settings, options)
# If the user provided a jQuery object to bind the "Add"
# bind it now or forever hold your peace.
@options.bindAddTo.click(@addClick) if @options.bindAddTo
# Cache all the items
@$items = @$container.children()
# If the user didn't provide a collectionName, autodetect it
unless @options.collectionName
@autodetectCollectionName()
# Initialize existing items
@$items.each (i, el) =>
$item = $(el)
# If the user wants us to attempt to collect Rail's ID attributes, do it now
# Using the default rails helpers, ID attributes will wind up right after their
# propper containers in the form.
if @options.collectIdAttributes and $item.is('input')
# Move the _id field into its proper container
$item.appendTo($item.prev())
# Remove it from the $items collection
@$items = @$items.not($item)
else
# Try to find and bind the destroy link if the user wanted one
@bindDestroy($item)
# Now that we've collected ID attributes
@hideIfAlreadyDestroyed $(item) for item in @$items
# Remove any items on load if the client implements a check and the check passes
if @options.removeOnLoadIf
@$items.each (i, el) =>
$el = $(el)
if $el.call(true, @options.removeOnLoadIf, i)
$el.remove()
########################
## ##
## Instance Methods ##
## ##
########################
autodetectCollectionName: ->
pattern = /\[(.[^\]]*)_attributes\]/
try
match = pattern.exec(@$items.first().find("#{@RELEVANT_INPUTS_SELECTOR}:first").attr('name'))[1]
if match != null
@options.collectionName = match
else
throw "Regex error"
catch error
console.log "Error detecting collection name", error
addClick: (event) =>
@addItem()
# Don't let the link do anything
event.preventDefault()
addItem: ->
# Piece together an item
newIndex = @$items.length
$newClone = @applyIndexToItem(@extractClone(), newIndex)
# Give the user a chance to make their own changes before we insert
if (@options.beforeAdd)
# Stop the add process if the callback returns false
return false if !@options.beforeAdd.call(undefined, $newClone, newIndex)
# Insert the new item after the last item
@$container.append($newClone)
# Give the user a chance to make their own changes after insertion
@options.afterAdd.call(undefined, $newClone, newIndex) if (@options.afterAdd)
# Add this item to the items list
@refreshItems()
extractClone: ->
# Are we restoring from an already created clone?
if @$restorableClone
$record = @$restorableClone
@$restorableClone = null
else
$record = @options.$clone || @$items.first()
# Make a deep clone (bound events and data)
$record = $record.clone(@options.deepClone)
@bindDestroy($record) if @options.$clone or !@options.deepClone
# Empty out the values of text inputs and selects
$record.find(':text, textarea, select').val('')
# Reset checkboxes and radios
$record.find(':checkbox, :radio').attr("checked", false)
# Empty out any hidden [id] or [_destroy] fields
$record.find('input[name$="\\[id\\]"]').remove()
$record.find('input[name$="\\[_destroy\\]"]').remove()
# Make sure it's not hidden as we return.
# It would be hidden in the case where we're duplicating an
# already removed item for its template.
return $record.show()
applyIndexToItem: ($item, index) ->
collectionName = @options.collectionName
$item.find(@RELEVANT_INPUTS_SELECTOR).each (i, el) =>
$el = $(el)
idRegExp = new RegExp("_#{collectionName}_attributes_\\d+_")
idReplacement = "_#{collectionName}_attributes_#{index}_"
nameRegExp = new RegExp("\\[#{collectionName}_attributes\\]\\[\\d+\\]")
nameReplacement = "[#{collectionName}_attributes][#{index}]"
newID = $el.attr('id').replace(idRegExp, idReplacement) if $el.attr('id')
newName = $el.attr('name').replace(nameRegExp, nameReplacement)
$el.attr
id: newID
name: newName
$item.find('label[for]').each (i, el) =>
$el = $(el)
try
forRegExp = new RegExp("_#{collectionName}_attributes_\\d+_")
forReplacement = "_#{collectionName}_attributes_#{index}_"
newFor = $el.attr('for').replace(forRegExp, forReplacement)
$el.attr('for', newFor)
catch error
console.log "Error updating label", error
return $item
hideIfAlreadyDestroyed: ($item) ->
$destroyField = $item.find("[name$='[_destroy]']")
if $destroyField.length && $destroyField.val() == "true"
@destroy $item
# Hides a item from the user and marks it for deletion in the
# DOM by setting _destroy to true if the record already exists. If it
# is a new escalation, we simple delete the item
destroyClick: (event) =>
event.preventDefault()
@destroy $(event.target).parentsUntil(@$container).last()
destroy: ($item) ->
# If you're about to delete the last one,
# cache a clone of it first so we have something to show
# the next time user hits add
@$restorableClone = @extractClone() unless @$items.length-1
index = @indexForItem($item)
itemIsNew = $item.find('input[name$="\\[id\\]"]').length == 0
if (@options.beforeDestroy)
# Stop the destroy process if the callback returns false
return false if !@options.beforeDestroy.call(undefined, $item, index, itemIsNew)
# Add a blank item row if none are visible after this deletion
@addItem() unless @$items.filter(':visible').length-1
if itemIsNew
$item.remove()
else
# Hide the item
$item.hide()
# Add the _destroy field
otherFieldName = $item.find(':input[name]:first').attr('name')
attributePosition = otherFieldName.lastIndexOf('[')
destroyFieldName = "#{otherFieldName.substring(0, attributePosition)}[_destroy]"
# First look for an existing _destroy field
$destroyField = $item.find("input[name='#{destroyFieldName}']")
# If it doesn't exist, create it
if $destroyField.length == 0
$destroyField = $("<input type=\"hidden\" name=\"#{destroyFieldName}\" />")
$item.append($destroyField)
$destroyField.val(true).change()
@options.afterDestroy.call($item, index, itemIsNew) if (@options.afterDestroy)
# Remove this item from the items list
@refreshItems()
# Rename the remaining items
@resetIndexes()
indexForItem: ($item) ->
regExp = new RegExp("\\[#{@options.collectionName}_attributes\\]\\[\\d+\\]")
name = $item.find("#{@RELEVANT_INPUTS_SELECTOR}:first").attr('name')
return parseInt(name.match(regExp)[0].split('][')[1].slice(0, -1), 10)
refreshItems: ->
@$items = @$container.children()
# Sets the proper association indices and labels to all items
# Used when removing items
resetIndexes: ->
@$items.each (i, el) =>
$el = $(el)
# Make sure this is actually a new position
oldIndex = @indexForItem($el)
return true if (i == oldIndex)
@options.beforeMove.call($el, i, oldIndex) if (@options.beforeMove)
# Change the number to the new index
@applyIndexToItem($el, i)
@options.afterMove.call($el, i, oldIndex) if (@options.afterMove)
bindDestroy: ($item) ->
$item.find(@options.destroySelector).click(@destroyClick) if (@options.destroySelector)