1

I have an array of objects I am using to render radio button lists using ng-repeat. My question is how can I bind the selected value of each question and bind those to some type of model. Here is the plunker I have so far, http://plnkr.co/edit/oXr6Un?p=preview but the code just loops through the radio button lists and get the selected values. I truly want that object to be bound and change its properties when the user selects an answer. how can I do this? Please understand I am very new to Angularjs. Thanks All, ck

Hcabnettek
  • 12,678
  • 38
  • 124
  • 190

3 Answers3

2

Like @Max said above, you need to set the ng-model on your inputs... However that's not all you need to do. I've forked your plunk and updated it to show you something that works

A few things to note:

  • You didn't need a service to "showAnswers", and you should never have to get values from inputs via JQuery while you're using Angular.
  • You needed to add the ng-model in the input, but you need to use $parent.answer in the ng-model, because the input was inside of an ng-repeat which has it's own child scope.
  • You don't need for="" on labels that wrap the input they're labeling.

Important Changes:

    <li ng-repeat="answer in question.answers">
      <label>
        <input type="radio" ng-model="$parent.question.selectedAnswer"
         value="{{ answer.answerText }}"  name="quest_{{$parent.$index}}_answers" />{{ answer.answerText }}</label>
    </li>

and

$scope.selectedAnswers = function(){
   var answers = [];
   angular.forEach($scope.questions, function(question) {
    answers.push(question.selectedAnswer);
   });
   return answers;
 };

I hope that's helpful.

Ben Lesh
  • 107,825
  • 47
  • 247
  • 232
  • Awesome! my next step is to convert to directives to clean up the html. How can I access $parent from a template? It doesn't seem to work. http://plnkr.co/edit/oXr6Un?p=preview – Hcabnettek Nov 29 '12 at 22:07
  • Here, I've updated your plunk to work: http://plnkr.co/edit/gFMISh ... notice I've moved your selectedAnswers() repeat inside of the QuizController block... also, I've added scope parameters to each directive, for example `scope: { 'question' : '=' }` (which sets up a two way binding between the directive's `$scope.question` and whatever is passed into the `question="???"` attribute. As well as `restrict`'s to restrict those directives to their parents (not really necessary, but I did it just to show you). – Ben Lesh Nov 30 '12 at 14:32
  • @blesh, I think you can remove the "require: 'question'" and "require: 'quiz'" lines. These directives aren't trying to access any controllers with those names. "restrict: 'E'" is required on at least the quiz and question directives, since they are being used as element names. – Mark Rajcok Dec 01 '12 at 05:31
1

You have to set the ng-model on the input (http://docs.angularjs.org/api/ng.directive:input.radio). With that, you will get two way data binding.

Max
  • 8,671
  • 4
  • 33
  • 46
1

Here is how I would write the directive version: http://plnkr.co/edit/Otikw0fX8rwx2gM8DMMl?p=preview

I streamlined the model. Each question and its possible answers look like this:

{ question: "Why is the sky blue?",
  answerChoices: [ "blah blah 1", "Rayleigh scattering", "blah blah 3" ] },

To each such object we will dynamically add an 'answer' element via the radio buttons. More about that later.

I streamlined the factory. It simply returns an array of objects:

.factory('questionFactory', function() {
  return [
   { question: ...

The controller can now simply do this:

$scope.questions_and_answers = questionFactory;

Now we have our model defined. Next, I like to think about what directives I'll need, and what the templates and behaviors of each directive will be. I kept your 3 directives -- quiz, question, answer -- but I renamed answer to possibleAnswer.

As a matter of personal choice, I thought it would be better to have the 'quiz' directive contain the main structure for the questions and answers, so I moved the ng-repeat for the answers out of the question directive's template and into the quiz directive's template. I also left quiz an element, but I changed question and possibleAnswer to attributes since I personally like to see a more familiar markup structure like
<li ng-repeat ...> something here </li>
instead of the unfamiliar
<li answer ng-repeat ...></li>

.directive('quiz', function() {
...
  template:'<ul class="unstyled">' +
    '<li ng-repeat="item in questionsAndAnswers">' + 
      '<span question="{{item.question}}"></span>' +
      '<ul class="unstyled">' + 
        '<li ng-repeat="possibleAnswer in item.answerChoices">' +
          '<span possible-answer="{{possibleAnswer}}" item=item id="{{$parent.$index}}"></span>' +
        '</li>' +
      '</ul>' +
    '</li></ul>'

The question directive template now just deals with the question:

template:'<strong class="question">{{ question }}</strong>'     

And the possibleAnswer directive just deals with one possible answer (like you already had):

template: '<label><input type="radio" ng-value="possibleAnswer"' + 
    ' ng-model="item.answer" name="q{{id}}" />{{possibleAnswer}}</label>'

There's a lot going on in those templates, and to understand what I did, we now need to talk about directive scope inheritance. I'm using isolated scope for all of the directives -- i.e., scope: { ... } -- since that is normally the best choice for reusable components. (See here for other choices.) The questions and answer choices will not be modified by the directives, so we don't need 2-way binding, only one-way binding (parent scope -> directive scope), so '@' is the appropriate choice:

.directive('question', function() {
  ...
  scope: { question: '@' }, 

.directive('possibleAnswer', function() {
  ...
  scope: { possibleAnswer: '@', ... }, 

We need to pass the directives an evaluated copy of the questions and possible answers from the parent scope, so we use the {{}} notation:

<span question="{{item.question}}">
<span possible-answer="{{possibleAnswer}}" ...

Now for the tricky part. The radio buttons associated with a single question all need the same name, and as @Max already mentioned, we need 2-way binding back to the parent model for all the radio buttons. I solved "same name" issue by using one-way binding of a question ID, which is really the ng-repeat question $index:

<span possible-answer="{{possibleAnswer}}" id="{{$parent.$index}}" ...>

.directive('possibleAnswer', function() {
  ...
  scope: { id: '@', ... },
  ...
  template: '...<input type="radio" ... name="q{{id}}"...

Since we want the same question ID/index (not the answer choice ID/index), we look up the the question scope using $parent and grab its ng-repeat $index, hence $parent.$index. An alternative would be to add an 'id' property to each object in the main model (as you have) and pass that to the directive:

<span possible-answer="..." id="{{item.id}}" ...>

(Actually, since below you'll see that we're passing 'item' into the directive, such an id property would come along for free, and hence there would not be any need for "id='...'" in the template.) I prefer to make the model as simple as possible. Since we're using an array of objects, the array index (ng-repeat $index) is sufficient, so I did not use an id property on the main model.

To obtain 2-way data binding, we need to use '='. We want to bind to a new 'answer' property that we're going to dynamically add to the objects in the main model (I mentioned this earlier). So we need a reference to the appropriate object in the model, while we're looping through the answer choices. We then assign this new, dynamic property to ng-model:

<li ng-repeat="item in questionsAndAnswers">' +   // pass this 'item' to possibleAnswer
   ...
   '<ul ..>' + 
      '<li ng-repeat="possibleAnswer in item.answerChoices">' +
           <span possible-answer="{{possibleAnswer}}" item=item ...">

.directive('possibleAnswer', function() {
  ...
  scope: { item: '=', ... },
  ...
  template: '...<input type="radio" ... ng-model="item.answer" ...'  // new property

NOTE: 'ng-value' should be used instead of 'value', according to a Disqus comment on the input radio API page.

template: '...<input type="radio" ng-value="possibleAnswer" ...

For the quiz element directive, I think the component is more reusable if you pass in the model, rather than assuming that a certain scope property exists in the parent scope, hence:

<quiz questions-and-answers=questions_and_answers></quiz>

Since the possibleAnswer directive needs to modify the model, we also have to use 2-way binding here as well, hence '=':

.directive('quiz', function() {
  ...
  scope: { questionsAndAnswers: '='},

Lastly, the controller: I removed function selectedAnswer() and slightly modified the ng-repeat in index.html to obtain the same functionality:

<div ng-repeat="item in questions_and_answers">
   A{{$index}}: {{item.answer}}

So the controller is nice and thin (a recommended best practice) -- one line of code.

There was no use of jQuery, so I removed it. (I didn't see any use of bootstrap.js either, and it was throwing an error, so I removed that too.)

This was a great exercise for me, thanks for the OP. (Sorry if this answer is way too long. I got carried away, but writing it all down helped clarify a few things for me.)

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492