getting into AmpersandJs, and having a big showstopper by not figuring out why my sub-view-form isn't rendering it's markup.
My MainView.render, works as should:
render: function() {
BaseView.prototype.render.apply(this, arguments);
this.collectionView = this.renderCollection(this.collection, HSEventView, '.item-container');
this.renderSubview(new HSEventEditView({
action: 'create'
}), '.create-event');
return this;
},
Next, my SubView.render (HSEventEditView):
render: function () {
this.renderWithTemplate();
this.form = new EditForm({
el: this.query('.edit-form'),
submitCallback: function (data) {
debug('submit', data);
}
});
this.registerSubview(this.form);
debug('render.form.el', this.form.el)
}
And finally my FormView:
module.exports = FormView.extend({
initialize: function() {
debug('initialize', this.el)
},
autoRender: true,
fields: function () {
debug('fields!')
var fields = [
new InputView({
label: 'Name',
name: 'name',
value: utils.getDotted(this, 'model.name'),
placeholder: 'Name',
parent: this
})
];
debug('fields', fields)
}
});
The MainView and SubView renders fine, but the 'div.edit-form' DOM-node, where the form markup should be, is empty.
I have tried all variations of including a subview I could dig up, but obviously I am blind to something.
Thanks!
PS! Here is the rendered markup:
<section class="page">
<h2>Events collection</h2>
<hr>
<div class="tools">(Tools comming...)</div>
<hr>
<div class="item-container events">
<div class="item event">
<h3>Event: <span data-hook="name">Event 1</span></h3>
</div>
</div>
<hr>
<div class="create-event">
<div class="item event">
<h3>Create event: <span data-hook="name"></span></h3>
<div class="edit-form"></div>
</div>
</div>
</section>
Since fields is a function, you need to return the fields. In your Formview:
fields: function () {
debug('fields!')
var fields = [
new InputView({
label: 'Name',
name: 'name',
value: this.model.name,
placeholder: 'Name',
parent: this
})
];
debug('fields', fields)
// need to return the fields you've declared
return fields;
}
Related
I'm making a list of items with v-for loop. I have some API data from server.
items: [
{
foo: 'something',
number: 1
},
{
foo: 'anything',
number: 2
}
]
and my template is:
<div v-for(item,index) in items #click=toggleActive>
{{ item.foo }}
{{ item.number }}
</div>
JS:
methods: {
toggleActive() {
//
}
}
How can i toggle active class with :class={active : something} ?
P.S I don't have boolean value in items
You can try to implement something like:
<div
v-for="(item, index) in items"
v-bind:key="item.id" // or alternativelly use `index`.
v-bind:class={'active': activeItem[item.id]}
#click="toggleActive(item)"
>
JS:
data: () => ({
activeItem: {},
}),
methods: {
toggleActive(item) {
if (this.activeItem[item.id]) {
this.removeActiveItem(item);
return;
}
this.addActiveItem(item);
},
addActiveItem(item) {
this.activeItem = Object.assign({},
this.activeItem,
[item.id]: item,
);
},
removeActiveItem(item) {
delete this.activeItem[item.id];
this.activeItem = Object.assign({}, this.activeItem);
},
}
I had the same issue and while it isn't easy to find a whole lot of useful information it is relatively simple to implement. I have a list of stores that map to a sort of tag cloud of clickable buttons. When one of them is clicked the "added" class is added to the link. The markup:
<div class="col-sm-10">
{{ store.name }}
</div>
And the associated script (TypeScript in this case). toggleAdd adds or removes the store id from selectedStoreIds and the class is updated automatically:
new Vue({
el: "#productPage",
data: {
stores: [] as StoreModel[],
selectedStoreIds: [] as string[],
},
methods: {
toggleAdd(store: StoreModel) {
let idx = this.selectedStoreIds.indexOf(store.id);
if (idx !== -1) {
this.selectedStoreIds.splice(idx, 1);
} else {
this.selectedStoreIds.push(store.id);
}
},
async mounted () {
this.stores = await this.getStores(); // ajax request to retrieve stores from server
}
});
Marlon Barcarol's answer helped a lot to resolve this for me.
It can be done in 2 steps.
1) Create v-for loop in parent component, like
<myComponent v-for="item in itemsList"/>
data() {
return {
itemsList: ['itemOne', 'itemTwo', 'itemThree']
}
}
2) Create child myComponent itself with all necessary logic
<div :class="someClass" #click="toggleClass"></div>
data(){
return {
someClass: "classOne"
}
},
methods: {
toggleClass() {
this.someClass = "classTwo";
}
}
This way all elements in v-for loop will have separate logic, not concerning sibling elements
I was working on a project and I had the same requirement, here is the code:
You can ignore CSS and pick the vue logic :)
new Vue({
el: '#app',
data: {
items: [{ title: 'Finance', isActive: false }, { title: 'Advertisement', isActive: false }, { title: 'Marketing', isActive: false }],
},
})
body{background:#161616}.p-wrap{color:#bdbdbd;width:320px;background:#161616;min-height:500px;border:1px solid #ccc;padding:15px}.angle-down svg{width:20px;height:20px}.p-card.is-open .angle-down svg{transform:rotate(180deg)}.c-card,.p-card{background:#2f2f2f;padding:10px;border-bottom:1px solid #666}.c-card{height:90px}.c-card:first-child,.p-card:first-child{border-radius:8px 8px 0 0}.c-card:first-child{margin-top:10px}.c-card:last-child,.p-card:last-child{border-radius:0 0 8px 8px;border-bottom:none}.p-title .avatar{background-color:#8d6e92;width:40px;height:40px;border-radius:50%}.p-card.is-open .p-title .avatar{width:20px;height:20px}.p-card.is-open{padding:20px 0;background-color:transparent}.p-card.is-open:first-child{padding:10px 0 20px}.p-card.is-open:last-child{padding:20px 0 0}.p-body{display:none}.p-card.is-open .p-body{display:block}.sec-title{font-size:12px;margin-bottom:10px}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<div id="app" class="p-5">
<div class="p-wrap mx-auto">
<div class="sec-title">NEED TO ADD SECTION TITLE HERE</div>
<div>
<div v-for="(item, index) in items" v-bind:key="index" class="p-card" v-bind:class="{'is-open': item.isActive}"
v-on:click="item.isActive = !item.isActive">
<div class="row p-title align-items-center">
<div class="col-auto">
<div class="avatar"></div>
</div>
<div class="col pl-0">
<div class="title">{{item.title}}</div>
</div>
<div class="col-auto">
<div class="angle-down">
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="angle-down" role="img"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"
class="svg-inline--fa fa-angle-down fa-w-10 fa-3x">
<path fill="currentColor"
d="M151.5 347.8L3.5 201c-4.7-4.7-4.7-12.3 0-17l19.8-19.8c4.7-4.7 12.3-4.7 17 0L160 282.7l119.7-118.5c4.7-4.7 12.3-4.7 17 0l19.8 19.8c4.7 4.7 4.7 12.3 0 17l-148 146.8c-4.7 4.7-12.3 4.7-17 0z"
class=""></path>
</svg>
</div>
</div>
</div>
<div class="p-body">
<div class="c-card"></div>
<div class="c-card"></div>
<div class="c-card"></div>
</div>
</div>
</div>
</div>
</div>
I need to dynamic create textarea for forms. I have the following model:
this.fields = {
isRequired: true,
type: {
options: [
{
label: 'Option 1',
value: '1'
},
{
label: 'Option 2',
value: '2'
}
]
}
};
And form:
this.userForm = this.fb.group({
isRequired: [this.fields.isRequired, Validators.required],
//... here a lot of other controls
type: this.fb.group({
options: this.fb.array(this.fields.type.options),
})
});
Part of template:
<div formGroupName="type">
<div formArrayName="options">
<div *ngFor="let option of userForm.controls.type.controls.options.controls; let i=index">
<textarea [formControlName]="i"></textarea>
</div>
</div>
</div>
So, as you can see I have an array of objects and I want to use label property to show it in a textarea. Now I see [object Object]. If I change options to a simple string array, like: ['Option 1', 'Option 2'], then all works fine. But I need to use objects. So, instead of:
<textarea [formControlName]="i"></textarea>
I have tried:
<textarea [formControlName]="option[i].label"></textarea>
But, it doesn't work.
How can I use an array of objects?
This is Plunkr
You need to add a FormGroup, which contains your label and value. This also means that the object created by the form, is of the same build as your fields object.
ngOnInit() {
// build form
this.userForm = this.fb.group({
type: this.fb.group({
options: this.fb.array([]) // create empty form array
})
});
// patch the values from your object
this.patch();
}
After that we patch the value with the method called in your OnInit:
patch() {
const control = <FormArray>this.userForm.get('type.options');
this.fields.type.options.forEach(x => {
control.push(this.patchValues(x.label, x.value))
});
}
// assign the values
patchValues(label, value) {
return this.fb.group({
label: [label],
value: [value]
})
}
Finally, here is a
Demo
The answer from AJT_82 was so useful to me, I thought I would share how I reused his code and built a similar example - one that might have a more common use case, which is inviting several people to sign-up at once. Like this:
I thought this might help others, so that's why I am adding it here.
You can see the form is a simple array of text inputs for emails, with a custom validator loaded on each one. You can see the JSON structure in the screenshot - see the pre line in the template (thanks to AJT), a very useful idea whilst developing to see if your model and controls are wired up!
So first, declare the objects we need. Note that 3 empty strings are the model data (which we will bind to the text inputs):
public form: FormGroup;
private control: FormArray;
private emailsModel = { emails: ['','','']} // the model, ready to hold the emails
private fb : FormBuilder;
The constructor is clean (for easier testing, just inject my userService to send the form data to after submit):
constructor(
private _userService: UserService,
) {}
The form is built in the init method, including storing a reference to the emailsArray control itself so we can check later whether its children (the actual inputs) are touched and if so, do they have errors:
ngOnInit() {
this.fb = new FormBuilder;
this.form = this.fb.group({
emailsArray: this.fb.array([])
});
this.control = <FormArray>this.form.controls['emailsArray'];
this.patch();
}
private patch(): void {
// iterate the object model and extra values, binding them to the controls
this.emailsModel.emails.forEach((item) => {
this.control.push(this.patchValues(item));
})
}
This is what builds each input control (of type AbstracControl) with the validator:
private patchValues(item): AbstractControl {
return this.fb.group({
email: [item, Validators.compose([emailValidator])]
})
}
The 2 helper methods to check if the input was touched and if the validator raised an error (see the template to see how they are used - notice I pass the index value of the array from the *ngFor in the template):
private hasError(i):boolean {
// const control = <FormArray>this.form.controls['emailsArray'];
return this.control.controls[i].get('email').hasError('invalidEmail');
}
private isTouched(i):boolean {
// const control = <FormArray>this.form.controls['emailsArray'];
return this.control.controls[i].get('email').touched;
}
Here's the validator:
export function emailValidator(control: FormControl): { [key: string]: any } {
var emailRegexp = /[a-z0-9._%+-]+#[a-z0-9.-]+\.[a-z]{2,3}$/;
if (control.value && !emailRegexp.test(control.value)) {
return { invalidEmail: true };
}
}
And the template:
<form [formGroup]="form" (ngSubmit)="onSubmit(form.value)" class="text-left">
<div formArrayName="emailsArray">
<div *ngFor="let child of form.controls.emailsArray.controls; let i=index">
<div class="form-group" formGroupName="{{i}}">
<input formControlName="email"
class="form-control checking-field"
placeholder="Email" type="text">
<span class="help-block" *ngIf="isTouched(i)">
<span class="text-danger"
*ngIf="hasError(i)">Invalid email address
</span>
</span>
</div>
</div>
</div>
<pre>{{form.value | json }}</pre>
<div class="form-group text-center">
<button class="btn btn-main btn-block" type="submit">INVITE</button>
</div>
</form>
For what it's worth, I had started with this awful mess - but if you look at the code below, you might more easily understand the code above!
public form: FormGroup;
public email1: AbstractControl;
public email2: AbstractControl;
public email3: AbstractControl;
public email4: AbstractControl;
public email5: AbstractControl;
constructor(
fb: FormBuilder
) {
this.form = fb.group({
'email1': ['', Validators.compose([emailValidator])],
'email2': ['', Validators.compose([emailValidator])],
'email3': ['', Validators.compose([emailValidator])],
'email4': ['', Validators.compose([emailValidator])],
'email5': ['', Validators.compose([emailValidator])],
});
this.email1 = this.form.controls['email1'];
this.email2 = this.form.controls['email2'];
this.email3 = this.form.controls['email3'];
this.email4 = this.form.controls['email4'];
this.email5 = this.form.controls['email5'];
}
and the above used 5 of these divs in the template - not very DRY!
<div class="form-group">
<input [formControl]="email1" class="form-control checking-field" placeholder="Email" type="text">
<span class="help-block" *ngIf="form.get('email1').touched">
<span class="text-danger" *ngIf="form.get('email1').hasError('invalidEmail')">Invalid email address</span>
</span>
</div>
I guess it's not possible with FormControlName.
You could use ngModel .. take a look at your modified plunker:
http://plnkr.co/edit/0DXSIUY22D6Qlvv0HF0D?p=preview
#Component({
selector: 'my-app',
template: `
<hr>
<form [formGroup]="userForm" (ngSubmit)="submit(userForm.value)">
<input type="checkbox" formControlName="isRequired"> Required Field
<div formGroupName="type">
<div formArrayName="options">
<div *ngFor="let option of userForm.controls.type.controls.options.controls; let i=index">
<label>{{ option.value.label }}</label><br />
<!-- change your textarea -->
<textarea [name]="i" [(ngModel)]="option.value.value" [ngModelOptions]="{standalone: true}" ></textarea>
</div>
</div>
</div>
<button type="submit">Submit</button>
</form>
<br>
<pre>{{userForm.value | json }}</pre>
`,
})
export class App {
name:string;
userForm: FormGroup;
fields:any;
ngOnInit() {
this.fields = {
isRequired: true,
type: {
options: [
{
label: 'Option 1',
value: '1'
},
{
label: 'Option 2',
value: '2'
}
]
}
};
this.userForm = this.fb.group({
isRequired: [this.fields.isRequired, Validators.required],
//... here a lot of other controls
type: this.fb.group({
// .. added map-function
options: this.fb.array(this.fields.type.options.map(o => new FormControl(o))),
})
});
}
submit(value) {
console.log(value);
}
constructor(private fb: FormBuilder) { }
addNumber() {
const control = <FormArray>this.userForm.controls['numbers'];
control.push(new FormControl())
}
}
You may try this
Typescript:
ngOnInit(): void {
this.user = this.fb.group({
Title: ['1'],
FirstName: ['', Validators.required],
LastName: ['', Validators.required],
ContactNumbers: this.fb.array([
this.fb.group({
PhoneNumber: ['', [Validators.required]],
IsPrimary: [true],
ContactTypeId: [1]
})
]),
Emails: this.fb.array([
this.fb.group({
Email: ['', [Validators.required, Validators.email]],
IsPrimary: [true]
})
]),
Address: this.fb.group({
Address1: ['', Validators.required],
Address2: [''],
Town: [''],
State: ['UP'],
Country: [{ value: 'India', disabled: true }],
Zip: ['', Validators.required]
})
});
}
get Emails() {
return this.landlord.get('Emails') as FormArray;
}
Add and remove
addMoreEmail(index: number) {
if (index == 0) {
this.Emails.push(this.fb.group({ Email: ['', [Validators.required, Validators.email]], IsPrimary: [false] }));
} else {
this.Emails.removeAt(this.Emails.value.indexOf(index));
}
}
HTML
<form [formGroup]="user"
<div formArrayName="Emails">
<div *ngFor="let email of Emails.controls; let i=index">
<div class="row" [formGroup]="email">
<div class="col-sm-10">
<div class="form-group">
<label for="i" class="label">Email</label>
<input type="email" nbInput fullWidth id="i" placeholder="Email"
formControlName="Email" required>
</div>
</div>
<div class="col-sm-2 position-relative">
<nb-icon icon="{{i==0?'plus':'minus'}}-round" pack="ion"
(click)="addMoreEmail(i)">
</nb-icon>
</div>
</div>
</div>
</div></div>
In my Ionic app, as you can see, this "select-on-focus" directive doesn't work in iOS...
Here's a video:
https://youtu.be/_bOWGMGesgk
Here's the code:
<div class="wrapperFlex withNextButton">
<div class="itemTitle">
<div class="text">
{{'paramQuestions.weight' | translate }}
</div>
</div>
<div id="weightdata" class="itemParameters weightdataclass row">
<input class="weightinput" type="number" name="userweight" ng-min="{{data.minWeight}}" ng-max="{{data.maxWeight}}" ng-model="data.realWeight" ng-change="updateViewGenVol(data.weightunit, data.userweight, data.BLfactorValue);saveUserWeight()" select-on-focus required></input>
<div class="weightunitradios">
<ion-checkbox class="checkboxes checkbox-blueboardline" ng-model="data.weightunit" ng-true-value="'kg'" ng-false-value="'lbs'" ng-change="saveWeightUnit(); changeMinMax(); convertWeightInput(); saveUserWeight();">kg</ion-checkbox>
<ion-checkbox class="checkboxes checkbox-blueboardline" ng-model="data.weightunit" ng-true-value="'lbs'" ng-false-value="'kg'" ng-change="saveWeightUnit(); changeMinMax(); convertWeightInput(); saveUserWeight();">lbs</ion-checkbox>
</div>
</div>
</div>
directives.js:
.directive('selectOnFocus', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var focusedElement = null;
element.on('focus', function () {
var self = this;
if (focusedElement != self) {
focusedElement = self;
$timeout(function () {
self.select();
}, 10);
}
});
element.on('blur', function () {
focusedElement = null;
});
}
}
})
I'm trying to construct a view in my app that will pop up polling questions in a modal dialog region. Maybe something like this for example:
What is your favorite color?
>Red
>Blue
>Green
>Yellow
>Other
Submit Vote
I've read that Marionette js doesn't support forms out of the box and that you are advised to handle on your own.
That structure above, branch and leaves (question and list of options), suggests CompositeView to me. Is that correct?
How do I trigger a model.save() to record the selection? An html form wants an action. I'm unclear on how to connect the form action to model.save().
My rough draft ItemView and CompositeView code is below. Am I in the ballpark? How should it be adjusted?
var PollOptionItemView = Marionette.ItemView.extend({
template: Handlebars.compile(
'<input type="radio" name="group{{pollNum}}" value="{{option}}">{{option}}<br>'
)
});
var PollOptionsListView = Marionette.CompositeView.extend({
template: Handlebars.compile(
//The question part
'<div id="poll">' +
'<div>{{question}}</div>' +
'</div>' +
//The list of options part
'<form name="pollQuestion" action="? what goes here ?">' +
'<div id="poll-options">' +
'</div>' +
'<input type="submit" value="Submit your vote">' +
'</form>'
),
itemView: PollOptionItemView,
appendHtml: function (compositeView, itemView, index) {
var childrenContainer = $(compositeView.$("#poll-options") || compositeView.el);
var children = childrenContainer.children();
if (children.size() === index) {
childrenContainer.append(itemView.el);
} else {
childrenContainer.children().eq(index).before(itemView.el);
}
}
});
MORE DETAILS:
My goal really is to build poll questions dynamically, meaning the questions and options are not known at runtime but rather are queried from a SQL database thereafter. If you were looking at my app I'd launch a poll on your screen via SignalR. In essence I'm telling your browser "hey, go get the contents of poll question #1 from the database and display them". My thought was that CompositeViews are best suited for this because they are data driven. The questions and corresponding options could be stored models and collections the CompositeView template could render them dynamically on demand. I have most of this wired and it looks good. My only issue seems to be the notion of what kind of template to render. A form? Or should my template just plop some radio buttons on the screen with a submit button below it and I write some javascript to try to determine what selection the user made? I'd like not to use a form at all and just use the backbone framework to handle the submission. That seems clean to me but perhaps not possible or wise? Not sure yet.
I'd use the following approach:
Create a collection of your survey questions
Create special itemviews for each type of question
In your CompositeView, choose the model itemView based on its type
Use a simple validation to see if all questions have been answered
Output an array of all questions and their results.
For an example implementation, see this fiddle: http://jsfiddle.net/Cardiff/QRdhT/
Fullscreen: http://jsfiddle.net/Cardiff/QRdhT/embedded/result/
Note:
Try it without answering all questions to see the validation at work
Check your console on success to view the results
The code
// Define data
var surveyData = [{
id: 1,
type: 'multiplechoice',
question: 'What color do you like?',
options: ["Red", "Green", "Insanely blue", "Yellow?"],
result: null,
validationmsg: "Please choose a color."
}, {
id: 2,
type: 'openquestion',
question: 'What food do you like?',
options: null,
result: null,
validationmsg: "Please explain what food you like."
}, {
id: 3,
type: 'checkbox',
question: 'What movie genres do you prefer?',
options: ["Comedy", "Action", "Awesome", "Adventure", "1D"],
result: null,
validationmsg: "Please choose at least one movie genre."
}];
// Setup models
var questionModel = Backbone.Model.extend({
defaults: {
type: null,
question: "",
options: null,
result: null,
validationmsg: "Please fill in this question."
},
validate: function () {
// Check if a result has been set, if not, invalidate
if (!this.get('result')) {
return false;
}
return true;
}
});
// Setup collection
var surveyCollection = Backbone.Collection.extend({
model: questionModel
});
var surveyCollectionInstance = new surveyCollection(surveyData);
console.log(surveyCollectionInstance);
// Define the ItemViews
/// Base itemView
var baseSurveyItemView = Marionette.ItemView.extend({
ui: {
warningmsg: '.warningmsg',
panel: '.panel'
},
events: {
'change': 'storeResult'
},
modelEvents: {
'showInvalidMessage': 'showInvalidMessage',
'hideInvalidMessage': 'hideInvalidMessage'
},
showInvalidMessage: function() {
// Show message
this.ui.warningmsg.show();
// Add warning class
this.ui.panel.addClass('panel-warningborder');
},
hideInvalidMessage: function() {
// Hide message
this.ui.warningmsg.hide();
// Remove warning class
this.ui.panel.removeClass('panel-warningborder');
}
});
/// Specific views
var multipleChoiceItemView = baseSurveyItemView.extend({
template: "#view-multiplechoice",
storeResult: function() {
var value = this.$el.find("input[type='radio']:checked").val();
this.model.set('result', value);
}
});
var openQuestionItemView = baseSurveyItemView.extend({
template: "#view-openquestion",
storeResult: function() {
var value = this.$el.find("textarea").val();
this.model.set('result', value);
}
});
var checkBoxItemView = baseSurveyItemView.extend({
template: "#view-checkbox",
storeResult: function() {
var value = $("input[type='checkbox']:checked").map(function(){
return $(this).val();
}).get();
this.model.set('result', (_.isEmpty(value)) ? null : value);
}
});
// Define a CompositeView
var surveyCompositeView = Marionette.CompositeView.extend({
template: "#survey",
ui: {
submitbutton: '.btn-primary'
},
events: {
'click #ui.submitbutton': 'submitSurvey'
},
itemViewContainer: ".questions",
itemViews: {
multiplechoice: multipleChoiceItemView,
openquestion: openQuestionItemView,
checkbox: checkBoxItemView
},
getItemView: function (item) {
// Get the view key for this item
var viewId = item.get('type');
// Get all defined views for this CompositeView
var itemViewObject = Marionette.getOption(this, "itemViews");
// Get correct view using given key
var itemView = itemViewObject[viewId];
if (!itemView) {
throwError("An `itemView` must be specified", "NoItemViewError");
}
return itemView;
},
submitSurvey: function() {
// Check if there are errors
var hasErrors = false;
_.each(this.collection.models, function(m) {
// Validate model
var modelValid = m.validate();
// If it's invalid, trigger event on model
if (!modelValid) {
m.trigger('showInvalidMessage');
hasErrors = true;
}
else {
m.trigger('hideInvalidMessage');
}
});
// Check to see if it has errors, if so, raise message, otherwise output.
if (hasErrors) {
alert('You haven\'t answered all questions yet, please check.');
}
else {
// No errors, parse results and log to console
var surveyResult = _.map(this.collection.models, function(m) {
return {
id: m.get('id'),
result: m.get('result')
}
});
// Log to console
alert('Success! Check your console for the results');
console.log(surveyResult);
// Close the survey view
rm.get('container').close();
}
}
});
// Create a region
var rm = new Marionette.RegionManager();
rm.addRegion("container", "#container");
// Create instance of composite view
var movieCompViewInstance = new surveyCompositeView({
collection: surveyCollectionInstance
});
// Show the survey
rm.get('container').show(movieCompViewInstance);
Templates
<script type="text/html" id="survey">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title" > A cool survey regarding your life </h3>
</div>
<div class="panel-body">
<div class="questions"></div>
<div class="submitbutton">
<button type="button" class="btn btn-primary">Submit survey!</button>
</div>
</div>
</div >
</script>
<script type="text/template" id="view-multiplechoice">
<div class="panel panel-success">
<div class="panel-heading">
<h4 class="panel-title" > <%= question %> </h4>
</div>
<div class="panel-body">
<div class="warningmsg"><%= validationmsg %></div>
<% _.each( options, function( option, index ){ %>
<div class="radio">
<label>
<input type="radio" name="optionsRadios" id="<%= index %>" value="<%= option %>"> <%= option %>
</label>
</div>
<% }); %>
</div>
</div>
</script>
<script type="text/template" id="view-openquestion">
<div class="panel panel-success">
<div class="panel-heading">
<h4 class="panel-title" > <%= question %> </h4>
</div>
<div class="panel-body">
<div class="warningmsg"><%= validationmsg %></div>
<textarea class="form-control" rows="3"></textarea>
</div>
</div >
</script>
<script type="text/template" id="view-checkbox">
<div class="panel panel-success">
<div class="panel-heading">
<h4 class="panel-title" > <%= question %> </h4>
</div>
<div class="panel-body">
<div class="warningmsg"><%= validationmsg %></div>
<% _.each( options, function( option, index ){ %>
<div class="checkbox">
<label>
<input type="checkbox" value="<%= option %>"> <%= option %>
</label>
</div>
<% }); %>
</div>
</div>
</script>
<div id="container"></div>
Update: Added handlebars example
Jsfiddle using handlebars: http://jsfiddle.net/Cardiff/YrEP8/
I have a KendoUI dropdownlist which fetches data from a Web Service, depending on the selected item a 2nd dropdown is populated. I am using MVVM binding.
My code looks like:
<div id="ddlDiv">
<div data-container-for="MEASURE" required class="k-input k-edit-field">
<select id="MEASURE"
name="MEASURE"
data-role="dropdownlist"
data-text-field="FIELD_NAME"
data-value-field="FIELD_ID"
data-bind="value: summaryDef.MeasureID, source: fieldList"
></select>
</div>
<div data-container-for="OPERATION" required class="k-input k-edit-field">
<select id="OPERATION"
data-cascade-from: "MEASURE"
data-role="dropdownlist"
data-text-field="TYPE"
data-value-field="OP_ID"
data-source=opListDS
data-bind="value: summaryDef.OperationID"
></select>
</div>
datasetMetaModel = kendo.observable({
fieldList: datasetModel.FieldDTOList,
summaryDef: datasetModel.SummaryDef
});
kendo.bind($("#ddlDiv"), datasetMetaModel);
var opListDS = BuildOperationFields();
function BuildOperationFields() {
opListDS = new kendo.data.DataSource({
transport: {
read: {
url: '#Url.Action("GetMeasureOperations", "Wizard")',
dataType: 'json',
contentType: "application/json; charset=utf-8",
serverFiltering: true,
type: "GET"
}
}
});
return opListDS;
}
Both lists have their data populated correctly initially and are correctly bound to the model. However changing the value of the first dropdown does not cause the 2nd dropdown to have it's data refreshed. The call to the web service is never triggered.
I've seen a similar situation here that uses local data:
MVVM binding for cascading DropDownList
I don't know if this is still an issue for you as it's been a while since the question was asked but I thought I would answer as I had a similar problem myself today and managed to solve it with a workaround.
What I did was to bind a handler to the onChange event of the parent combo box and in that manually call read() on the data source of the child combo box:
e.g.
HTML:
<div id="content-wrapper">
<div class="editor-row">
<div class="editor-label"><label>Country:</label></div>
<div class="editor-field">
<select id="Country" name="Country" data-role="combobox"
data-text-field="CountryName"
data-value-field="CountryId"
data-filter="contains"
data-suggest="false"
required
data-required-msg="country required"
data-placeholder="Select country..."
data-bind="source: dataSourceCountries, value: country, events: { change: refreshCounties }">
</select>
<span class="field-validation-valid" data-valmsg-for="Country" data-valmsg-replace="true"></span>
</div>
</div>
<div class="editor-row">
<div class="editor-label"><label>County:</label></div>
<div class="editor-field">
<select id="County" name="County" data-role="combobox"
data-text-field="CountyName"
data-value-field="CountyId"
data-filter="contains"
data-auto-bind="false"
data-suggest="false"
data-placeholder="Select county..."
data-bind="source: dataSourceCounties, value: county">
</select>
<span class="field-validation-valid" data-valmsg-for="County" data-valmsg-replace="true"></span>
</div>
</div>
then my view model:
$ns.viewModel = kendo.observable({
dataSourceCountries: new kendo.data.DataSource({
transport: {
read: {
url: href('~/Api/GeographicApi/ListCountries'),
dataType: 'json'
}
},
schema: {
data: function (response) { return response.Data || {}; }
}
}),
dataSourceCounties: new kendo.data.DataSource({
transport: {
read: {
url: function () { var combobox = $('#Country').data('kendoComboBox'), id = combobox !== undefined ? combobox.value() : 0; return href('~/Api/GeographicApi/ListCountiesByCountryId/' + id) },
dataType: 'json'
}
},
schema: {
data: function (response) { return response.Data || {}; }
}
}),
refreshCounties: function (e) {
var countiesList = $('#County').data('kendoComboBox');
e.preventDefault();
countiesList.dataSource.read();
}
});
kendo.bind($('#content-wrapper'), $ns.viewModel);
Hope this helps if you have not already found a solution...