Cannot work out how to access property of self in ViewModel - javascript

I am very new to js and html - trying to make a basic front end for a C# web api.
I'm making a simple app for tracking bugs. I have a panel for the list of bugs, where I can click "Details" to see more info on each bug (I would post an image, but my reputation is too low). Then a new panel opens with with the details of the bug, including a button to close the bug, ie change set the status to "closed". It's with this button that I have the problem.
I have this in my Index.cshtml:
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title">Bugs</h2>
</div>
<div class="panel-body">
<ul class="list-unstyled" data-bind="foreach: bugs">
<li>
<strong><span data-bind="text: Title"></span></strong>: <span data-bind="text: Description"></span>
<small>Details</small>
</li>
</ul>
</div>
</div>
<div class="alert alert-danger" data-bind="visible: error"><p data-bind="text: error">
</p></div>
<!-- ko if:detail() -->
#* Bug Detail with Close Button *#
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title">Detail</h2>
</div>
<table class="table">
<tr><td>Title</td><td data-bind="text: detail().Title"></td></tr>
<tr><td>Description</td><td data-bind="text: detail().Description"></td></tr>
<tr><td>Status</td><td data-bind="text: detail().Status"></td></tr>
<tr><td>Created</td><td data-bind="text: detail().Created"></td></tr>
<tr><td>Owner</td><td data-bind="text: detail().Owner"></td></tr>
</table>
<div class="panel-body">
<form class="form-horizontal" data-bind="submit: closeBug(detail())">
<button type="submit" class="btn btn-default">Close bug</button>
</form>
</div>
</div>
</div>
<!-- /ko -->
Then this is the relevant stuff in app.js:
var ViewModel = function () {
var self = this;
self.bugs = ko.observableArray();
self.error = ko.observable();
self.detail = ko.observable();
self.getBugDetail = function (item) {
ajaxHelper(bugsUri + item.Id, 'GET').done(function (data) {
self.detail(data);
});
}
var bugsUri = '/api/bugs/';
function ajaxHelper(uri, method, data) {
self.error(''); // Clear error message
return $.ajax({
type: method,
url: uri,
dataType: 'json',
contentType: 'application/json',
data: data ? JSON.stringify(data) : null
}).fail(function (jqXHR, textStatus, errorThrown) {
self.error(errorThrown);
});
}
// get open bugs
function getAllBugs() {
ajaxHelper(bugsUri, 'GET').done(function (data) {
self.bugs(data);
});
}
// Fetch the initial data.
getAllBugs();
//close bug
self.closeBug = function (localDetail) {
var closedBug = {
OwnerId: self.localDetail.OwnerId,
Description: self.localDetail.Description,
Status: "closed",
Title: self.localDetail.Title,
Created: self.localDetail.Created
};
ajaxHelper(bugsUri + self.localDetail.Id, 'DELETE', self.localDetail.Id);
ajaxHelper(bugsUri, 'POST', closedBug).done(function (item) {
self.bugs.push(item);
});
}
};
To update the status of a bug, I want to take the Id of the bug currently open in the detail panel and create an identical bug except with Status set to "closed". The trouble is that there's always a problem access self.localDetail in the new variable closedBug. I've tried it here by parameterizing the closeBug method, but I've also tried accessing self.Detail, but it's done no good, so I'm here. My next step, if this fails, is to create a separate panel entirely with a form for bugId which closes the bug when you submit, but it would be better to be in the bug details window.

you're already passing localDetail as the param in the closeBug fn, so you don't need to refer to it by adding self. Try this (removed all references to self.):
//close bug
self.closeBug = function (localDetail) {
var closedBug = {
OwnerId: localDetail.OwnerId,
Description: localDetail.Description,
Status: "closed",
Title: localDetail.Title,
Created: localDetail.Created
};
ajaxHelper(bugsUri + localDetail.Id, 'DELETE', localDetail.Id);
ajaxHelper(bugsUri, 'POST', closedBug).done(function (item) {
self.bugs.push(item);
});
}

Your first issue is in your submit binding itself. It's being called as soon as it's rendered, not on submit. You want to pass the function object (optionally with bound arguments) instead of calling it in your html.
Explicit Bound Arguments
<form class="form-horizontal" data-bind="submit: closeBug.bind(null, detail)">
<button type="submit" class="btn btn-default">Close bug</button>
</form>
which will bind null as the this value of the function and pass the detail observable as the first argument. With this, your closeBug looks like
self.closeBug = function (localDetail) {
var closedBug = {
OwnerId: localDetail().OwnerId
}
}
Note, you want to unwrap it in the handler, not the html so you get the latest value and not the initial value.
Binding Context as this
Alternatively (and in more idiomatic knockout fashion) you can bind to the function object and it will be called with the binding context as this (the same as explicitly using closeBug.bind($data)).
<form class="form-horizontal" data-bind="submit: closeBug">
<button type="submit" class="btn btn-default">Close bug</button>
</form>
self.closeBug = function () {
var closedBug = {
OwnerId: this.localDetail().OwnerId
}
}
Aside: this may be helpful for better understanding this, self, and function.bind

Related

How to pass variable to child function from HTML page

I want to pass a value from HTML page to child function from parent function.
HTML Page:
<div class="bottom_wrapper clearfix">
<div class="message_input_wrapper">
<input class="message_input" placeholder="Type your message here..." />
</div>
<div class="send_message">
<div class="icon"></div>
<div class="text">Send/div>
</div>
</div>
Parent Function Call:
$('.send_message').click(function (e) {
return [sendMessage(getMessageText()),sendMessage1(getMessageText1())];
});
$('.message_input').keyup(function (e) {
if (e.which === 13) {
return [sendMessage(getMessageText()),sendMessage1(getMessageText1())];
}
});
here getMessageText1 is child function.
Child Function:
getMessageText1 = function () {
var result="";
var id = Parent_FUNC_INPUT;
$.ajax({
url:"func.php",
type: 'POST',
data: ({id:id}),
async: false,
success:function(data) {
result = data;
}
});
I want to populate [[id]] variable in child function from parent function.
First, I'll do my best to clean up the HTML:
<div class="bottom_wrapper clearfix">
<div class="message_input_wrapper">
<input class="message_input" placeholder="Type your message here..." />
</div>
<div class="send_message">
<div class="icon"></div>
</div>
<div class="text">Send</div>
</div>
Using proper indentation will make things far easier to read. And while we're on the subject, you may want to use dashes - instead of underscores _ in your class names, as that's the common convention.
On to your question, it seems like what you want to do is simply pass an argument to getMessageText1 from within (as you refer to it) a "parent" function bound to an event listener.
So you'd define this "child" function with a single parameter:
function getMessageText1(Parent_FUNC_INPUT) {
...
var id = Parent_FUNC_INPUT;
...
}
And then you can just call it with getMessageText1(value) where value is whatever you want to pass in.
One more note: for readability's sake I recommend you do not name your functions the way you have. Having two functions getMessageText and getMessageText1 will just be a source of confusion later on. Instead, think of something more descriptive, ala getMessageTextFromID.
Hopefully I answered the question you meant to ask. Let me know.

How to pass data from one view to another using RenderAction

I am trying to call a view as modal dialog using RenderAction method. I have to pass some data from the view to the modal dialog's View. How can I achive this?
Below is my code (trimmed as per required) so far.
<table class="table">
<tr>
<th>Project No</th>
<th>Drawing No</th>
<th>Revision</th>
<th>Fabrication</th>
</tr>
#foreach (var irn in Model)
{
<tr>
<td class="projno">#irn.PROJECTNO</td>
<td class="drawingno">#irn.DRAWINGNO</td>
<td class="revno">#irn.REVNO</td>
<td>
<button class="button" type="button" class="btn btn-sm" data-toggle="modal">Add</button>
</td>
</tr>
}
Here is the modal dialog using RenderAction to call another view
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
<h4 class="modal-title" id="myModalLabel">Enter Fabrication Information</h4>
</div>
<div class="modal-body">
<div>
#{Html.RenderAction("Create", "Fabrication");}
</div>
</div>
</div>
</div>
Here are two ways I have tried to invoke the modal dialog
<script type="text/jscript">
$(document).ready(function ()
{
$('button').click(function ()
{
var $row = $(this).closest("tr") // Finds the closest row <tr>
var $projectNo = $row.find(".projno") // Gets a descendent with class="nr"
.text(); // Retrieves the text within <td>
var link = '#Url.Action("Create", "Fabrication")'
// Method 1 -
$('#myModal').modal('show');
//Method 2
$.ajax({
type: "GET",
url: link,
error: function (data)
{ },
success: function (data)
{
$("#myModal.modal-body").html(data);
$('#myModal').modal('show');
},
// in method 2, when I close the dialog, the screen becomes tinted and freezes while it works ok in method 1. why is that?
});
});
});
Here is the Fabrication/Create Conroller method
public ActionResult Create(string projectNo, string drawingNo, string revisionNo)
{
ViewBag.ProjectNo = projectNo;
ViewBag.DrawingNo = drawingNo;
ViewBag.RevisionNo = revisionNo;
return PartialView("Create");
}
When user click Add button, modal dialog should appear carrying ProjectNo information from parent View.
You need to pass the data when invoking controller action.
via JavaScript
When you're sending AJAX request via jQuery, you can use data option property, like in the example below.
If you're sending GET requst, jQuery will automagically append this object to the URL, like so: /Fabrication/Create?projectNo=123&drawingNo=456&revisionNo=789.
Hovewer, if you're sending POST request, URL will not be changed and the data object will be passed inside a request body.
$.ajax({
type: "GET",
url: link,
data: {
projectNo: 123,
drawingNo: 456,
revisionNo: 789
}
error: function (data)
{ },
success: function (data)
{
$("#myModal .modal-body").html(data); // Note that you missed a space between selectors
$('#myModal').modal('show');
},
// in method 2, when I close the dialog, the screen becomes tinted and freezes while it works ok in method 1. why is that?
});
via Razor
You can also use one of the parameter of Html.RenderAction or Url.Action to pass any additional data using anonymous object. This object is always the last function argument, no matter how many arguments you pass before (controller and area names are optional). Note that it's more of a fun fact, because you can't access JavaScript variables directly when using Server-Side methods. It'd be good when rendering default state of your form.
#* Pass custom parameters to controller's action and render it *#
#Html.RenderAction("Create", "Fabrication", new {
projectNo = 123,
drawingNo = 456,
revisionNo = 789
})
#* Create an URL to a controller's action with custom parameters *#
#Url.Action("Create", "Fabrication", new {
projectNo = 123,
drawingNo = 456,
revisionNo = 789
})

Child view model modying a different child viewmodel

I have a main View Model for my screen. It consists of 2 child view models.
One handles the registration section.
One handles the login section.
One handles the menu section (If authenticated and what menu items can appear, as well as the "Welcome "Username" type stuff).
$(document).ready(function () {
// Create the main View Model
var vm = {
loginVm: new LoginViewModel(),
registerVm: new RegisterViewModel(),
layoutVm: new LayoutViewModel()
}
// Get the Reference data
var uri = '/api/Reference/GetTimezones';
$.getJSON({ url: uri, contentType: "application/json" })
.done(function (data) {
vm.registerVm.Timezones(data);
});
// Bind.
ko.applyBindings(vm);
});
Once my Login model's "Login" method completes, I want to set the "IsAthenticated" value within the Menu model, as well as some other user info.
So in my login model, I have a SignIn method.
$.post({ url: uri, contentType: "application/json" }, logindata)
.done(function (data) {
toastr[data.StatusText](data.DisplayMessage, data.Heading);
if (data.StatusText == 'success') {
alert($parent.layoutVm.IsAuthenticated());
}
else {
}
})
.fail(function () {
toastr['error']("An unexpected error has occured and has been logged. Sorry about tbis! We'll resolve it as soon as possible.", "Error");
});
The alert code is my testing. I am hoping to access (and set) the IsAuthenticated property of the layoutVm model. That's one of the child models on my main View model.
However, "$parent" is not defined.
How can I update values in the layoutVm, from my loginVm?
$parent is part of the binding context, which is only available during the evaluation of the data-bind (i.e. to the binding handler).
In your viewmodel structure, you'll have to come up with a way to communicate between models yourself. For example, by passing parent view models, or by passing along shared observables. The problem you're describing can be solved by using data-bind="visible: $root.userVM.IsAuthenticated", like I answered in your previous question.
If you'd like to go with the other approach, here's an example on how to share an observable between viewmodels.
var ChildViewModel = function(sharedObs) {
this.myObs = sharedObs;
this.setObs = function() {
this.myObs(!this.myObs());
}.bind(this);
}
var RootViewModel = function() {
this.myObs = ko.observable(false);
this.vm1 = new ChildViewModel(this.myObs);
this.vm2 = new ChildViewModel(this.myObs);
this.vm3 = new ChildViewModel(this.myObs);
}
ko.applyBindings(new RootViewModel());
div { width: 25%; display: inline-block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="with: vm1">
<h4>vm1</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
<div data-bind="with: vm2">
<h4>vm2</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
<div data-bind="with: vm3">
<h4>vm3</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
Note that each of the child view models also have write permission, so you'll have to be careful to not accidentally update the observable

AngularJS load ng-repeat into another ng-repeat with ajax call

I'm new to angular and would like some help in solving the following issue. This is the code I currently have, simply getting an array of results from the server using a post request and displaying them using the ng-repeat directive:
<div id="mainW" ng-controller="MediaController">
<div id="mediaBodyW" ng-repeat="media in medias">
<div class="mediaW" data-id="{{media.id}}">
<div class="mediaNum">{{media.num}}</div>
<div class="mediaN">{{media.name}}</div>
<div id="loadSubs" ng-click="loadSubMedias(media.id)">load sub medias</div>
<div id="subMediaW"></div>
</div>
</div>
This is my controller:
app.controller("MediaController",['$scope','$http','$httpParamSerializerJQLike',function($scope,$http,$httpParamSerializerJQLike){
$scope.medias = [];
try {
$http({
method: 'POST',
url: 'media.php',
data: $httpParamSerializerJQLike({"request":"getAllMedia"}),
headers: {'Content-Type':'application/x-www-form-urlencoded'}
}).then(function (ans) {
$scope.medias = ans.data;
}, function (error) {
console.error(JSON.stringify(error));
});
}
catch (ex) {
console.error("Error: " + ex.toString());
}
}]);
Now, what I would like to achieve, is: on clicking the div with id of "loadSubs", run another $http post query which will load an array of results into the "subMediaW". Of course the query and appending of html should be unique for each media element, and each time a data is loaded for a particular element all previous results should be cleared, all this while taking into account that the loaded data will be also manipulated in the future.
Can someone please help me understand how can I do this using AngularJS?
Try this,
$scope.prevMedia = null; //use this to store last clicked media object
$scope.loadSubMedias = function(media){
$http.post().... //make api call for subMedia here
.then(function(res){
media.subMediaW = res.data // attach a subMediaW array to the media object
media.showSubMedia = true; // set variable true to make submedia visible for current media object, this will be used with ng-if attribute in html
if($scope.prevMedia != null) $scope.prevMedia.showSubMedia = false; // if last selected media object is not null, hide its submedia
})
}
and html
<div id="mainW" ng-controller="MediaController">
<div id="mediaBodyW" ng-repeat="media in medias">
<div class="mediaW" data-id="{{media.id}}">
<div class="mediaNum">{{media.num}}</div>
<div class="mediaN">{{media.name}}</div>
<div id="loadSubs" ng-click="loadSubMedias(media)">load sub medias</div>
<div id="subMediaW" ng-repeat="submedia in media.subMediaW" ng-if="media.showSubMedia"></div>
</div>
</div>
Firstly you should have a function in your controller with the name loadSubMedias and instead of simply taking media.id you can send whole media object to it (later on we will add new data into this object as an another property).
$scope.loadSubMedias = function (media) {
$http({
method: 'POST',
url: 'media.php',
data: $httpParamSerializerJQLike({"mediaId":media.id}),
headers: {'Content-Type':'application/x-www-form-urlencoded'}
}).then(function (response) {
// now add subDatas into main data Object
media.subMedias = response.data;
});
}
and in your controller just use ng-repeat like this
<div id="mainW" ng-controller="MediaController">
<div id="mediaBodyW" ng-repeat="media in medias">
<div class="mediaW" data-id="{{media.id}}">
<div class="mediaNum">{{media.num}}</div>
<div class="mediaN">{{media.name}}</div>
<div id="loadSubs" ng-click="loadSubMedias(media)">load sub medias</div>
<div id="subMediaW" ng-repeat="subMedia in media.subMedias">
<pre>{{subMedia | json}}</pre>
</div>
</div>
</div>

Webgrid needs to update with new content in MVC

I am still very new to MVC, JavaScript, and jQuery so please bear with me.
I have a webgrid that contains different terms and their translations. The list of terms is dependent on the 'VMID' chosen from the drop down list above the grid. (Or at least it would be, if it were working correctly.)
The left-most column has an edit link for each term that leads to a Boostrap modal, which is populated with all the values assigned to the ID chosen in that drop down list. I need the terms in the grid to also depend on the value chosen from that list.
The approach I am currently trying goes like this (only pasting the bits relevant to the question) -
Main view (strongly typed with model reference, not included):
<div class="container-fluid">
<div style=" margin-bottom: 1.4%;">
<table>
<tr>
<td style="font-size: medium; margin-bottom: 5px">
#Model.lblVMID:
</td>
<td>
#Html.DropDownListFor(model => model.VMID, new System.Web.Mvc.SelectList(Model.locations, "Key", "Value"), new { #class = "form-control", id = "ddlVMID", onchange = "RepopGrid()" })
</td>
</tr>
</table>
</div>
<div class="table-scrollable well" id="termGrid">
#{Html.RenderPartial("_TermGrid", Model);}
</div>
</div>
<script type="text/javascript">
function RepopGrid() {
VMID = $("#ddlVMID").val();
ShowLoadingDialog();
$.ajax({
url: URLPrefix() + "/Terminology/RepopGrid/" + VMID,
type: "POST",
success: function () {
HideLoadingDialog();
},
error: function (jqXHR, textStatus, errorThrown) {
HideLoadingDialog();
ShowAlert(false, 'Failed to change location\r\n' + errorThrown);
}
})
}
</script>
Partial view (strongly typed with model reference, not included. Same model that the main view uses):
#Model.grid.GetHtml(columns: Model.columns, alternatingRowStyle: "info", nextText: "Next",
previousText: "Previous", tableStyle: "table")
Controller:
public ActionResult Index()
{
TerminologyModel model = new TerminologyModel(clsUtils.PreferredVMID());
return View(model);
}
[HttpPost]
public ActionResult RepopGrid(int VMID)
{
TerminologyModel model = new TerminologyModel(VMID);
return PartialView("_TermGrid", model);
}
The model accepts an 'int VMID' and uses that to retrieve the list of terms from the database, then a foreach runs through each term and assigns them to the grid. This works fine, so I didn't feel a need to post it here (it's a bit long, because there are some special columns that need extra work to get set up).
We have a route configuration file that maps URLS to their corresponding actions in the controllers, in this case:
routes.MapRoute(
name: "TerminologyRepopGrid",
url: "Terminology/{action}/{VMID}",
defaults: new { controller = "Terminology", action = "RepopGrid", VMID = UrlParameter.Optional }
);
I'm not familiar with Ajax, so I'm probably using it completely wrong.
This approach is based on a few places where I've read to put the grid in a partial view, so that's what I've done here.
After I choose a new option, I can see that a whole new grid is being returned in Chrome's element inspector, but that grid is not being applied on top of the existing one.
Again, I have been searching and trying and reading and experimenting and I just can't figure out why mine won't work.
I moved the drop down list to the partial view where the grid is, wrapped everything in an Ajax Form, removed the "RepopGrid" JavaScript and controller actions, and added a parameter to the Index action for a VMID. If the VMID is null or empty (when the page is first loaded or refreshed), it uses the default VMID to generate the model. If a valid VMID is received, then it uses that number to generate the model instead.
Here is the new code for those who might be looking for a similar solution (like last time, only the relevant parts):
Index.cshtml -
<div class="table-scrollable well" id="termGrid">
#Html.Partial("_TermGrid", Model)
</div>
<div class="modal fade" id="editTerm" tabindex="-1" role="dialog" aria-labelledby="editTerm-label" aria-hidden="true">
<div class="modal-dialog" style="width: 290px">
<div class="modal-content" style="width: 290px">
<div class="modal-header" style="border-bottom: none; padding-bottom: 0px;">
<h4 id="lblParamName" align="center"></h4>
</div>
<div class="modal-body" id="editTermBody" style="padding: 8px">
</div>
</div>
</div>
</div>
Partial View -
#{
var ajaxOptions = new AjaxOptions()
{
OnSuccess = "OnSuccess",
OnFailure = "OnFailure",
OnBegin = "OnBegin",
HttpMethod = "Post"
};
using (Ajax.BeginForm("Index", "Terminology", ajaxOptions))
{
<div class="container-fluid" id="termGrid">
<div style=" margin-bottom: 1.4%;">
<table>
<tr>
<td style="font-size: medium; margin-bottom: 5px">
#Model.lblVMID<label>: </label>
</td>
<td>
#Html.DropDownListFor(model => model.VMID, new System.Web.Mvc.SelectList(Model.locations, "Key", "Value"), new { #class = "form-control", id = "ddlVMID", onchange = "this.form.submit()" })
</td>
</tr>
</table>
</div>
</div>
}
}
#Model.grid.GetHtml(columns: Model.columns, alternatingRowStyle: "info", nextText: "Next",
previousText: "Previous", tableStyle: "table")
<script type="text/javascript">
function OnSuccess(data, textStatus, jqXHR) {
HideLoadingDialog();
}
function OnFailure(data, textStatus, jqXHR) {
HideLoadingDialog();
ShowAlert(false, "Oops! Something went wrong. :(");
}
function OnBegin() {
ShowLoadingDialog();
VMID = $("#ddlVMID").val()
ShowLoadingDialog();
$.ajax({
url: URLPrefix() + "/Terminology/Index/" + VMID,
type: "POST",
success: function () {
HideLoadingDialog();
},
error: function (jqXHR, textStatus, errorThrown) {
HideLoadingDialog();
ShowAlert(false, 'Failed to change location\r\n' + errorThrown);
}
})
}
</script>
Controller -
public ActionResult Index(string VMID)
{
if (string.IsNullOrEmpty(VMID))
{
TerminologyModel model = new TerminologyModel(clsUtils.PreferredVMID());
return View(model);
}
else
{
TerminologyModel model = new TerminologyModel(int.Parse(VMID));
return View(model);
}
}
The model's code has not changed since the question was originally asked.

Categories