I have a web API which successfully returns an array of blog posts in JSON format:
[{"ID":1,"Title":"First Blog Post","Body":"Some Content"},{"ID":2,"Title":"Second BlogPost","Body":"Some other content"}]
For exercise purposes, I want to display all posts in a list using Knockout.js.
Here is my code:
<script>
$(document).ready(function () {
function AppViewModel() {
var self = this;
self.posts = ko.observableArray([
{ Title: 'Default Title', Body: 'Default Body' },
]);
$.getJSON('api/posts', function (data) {
ko.mapping.fromJSON(data, {}, self.posts);
});
}
ko.applyBindings(new AppViewModel());
});
My bindings:
<tbody data-bind="foreach: posts">
<tr>
<td data-bind="text: Title"></td>
<td data-bind="text: Body"></td>
</tr>
</tbody>
My table shows up empty, it's not showing the JSON data for some reason...
ANSWER: I had to change fromJSON to fromJS and it works! Thanks so much for your help everyone
In your case you need to specify the update target for the mapping.
From the Knockout Mapping plugin documentation:
If, like in the example above, you are performing the mapping inside
of a class, you would like to have this as the target of your mapping
operation. The third parameter to ko.mapping.fromJS indicates the
target. For example,
ko.mapping.fromJS(data, {}, someObject);
So in your case:
$.getJSON('api/posts', function (data) {
ko.mapping.fromJSON(data, {}, self.posts);
});
A working JSFiddle
Also note that that the property names are case sensitive and they should match in the JSON and in your viewmodel and bindings. e.g you retrive "Title" but you are using title
The json being returned has title case property names, but the original in self.posts has lowercase property names. I assume you are binding to the lowercase version in your template. Consider changing the json returned to lowercase too.
Related
I keep hitting a wall when trying to get the parent data passed down to the child component.
My view:
<%= react_component 'Items', { data: #items } %>
My Items component makes an ajax call, sets state, and renders Item. Leaving key={this.props.id} out of the Item instance passed into the mapping function makes it so that the component html renders to the page. But add the key in, and I get a console error: Uncaught TypeError: Cannot read property 'id' of undefined
Here's 'Items':
var Items = React.createClass({
loadItemsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
componentDidMount: function() {
this.loadItemsFromServer();
},
render: function() {
var itemNodes = this.props.data.map(function() {
return (
<Item key={this.props.id} />
);
});
return (
<div className="ui four column doubling stackable grid">
{itemNodes}
</div>
);
}
});
My item.js.jsx component just formats each Item:
var Item = React.createClass({
render: function() {
return (
<div className="item-card">
<div className="image">
</div>
<div className="description">
<div className="artist">{this.props.artist}</div>
</div>
</div>
);
}
});
The React dev tools extension shows the props and state data inside Items. The children, however, are empty.
I'm aware of this, but I'm setting key with this.props.id. I'm not sure what I'm missing?
I found a couple of problems with the code you posted, in the Items component
You're rendering this.props.data while in fact this.state.data is the one being updated with the ajax request. You need to render this.state.data but get the initial value from props
The map iterator function takes an argument representing the current array element, use it to access the properties instead of using this which is undefined
The updated code should look like this
var Item = React.createClass({
render: function() {
return (
<div className="item-card">
<div className="image">
</div>
<div className="description">
<div className="artist">{this.props.artist}</div>
</div>
</div>
);
}
});
var Items = React.createClass({
getInitialState: function() {
return {
// for initial state use the array passed as props,
// or empty array if not passed
data: this.props.data || []
};
},
loadItemsFromServer: function() {
var data = [{
id: 1,
artist: 'abc'
}, {
id: 2,
artist: 'def'
}]
this.setState({
data: data
});
},
componentDidMount: function() {
this.loadItemsFromServer();
},
render: function() {
// use this.state.data not this.props.data,
// since you are updating the state with the result of the ajax request,
// you're not updating the props
var itemNodes = this.state.data.map(function(item) {
// the map iterator function takes an item as a parameter,
// which is the current element of the array (this.state.data),
// use (item) to access properties, not (this)
return (
// use key as item id, and pass all the item properties
// to the Item component with ES6 object spread syntax
<Item key={item.id} {...item} />
);
});
return (
<div className="ui four column doubling stackable grid">
{itemNodes}
</div>
);
}
});
And here is a working example http://codepen.io/Gaafar/pen/EyyGPR?editors=0010
There are a couple of problems with your implementation.
First of all, you need to decide: Do you want to render the #items passed to the Items component from your view? Or do you want to load them asynchronous?
Because right now I get the impression you are trying to do both...
Render items passed from view
If you want to render the items from your view passed to the component, make sure it's proper json. You might need to call 'as_json' on it.
<%= react_component 'Items', { data: #items.as_json } %>
Then, in your Component, map the items to render the <Item /> components. Here is the second problem, regarding your key. You need to define the item variable to the callback function of your map function, and read the id from it:
var itemNodes = this.props.data.map(function(item) {
return (
<Item key={item.id} artist={item.artist} />
);
});
Note, I also added the author as prop, since you are using it in your <Item /> Component.
You can remove your componentDidMount and loadItemsFromServer functions, since you are not using them.
Load items asynchronous
If you want to load the items asynchronously, like you are trying to do in your loadItemsFromServer function, first of all, pass the url from your view and remove the {data: #items} part, since you will load the items from your component, something like:
<%= react_component 'Items', { url: items_path(format: :json) } %>
If you want to render the asynchronous fetched items, use:
var itemNodes = this.state.data.map(function(item) {
return (
<Item key={item.id} artist={item.artist} />
);loadItemsFromServer
});
Note I changed this.props.map to this.state.map
You can now use your componentDidMount and loadItemsFromServer functions to load the data and save them to state.
I need to pass props to a child component Product but i don't know what i'm missing here.
Parent Component:
var Products = React.createClass({
getInitialState: function(){..},
deleteProduct: function(){..},
updateProduct: function(){..},
render: function(){
var products = _.map(this.state.products, function(product){
return(
<Product key={product.id} product={product} handleDeleteProduct={this.deleteProduct} handleEditProduct={this.editProduct} formData={this.props.formData}/>
)
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Quantity</th>
<th>Price</th>
<th colSpan="4">Actions</th>
</tr>
</thead>
<tbody>
{products}
</tbody>
</table>
)
}
});
Child Component:
var Product = React.createClass({
console.log(this); //props: handleDeleteProduct: undefined, handleEditProduct: undefined
handleEdit: function() {
e.preventDefault();
data = {..};
$.ajax({
..,
success: (function(_this) {
console.log(_this); //props: handleDeleteProduct: undefined, handleEditProduct: undefined
return function(data) {
_this.setState({
edit: false
});
return _this.props.handleEditProduct(_this.props.product, data);
};
})(this)
});
}
});
I'm able to use key and product as a props inside the component but not this.props.handleDeleteProduct and this.props.handleEditProduct.
I think may be i'm using the props inside the success callback of the $.ajax and then may be some thing related to async request. Not sure.
The error i'm getting is
Uncaught TypeError: _this.props.handleEditProduct is not a function
I'm not sure what i'm doing wrong here. I tried to loop directly in between <tbody> but still no luck.
Also here i'm calling the functions like this.deleteProduct as a reference but not by function call. And if i do by calling by function then it is reporting execjs error.
I took this as a reference for looping inside JSX: loop inside React JSX
But no luck. Please help.
You are passing handleEditProduct={this.editProduct}, when the function is called updateProduct in your parent component. Change it to handleEditProduct={this.updateProduct} and I'll bet it works
EDIT:
Since that didn't work, I looked a little harder and I think I see what the problem is. I'm fairly sure that _ doesn't autobind this like React.createClass does. So when you map over your products here:
var products = _.map(this.state.products, function(product){
return(
<Product key={product.id} product={product} handleDeleteProduct={this.deleteProduct} handleEditProduct={this.editProduct} formData={this.props.formData}/>
)
});
this is not set to your react element. Try keeping a reference to this before you map, explicitly bind this to your map function, or use ES6 arrow functions: https://babeljs.io/docs/learn-es2015/#arrows-and-lexical-this. The simplest way to achieve what you want would be to save this in a variable:
var self = this;
var products = _.map(this.state.products, function(product){
return(
<Product key={product.id} product={product} handleDeleteProduct={self.deleteProduct} handleEditProduct={self.editProduct} formData={self.props.formData}/>
)
});
You can also use bind to achieve the same effect: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
I'm trying to create a custom knockout bindingHandler to add a custom jQuery UI widget but have run into trouble trying to access the elements created during binding. I'm sure there's something fundamental about this that I'm missing. I have the following html:
<table data-bind="myGrid: {}">
<thead>
<tr data-bind="foreach: { data: columns, as: 'column' }">
<th data-bind="text: column"></th>
</tr>
</thead>
<tbody data-bind="foreach: { data: rows, as: 'row' }">
<tr data-bind="foreach: { data: $parent.columns, as: 'column' }">
<td data-bind="text: row[column]"></td>
</tr>
</tbody>
</table>
And the following javascript:
var vm = {
columns: [
'A', 'B'
],
rows: []
};
$.widget("my.grid", {
_create: function() {
var columns = this.element.find('th');
}
});
ko.bindingHandlers.myGrid = {
init: function (element) {
//$(element).grid();
},
update: function(element) {
$(element).grid();
}
};
ko.applyBindings(vm);
When the widget is created, it needs to find each th element created from the binding. However, the elements don't appear to be created at that point in time. I have tried both the init and update methods of the bindinghandler.
This works if I manually add the widget to the element, just not within the bindinghandler.
When and how do I access the elements created from data-binding so that my jQuery widget can modify them?
You need to take control of the bindings to your descendant elements within your custom binding handler.
See http://knockoutjs.com/documentation/custom-bindings-controlling-descendant-bindings.html
But basically, do something like:
ko.bindingHandlers.myGrid = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
// bind our child elements (which will create the virtual foreach elements)
ko.applyBindingsToDescendants(bindingContext, element);
// make your grid widget
$(element).grid();
// tell KO we have already bound the children
return { controlsDescendantBindings: true };
},
update: function() { ... }
};
I'm still learning about MVC4.
Imagine the following scenario: I've got three dropDownLists and one big div for the content. I don't know how to deal the loading on demand.
The flow is simple, when page is loaded, display the data in the first dropdownlist. When this one has a value, the second one should load the information on demand (using the selected value from ddl1) and so on, until change the value on the ddl3 and displays the data.
Until here I have detected two partial views. I'm not sure if I should create 5 because each ddl must be in one partial view.
Another thing, what would you recomend to maintain the SelectList, should I have to use ViewBag or maintain in a viewModel the collections foreach ddl?
I just one to know if you can clarrify this scenario. I mean, give an idea about how can I start doing this? In fact, I forgot to mention this doubt but I don't if I have to use AJAX.
I got stuck , few weeks back in same kind of stuff:-
Let your MVC calls be like :-
private void LoadDropdown1()
{
var _data;//Your logic to get the data
ViewData["Dropdown1"] = new SelectList(_data, "Dropdown1Id", "Name");
}
private void LoadDropdown2(int dropdownParameterId)
{
var _data = "";//Use your ID to get the data
ViewData["Dropdown2"] = new SelectList(_data, "Dropdown2Id", "Name");
}
Your .cshtml be :-
#using (Html.BeginForm())
{
<div>
<table>
<tr>
<td><b>Select a District:</b></td>
<td>#Html.DropDownListFor(m => m.Dropdown1Id, ViewData["Dropdown1"] as IEnumerable<SelectListItem>, "Select One", new {#id="Dropdown1Id"})</td>
</tr>
<tr>
<td><b>Select:</b></td>
<td>#Html.DropDownListFor(m => m.Dropdown2Id, ViewData["Dropdown2"] as IEnumerable<SelectListItem>, "Select One")</td>
</tr>
</table>
</div>
}
Now AJAX call is best to load data to your dropdown:-
$(function () {
$('select#Dropdown1').change(function () {
var id = $(this).val();
$.ajax({
url: 'Bla Bla',
type: 'POST',
data: JSON.stringify({ id: id }),
dataType: 'json',
contentType: 'application/json',
success: function (data) {
$.each(data, function (key, data) {
$('select#Dropdown1').append('<option value="0">Select One</option>');
// loop through the LoadDropdown1 and fill the dropdown
$.each(data, function (index, item) {
$('select#Dropdown1').append(
'<option value="' + item.Id + '">'
+ item.Name +
'</option>');
});
});
}
});
});
});
What i am trying to say is.. Load your 1st dropdown the way you prefer. Then On change event of 1st dropdown, you can fire an ajax call to fetch data for 2nd dropdown.... Similarly..
Reference:- on select change event - Html.DropDownListFor
I need some guidance/suggestions for an optimal way to save the order of a sortable list that takes advantage of Meteor.
The following is a scaled down version of what I'm trying to do. The application is a simple todo list. The end goal for the user is to sort their list where the data is picked up from the database. As the user sorts tasks, I would like to save the order of the tasks.
I've implemented this application without Meteor using php/ajax calls using sortable's update event that would delete the entry in the database and replace it with what was currently in the DOM. I'm curious to know if there are a better ways to do this taking advantage of Meteor's capabilities.
The following sample code is straight off of a live demo.
HTML:
<template name="todo_list">
<div class="todo_list sortable">
{{#each task}}
<div class="task">
<h1>{{title}}</h1>
{{description}}
</div>
{{/each}}
</div>
</template>
JS(Without the Meteor.isServer that simply populates the database.):
if (Meteor.isClient) {
//Populate the template
Template.todo_list.task = function () {
return Tasks.find({});
};
//Add sortable functionality
Template.todo_list.rendered = function () {
$( ".sortable" ).sortable();
$( ".sortable" ).disableSelection();
};
}
Sample data (Output of Tasks.find({})):
[{
title:"CSC209",
description:"Assignment 3"
},
{
title:"Laundry",
description:"Whites"
},
{
title:"Clean",
description:"Bathroom"
}]
You'd probably want to first sort your items by a new field on you collection then, you'll want to hook into the jQuery sortable update event:
if (Meteor.isClient) {
// Populate the template
Template.todo_list.task = function () {
return Tasks.find({}, { sort: ['order'] });
};
// Add sortable functionality
Template.todo_list.rendered = function () {
$('.sortable').sortable({
update: function (event, ui) {
// save your new list order based on the `data-id`.
// when you save the items, make sure it updates their
// order based on their index in the list.
some_magic_ordering_function()
}
});
$( ".sortable" ).disableSelection();
};
}
You template would look a bit like this:
<template name="todo_list">
<div class="todo_list sortable">
{{#each task}}
<div class="task" data-id="{{_id}}">
<h1>{{title}}</h1>
{{description}}
</div>
{{/each}}
</div>
</template>
And when that event is triggered, it would determine the order of the list and save the new order in the documents for the collection.
This isn't really a complete answer, but hopefully it helps a bit.