Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save johan--/613a0ba530c53de41e2b41228b330c44 to your computer and use it in GitHub Desktop.
Save johan--/613a0ba530c53de41e2b41228b330c44 to your computer and use it in GitHub Desktop.
formbuilder: nested fields for radios
diff --git index.html index.html
index a65e625..30503cf 100644
--- index.html
+++ index.html
@@ -64,10 +64,20 @@
"field_options": {
"options": [{
"label": "Yes",
- "checked": false
+ "checked": false,
+ "nested_fields": true,
+ "fields": [{
+ "label": "Start year",
+ "field_type": "number",
+ "required": true,
+ "field_options": {},
+ "cid": "c15"
+ }]
}, {
"label": "No",
- "checked": false
+ "checked": false,
+ "nested_fields": false,
+ "fields": null
}],
"include_other_option": true
},
diff --git src/scripts/fields/radio.coffee src/scripts/fields/radio.coffee
index 14dbfaf..fb88fe9 100644
--- src/scripts/fields/radio.coffee
+++ src/scripts/fields/radio.coffee
@@ -9,6 +9,9 @@ Formbuilder.registerField 'radio',
<input type='radio' <%= rf.get(Formbuilder.options.mappings.OPTIONS)[i].checked && 'checked' %> onclick="javascript: return false;" />
<%= rf.get(Formbuilder.options.mappings.OPTIONS)[i].label %>
</label>
+ <% if (rf.get(Formbuilder.options.mappings.OPTIONS)[i].nested_fields) { %>
+ <div class='fb-nested-<%= rf.cid %>-<%= i %>'></div>
+ <% } %>
</div>
<% } %>
@@ -36,10 +39,14 @@ Formbuilder.registerField 'radio',
# @todo
attrs.field_options.options = [
label: "",
- checked: false
+ checked: false,
+ nested_fields: false,
+ fields: null
,
label: "",
- checked: false
+ checked: false,
+ nested_fields: false,
+ fields: null
]
attrs
\ No newline at end of file
diff --git src/scripts/main.coffee src/scripts/main.coffee
index 54aeb6b..b5df333 100644
--- src/scripts/main.coffee
+++ src/scripts/main.coffee
@@ -1,11 +1,14 @@
class FormbuilderModel extends Backbone.DeepModel
sync: -> # noop
- indexInDOM: ->
- $wrapper = $(".fb-field-wrapper").filter ( (_, el) => $(el).data('cid') == @cid )
- $(".fb-field-wrapper").index $wrapper
+ indexInDOM: (view) ->
+ $wrapper = view.$el.find(".fb-field-wrapper").filter ( (_, el) => $(el).data('cid') == @cid )
+ view.$el.find(".fb-field-wrapper").index $wrapper
is_input: ->
Formbuilder.inputFields[@get(Formbuilder.options.mappings.FIELD_TYPE)]?
+ toJSON: (options) ->
+ json = _.clone(@attributes)
+
class FormbuilderCollection extends Backbone.Collection
initialize: ->
@@ -14,7 +17,7 @@ class FormbuilderCollection extends Backbone.Collection
model: FormbuilderModel
comparator: (model) ->
- model.indexInDOM()
+ model.indexInDOM(@view)
copyCidToModel: (model) ->
model.attributes.cid = model.cid
@@ -29,7 +32,7 @@ class ViewFieldView extends Backbone.View
'click .js-clear': 'clear'
initialize: (options) ->
- {@parentView} = options
+ {@builderView} = options
@listenTo @model, "change", @render
@listenTo @model, "destroy", @remove
@@ -38,17 +41,33 @@ class ViewFieldView extends Backbone.View
.data('cid', @model.cid)
.html(Formbuilder.templates["view/base#{if !@model.is_input() then '_non_input' else ''}"]({rf: @model}))
+ that = @
+ model = @model
+ builderView = @builderView
+ if @model.attributes.field_options.options
+ _.each(@model.attributes.field_options.options, (e, i) ->
+ if e.nested_fields
+ if that.$responseFieldsView == undefined
+ that.$responseFieldsView = []
+ if that.$responseFieldsView[i] == undefined
+ that.$responseFieldsView[i] = new ResponseFieldsView({builderView: builderView, bootstrapData: e.fields})
+ e.fields = that.$responseFieldsView[i].collection
+ that.$responseFieldsView[i].setElement(that.$el.find(".fb-nested-#{model.cid}-#{i}"))
+ that.$responseFieldsView[i].render()
+ )
+
return @
focusEditView: ->
- @parentView.createAndShowEditView(@model)
+ @builderView.createAndShowEditView(@model)
+ return false;
clear: (e) ->
e.preventDefault()
e.stopPropagation()
cb = =>
- @parentView.handleFormUpdate()
+ @builderView.handleFormUpdate()
@model.destroy()
x = Formbuilder.options.CLEAR_FIELD_CONFIRM
@@ -65,7 +84,7 @@ class ViewFieldView extends Backbone.View
attrs = _.clone(@model.attributes)
delete attrs['id']
attrs['label'] += ' Copy'
- @parentView.createField attrs, { position: @model.indexInDOM() + 1 }
+ @builderView.createField attrs, { position: @model.indexInDOM() + 1 }
class EditFieldView extends Backbone.View
@@ -127,6 +146,94 @@ class EditFieldView extends Backbone.View
forceRender: ->
@model.trigger('change')
+class ResponseFieldsView extends Backbone.View
+ initialize: (options) ->
+ {@builderView, @bootstrapData} = options
+ @collection = new FormbuilderCollection
+ @collection.view = @
+ @collection.bind 'add', @addOne, @
+ @collection.bind 'reset', @reset, @
+ @collection.bind 'change', @builderView.handleFormUpdate, @builderView
+ @collection.bind 'destroy add reset', @hideShowNoResponseFields, @
+ @collection.bind 'destroy', @builderView.ensureEditViewScrolled, @builderView
+
+ render: ->
+ @$el.html Formbuilder.templates['partials/response_fields']()
+ @hideShowNoResponseFields()
+ @$responseFields = @$el.find('.fb-response-fields')
+ if @collection.isEmpty() then @collection.reset @bootstrapData else @addAll()
+
+ reset: ->
+ @addAll()
+
+ addAll: ->
+ @collection.each @addOne, @
+ @setSortable()
+
+ addOne: (responseField, _, options) ->
+ view = new ViewFieldView
+ model: responseField
+ builderView: @builderView
+
+ #####
+ # Calculates where to place this new field.
+ #
+ # Are we replacing a temporarily drag placeholder?
+ if options.$replaceEl?
+ options.$replaceEl.replaceWith view.render().el
+
+ # Are we adding to the bottom?
+ else if !options.position? || options.position == -1
+ @$responseFields.append view.render().el
+
+ # Are we adding to the top?
+ else if options.position == 0
+ @$responseFields.prepend view.render().el
+
+ # Are we adding below an existing field?
+ else if ($replacePosition = @$el.find(".fb-field-wrapper").eq(options.position))[0]
+ $replacePosition.before view.render().el
+
+ # Catch-all: add to bottom
+ else
+ @$responseFields.append view.render().el
+ @collection.sort()
+
+ setSortable: ->
+ @$responseFields.sortable('destroy') if @$responseFields.hasClass('ui-sortable')
+ @$responseFields.sortable
+ forcePlaceholderSize: true
+ placeholder: 'sortable-placeholder'
+ stop: (e, ui) =>
+ if ui.item.data('field-type')
+ rf = @collection.create Formbuilder.helpers.defaultFieldAttrs(ui.item.data('field-type')), {$replaceEl: ui.item}
+ @builderView.createAndShowEditView rf
+
+ @builderView.handleFormUpdate()
+ return true
+ update: (e, ui) =>
+ # ensureEditViewScrolled, unless we're updating from the draggable
+ @builderView.ensureEditViewScrolled() unless ui.item.data('field-type')
+
+ @setDraggable()
+
+ setDraggable: ->
+ $addFieldButtons = @builderView.$el.find("[data-field-type]")
+
+ $addFieldButtons.draggable
+ connectToSortable: @builderView.$el.find('.fb-response-fields')
+ helper: =>
+ $helper = $("<div class='response-field-draggable-helper' />")
+ $helper.css
+ width: @$responseFields.width() # hacky, won't get set without inline style
+ height: '80px'
+
+ $helper
+
+ hideShowNoResponseFields: ->
+ @$el.find(".fb-no-response-fields")[if @collection.length > 0 then 'hide' else 'show']()
+
class BuilderView extends Backbone.View
SUBVIEWS: []
@@ -145,16 +252,9 @@ class BuilderView extends Backbone.View
if selector?
@setElement $(selector)
- # Create the collection, and bind the appropriate events
- @collection = new FormbuilderCollection
- @collection.bind 'add', @addOne, @
- @collection.bind 'reset', @reset, @
- @collection.bind 'change', @handleFormUpdate, @
- @collection.bind 'destroy add reset', @hideShowNoResponseFields, @
- @collection.bind 'destroy', @ensureEditViewScrolled, @
-
@render()
- @collection.reset(@bootstrapData)
+ @$responseFieldsView = new ResponseFieldsView({builderView: @, bootstrapData: @bootstrapData, el: '.fb-right'})
+ @$responseFieldsView.render()
@bindSaveEvent()
bindSaveEvent: ->
@@ -170,19 +270,14 @@ class BuilderView extends Backbone.View
$(window).bind 'beforeunload', =>
if @formSaved then undefined else Formbuilder.options.dict.UNSAVED_CHANGES
- reset: ->
- @$responseFields.html('')
- @addAll()
-
render: ->
@$el.html Formbuilder.templates['page']()
# Save jQuery objects for easy use
@$fbLeft = @$el.find('.fb-left')
- @$responseFields = @$el.find('.fb-response-fields')
+ @$fbRight = @$el.find('.fb-right')
@bindWindowScrollEvent()
- @hideShowNoResponseFields()
# Render any subviews (this is an easy way of extending the Formbuilder)
new subview({parentView: @}).render() for subview in @SUBVIEWS
@@ -193,7 +288,7 @@ class BuilderView extends Backbone.View
$(window).on 'scroll', =>
return if @$fbLeft.data('locked') == true
newMargin = Math.max(0, $(window).scrollTop() - @$el.offset().top)
- maxMargin = @$responseFields.height()
+ maxMargin = @$fbRight.height()
@$fbLeft.css
'margin-top': Math.min(maxMargin, newMargin)
@@ -209,72 +304,6 @@ class BuilderView extends Backbone.View
if target == '#editField' && !@editView && (first_model = @collection.models[0])
@createAndShowEditView(first_model)
- addOne: (responseField, _, options) ->
- view = new ViewFieldView
- model: responseField
- parentView: @
-
- #####
- # Calculates where to place this new field.
- #
- # Are we replacing a temporarily drag placeholder?
- if options.$replaceEl?
- options.$replaceEl.replaceWith(view.render().el)
-
- # Are we adding to the bottom?
- else if !options.position? || options.position == -1
- @$responseFields.append view.render().el
-
- # Are we adding to the top?
- else if options.position == 0
- @$responseFields.prepend view.render().el
-
- # Are we adding below an existing field?
- else if ($replacePosition = @$responseFields.find(".fb-field-wrapper").eq(options.position))[0]
- $replacePosition.before view.render().el
-
- # Catch-all: add to bottom
- else
- @$responseFields.append view.render().el
-
- setSortable: ->
- @$responseFields.sortable('destroy') if @$responseFields.hasClass('ui-sortable')
- @$responseFields.sortable
- forcePlaceholderSize: true
- placeholder: 'sortable-placeholder'
- stop: (e, ui) =>
- if ui.item.data('field-type')
- rf = @collection.create Formbuilder.helpers.defaultFieldAttrs(ui.item.data('field-type')), {$replaceEl: ui.item}
- @createAndShowEditView(rf)
-
- @handleFormUpdate()
- return true
- update: (e, ui) =>
- # ensureEditViewScrolled, unless we're updating from the draggable
- @ensureEditViewScrolled() unless ui.item.data('field-type')
-
- @setDraggable()
-
- setDraggable: ->
- $addFieldButtons = @$el.find("[data-field-type]")
-
- $addFieldButtons.draggable
- connectToSortable: @$responseFields
- helper: =>
- $helper = $("<div class='response-field-draggable-helper' />")
- $helper.css
- width: @$responseFields.width() # hacky, won't get set without inline style
- height: '80px'
-
- $helper
-
- addAll: ->
- @collection.each @addOne, @
- @setSortable()
-
- hideShowNoResponseFields: ->
- @$el.find(".fb-no-response-fields")[if @collection.length > 0 then 'hide' else 'show']()
-
addField: (e) ->
field_type = $(e.currentTarget).data('field-type')
@createField Formbuilder.helpers.defaultFieldAttrs(field_type)
@@ -287,6 +316,8 @@ class BuilderView extends Backbone.View
createAndShowEditView: (model) ->
$responseFieldEl = @$el.find(".fb-field-wrapper").filter( -> $(@).data('cid') == model.cid )
$responseFieldEl.addClass('editing').siblings('.fb-field-wrapper').removeClass('editing')
+ $responseFieldEl.parents('.fb-field-wrapper').removeClass('editing')
+ $responseFieldEl.find('.fb-field-wrapper').removeClass('editing')
if @editView
if @editView.model.cid is model.cid
@@ -313,7 +344,7 @@ class BuilderView extends Backbone.View
scrollLeftWrapper: ($responseFieldEl) ->
@unlockLeftWrapper()
return unless $responseFieldEl[0]
- $.scrollWindowTo ((@$el.offset().top + $responseFieldEl.offset().top) - @$responseFields.offset().top), 200, =>
+ $.scrollWindowTo ((@$el.offset().top + $responseFieldEl.offset().top) - @$fbRight.offset().top), 200, =>
@lockLeftWrapper()
lockLeftWrapper: ->
@@ -331,8 +362,7 @@ class BuilderView extends Backbone.View
return if @formSaved
@formSaved = true
@saveFormButton.attr('disabled', true).text(Formbuilder.options.dict.ALL_CHANGES_SAVED)
- @collection.sort()
- payload = JSON.stringify fields: @collection.toJSON()
+ payload = JSON.stringify fields: @$responseFieldsView.collection
if Formbuilder.options.HTTP_ENDPOINT then @doAjaxSave(payload)
@formBuilder.trigger 'save', payload
diff --git src/styles/formbuilder.styl src/styles/formbuilder.styl
index 62925db..0fea3bc 100644
--- src/styles/formbuilder.styl
+++ src/styles/formbuilder.styl
@@ -136,6 +136,11 @@ $editing-border = darken($editing-bg, 7%)
}
}
+div[class^="fb-nested"] > .fb-response-fields {
+ padding-bottom: 0px;
+ min-height: 80px;
+}
+
.fb-response-fields {
padding-bottom: 150px;
}
diff --git src/templates/edit/options.html src/templates/edit/options.html
index 6a7ee08..929d8e3 100644
--- src/templates/edit/options.html
+++ src/templates/edit/options.html
@@ -12,6 +12,10 @@
<input type="text" data-rv-input="option:label" class='option-label-input' />
<a class="js-add-option <%= Formbuilder.options.BUTTON_CLASS %>" title="Add Option"><i class='fa fa-plus-circle'></i></a>
<a class="js-remove-option <%= Formbuilder.options.BUTTON_CLASS %>" title="Remove Option"><i class='fa fa-minus-circle'></i></a>
+ <label>
+ <input type='checkbox' data-rv-checked='option:nested_fields' />
+ Nested fields?
+ </label>
</div>
<% if (typeof includeOther !== 'undefined'){ %>
diff --git src/templates/partials/right_side.html src/templates/partials/right_side.html
index 7164c13..5b42836 100644
--- src/templates/partials/right_side.html
+++ src/templates/partials/right_side.html
@@ -1,4 +1,2 @@
<div class='fb-right'>
- <div class='fb-no-response-fields'>No response fields</div>
- <div class='fb-response-fields'></div>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment