11

I want to render a form, based on a dynamic field configuration:

$scope.fields = [
    { title: 'Label 1', type: 'text', value: 'value1'},
    { title: 'Label 2', type: 'textarea', value: 'value2'}
];

This should output something that behaves like:

<div>
    <label>{{field.title}}<br />
        <input type="text" ng-model="field.value"/>
    </label>
</div>
<div>
    <label>{{field.title}}<br />
        <textarea ng-model="field.value" rows="5" cols="50"></textarea>
    </label>
</div>

The simple implementation would be to use if statements to render the templates for each field type. However, as Angular doesn't support if statements, I'm lead to the direction of directives. My problem is understanding how the data binding works. The documentation for directives is a bit dense and theoretical.

I've mocked up a bare bones example of what I try to do here: http://jsfiddle.net/gunnarlium/aj8G3/4/

The problem is that the form fields aren't bound to the model, so the $scope.fields in submit() isn't updated. I suspect the content of my directive function is quite wrong ... :)

Going forward, I need to also support other field types, like radio buttons, check boxes, selects, etc.

1

3 Answers 3

23

The first problem you are running into regardless of the directive you are trying to create is using ng-repeat within a form with form elements. It can be tricky do to how ng-repeat creates a new scope.

This directive creates new scope.

I recommend also instead of using element.html that you use ngSwitch instead in a partial template.

<div class="form-row" data-ng-switch on="field.type">
    <div data-ng-switch-when="text">
        {{ field.title }}: <input type="text" data-ng-model="field.value" />
    </div>
    <div data-ng-switch-when="textarea">
        {{ field.title }}: <textarea data-ng-model="field.value"></textarea>
    </div>
</div>

This still leaves you with the problem of modifying form elements in child scope due to ng-repeat and for that I suggest using the ngChange method on each element to set the value when an item has changed. This is one of the few items that I don't think AngularJS handles very well at this time.

Sign up to request clarification or add additional context in comments.

2 Comments

Actually, ngSwitch was exactly what I needed. No scope issues encountered :) (yet). I still don't quite understand directives, but that's a task for another day ..
@GunnarLium Were you able to solve this problem? I'm trying to implement a similar solution but got stuck because of the same exact issues.. Wondering if you can share your insights if/how you solved the problem
4

You might consider Metawidget for this. It uses JSON schema, but is otherwise very close to your use case. Complete sample:

<html ng-app="myApp">
    <head>
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js" type="text/javascript"></script>
        <script src="http://metawidget.org/js/3.5/metawidget-core.min.js" type="text/javascript"></script>
        <script src="http://metawidget.org/js/3.5/metawidget-angular.min.js" type="text/javascript"></script>
        <script type="text/javascript">
            angular.module( 'myApp', [ 'metawidget' ] )
                .controller( 'myController', function( $scope ) {

                    $scope.metawidgetConfig = {
                        inspector: function() {
                            return {
                                properties: {
                                    label1: {
                                        type: 'string'
                                    },
                                    label2: {
                                        type: 'string',
                                        large: true
                                    }
                                }
                            }
                        }
                    }

                    $scope.saveTo = {
                        label1: 'value1',
                        label2: 'value2'
                    }

                    $scope.save = function() {
                        console.log( $scope.saveTo );
                    }
                } );
        </script>
    </head>
    <body ng-controller="myController">
        <metawidget ng-model="saveTo" config="metawidgetConfig">
        </metawidget>
        <button ng-click="save()">Save</button>
    </body>
</html>

Comments

0

The type attribute can be changed when the element is out of DOM, so why not a small directive which removes it from DOM, changes it type and then add back to the same place?

The $watch is optional, as the objective can be change it dynamically once and not keep changing it.

var app = angular.module('app', []);

app.controller('MainCtrl', function($scope) {
  $scope.rangeType = 'range';
  $scope.newType = 'date'
});

app.directive('dynamicInput', function(){
    return {
      restrict: "A",
      link: linkFunction
    };
    
    function linkFunction($scope, $element, $attrs){
      
      if($attrs.watch){
        $scope.$watch(function(){ return $attrs.dynamicInput; }, function(newValue){
          changeType(newValue);
        })
      }
      else
        changeType($attrs.dynamicInput);
      
      
      function changeType(type){
        var prev = $element[0].previousSibling;
        var parent = $element.parent();
        $element.remove().attr('type', type);
        if(prev)
          angular.element(prev).after($element);
        else
          parent.append($element);
      }
    }
});
span {
font-size: .7em;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="MainCtrl">
    <h2>Watching Type Change</h2>
    Enter Type: <input ng-model="newType" /><br/>
    Using Type (with siblings): <span>Before</span><input dynamic-input="{{newType}}" watch="true" /><span>After</span><Br>
    Using Type (without siblings): <div><input dynamic-input="{{newType}}" watch="true" /></div>
    
    <br/><br/><br/>
    <h2>Without Watch</h3>
    Checkbox: <input dynamic-input="checkbox" /><br />
    Password: <input dynamic-input="{{ 'password' }}" value="password"/><br />
    Radio: <input dynamic-input="radio"  /><br/>
    Range: <input dynamic-input="{{ rangeType }}" />
</div>

Tested in latest Chrome and IE11.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.