Bind dropdown list of another object - javascript

I will start my question by describing desired outcome:
I want to build an input form to post to my API using knockout JS, however the Entity object I want to input has foreign keys so I need to give a select option for all of the options in the foreign table.
Lessons View Model
var lessonRegisterViewModel;
function Lesson(id, name, teacher, room, subject, startTime, endTime) {
var self = this;
self.Id = ko.observable(id);
self.Name = ko.observable(name);
self.Teacher = ko.observable(teacher);
self.Room = ko.observable(room);
self.Subject = ko.observable(subject);
self.StartTime = ko.observable(startTime);
self.EndTime = ko.observable(endTime);
self.addLesson = function() {
var dataObject = ko.toJSON(this);
$.ajax({
url: '/api/Lessons',
type: 'post',
data: dataObject,
contentType: 'application/json',
success: function(data) {
lessonRegisterViewModel.lessonListViewModel.lessons.push(new Lesson(data.Id, data.Name, data.Teacher, data.Room, data.Subject, data.StartTime, data.EndTime));
self.Id(null);
self.Name('');
self.Teacher('');
self.Room('');
self.Subject('');
self.StartTime('');
self.EndTime('');
}
});
}
}
function LessonList() {
var self = this;
self.lessons = ko.observableArray([]);
self.getLessons = function() {
self.lessons.removeAll();
$.getJSON('/api/Lessons', function(data) {
$.each(data, function(key, value) {
self.lessons.push(new Lesson(value.id, value.name, value.teacher, value.room, value.subject, value.startTime, value.endTime));
console.log(self);
});
});
};
self.removeLesson = function(lesson) {
$.ajax({
url: '/api/Lessons/' + lesson.Id(),
type: 'delete',
contentType: 'application/json',
success: function() {
self.lessons.remove(lesson);
}
});
}
}
lessonRegisterViewModel = {
addLessonViewModel: new Lesson(),
lessonListViewModel: new LessonList()
};
$(document).ready(function() {
// bind view model to referring view
ko.applyBindings(lessonRegisterViewModel);
// load lesson data
lessonRegisterViewModel.lessonListViewModel.getLessons();
});
Example JSON we get out:
[
{
"id":1,
"name":"Lesson 1",
"teacher":{
"id":3,
"firstName":"Sophie",
"lastName":"Adams",
"emailAddress":"teacher3#foo.com"
},
"classroom":{
"id":1,
"name":"Great Hall"
},
"subject":{
"id":4,
"name":"jQuery"
},
"startTime":"2016-02-10T09:30:00",
"endTime":"2016-02-10T10:30:00"
},
{
"id":2,
"name":"Lesson 2",
"teacher":{
"id":4,
"firstName":"Tristan",
"lastName":"Sanchez",
"emailAddress":"teacher4#foo.com"
},
"classroom":{
"id":2,
"name":"Room 1A"
},
"subject":{
"id":3,
"name":"SQL"
},
"startTime":"2016-02-10T09:00:00",
"endTime":"2016-02-10T10:30:00"
}
]
So essentially I am inserting a lesson, which comprises of
Name
Teacher
Room
Subject
StartTime
EndTime
I need to go off, and offer dropdown lists for all the teachers in the database, I also need to do this for rooms and subjects. I have working View Models for each of the individual entities with no dependencies, so I can physically complete all CRUD on Teachers, Rooms and Subjects.
Any suggestions on how to implement this would be welcomed.

It looks like you don't have a "I can't make it do what I want" kind of issue, but more of a mental block about where things fit into your model. So let me suggest a different mental approach.
What you have built is a thorough data model of your primary data object. But your goal is to build is an application, which will need that data model, but also other things, all in its viewmodel.
A viewmodel is specified entirely by a view: you need to implement exactly the set of things that your view will use. So start by writing the HTML with its data bindings. If your application is going to have a <select> item that needs a list of rooms, just go ahead and assume such a member exists in your view model and make the binding.
Once you have laid out the application this way, you have fully specified your viewmodel. You've created an interface (all the viewmodel members) that you need to implement. The application — not the data — defines the viewmodel. So start with the application and leave the data modeling to the implementation phase.

Related

Binding a Kendo control to an HtmlHelper control using JavaScript and MVC / Razor

I have an MVC ListBoxFor control that I'm trying to bind data to and update using a Kendo MultiSelectFor.
The idea being that there is a list of users in the ListBox, and a list of available users in the MultiSelect box. When users are selected from the MultiSelect box and the add button clicked, an Ajax call is made to an action that updates the users list server side (through various API calls, which all work fine) and client side JavaScript is used to update the users and available users array object and the binding keeps the controls up to date with the updated lists.
I wish I could pin this down to just one issue, but honestly every time I try something I come up with different errors, so I'll just go with the latest iteration.
Model:
public IEnumerable<UserInformation> Users { get; set; }
public IEnumerable<UserInformation> AvailableUsers { get; set; }
JavaScript ViewModel:
var viewModel = kendo.observable({
availableUsersSelected: [],
users: #(Html.Raw(Json.Encode(this.Model.Users))),
availableUsers: #(Html.Raw(JsonConvert.SerializeObject(this.Model.AvailableUsers))),
moveToUsers: function () {
this.availableUsersSelected = this.get('availableUsersSelected');
this.users.push(this.availableUsers);
if (this.availableUsersSelected.length > 0) {
var formAction = '#Url.Combine(Url.Content("~/"), ControllerActions.Groups.GroupDefault, ControllerActions.Groups.AddUser)';
$.ajax({
url: formAction,
type: 'POST',
data: {
model: JSON.stringify(
{
groupId: $('#GroupId').val(),
users: this.availableUsersSelected
}
)
},
success: function (result) {
if (result) {
this.users.remove(this.availableUsersSelected);
}
}
});
}
}
});
MultiSelectFor control
#(Html.Kendo()
.MultiSelectFor(u => u.AvailableUsers)
.Placeholder("Please select")
.BindTo(new SelectList(Model.AvailableUsers, "Id", "Name"))
.HtmlAttributes(new { data_bind = "value: availableUsersSelected" })
)
ListBox control
#(Html.EditorLine(Language.Fields.Users, Html.ListBoxFor(u => u.Users, new SelectList(Model.Users, "Id", "Name"), new { #class = "form-control", data_bind = "source: users", data_value_field ="Id", data_text_field = "Name" })))
Add control
<img src="~/Content/images/up-arrow.jpg" alt="Move to users" width="30" data-bind="events: {click: moveToUsers}" />
To reiterate, the Ajax call and updating server side all work fine, it's the client side control binding that I'm struggling to understand.
The errors I'm getting are 1) a syntax error with the comma on this line users: #(Html.Raw(Json.Encode(this.Model.Users))), and the line after it (same thing, effectively), and 2) a "ReferenceError: Id is not defined" on the moveToUsers function call when the add button is pressed.
(I can honestly say that the amount of frustration I'm experiencing with this is driving me insane, so sorry if it came across in the question)
So after calming down a bit, reading a few more bits of the documentation about data binding and observable arrays, I realised I was making a few fundamental errors.
JavaScript ViewModel:
var viewModel = {
availableUsersSelected: new kendo.data.ObservableArray([]),
users: new kendo.data.ObservableArray(#(Html.Raw(Json.Encode(this.Model.Users)))),
availableUsers: new kendo.data.ObservableArray(#(Html.Raw(Json.Encode(this.Model.AvailableUsers)))),
moveToUsers: function () {
if (viewModel.availableUsersSelected.length > 0) {
var formAction = '#Url.Combine(Url.Content("~/"), ControllerActions.Groups.GroupDefault, ControllerActions.Groups.AddUser)';
$.ajax({
url: formAction,
type: 'POST',
data: {
model: JSON.stringify(
{
groupId: $('#GroupId').val(),
users: viewModel.availableUsersSelected
}
)
},
success: function (result) {
if (result) {
removeFromAvailableUsers();
}
else
alert('add failed!');
},
failure: function () {
alert('ajax failed!');
}
});
}
}
};
function removeFromAvailableUsers() {
for (var i = 0; i < viewModel.availableUsersSelected.length; ++i) {
viewModel.users.push(viewModel.availableUsersSelected[i]);
viewModel.availableUsers.remove(viewModel.availableUsersSelected[i]);
}
var ele = $('#AvailableUsers').data("kendoMultiSelect");
ele.value("");
ele.input.blur();
};
The main differences are instead of declaring the entire object as a kendo observable are declaring each array as an observable array, then referencing them through the viewModel object instead of assuming that the "this" scope will encapsulate them.
Then, as D_Learning mentioned in the comments above, I was unnecessarily using two bindings for the MultiSelect control, so that then became:
#(Html.Kendo()
.MultiSelectFor(u => u.AvailableUsers)
.Placeholder("Please select")
.HtmlAttributes(new { data_bind = "source: availableUsers, value: availableUsersSelected", data_value_field = "Id", data_text_field = "Name" })
)
(Notice no ".BindTo" property)
Aside from that, the MVC side of things stayed the same and it all words perfectly.
If you wish to remove or add data to the Kendo Multiselect then you will need to add them via the DataSource as:
$("#AvailableUsers").data("kendoMultiSelect").dataSource.add({"text": "new Item", "value": 1000});
For more detail about Adding or removing Items to Multiselect (Kendo DataSrouce) see: Kendo DataSource Adding Removing Items
Similarly you can remove the item from the Listbox as below:
var selectedIndex = ListBox1.selectedIndex();
clearSelection();
if (selectedIndex != -1) {
ListBox1.options.remove(selectedIndex);
For more detail about Adding or removing Items from HTML Listbox see: HTML Listbox Items Manipulation.
Please let me know if you have any error after this.

Backbone and best practice getting config JSON

I've got a JSON file that looks like this.
{
"config": {
"setting1": 'blabla',
"setting2": 'blablabla'
},
"content": {
"title": "Title of an exercise.",
"author": "John Doe",
"describtion": "Exercise content."
},
"answers": [
{
"id": "1",
"content": "Dog",
"correct": true
},
{
"id": "2",
"content": "Fish",
"correct": false
}
]
}
Than, I create a Backbone View, combined from content model, and answers (which are randomly selected, but It's not most important now).
I've also got a config, which has settings that will determinate which view and collection methods to use.
It seems like a simple task, but as I'm new to Backbone, I'm wondering which is the best way to fetch JSON file, creating one model with url to JSON and than using parse and initialize creating another models and collections (with answers), or using $.getJSON method that will create exactly the models that I need?
I was trying using $.getJSON
$.getJSON(source, function(data) {
var contentModel = new ContentModel(data.content);
var contentView = new ExerciseView({ model: contentModel });
var answerCollection = new AnswersCollection();
_.each(data.answers, function(answer) {
answerCollection.add(answer);
});
var answersView = new AnswersView({collection: answerCollection});
$(destination).html( contentView.render().el );
$('.answers').append( answersView.el );
)};
But It doesn't seem very elegant solution, I know that this application needs good architecture, cause It will be developed with many other Views based on 'config'.
Hope you guys give me some suggestions, have a good day!
I think what you've done works fine and is correct. But you may need to refactor a little bit since "it will be developed with many other Views based on 'config'".
IMHO, the first thing you need to do is to handle failure in your getJson callback to make the process more robust.
Second, it is useful to create a Factory to generate your views because your logic is to generate different views based on the config data from server. So the factory maybe:
contentViewFactory.generate = function(data) {
var config = data.config;
....
var ActualContentView = SomeContentView;
var contentModel = new ContentModel(data.content);
return = new ActualContentView({ model: contentModel });
}
If your logic is simple, you can have a dict map from config to view class like:
var viewMaps = {
"exercise" : ExerciseView,
"other": SomeOtherView,
//....
}
And if every workflow has a AnswersView you can keep that in your getJSON callback. So maybe now your getJSON looks like this:
$.getJSON(source, function(data) {
// keep the config->view logic in the factory
var contentView = contentViewFactory.generate(data);
var answerCollection = new AnswersCollection();
_.each(data.answers, function(answer) {
answerCollection.add(answer);
});
var answersView = new AnswersView({collection: answerCollection});
$(destination).html( contentView.render().el );
$('.answers').append( answersView.el );
})
.fail(){
//some failure handling
};
Furthermore, if you have common logics in you "ContentView"s, it's natural that you can have a "BaseContentView" or "ContentViewMixin" to extract the common logic and use extends to make your code more OO:
Backbone.View.extend(_.extend({}, ContentViewMixin, {
//.....
}
So if someone is trying to add a new ContentView, he/she just needs to add some code in the factory to make the new View be generated by config. Then extends the ContentViewMixin to implement the new View.

Dynamically changing url in Backbone

I was trying to dynamically change the url inside the router but couldn't manage to do it, it keeps returning to the base Collection URL. Here i posted the code with the 3 different collections which apart from pointing to three different urls they do exactly the same.
I have only one model and three collections that depend on that model and they even render the same view. How can i dynamically change the url so i can create only one Collection and one Model? Is it best pracitce for a case like this?
// MODELS & COLLECTIONS
window.Post = Backbone.Model.extend({
urlRoot: function() {
return 'http://localhost:5000/json/guides/:id'
}
})
App.Collections.RecentPosts = Backbone.Collection.extend({
model: Post,
url:'http://localhost:5000/json/posts/recent',
})
App.Collections.PopularPosts = Backbone.Collection.extend({
model: Post,
url:'http://localhost:5000/json/posts/popular',
})
App.Collections.FeaturedPosts = Backbone.Collection.extend({
model: Post,
url:'http://localhost:5000/json/posts/featured',
})
// CONTROLLER
App.Controllers.Documents = Backbone.Router.extend({
routes:{
"recent" : "recent",
"popular" : "popular",
"featured" : "featured",
},
recent: function(){
//.... same as featured ?
},
popular: function(){
//.... same as featured ?
},
featured: function(){
$("#browser").empty();
var collection = new App.Collections.Posts();
collection.fetch({
success: function(col,posts){
new App.Views.GuideView({collection: posts});
},
error: function(error){
console.log(error)
}
})
}
});
There are numerous different ways of doing this. Here's what's probably going to be 'best practice'.
App.Controllers.Documents = Backbone.Router.extend({
routes:{
"recent" : "recent",
"popular" : "popular",
"featured" : "featured",
},
initialize: function () {
this.collection = new App.Collections.Posts();
},
_showPage: function (config) {
$("#browser").empty();
this.collection.fetch({
url: config.url,
success: function(col,posts){
new App.Views.GuideView({collection: posts});
},
error: function(error){
console.log(error)
}
});
},
recent: function(){
this._showPage({url:'http://localhost:5000/json/posts/recent'});
},
popular: function(){
this._showPage({url:'http://localhost:5000/json/posts/popular'});
},
featured: function(){
this._showPage({url:'http://localhost:5000/json/posts/featured'});
}
});
Since I really don't know how complicated your page is going to get, this is probably the best I can do without more information. But, the idea is that "this.collection" is set on the routers initialization.. so you can keep reusing it. The _showPage method does whatever basic tasks you need done to show the page, and the methods called by the routes use it to do whatever basic stuff needs done before going into detail. The url passed into the config would simply tell the collection where to get its information from - I'm assuming that all of your data has the same format and 'is the same thing'.. just different filters.
You can probably do a similar thing with App.Views.GuideView:
App.Controllers.Documents = Backbone.Router.extend({
routes:{
"recent" : "recent",
"popular" : "popular",
"featured" : "featured",
},
initialize: function () {
this.collection = new App.Collections.Posts();
this.view = new App.Views.GuideView({collection: this.collection});
},
_showPage: function (config) {
$("#browser").empty();
this.collection.fetch({
url: config.url,
success: _.bind(function(col,posts){
this.view.render();
}, this),
error: function(error){
console.log(error)
}
});
},
recent: function(){
this._showPage({url:'http://localhost:5000/json/posts/recent'});
},
popular: function(){
this._showPage({url:'http://localhost:5000/json/posts/popular'});
},
featured: function(){
this._showPage({url:'http://localhost:5000/json/posts/featured'});
}
});
The 'render' would just rebuild the view, and since you've already got the collection referenced in the view as "this.options.collection" (or you could add an 'initialize' to the view and set this.collection to be this.options.collection). When the collection gets updated, all of that information is by reference in the view.. so no need to reset it.
I think the best pratice would be to have 3 collections, each with it's on URL and properties.
This makes the code easier to maintain as you can assign different events and listeners to them in a separate file instead of having a "God Collection" that have all the logic inside it.
Of course you can still be DRY and keep a helper object or a parent collection with code that is commmon to all those collections.

Backbone JS complex model fetch

I have two backbone models, loaded from server:
var Model = Backbone.Model.extend({});
var SubModel = Backbone.Model.extend({});
var SubCollection = Backbone.Collection.extend({
model: SubModel
});
var m = new Model();
m.fetch({success: function(model)
{
model.submodels = new SubCollection();
model.submodels.url = "/sub/" + model.get("id");
model.submodels.fetch();
}});
So, the server has to send two separate responses. For example:
{ name: "Model1", id: 1 } // For Model fetch
and
[{ name: "Submodel1", id: 1 }, { name: "Submodel2", id: 2 }] // For Submodel collection fetch
Is there a way to fetch a Model instance with Submodel collection at once, like:
{
name: "Model1",
id: 1,
submodels: [{ name: "Submodel1", id: 2 }, { name: "Submodel1", id: 2 }]
}
To be able to do that is up to your back-end - it doesn't really have anything to do with Backbone.
Can you configure your back-end technology to return related models as nested resources?
If your back-end is Rails, for instance, and your models are related in ActiveRecord, one way of doing this is something like
respond_to do |format|
format.json { render :json => #model.to_json(:include => [:submodels])}
end
What back-end technology are you using?
Edit:
Sorry, misunderstood the gist of your question, once you've got your back-end returning the JSON in the proper format, yeah, there are things you need to do in Backbone to be able to handle it.
Backbone-Relational
One way to deal with it is to use Backbone-Relational, a plugin for handling related models.
You define related models through a 'relations' property:
SubModel = Backbone.RelationalModel.extend({});
SubCollection = Backbone.Collection.extend({
model: SubModel
});
Model = Backbone.RelationalModel.extend({
relations: [
{
type: 'HasMany',
key: 'submodels',
relatedModel: 'SubModel',
collectionType: 'SubCollection'
}
]
});
When your Model fetches the JSON, it will automatically create a SubCollection under the 'submodels' property and populate it with SubModels - one for each JSON object in the array.
jsfiddle for backbone-relational: http://jsfiddle.net/4Zx5X/12/
By Hand
You can do this by hand if you want as well. In involves overriding the parse function for your Model class (forgive me if my JS is not 100% correct - been doing CoffeeScript so much lately its hardwired in my brain)
var Model = Backbone.Model.extend({
parse: function(response) {
this.submodels = new SubCollection();
// Populate your submodels with the data from the response.
// Could also use .add() if you wanted events for each one.
this.submodels.reset(response.submodels);
// now that we've handled that data, delete it
delete response.submodels;
// return the rest of the data to be handled by Backbone normally.
return response;
}
});
parse() runs before initialize() and before the attributes hash is set up, so you can't access model.attributes, and model.set() fails, so we have to set the collection as a direct property of the model, and not as a "property" that you would access with get/set.
Depending on what you want to happen on "save()" you may have to override `toJSON' to get your serialized version of the model to look like what your API expects.
jsfiddle:
http://jsfiddle.net/QEdmB/44/

How to get id on a successful model.save()?

Have checked some backbone.js tutorials and can't
understand how to get model id from the server within
the model saving process. I have a model:
var Game = Backbone.Model.extend({
defaults: {
name: '',
releaseDate: ''
},
url: function(){
return '/data.php';
}
});
How to implement getting the id algorithm? It seams to me, there should
be a kind of callback function, but can't realise where to put it.
See Backbone's documentation on model save.
You can pass a success callback function to save, something like this:
var game = new Game({
name: 'Duke Nukem 3D',
releaseDate: '1996'
});
game.save({}, {
success: function(model, response) {
// get model id from response?
}
);

Categories

Resources