Angular - how to switch between expressions without losing user input - angularjs

I'm new to Angular so please ELI5 if possible :)
I have a multiple-choice questionnaire with the setup for each question as below:
Question
Choice A
Choice B
Choice C
My controllers look like this:
scope.questionnaire = [
{
questionEN: "Thing?",
choice1EN: "Yes",
choice2EN: "No",
choice3EN: "Maybe",
questionPR: "Treasure?",
choice1PR: "Yarrr",
choice2PR: "Arrrrr",
choice3PR: "Parrrrley"
}
]
My current questionnaire looks like this:
<div ng-bind="questionnaire.questionEN"></div>
<input type="radio" name="{{questionnaire.questionEN}}" ng-model="questionnaire.choice" ng-value="questionnaire.choice1EN">{{questionnaire.choice1EN}}</input> <br />
<input type="radio" name="{{questionnaire.questionEN}}" ng-model="questionnaire.choice" ng-value="questionnaire.choice2EN">{{questionnaire.choice2EN}}</input> <br />
<input type="radio" name="{{questionnaire.questionEN}}" ng-model="questionnaire.choice" ng-value="questionnaire.choice3EN">{{questionnaire.choice3EN}}</input>
What I want to do is that at the start of the questionnaire, there are two buttons that will let the user select between languages.
[English] [Pirate]
So I have two questions, actually:
How do I do the language swapping without losing the user input (for example: they have answered questions 1-5 in [English], and while reading #6, scroll back up and hit the button to swap to [Pirate]. Their selected answers for 1-5 should remain, but all questions+choices have been "translated").
Is there a better way of arranging my controllers?

The answer to this really varies on your particular set of needs. Start with developing a logical model of the problem you are trying to solve.
Things to ask yourself:
Are the questions dynamic (loaded from a database) or static?
How do I need to store the answer? Can I store a choice ID, key, etc. or do I have to use the human readable value?
Does the model that captures the answers have to be different than the model that presents the question?
Do you need to store the language they selected along with the answers?
As you think about it more, other questions will follow.
Anyways, to give you something close to an answer here is a suggestion. If the questions are dynamic, you'll want to create a better structure for defining questions and the choices that are related to them. I'd also give them a key or ID so they can be referred to as concrete concepts. Not only does this help tie the model to database entries, making storing and retrieving easy, it makes it easier for you to handle the language issue.
Here's an example of such a design. You may wish to track the answers separately if your design requires, in my case I just kept it simple and kept the questions and answers in one model.
The question model is an array of objects that look like this:
{
key: 'q1', // unique key for question
text: { // a dictionary of language codes and the text of the question in that language
'en': 'What is the answer the question 1?',
'pr': 'Ahoy 1?'
},
choices: [{
key: 'q1a1', // a unique key for the choice
text: { // a dictionary of language codes and the text of the choice in that language
'en': 'Answer 1',
'pr': 'Arr 1'
}
}, {
key: 'q1a2',
text: {
'en': 'Answer 2',
'pr': 'Arr 2'
}
}],
value: null // the field that stores the user's selected choice
}
There is a helper function to get the language-specific text of the selected choice:
$scope.getAnswer = function(question) {
var answer = $filter('filter')(question.choices, {key: question.value});
if (answer.length == 1) return answer[0].text[$scope.selectedLanguage];
return '(no answer)';
};
The meat of your HTML template that displays the questions looks like this:
<div ng-repeat="question in model">
<label>{{question.text[selectedLanguage]}}</label>
<div>
<label ng-repeat="choice in question.choices">
<input type="radio" name="{{question.key}}" ng-value="choice.key" ng-model="question.value" />{{choice.text[selectedLanguage]}}
</label>
</div>
</div>
Alternative
For the heck of it, here's yet another example of how you might do it. This method is useful if you do not have dynamic question/choice data, but with some effort could still be used for dynamic data. It involves keeping localized UI strings in separate tables, assigning questions and choices unique keys, and then using a translation service to retrieve the correct string. In this example, I use the angular-translate module. I load it with tables of localization strings during .config of my module, specify a default language, and just use the translate filter when displaying questions and choices. This lets me greatly simplify my question model and get rid of some extra work in the views and controller.
My questions simply become:
{
key: 'q1',
choices: [
'q1a1',
'q1a2'
],
value: null
}
I add a watch on the language dropdown to change the current language:
$scope.$watch('selectedLanguage', function(value) {
if (value === null) return;
$translate.use(value);
});
And clean up the template:
<label>{{question.key | translate }}</label>
<div>
<label ng-repeat="choice in question.choices">
<input type="radio" name="{{question.key}}" ng-value="choice" ng-model="question.value" />{{ choice | translate }}
</label>
</div>

You could add a function using ng-click then set your $scope to a new questionnaire.
So you would have something like this:
var english = {
question: "Thing?",
answers: {
choice1: {text: "Yes", val: 1},
choice2: {text: "No", val: 2},
choice3: {text: "Maybe", val: 3}
}
}
var pirate= {
question: "Treasure?",
answers: {
choice1: {text: "Yarrr", val: 1},
choice2: {text: "Arrrrr", val: 2},
choice3: {text: "Parrrrley", val: 3}
}
}
// default
$scope.questionnaire = english;
$scope.choice;
// change language
$scope.changeLanguage = function (lang) {
switch (lang) {
case 0:
$scope.questionnaire = english;
break;
case 1:
$scope.questionnaire = pirate;
break;
}
}
Then for your buttons
<button ng-click="changeLanguage(0)">English</button>
<button ng-click="changeLanguage(1)">Pirate</button>
and finally, I would suggest using ng-repeat in your questionnaire
<div ng-bind="questionnaire.question"></div>
<label ng-repeat="answer in questionnaire.answers" ><input type="radio" name="{{questionnaire.question}}" ng-model="choice" ng-value="answer.val" />{{answer.text}} <br /></label>

Related

Vue JS for updating checkboxes based on radio selection

I basically have the same question as this guy, but using Vue JS instead of jQuery.
I have a list of N groups bound to my array ensemble_groups and represented by radio buttons. Selected value is mapped to selected_group.
I have a list of actors bound to my array cast with the variables actor_id, actor_name and groups. Each actor is pre-assigned to any number of groups. They're represented by checkboxes and mapped to an array visible_actors (when checked).
Here's my Vue JS powering the above data (I imagine the method is all jacked up, and I probably need a computed property of some sort):
new Vue({
el: '#schedule-builder',
data: {
selected_group: 'Entire Cast',
visible_actors: [],
ensemble_groups: [
"Entire Cast",
"Leads",
"Dancers",
"Children",
"Deselect All",
],
cast: [
{
actor_id: "123",
actor_name: "Carl",
groups: ["Entire Cast","Leads",],
},
{
actor_id: "234",
actor_name: "Max",
groups: ["Entire Cast","Leads","Children",],
},
{
actor_id: "345",
actor_name: "Sheryl",
groups: ["Entire Cast","Dancers",],
},
{
actor_id: "456",
actor_name: "Chip",
groups: ["Entire Cast","Children",],
},
],
},
methods: {
selectGroup: function() {
// uncheck all
visible_actors=[];
// add people in this group to visible_actors
for person in cast {
if (person.groups.indexOf(selected_group) > -1) {
visible_actors.push(person.actor_id);
}
}
}
})
When a user clicks on a radio button for a group, I want to select only the actors' checkboxes who are in that group. So if the user selects "Children", only the actors in the "Children" group should be checked (Max and Chip, per my example).
And, obviously, checking an actor (rather than a group) shouldn't affect the rest of the selections. I mention this because I got it partially working at one point, where selecting a group also selected the correct people, but when I clicked on a person suddenly everyone else was deselected. User can click either a group OR a person, and the expected behavior is that
Here's my HTML template:
<div id="schedule-builder">
<div class="select-groups">
<h3>Ensemble Groups</h3>
<template v-for="group in ensemble_groups">
<input name="select_group[]" id="group_#{{ $index }}"
v-model="selected_group"
:value="group"
#click="selectGroup"
type="radio">
<label for="group_#{{ $index }}">#{{ group }}</label>
</template>
</div>
<div class="select-individuals">
<h3>Cast Members</h3>
<template v-for="person in cast">
<input name="actors[]" id="actor-#{{ $index }}"
v-model="visible_actors"
:value="person.actor_id"
:checked="visible_actors.indexOf(person.actor_id) > -1"
type="checkbox">
<label for="actor-#{{ $index }}">
#{{ person.actor_name }}
</label>
</template>
</div>
</div>
Any help is appreciated... I've been banging my head on it for a couple days already.
This is a tough question to answer well but I'll try.
I would not rely on a computed property for the checked state. Let v-model handle that for you. You can do
<input
type="checkbox"
name="actors[]"
v-model="selected_actors"
:value="actor">
and that will manage the array of selected_actors for you as their values change.
I'm at work and plan on elaborating on this answer a little later but here's a fiddle of how I'd approach the situation: https://jsfiddle.net/crswll/806shzzg/7/

How to run multiple successive select buttons in AngularJS

I facing an issue with having multiple selects in angularJS where each one of them is linked to the previous one and the value depended on the previous item selected which looks like could be done easily by angular but I am having a hard time figuring out how do I make the index of one select be passed to another select and at the same time making it unresponsive until some value is selected.
I also created a fiddle for the same for people to fiddle around with it.
Here is the concerned HTML
<div ng-app="myApp">
<div ng-controller="testController">
<select ng-model="carBrand" name="carBrand" required ng-options=" brand for brand in brands"></select>
<select ng-model="carModel" name="carModel" required ng-options="model.name for model in cars[0]"></select>
<!--I want the car brand(cars[0]) to be dynamic here. It should be prefreberably blacked out or uneditable until a Car brand is selected and once that particular brand is selected all the models pertaining to that brand only should be displayed in the ajoining select button-->
</div>
</div>
and an example app.js. Find the complete one at the fiddle
var app = angular.module('myApp', []);
app.controller("testController", function($scope) {
$scope.brands = ['Ford', 'Honda', 'Hyundai', 'Mahindra',
'Maruti Suzuki', 'Nissan', 'Renault', 'Skoda', 'Tata', 'Toyota', 'Volksvagen'
];
$scope.carBrand = $scope.brands[0];
$scope.cars = [];
/*These cars[0] and cars[1] are static declared but could well be called from a REST API endpoint in angular. For simplicity lets say they are already present. */
$scope.cars[0] = $scope.cars[0] = [{
name: "Figo",
capacity: 45
}, {
name: "Ecosport",
capacity: 52
}, {
name: "Fiesta",
capacity: 45
}, {
name: "Endeavour",
capacity: 71
}];
});
How do I solve the issue of getting an index from one select and passing it to the other to make this work and probably an additional perk would be to make it unresponsive in case no brand is selected.
Try ng-change:
<select ng-model="carBrand" name="carBrand" required ng-options=" brand for brand in brands"
ng-change="selectedCar(carBrand)"></select>
This returns the index of the selected brand:
$scope.selectedCar = function(brand) {
$scope.carIndex = $scope.brands.indexOf(brand);
};
Use it with the other dropdown as:
<select ng-model="carModel" name="carModel" required
ng-options="model.name for model in cars[carIndex]"></select>
Working Fiddle
When you select something from the first select, carBrand goes from undefined to the selected brand. You thus want the second select to be disabled if the carBrand is undefined (falsy):
<select ng-disabled="!carBrand" ...>
Then, you need to second select to contain the models associated to the selected brand (which is carBrand). So you need something like
<select ng-options="model.name for model in getModelsOfBrand(carBrand)" ...>
Now just implement this getModelsOfBrand(carBrand) function in the scope. It would be much easier if you had a better object model, like for example:
$scope.brands = [
{
name: 'Ford',
models: [
{
name: 'Figo',
capacity: 45
},
...
]
},
...
];
Then it would be as easy as
<select ng-options="model.name for model in carBrand.models" ...>

How do perform an "except" filter in Angular?

Suppose that I have an Angular view that allows a user to check books out of a library. My data model consists of two arrays of Book entities which each have a unique ID field plus a title field. The first array contains an entity for every book in the library and the second array contains an entity for every book that the user has checked out.
libraryBooks = [{
id: 0,
title: "The Adventure of Tom Sawyer"}, {
id: 1,
title: "Moby Dick" }, {
id: 2,
title: "To Kill a Mockingbird" }, {
id: 3,
title: "The Three Little Pigs" }];
checkedOutBooks = [{
id: 0,
title: "The Adventure of Tom Sawyer"}, {
id: 3,
title: "The Three Little Pigs" }];
In short, the library has four books and the user has checked out two. If I want to list the books from both arrays, I can write this:
<h1>Library Books</h1>
<div ng-repeat="book in libraryBooks">
{{ book.title }}
</div>
<h1>Checked out Books</h1>
<div ng-repeat="book in checkedOutBooks">
{{ book.title }}
</div>
Suppose I want to display a third list: the subset of library books that the user has not checked out.
I have seen examples where the Angular "filter" is used to specify one particular value that should not be matched in order to narrow down a list, but in this case, I want to exclude multiple values, so how do I go about doing this?
I have seen examples where a custom filter is added to an Angular module, but I think that in this case, any custom filter should be scoped to this controller.
I've got this figured out. The solution is to write a filter function and attach it to $scope like so:
function filter_notCheckedOut(book) {
var i;
for (i = 0; i < that.libraryBooks.length; i += 1) {
if (that.libraryBooks[i].id === page.id) {
return false;
}
}
return true;
}
In the view, it can then be referenced like this:
<h1>Books not checked out</h1>
<div ng-repeat="book in libraryBooks | filter:filter_notCheckedOut">
{{ book.title }}
</div>

How can I move this code from static AngularJS/jQuery to dynamic AngularJS?

My goal is to create in pure AngularJS a fill-in-the-blank activity which takes collection of records that contain phrases with markers where the blanks are, the correct answers, and incorrect answers to display along with them.
I've programmed a prototype in AngularJS/jQuery, below, which has the interactive functionality that I want, also here: http://jsfiddle.net/kkLdzngv/2/
What do I need to do to now is:
replace all the jQuery code with AngularJS code
make it dynamic so that not just one phrase is displayed, but all phrases in the collection are displayed with input fields each with the functionality that is in my prototype
In particular, I don't understand the correct AngularJS approach. I assume I need to use a ng-repeat to iterate through $scope.texts and inside the ng-repeat I would have my <div class="item">. But at that point, what functionality of AngularJS do I need to use to create the functionality shown in the prototype?
HTML:
<div class="item">
<div class="text">Customer 1 is from <input id="record_1_blank_1" ng-change="checkEntry_record_1_blank_1()" ng-model="record_1_blank_1"/> and Customer 2 is from <input id="record_1_blank_2" ng-model="record_1_blank_2"/>.</div>
<div class="answers">Munich, <span id="answer2">Berlin</span>, Frankfurt, Stuttgart, Hamburg</div>
</div>
JavaScript:
$scope.texts = [
{
body: 'Customer 1 is from [##blank] and Customer 2 is from [##blank].',
correctAnswers: 'Berlin;Hamburg',
incorrectAnswers: 'Stuttgart;Munich;Frankfurt'
},
{
body: 'Company 3 is located in [##blank].',
answers: 'Bremen'
}
];
$scope.checkEntry_record_1_blank_1 = function (obj) {
if ($scope.record_1_blank_1 == 'Berlin') {
$('#record_1_blank_1').css(
{
background: 'lightgreen'
})
.attr('disabled', 'disable');
$('#record_1_blank_2').focus();
$('#answer2').css('text-decoration', 'line-through');
}
};

AngularJS checkboxes for quiz

i'm new to angular, stuck with, what i hope is a rather easy problem.
I Have a question and several answers. There might be 1-3 correct answers.
Answers can be selected by checkbox. In the value field i save if the answers is correct (true) or wrong (false). I was hopping that i can simply get things work by getting ng-valid and ng-invalid.
E.g. The answers is wrong, when clicked on the checkbox for this answer the anwswer text should be red (or sth.). When deselecting it should go back to normal. Same with correct answers.
That's how i currently try to do it:
<label ng-repeat="a in q.answers"><input type="checkbox" value="{{ a.correct }}" ng-pattern="true" ng-model="field.value"> {{ a.title }}</label>
So if the value is true it should match the pattern 'true'. Does not working, ng-pattern does not seem to have any effect.
Anyone know how to solve this.
If it's more sophisticated than i thought i might be doing the check with jquery. But i want to try pure angular first.
PS: i tried to find a answer to this, but i couldn't find anything that suits my problem.
Adding onto the class suggestion from Jared Reeves, here is one way that you might implement the checkbox answers:
<label ng-repeat="answer in question.answers" ng-class="{'invalid': answer.checked && !answer.correct, 'valid': answer.checked && answer.correct}">
<input type="checkbox" ng-change="select(field, answer)" ng-model="answer.checked"> {{answer.title}}
</label>
Selected answer: {{field.value}}
// inside the controller
$scope.field = {
value: null
};
$scope.question = {
answers: [
{title: "A (incorrect)", correct: false},
{title: "B (incorrect)", correct: false},
{title: "C (correct)", correct: true},
{title: "D (incorrect)", correct: false}
]
};
$scope.select = function(field, answer) {
if (answer.checked) {
field.value = answer;
} else {
field.value = null;
}
};
Here's a working example: http://plnkr.co/edit/Qa2VrukMD61y8Jv4iKjx?p=preview

Resources