How to bind one model to multiple inputs with Angular JS

11,015

Solution 1

A directive is probably best, but these examples look overly complex. Anyway if you are hoping to avoid a directive, just use $scope.$watch and re-build your date string each time one of the important variables are updated.

Something like this might be in your controller:

$scope.year = '';
$scope.month = '';
$scope.day = '';

// this might be able to be refactored
$scope.$watch('year', buildDate);
$scope.$watch('month', buildDate);
$scope.$watch('day', buildDate);

function buildDate() {
  $scope.date = $scope.year + "-" + $scope.month + "-" + $scope.day;
}

As a side note, this is probably what my directive logic would look like too.

Edit: Code cleanup and fiddle

Cleaner example - I prefer this because it groups all the date-related items with an object, which also makes watching for changes easier.

$scope.date = {
    year: '',
    month: '',
    day: ''
};

// use watch collection to watch properties on the main 'date' object
$scope.$watchCollection('date', buildDate);

function buildDate(date) {
  $scope.dateString = date.year + "-" + date.month + "-" + date.day;
}

Fiddle

Solution 2

here's an interesting demo that uses custom directives that are a lot less intimidating than the ones you linked to. You should be able to apply them to your inputs without too much conflict with other stuff:

http://plnkr.co/edit/3a8xnCfJFJrBlDRTUBG7?p=preview

The trick is setting the parser and formatter for a model using the directive. This lets you intercept changes to the model and interact with the rest of your scope:

app.controller('MainCtrl', function($scope) {
  $scope.name = 'World';
  $scope.date = new Date();
});

app.directive('datePartInput', function() {
  return {
    restrict: 'A',
    require: 'ngModel',
    link: function(scope, elem, attrs, ngModel) {
      var part = attrs.part;
      var modelToUser, userToModel
      console.log('part:', part);
      if (part == 'year') {
        modelToUser = function(date) {
          return date.getFullYear();
        }
        userToModel = function(year) {
          ngModel.$modelValue.setYear(year);
          return ngModel.$modelValue
        }
      }
      else if (part == 'month') {
        modelToUser = function(date) {
          return date.getMonth();
        }
        userToModel = function(month) {
          ngModel.$modelValue.setMonth(month);
          return ngModel.$modelValue;
        }
      }
      else if (part == 'day') {
        modelToUser = function(date) {
          return date.getUTCDate();
        };
        userToModel = function(day) {
          ngModel.$modelValue.setUTCDate(day);
          return ngModel.$modelValue;
        };
      }
      ngModel.$formatters.push(modelToUser);
      ngModel.$parsers.push(userToModel);
    }
  }
})

And the template:

<body ng-controller="MainCtrl">
  <p>Hello {{name}}!</p>
  {{date |  date}}
  <input date-part-input part="year" ng-model="date">
  <input date-part-input part="month" ng-model="date">
  <input date-part-input part="day" ng-model="date">
</body>

Solution 3

You can create a re-usable directive that has the three fields in it so it can be used for all date fields. The model for the directive is aliased to date on the isolated scope. To get each of the date parts the date is then split and then year, month and day are assigned to scope properties. Then when one of the fields is changed the date property is updated by appending them together with the - separator.

For this directive I've just hard coded, years months and days. I'd recommend to use some javascript date functions to populate them so they aren't hard coded.

angular
.module('app')
.directive('dateSelect', function (){
    return {
        restrict: 'E',
        replace: true,
        scope: {
          date:'=ngModel'
        },
        template: '<div class="dateSelect"><div class="dateField"><selectize placeholder="Select a year..." config="yearConfig" ng-model="year" ng-change="dateChanged()"></selectize></div>' +
        '<div class="dateField"><selectize placeholder="Select a month..." config="monthConfig" ng-model="month" ng-change="dateChanged()"></selectize></div>' + 
        '<div class="dateField"><selectize placeholder="Select a day..." config="dayConfig" ng-model="day" ng-change="dateChanged()"></selectize></div></div>',
        controller: function ($scope) {
          $scope.yearConfig = {
          options: [{value: 2013, text: '2013'}, {value: 2014, text:'2014'}, {value: 2015, text:'2015'}],
          create: true,
          sortField: 'value',
          maxItems: 1,
        };
        $scope.monthConfig = {
          options: [{value: '01', text: '1'}, {value: '02', text: '2'}, {value: '03', text:'3'}, 
          {value: '04', text: '4'}, {value: '05', text:'5'}, {value: '06', text:'6'}, {value: '07', text: '7'}, {value: '08', text:'8'}, {value: '09', text:'9'},
          {value: '10', text: '10'}, {value: '11', text:'11'}, {value: '12', text:'12'}],
          create: true,
          sortField: 'value',
          maxItems: 1,
        };

        $scope.dayConfig = {
          options: [{value: '01', text: '1'}, {value: '02', text: '2'}, {value: '03', text:'3'}, 
          {value: '04', text: '4'}, {value: '05', text:'5'}, {value: '06', text:'6'}, {value: '07', text: '7'}, {value: '08', text:'8'}, {value: '09', text:'9'},
          {value: '10', text: '10'}, {value: '11', text:'11'}, {value: '12', text:'12'}],
          create: true,
          sortField: 'value',
          maxItems: 1,
        };

        $scope.dateChanged = function () {
          if (!angular.isUndefined($scope.year) && !angular.isUndefined($scope.month) && !angular.isUndefined($scope.day)) {
            $scope.date = $scope.year + "-" + $scope.month + "-" + $scope.day;
          }
        }

        if (!angular.isUndefined($scope.date)) {
          var dateParts = $scope.date.split("-");

          if (dateParts.length === 3) {
            $scope.year = dateParts[0];
            $scope.month = dateParts[1];
            $scope.day = dateParts[2];
          }
        }
      }
    };
});

Plunkr

Share:
11,015
emersonthis
Author by

emersonthis

I am a designer, developer, and problem solver. I make websites and stuff. I work with brazen startups, modest individuals, earnest small business, and everyone in between. I care as much about how things look as how they work. I enjoy writing and teaching what I know. The best part about my job is constantly learning new things.

Updated on July 24, 2022

Comments

  • emersonthis
    emersonthis almost 2 years

    I have an form input which is for a MySQL date field. Ex: 2015-01-31.

    I want to allow the user to input this using 3 different form inputs. One for the year, one for the month, and one for the day.

    Obviously ng-model isn't going to work right out of the box, because I'm trying to bind just one part of the date string to each input. I'm pretty sure the way to do it is bye creating three "temporary" scope vars/models

    $scope.year;
    $scope.month;
    $scope.day;
    

    ...and then somehow combine/binding them to the actual value.

    //If only it were this easy!
    $scope.date = $scope.year + "-" + $scope.month + "-" + $scope.day;
    

    The line above of course won't work because the values aren't two-way-bound. If the form were only for saving new data, I could could get away with that by just combining the inputs on submit. But I need it to handle/show existing data also. And it's going to get super ugly if I can't figure out a way to wrangle Angular's binding magic to do what I want.

    I found this question which I think is trying to do the same thing, but they solve it with a custom directive, which is something I'm hoping to avoid. I know this may be a more maintainable/portable/modular way to do it, but I'm new to Angular and a bit intimidated by that. Also, the inputs are using the lovely angular-selectize directive, which adds an additional layer of complexity to that approach.