im new to React and I need some help figuring this out!
I have a Json object:
var exampleJson = {
title: "title",
blocks: [
{
type: "block1",
items: [{id: "1", count: 1},
{id: "2", count: 1},]
},
{
type: "block2",
items: [{id: "3", count: 1},
{id: "4", count: 1},]
},
]};
I want to get every block in the object and render the data inside each block(including looping through items inside the block) in a div. The div should have a dynamic onClick-event that registers wich block was clicked.
This is what I got:
var BlockListClass = React.createClass({
blockClicked: function() {
// Here I dont know what block I clicked
},
loopBlocks: function(_data, blockClicked) {
// loop blocks
{_data.map(function(object, i){
var result = <div className="block" key={i}>
{[
<h2 onClick={blockClicked} key={i}> {object.type} </h2>
]}
</div>;
// Loop items
for (var key in object.items) {
if (object.items.hasOwnProperty(key)) {
result.props.children.push(<h2 key={key+10}> {object.items[key].id} </h2>);
}
}
return result;
})}
},
render: function() {
var _data = exampleJson.blocks;
var blockClicked = this.blockClicked;
var renderer = this.loopBlocks(_data,blockClicked);
return(
<div className="BlockList">
{renderer}
</div>
);
}
});
And then render BlockListClass like this:
<BlockListClass />
The question that you asked is essentially, "how do I pass arguments to onClick events on React Elements". This is a common pattern you will have to follow in React. The easiest way to do this, is just to bind the value to the function call. i.e.
<h2 onClick={blockClicked.bind(this, i)} key={i}> {object.type} </h2>
Then your handler would look like the following
blockClicked: function(i) {
console.log(i);
},
If you still need access to the click event, then you could initially pass in the index value, and return a function to be called when the onClick event is triggered:
<h2 onClick={blockClicked(i)} key={i}> {object.type} </h2>
With a resulting handler:
blockClicked: function(i) {
return function(e) {
console.log(i);
// do what you want with 'e'
}
}
Edit: I should also note that my answer is very general. Since your element is nested in a map(function(){...}), binding 'this' will cause an error. You will need to bind it to the correct context. However, I think that is out of the scope of this question. Just wanted to let you know if you run into that issue.
// you could do something like this
var BlockListClass = React.createClass({
blockClicked: function(e) {
var id = e.currentTarget.id;
console.log(id);
},
loopBlocks: function(_data, blockClicked) {
// loop blocks
{_data.map(function(object, i){
var result = <div className="block" key={i}>
{[
<h2 onClick={blockClicked} id={i} key={i}> {object.type} </h2>
]}
</div>;
// Loop items
for (var key in object.items) {
if (object.items.hasOwnProperty(key)) {
result.props.children.push(<h2 key={key+10}> {object.items[key].id} </h2>);
}
}
return result;
})}
},
render: function() {
var _data = exampleJson.blocks;
var blockClicked = this.blockClicked;
var renderer = this.loopBlocks(_data,blockClicked);
return(
<div className="BlockList">
{renderer}
</div>
);
}
});
Related
I am having a h1 with v-for and i am writing out things from my array ,it looks like this:
<h1
v-for="(record, index) of filteredRecords"
:key="index"
:record="record"
:class="getActiveClass(record, index)"
>
<div :class="getClass(record)">
<strong v-show="record.path === 'leftoFront'"
>{{ record.description }}
</strong>
</div>
</h1>
as you can see i am bindig a class (getActiveClass(record,index) --> passing it my record and an index)
This is my getActiveClass method:
getActiveClass(record, index) {
this.showMe(record);
return {
"is-active": index == this.activeSpan
};
}
i am calling a function called showMe passing my record to that and thats where the problem begins
the showMe method is for my setInterval so basically what it does that i am having multiple objects in my array and it is setting up the interval so when the record.time for that one record is over then it switches to the next one. Looks like this:
showMe(record) {
console.log(record.time)
setInterval(record => {
if (this.activeSpan === this.filteredRecords.length - 1) {
this.activeSpan = 0;
} else {
this.activeSpan++;
}
}, record.time );
},
this activeSpan is making sure that the 'is-active' class (see above) is changing correctly.
Now my problem is that the record.time is not working correctly when i print it out it gives me for example if iam having two objects in my array it console logs me both of the times .
So it is not changing correctly to its record.time it is just changing very fastly, as time goes by it shows just a very fast looping through my records .
Why is that? how can i set it up correctly so that when i get one record its interval is going to be the record.time (what belongs to it) , and when a record changes it does again the same (listening to its record.time)
FOR EXAMPLE :
filteredRecords:[
{
description:"hey you",
time:12,
id:4,
},
{
description:"hola babe",
time:43,
id:1
},
]
it should display as first the "hey you" text ,it should be displayed for 12s, and after the it should display the "hola babe" for 43 s.
thanks
<template>
<h1 ...>{{ filteredRecords[index].description }}</h1>
</template>
<script>
{
data() {
return {
index: 0,
// ...
};
},
methods: {
iterate(i) {
if (this.filteredRecords[i]) {
this.index = i;
window.setTimeout(() => iterate(i + 1), this.filteredRecords[i].time * 1000);
}
},
},
mounted() {
this.iterate(0);
},
}
</script>
How about this? Without using v-for.
I am making a pie chart for my data. I am using Angular Chart (and subsequently, charts.js).
My data looks like this (vm being the controller):
vm.persons = [
{
name:'smith',
cart: [
{
id: 1,
category: 'food'
},
{
id: 2,
category: 'clothes'
}
]
},
{
name: 'adams',
cart: [
{
id: 3,
category: 'automobile'
},
{
id:1, category: 'food'
}
]
}
]
As such, my template looks like:
<div ng-repeat="person in vm.persons">
<div class="person-header">{{person.name}}</div>
<!-- chart goes here -->
<canvas class="chart chart-pie" chart-data="person.cart | chart : 'category' : 'data'" chart-labels="person.cart | chart : 'category' : 'labels'"></canvas>
<div class="person-data" ng-repeat="item in person.cart">
<div>{{item.category}}</div>
</div>
</div>
I decided to go with a filter for the chart as I thought that would be appropriate, DRY and reusable:
angular.module('myModule').filter('chartFilter', function() {
return function(input, datum, key) {
const copy = JSON.parse(JSON.stringify([...input.slice()])); // maybe a bit overboard on preventing mutation...
const entries = Object.entries(copy.reduce((o,n) => {o[n[datum]] = (o[n[datum]] || 0) + 1}, {}));
const out = {
labels: entries.map(entry => entry[0]);
data: entries.map(entry => entry[1]);
};
return out[key];
}
});
THIS WORKS, and the chart does show up, with the proper data. However per the console, it throws an infdig error every time. Per the docs, it's because I am returning a new array, which I am because it is almost a different set of data. Even if I get rid of copy (which is meant to be a separate object entirely) and use input directly (input.reduce(o,n), etc.) it still throws the error.
I tried also making it into a function (in the controller):
vm.chartBy = (input, datum, key) => {
const copy = JSON.parse(JSON.stringify([...input.slice()])); // maybe a bit overboard on preventing mutation...
const entries = Object.entries(copy.reduce((o,n) => {o[n[datum]] = (o[n[datum]] || 0) + 1}, {}));
const out = {
labels: entries.map(entry => entry[0]);
data: entries.map(entry => entry[1]);
};
return out[key];
};
and in the template:
<canvas class="chart chart-pie" chart-data="vm.chartBy(person.cart, 'category', 'data')" chart-labels="vm.chartBy(person.cart, 'category', 'labels')"></canvas>
However this is throwing an infdig error as well.
Does anyone know how to not get it to through an infdig error each time? That is what I am trying to solve.
As you pointed out, you can't bind to a function which produces a new array or the digest cycle will never be satisfied that the new value matches the old, because the array reference changes each time.
Instead, bind only to the data and then implement the filter in the directive, so that the filtered data is never bound, just shown in the directive's template.
HTML
<canvas class="chart chart-pie" chart-data="person.cart" chart-labels="person.cart"></canvas>
JavaScript
app.directive('chartData', function(){
return {
template: '{{chartData | chart : "category" : "data"}}',
scope: {
'chartData': '='
}
}
});
app.directive('chartLabels', function(){
return {
template: '{{chartLabels | chart : "category" : "labels"}}',
scope: {
'chartLabels': '='
}
}
});
app.filter('chart', function() {
return function(input, datum, key) {
...
return out[key];
}
});
I've hardcoded the datum/key strings in the directives but you could pass those in as additional bindings if needed.
Simple Mock-up Fiddle
I'm building a key-command resource and giving VueJS a whirl while doing so. I'm a newbie but am gaining the grasp of things (slowly...).
I want to be able to search in a global search form for key commands I'm defining as actions within sections of commands (see data example below). I would like to search through all the actions to show only those that match the search criteria.
My HTML is below:
<div id="commands">
<input v-model="searchQuery" />
<div class="commands-section" v-for="item in sectionsSearched"
:key="item.id">
<h3>{{ item.section }}</h3>
<div class="commands-row" v-for="command in item.command" :key="command.action">
{{ command.action }}
</div>
</div>
</div>
My main Vue instance looks like this:
import Vue from 'vue/dist/vue.esm'
import { commands } from './data.js'
document.addEventListener('DOMContentLoaded', () => {
const element = document.getElementById("commands")
if (element != null) {
const app = new Vue({
el: element,
data: {
searchQuery: '',
commands: commands
},
computed: {
sectionsSearched() {
var self = this;
return this.commands.filter((c) => {
return c.command.filter((item) => {
console.log(item.action)
return item.action.indexOf(self.searchQuery) > -1;
});
});
},
}
});
}
});
And finally the data structure in data.js
const commands = [
{
section: "first section",
command: [
{ action: '1' },
{ action: '2' },
{ action: '3' },
],
},
{
section: "second section",
command: [
{ action: 'A' },
{ action: 'B' },
{ action: 'C' },
]
},
]
export { commands };
I'm able to output the commands using the console.log(item.action) snippet you see in the computed method called sectionsSearched.
I see no errors in the browser and the data renders correctly.
I cannot however filter by searching in real-time. I'm nearly positive it's a combination of my data structure + the computed method. Can anyone shed some insight as to what I'm doing wrong here?
I'd ideally like to keep the data as is because it's important to be sectioned off.
I'm a Rails guy who is new to this stuff so any and all feedback is welcome.
Thanks!
EDIT
I've tried the proposed solutions below but keep getting undefined in any query I pass. The functionality seems to work in most cases for something like this:
sectionsSearched() {
return this.commands.filter((c) => {
return c.command.filter((item) => {
return item.action.indexOf(this.searchQuery) > -1;
}).length > 0;
});
},
But alas nothing actually comes back. I'm scratching my head hard.
There is a issue in your sectionsSearched as it is returning the array of just commands.
See this one
sectionsSearched() {
return this.commands.reduce((r, e) => {
const command = e.command.filter(item => item.action.indexOf(this.searchQuery) > -1);
const section = e.section;
r.push({
section,
command
});
}, []);
}
const commands = [
{
section: "first section",
command: [
{ action: '1' },
{ action: '2' },
{ action: '3' },
],
},
{
section: "second section",
command: [
{ action: 'A' },
{ action: 'B' },
{ action: 'C' },
]
},
]
const element = document.getElementById("commands")
if (element != null) {
const app = new Vue({
el: element,
data: {
searchQuery: '',
commands: commands
},
computed: {
sectionsSearched() {
var self = this;
return this.commands.filter((c) => {
// the code below return an array, not a boolean
// make this.commands.filter() not work
// return c.command.filter((item) => {
// return item.action.indexOf(self.searchQuery) > -1;
// });
// to find whether there has command action equal to searchQuery
return c.command.find(item => item.action === self.searchQuery);
});
},
}
});
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="commands">
<input v-model="searchQuery" />
<div class="commands-section" v-for="item in sectionsSearched"
:key="item.id">
<h3>{{ item.section }}</h3>
<div class="commands-row" v-for="command in item.command" :key="command.action">
{{ command.action }}
</div>
</div>
</div>
Is that work as you wish ?
sectionsSearched() {
return this.commands.filter((c) => {
return c.command.filter((item) => {
return item.action.indexOf(this.searchQuery) > -1;
}).length > 0;
});
},
}
since filter will always return an array(empty or not) which value always is true.
HTML with bindings:
<div class="container" data-bind="foreach: { data: Conferences, as: 'conf' }">
<div class="conf" data-bind="attr: { id : conf.Id }">
<div class="conf-month" data-bind="text: conf.StartTime"></div>
<div class="conf-day" data-bind="text: conf.EndTime"></div>
<div class="add-user">
<input type="tel" data-bind="value: $root.PhoneNumber />
<input type="email" data-bind="value: $root.Email" />
<a id="add-user" title="Add new user" data-bind="attr: { value: conf.Id }, click: $root.addUserToConference">Add</a>
</div>
<div class="conf-users" data-bind="foreach: { data: conf.ConferenceUsers, as: 'user' }">
<div class="conf-user" data-bind="attr: { id: 'confuser-' + user.Id}">
<span data-bind="text: user.Name"></span>
<span data-bind="text: user.PhoneNumber"></span>
<span data-bind="text: user.Email"></span>
</div>
</div>
</div>
</div>
KnockoutJs ViewModel:
function ConferenceViewModel() {
var self = this;
self.Conferences = ko.observableArray([]);
self.PhoneNumber = ko.observable("");
self.Email = ko.observable("");
self.getAllConfs = function () {
$.ajax({
type: 'GET',
url: '/confs/getconfs',
}).done(function (confs) {
$.each(confs, function (index, conf) {
//populate the list on document ready
confVm.Conferences.push(conf);
};
}).fail(showError);
}
self.addUserToConference = function (viewModel, event) {
var user = {
"Id": 0,
"ConferenceId": viewModel.Id(),
"Email": "self.Email()",
"PhoneNumber": self.PhoneNumber(),
};
// here we should insert a new ConferenceUser into the Conferences observable array
// and also update the DOM
// ajax to insert user to db
}
After I populate the above Conferences observableArray via ajax,
the output of console.log(ko.toJSON(confVm.Conferences())) is as follows:
[
{
"ConferenceUsers":[
{
"Id":3006,
"ConferenceId":8,
"Name":null,
"Email":"mail#lala.com",
"UserName":null,
"PhoneNumber":"234234234234",
"ChannelId":null,
"State":null,
"Type":null,
"RecordingId":null
}
],
"Id":8,
"Subject":"YYYhaaaaa",
"StartTime":"2016-05-29T18:30:00",
"EndTime":"2016-05-29T19:30:00",
"OrganizerEmail":"elpas#live.com",
"OrganizerName":"dasdasd",
"Pin":"6402",
"BridgeId":null,
"State":null
},
{
"ConferenceUsers":[
{
"Id":3013,
"ConferenceId":12,
"Name":null,
"Email":"dsfdfsdfdsf#dfdfdf.com",
"UserName":null,
"PhoneNumber":null,
"ChannelId":null,
"State":null,
"Type":null,
"RecordingId":null
}
],
"Id":12,
"Subject":"dsfsdfdsfsdf",
"StartTime":"2016-05-31T22:00:00",
"EndTime":"2016-05-31T23:00:00",
"OrganizerEmail":"d#adssad.com",
"OrganizerName":"dsfdsfsdf",
"Pin":"3402",
"BridgeId":null,
"State":null
}
]
Q: How can I insert a new ConferenceUser by ConferenceId and update the DOM accordingly?
You'll need to execute four steps:
Get the current list of conferences from the observable array Conferences
Find the conference with the right id
Push a new user to this conference's ConferenceUsers
Make sure Conferences is set with the new data
While all steps are pretty straight forward to execute, there'll be some drawbacks to how they'd work:
The Conference objects and the ConferenceUsers arrays aren't observable. Knockout isn't automatically aware of any changes inside the Conference objects. So, after step 3 and 4, to knockout, it won't appear as if anything's changed: the Conferences array still has the same objects in it.
My advice:
If adding new users to conferences is something that will happen regularly, I'd suggest creating a Conference viewmodel that has a ko.observableArray of users. Alternatively, you could create new Conference objects for every minor change, which will trigger knockout to re-render the entire UI instead of just the relevant part (assuming you've used a foreach data-bind somewhere).
A quick example of how you could map your regular Conference objects to viewmodels:
// Your JSON data from the ajax request (an array of objects)
var conferencesData = [];
var Conference = function(conferenceJSON) {
// It's better to map all properties you're using individually,
// but this at least exposes the original JSON object
this.props = conferenceJSON;
this.users = ko.observableArray(conferenceJSON.conferenceUsers);
};
var createConference = function(conferenceJSON) {
return new Conference(conferenceJSON);
};
var ConferenceList = function(conferencesJSON) {
var self = this;
this.conferences = ko.observableArray(conferencesJSON.map(createConference));
this.addConference = function(conferenceJSON) {
self.conferences.push(createConference(conferenceJSON));
};
this.addUserToConference = function(userJSON) {
var conferences = self.conferences();
for (var c = 0; c < conferences.length; c += 1) {
// Step 2: Find the conference with the required id
if (conferences[c].props.id === userJSON.ConferenceId) {
// Step 3: We're pushing to an observableArray, so no need
// to worry about Step 4.
conferences[c].users.push(userJSON);
return true;
}
}
return false;
};
};
ko.applyBindings(new ConferenceList(conferencesData));
Try this:
var obj = [
{
"ConferenceUsers":[
{
"Id":3006,
"ConferenceId":8,
"Name":null,
"Email":"mail#lala.com",
"UserName":null,
"PhoneNumber":"234234234234",
"ChannelId":null,
"State":null,
"Type":null,
"RecordingId":null
}
],
"Id":8,
"Subject":"YYYhaaaaa",
"StartTime":"2016-05-29T18:30:00",
"EndTime":"2016-05-29T19:30:00",
"OrganizerEmail":"elpas#live.com",
"OrganizerName":"dasdasd",
"Pin":"6402",
"BridgeId":null,
"State":null
},
{
"ConferenceUsers":[
{
"Id":3013,
"ConferenceId":12,
"Name":null,
"Email":"dsfdfsdfdsf#dfdfdf.com",
"UserName":null,
"PhoneNumber":null,
"ChannelId":null,
"State":null,
"Type":null,
"RecordingId":null
}
],
"Id":12,
"Subject":"dsfsdfdsfsdf",
"StartTime":"2016-05-31T22:00:00",
"EndTime":"2016-05-31T23:00:00",
"OrganizerEmail":"d#adssad.com",
"OrganizerName":"dsfdsfsdf",
"Pin":"3402",
"BridgeId":null,
"State":null
}
];
/* Iterate all conferences */
for (var i in obj) {
/* Find conference with ID = 8 */
if (obj[i].Id === 8) {
/* Add a new user to the conference */
obj[i].ConferenceUsers.push({
"Id":1111,
"ConferenceId":1,
"Name":null,
"Email":"test#example.com",
"UserName":null,
"PhoneNumber":null,
"ChannelId":null,
"State":null,
"Type":null,
"RecordingId":null
});
break;
}
}
console.log(obj); // output result
I have a table with these fields: product, lot, input1, input2. You can clone a line, and you can add a new line.
What I want to do is that for each row you can add a new Lot created by a "number" and by "id" that user write in the input field under the Select lot. And I wanted that the script add the new Lot in the json data and the lot 's option list.
This is the function for add that I tried to do:
$scope.addLot = function() {
var inWhichProduct = row.selectedProduct;
var newArray = {
"number": row.newLot.value,
"id": row.newLot.id
};
for (var i = 0; i < $scope.items.length; i++) {
if ($scope.items[i].selectedProduct === inWhichProduct) {
$scope.items[i].selectedLot.push(newArray);
}
}
};
-->> THIS <<-- is the full code.
Can you help me?
I think your question is a little too broad to answer on Stack Overflow, but here's an attempt:
<div ng-app="myApp" ng-controller="myCtrl">
<table>
<tr ng-repeat="lot in lots">
<td>{{ lot.id }}</td>
<td>{{ lot.name }}</td>
</tr>
</table>
<p>name:</p> <input type="text" ng-model="inputName">
<p>id:</p> <input type="text" ng-model="inputId">
<button ng-click="addLotButton(inputId, inputName)">Add</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.0-beta.2/angular.min.js"></script>
<script>
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.lots = [{
name: "test",
id: 1
},
{
name: "test2",
id: 2
}
];
$scope.addLot = function(lotId, lotName) {
var newLotObject = {
name: lotName,
id: lotId
};
$scope.lots.push(newLotObject);
};
$scope.addLotButton = function(id, name) {
$scope.addLot(id, name);
};
$scope.addLot(3, "Another test");
});
</script>
Basically this code just takes some input and adds an object to the scope for that input. The table is created using an ng-repeat of this data. It's not great code at all but it's just a quick example.
The push method adds newArray to selectedLot array. It's not working on the JSON data but on arrays. If you want to have the JSON, you can give a try to :
var myJsonString = JSON.stringify(yourArray);
It will create a JSON string based on the parameter
Maybe you should try to structure your data to make lots as properties of products.
{
products: [
{id: 1, lots: [{id:1}, {id:2}]},
{id: 2, lots: [{id:1}, {id:2}]}
]
}
To add a lot to a product :
product = products[0];
product.lots.push(newArray);
Change the fallowing:
html:
<button ng-click="addLot(row.selectedProduct.id,row.newLot.value,row.newLot.id)">Add</button>
js:
$scope.addLot = function(id,val,lotId) {
// console.log(id);
var inWhichProduct = id;
var newArray = { "value": val, "id": lotId };
//console.log($scope.items)
angular.forEach($scope.items,function(v,i){
if($scope.items[i].id == id )
{
$scope.items[i].lots.push(newArray);
console.log($scope.items[i].lots);
}
});
};
http://plnkr.co/edit/W8eche8eIEUuDBsRpLse?p=preview