Edit on
.content(ng-app="DemoApp" ng-controller="DemoCtrl")
	.row
		.col-md-4
			.panel.panel-default
				.panel-heading
					h4.panel-title Form
				.panel-body
					.list-group
						.list-group-item
							button.btn.btn-success(ng-click="description.add()" type="button")
								i.fa.fa-plus
								|  Add New
						.list-group-item(ng-repeat="row in data.description")
							.form-group
								button.btn.btn-default(bs-select ng-options="type.value as type.label for type in description.types" placeholder="Select Input Type" ng-model="row.type" type="button" required)
								button.btn.btn-danger.pull-right(ng-click="description.remove($index)" type="button")
									i.fa.fa-times-circle
									|  Remove
							.form-group
								input.better-placeholder.form-control(placeholder="Name/Label" ng-model="row.name" required)
							.form-group(ng-if="description.isMultiSelect(row)")
								input.better-placeholder.form-control(placeholder="A comma separated list of options" ng-model="row.options" ng-list=',' required)
		.col-md-8
			.panel.panel-default
				.panel-heading
					h4.panel-title Preview
				.panel-body
					.col-md-6
						h4.text-primary Input Form
						form-description.clearfix(template="data.description" ng-model="description.previewdata")
					.col-md-6
						h4.text-primary Output of Form
						div(ng-bind-html="description.previewdata")
View Compiled
@import "nib"

.better-placeholder
	transition all default-transition-duration ease
	&::placeholder
		opacity 0
	&::-webkit-datetime-edit
		transition all default-transition-duration ease
		opacity 0
	&:focus
		padding-top 18px
		height auto
		&::-webkit-datetime-edit
			opacity 1
	&.fixed-height
		padding-top 18px
		height auto
		&:not(:focus):not(.better-placeholder-active) ~ .better-placeholder-text:not(.active)
			top 14px

.better-placeholder-active, .ui-select-bootstrap.dropdown.open .better-placeholder
	padding-top 18px
	height auto
	&::-webkit-datetime-edit
		opacity 1
	&::placeholder
		opacity 0

.input-group .better-placeholder ~ .input-group-btn .btn, .better-placeholder-button .btn
	transition all default-transition-duration ease
.input-group .better-placeholder-active ~ .input-group-btn .btn, .better-placeholder-button-active .btn
	padding 12px

.dropdown-menu .better-placeholder-text
	z-index 1000
.better-placeholder-text, .help-block.better-placeholder-text
	z-index 10
	transition all default-transition-duration ease
	position absolute
	top 2px
	margin-left 12px
	line-height 20px
	height 20px
	color placeholder
	transform-origin 0 0
	max-width 85%
	margin-right 12px
	white-space nowrap
	overflow hidden
	text-overflow ellipsis
.better-placeholder-text.active, .better-placeholder-active ~ .better-placeholder-text, .better-placeholder:focus ~ .better-placeholder-text
	max-width 110%
	visibility visible
	transform scale(.8)
	transform-origin 0 0
	color #428bca
	top -2px

.better-placeholder.disabled, .better-placeholder.disabled ~ .better-placeholder-text
	color #999

.fa-required
	margin-left 6px
	font-size 70%
	vertical-align super

.form-group
	position relative
View Compiled
templateModule = angular.module 'DemoApp', [
	'mgcrea.ngStrap', 'textAngular'
]
templateModule.run ->
	angular.element.prototype.previous = -> angular.element this[0].previousElementSibling
templateModule.controller 'DemoCtrl', [
	'$scope', '$timeout',
	(scope,$timeout) ->
		scope.data =
			description: [
				name: "Description"
				type: "wysiwyg"
			,
				name: "Select One"
				type: "checkbox"
				options: ['A','B','C']
			]
		
		scope.description =
			add: () -> scope.data.description.push
				type: 'text'
			remove: (index) -> scope.data.description.splice index, 1
			isMultiSelect: (row) -> -1 isnt ['checkbox','radio','select'].indexOf row.type
			previewdata: ''
			types: [
				value: 'radio'
				label: 'Radio Switch'
			,
				value: 'checkbox'
				label: 'Multi Select Box'
			,
				value: 'textarea'
				label: 'Basic Textarea'
			,
				value: 'wysiwyg'
				label: 'Rich Text Editor'
			,
				value: 'select'
				label: 'Dropdown Select'
			,
				value: 'number'
				label: 'Numeric Input'
			,
				value: 'text'
				label: 'Simple Text Input'
			]
]
###
	types are: radio, checkbox, textarea, wysiwyg, select, text/string
###

templateModule.directive 'dynamicInput', ['$compile', ($compile) ->
	getTemplate = (model, options) ->
		optionExp = if options.optionExp? then options.optionExp else if options.type is 'radio' or options.type is 'checkbox' then 'option in options.options' else 'option as option for option in options.options'
		valueExp = if options.valueExp? then options.valueExp else 'option'
		textExp = if options.textExp? then options.textExp else 'option'
		attributes = if options.attributes? then " #{options.attributes}" else ''
		"<span>#{
			if options.type is 'radio' and options.options? and angular.isArray(options.options)
				"""<label>#{options.name}</label><div class="radio" ng-repeat='#{optionExp}'><label><input type="radio" ng-model='$parent.$parent.#{model}' value="{{#{valueExp}}}"#{attributes}> {{#{textExp}}}</label></div>"""
			else if options.type is 'checkbox' and options.options? and angular.isArray(options.options)
				"""<label>#{options.name}</label><div class="checkbox" ng-repeat='#{optionExp}'><label><input type="checkbox" ng-model='$parent.$parent.#{model}[$index]'#{attributes}> {{#{textExp}}}</label></div>"""
			else if options.type is 'textarea'
				"""<textarea class="form-control better-placeholder" placeholder="#{options.name}" ng-model='$parent.#{model}' #{attributes}></textarea>"""
			else if options.type is 'wysiwyg'
				"""<text-angular ng-model='$parent.#{model}' placeholder="#{options.name}" #{attributes}></text-angular>"""
			else if options.type is 'select' and options.options? and angular.isArray(options.options)
				"""<button type='button' class="btn btn-default" ng-model='$parent.#{model}' data-html='1' ng-options='#{optionExp}'#{attributes} bs-select#{if options.default? then ' placeholder="{{options.default}}"' else ''}></button>"""
			else
				"<input class='form-control better-placeholder' ng-placeholder='{{options.name}}' type='#{options.type}' ng-model='$parent.#{model}'#{attributes}>"
		}</span>"
	
	restrict: 'E'
	scope:
		options: '='
	link: (scope, element, attrs) -> element.replaceWith $compile(getTemplate attrs.ngModel, scope.options) scope
]
templateModule.directive 'formDescription', ->
	restrict: 'E'
	require: 'ngModel'
	template: "<div class='form-group clearfix' ng-repeat='options in editingTemplate'><dynamic-input ng-model='fields[options.name]' options='options'></dynamic-input></div>"
	scope:
		template: '='
	link: (scope, element, attrs, ngModel) ->
		scope.fields = {}
		_load = (newVal) ->
			if not newVal? then return
			scope.editingTemplate = []
			for option in newVal
				_option = angular.extend {}, option
				if not _option.attributes? then _option.attributes = ''
				_option.attributes += ' ng-change="$parent.compile()"'
				if (
					(_option.type is 'checkbox' or _option.type is 'radio') and
					(not scope.fields[_option.name]? or not angular.isArray scope.fields[_option.name])
				) then scope.fields[_option.name] = []
				
				scope.editingTemplate.push _option
		_compile = ->
			if not scope.editingTemplate? or scope.editingTemplate.length is 0 then ngModel.$setViewValue ''
			else ngModel.$setViewValue ("<p><b>#{value.name ? ''}</b> #{_getValue scope.fields[value.name], value}</p>" for value in scope.editingTemplate).join ''
		_getValue = (formVal, options) ->
			if options.type is 'checkbox'
				_result = ''
				if angular.isArray formVal
					_result = (formVal.reduce ((result, el, index) ->
						if el then result.push options.options[index]
						result
					), []).join ', '
				_result
			else formVal ? ''
		scope.compile = _compile
		scope.$watch 'fields', _compile, true
		scope.$watch 'template', _load, true
		_load()
		_compile()
templateModule.directive 'betterPlaceholder', ->
	restrict: 'C'
	require: '?ngModel'
	link: (scope, element, attrs, ngModel) ->
		# if ngModel is invalid then it can occur that the viewValue is empty
		isEmpty = (value= if ngModel and ngModel? then ngModel.$viewValue else element.val()) ->
			###
			console.log """
				(
				not element[0].validity.badInput (#{not element[0].validity.badInput}) and # empty if not badInput, if badinput model gets set to undefined but there IS content
				not ( # negate the following test which tests if it has content
					value? (#{value?}) and #value is not null/undefined
					(
						not angular.isString(value) (#{not angular.isString(value)}) or value isnt '' (#{not angular.isString(value) or value isnt ''}) # if it's a string it's not the empty stryng
					) and
					(
						not angular.isArray(value) (#{not angular.isArray(value)}) or value.isEmpty() (#{not angular.isArray(value) or not value.isEmpty()}) # if it's an array (case of using ng-list) it's more than length 0
					) and
					(
						angular.isString(value) or angular.isArray(value) or (
							element.val().trim().length > 0 or # test if the actual content has length, to prevent false positives
							not isNaN(value) # otherwise check it is a number
						) (#{angular.isString(value) or angular.isArray(value) or (
							element.val().trim().length > 0 or # test if the actual content has length, to prevent false positives
							not isNaN(value) # otherwise check it is a number
						)})
					)
				)
			)
			"""
			###
			(
				not element[0].validity.badInput and # empty if not badInput, if badinput model gets set to undefined but there IS content
				not ( # negate the following test which tests if it has content
					value? and #value is not null/undefined
					(
						not angular.isString(value) or value isnt '' # if it's a string it's not the empty stryng
					) and
					(
						not angular.isArray(value) or not value.isEmpty() # if it's an array (case of using ng-list) it's more than length 0
					) and
					(
						angular.isString(value) or angular.isArray(value) or (
							element.val().trim().length > 0 or # test if the actual content has length, to prevent false positives
							not isNaN(value) # otherwise check it is a number
						)
					)
				)
			)
		if attrs.ngPlaceholder?
			_placeholder = attrs.ngPlaceholder
			element.attr 'placeholder', _placeholder
			attrs.$observe 'ngPlaceholder', (val) ->
				element.attr 'placeholder', val
				placeholder.html val
				if attrs.required then placeholder.append required
		else if attrs.placeholder? and attrs.placeholder isnt '' then _placeholder = attrs.placeholder
		else throw "better-placeholder requires an ng-placeholder or placeholder attribute"
		
		required = angular.element '<i class="fa fa-asterisk text-danger fa-required"></i>'
		placeholder = angular.element "<span class='help-block better-placeholder-text'>#{_placeholder}</span>"
		element.after placeholder
		placeholder.on 'click', -> element[0].focus()
		
		attrs.$observe 'required', (val) ->
			if val then placeholder.append required
			else required.remove()
		
		activate = ->
			element.addClass 'better-placeholder-active'
			placeholder.addClass 'active'
			if element.previous()? and element.previous().hasClass 'input-group-btn' then element.previous().addClass 'better-placeholder-button-active'
		deactivate = ->
			element.removeClass 'better-placeholder-active'
			placeholder.removeClass 'active'
			if element.previous()? and element.previous().hasClass 'input-group-btn' then element.previous().removeClass 'better-placeholder-button-active'
		
		# catch changes from the DOM
		element.on 'focus', activate
		element.on 'blur', -> if isEmpty() then deactivate()
		element.on 'change', ->
			if isEmpty() then deactivate()
			else activate()
		if element.previous()? and element.previous().hasClass 'input-group-btn' then element.previous().addClass 'better-placeholder-button'
		if ngModel? then ngModel.$formatters.push (value) ->
			if isEmpty value then deactivate()
			else activate()
			value
		else if not isEmpty() then activate()
View Compiled
Rerun