How to set event handler in React sub-component - onclick

I'm having trouble getting menu items connected to an event handler. Here's a mock of the UI showing state changes over time. It's a dropdown menu (via Bootstrap), with the root menu item showing the current selection:
[ANN]<click ... [ANN] ... [BOB]<click ... [BOB]
[Ann] [Ann]
[Bob]<click + ajax [Bob]
[Cal] [Cal]
The end goal is to change the page content asynchronously based on the user's selection. Clicking on Bob should trigger the handleClick, but it's not.
As a side note, I'm not terribly happy with the way componentDidMount calls this.handleClick();, but it works for now as a way to get initial menu content from the server.
/** #jsx React.DOM */
var CurrentSelection = React.createClass({
componentDidMount: function() {
this.handleClick();
},
handleClick: function(event) {
alert('clicked');
// Ajax details ommitted since we never get here via onClick
},
getInitialState: function() {
return {title: "Loading items...", items: []};
},
render: function() {
var itemNodes = this.state.items.map(function (item) {
return <li key={item}><a href='#' onClick={this.handleClick}>{item}</a></li>;
});
return <ul className='nav'>
<li className='dropdown'>
<a href='#' className='dropdown-toggle' data-toggle='dropdown'>{this.state.title}</a>
<ul className='dropdown-menu'>{itemNodes}</ul>
</li>
</ul>;
}
});
$(document).ready(function() {
React.renderComponent(
CurrentSelection(),
document.getElementById('item-selection')
);
});
I'm almost positive that my hazy understanding of javascript scoping is to blame, but everything I've tried so far has failed (including trying to pass the handler down through props).

The problem is that you're creating the item nodes using an anonymous function, and inside that this means the window. The fix is to add .bind(this) to the anonymous function.
var itemNodes = this.state.items.map(function (item) {
return <li key={item}><a href='#' onClick={this.handleClick}>{item}</a></li>;
}.bind(this));
Or create a copy of this and use that instead:
var _this = this, itemNodes = this.state.items.map(function (item) {
return <li key={item}><a href='#' onClick={_this.handleClick}>{item}</a></li>;
})

As I can understand the specification of the task for "Anna", "Bob", "Cal, the solution can be the following (based on a react component and ES6):
Basic live demo is here
import React, { Component } from "react"
export default class CurrentSelection extends Component {
constructor() {
super()
this.state = {
index: 0
}
this.list = ["Anna", "Bob", "Cal"]
}
listLi = list => {
return list.map((item, index) => (
<li key={index}>
<a
name={item}
href="#"
onClick={e => this.onEvent(e, index)}
>
{item}
</a>
</li>
))
}
onEvent = (e, index) => {
console.info("CurrentSelection->onEvent()", { [e.target.name]: index })
this.setState({ index })
}
getCurrentSelection = () => {
const { index } = this.state
return this.list[index]
}
render() {
return (
<div>
<ul>{this.listLi(this.list)}</ul>
<div>{this.getCurrentSelection()}</div>
</div>
)
}
}

Related

Firebase: Why value event gets fired before new child ref gets added

Following code, is a very simple Firebase - VueJS app, (codeSandBox demo)
app.vue
<template>
<div class="container">
<!-- Adding Quote -->
<add-quote/>
<!-- Display Quotes -->
<quote-list/>
</div>
</template>
<script>
import addQuote from "./components/AddQuote.vue";
import quoteList from "./components/QuoteList.vue";
export default {
components: {
addQuote,
quoteList
},
methods: {
get_allQuotes: function() {
// var vm = this;
var localArr = [];
quotesRef
.once("value", function(snapshot) {
snapshot.forEach(function(snap) {
localArr.push({
key: snap.key,
category: snap.val().category,
quoteTxt: snap.val().quoteTxt
});
});
})
.then(data => {
this.$store.commit("set_allQuotes", localArr);
});
}
},
mounted() {
this.get_allQuotes();
console.log("App: mounted fired");
}
};
</script>
store.js(vuex store)
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
quotesList: []
},
getters: {
get_quotesList(state) {
return state.quotesList;
}
},
mutations: {
set_allQuotes(state, value) {
state.quotesList = value;
}
}
});
AddQuote.vue
<template>
<div class="row quote-edit-wrapper">
<div class="col-xs-6">
<textarea v-model.lazy="newQuoteTxt"
rows="4"
cols="50"></textarea>
<button #click="addQuote">Add Quote</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
newQuoteTxt: '',
}
},
computed: {
allQuotes() {
return this.$store.getters.get_quotesList;
},
newQuoteIdx() {
var localArr = [...this.allQuotes]
if(localArr.length > 0) {
var highestKEY, currKEY
localArr.forEach((element, idx) => {
currKEY = parseInt(element.key)
if(idx == 0) {
highestKEY = currKEY
} else {
if(highestKEY < currKEY) {
highestKEY = currKEY
}
}
})
return highestKEY + 1
} else {
return 1
}
}
},
methods: {
// ADD new Quote in DB
addQuote: function() {
var vm = this
var localArr = [...this.allQuotes]
//1. First attach 'value' event listener,
// Snapshot will contain data from that ref
// when any child node is added/updated/delete
quotesRef.on('value', function (snapshot) {
snapshot.forEach(function(snap) {
var itemExists = localArr.some(function (item, idx) {
return item.key == snap.key
})
// If newly added item doesn't yet exists then add to local array
if (!(itemExists)) {
localArr.push({
key: snap.key,
category: snap.val().category,
quoteTxt: snap.val().quoteTxt })
vm.$store.commit('set_allQuotes', localArr)
}
})
})
//2. Second set/create a new quotes in Firebase,
// When this quote gets added in Firebase,
// value event (attached earlier) gets fired
// with
var newQuoteRef = quotesRef.child(this.newQuoteIdx)
newQuoteRef.set({
category: 'motivation',
quoteTxt: this.newQuoteTxt
})
}
}
}
</script>
quoteList.vue
<template>
<div class="row">
<div class="col-xs-12 quotes-list-wrapper">
<template v-for="(quote,idx) in allQuotes">
<!-- Quote block -->
<div class="quote-block-item">
<p class="quote-txt"> {{quote.quoteTxt}} </p>
</div>
</template>
</div>
</div>
</template>
<script>
export default {
computed: {
allQuotes() {
return this.$store.getters.get_quotesList;
}
}
}
</script>
Note: The main code of concern is of addQuote.vue
User enter newQuoteTxt that gets added to Firebase (addQuote()) as a quote item under quotesRef. As soon as quote is added (on firebase), Firebase client side SDK's value event fires, and adds the new quote (via callback) to localArray (allQuotes). VueJS then updates the DOM with newly added Quote.
The addQuote() method works in the following manner:
First, attach a callback/listener to 'value' event on quotesRef
quotesRef.on('value', function (snapshot) {
....
})
Next, A firebase ref (child of quotesRef) is created with a ID this.newQuoteIdx
var newQuoteRef = quotesRef.child(this.newQuoteIdx)
Then set() is called (on this newly created Ref) adding newquote to firebase RealTime DB.
value event gets triggered (attached from step 1) and listener /callback is called.
The callback looks for this new quote's key in existing list of items by matching keys of localArr and snap.key, if not found, adds the newly quote to localArr. localArr commits to a vuex store.
`vm.$store.commit('set_allQuotes', localArr)`
VueX then updates all subscriber component of this array. VueJS then adds the new quote to the existing list of quotes (updates the DOM)
While debugging the addQuote method, the problem I notice, the execution/flow of script (via F8 in chrome debugger) first steps into the listener/callback attached to value event before the code newQuoteRef.set({ ... }) that adds new quote (on firebase), which in turn will cause 'value' event to trigger.
I am not sure why this occurs. Can anybuddy explain why the listener/callback is called before the quotes is created.
Are child nodes (of QuotesRef) are cached at clientside such that 'value' fires even before new quote is added.
Thanks
If I correctly understand your question (Your code is not extremely easy to follow! :-)) it is the normal behaviour. As explained in the documentation:
The value event will trigger once with the initial data stored at
this location, and then trigger again each time the data
changes.
Your sandbox demo does not actually shows how the app works, but normally you should not set-up the listener in the method that saves a new node to the database. These two things should be decoupled.
One common approach is to set the listener in the created hook of a component (see https://v2.vuejs.org/v2/guide/instance.html#Instance-Lifecycle-Hooks and https://v2.vuejs.org/v2/api/#created) and then in your addQuote method you just write to the database. As soon as you write, the listener will be fired.

Vuefire get Firebase Image Url

I am storing relative paths to images in my firebase database for each item I wish to display. I am having trouble getting the images to appear on the screen, as I need to get the images asynchronously. The firebase schema is currently as follows:
{
items: {
<id#1>: {
image_loc: ...,
},
<id#2>: {
image_loc: ...,
},
}
}
I would like to display each of these images on my page with code such as:
<div v-for="item in items">
<img v-bind:src="item.image_loc">
</div>
This does not work, as my relative location points to a place in firebase storage. The relavent code to get the true url from this relative url is:
firebase.storage().ref('items').child(<the_image_loc>).getDownloadURL()
which returns a promise with the true url. Here is my current vue.js code:
var vue = new Vue({
el: '.barba-container',
data: {
items: []
},
firebase: function() {
return {
items: firebase.database().ref().child('items'),
};
}
});
I have tried using computed properties, including the use of vue-async-computed, but these solutions do not seem to work as I cannot pass in parameters.
Basically, how do I display a list of elements where each element needs the result of a promise?
I was able to solve this by using the asyncComputed library for vue.js and by making a promise to download all images at once, instead of trying to do so individually.
/**
* Returns a promise that resolves when an item has all async properties set
*/
function VotingItem(item) {
var promise = new Promise(function(resolve, reject) {
item.short_description = item.description.slice(0, 140).concat('...');
if (item.image_loc === undefined) {
resolve(item);
}
firebase.storage().ref("items").child(item.image_loc).getDownloadURL()
.then(function(url) {
item.image_url = url;
resolve(item);
})
.catch(function(error) {
item.image_url = "https://placeholdit.imgix.net/~text?txtsize=33&txt=350%C3%97150&w=350&h=150";
resolve(item);
});
});
return promise;
}
var vue = new Vue({
el: '.barba-container',
data: {
items: [],
is_loading: false
},
firebase: function() {
return {
items: firebase.database().ref().child('items'),
};
},
asyncComputed: {
processedItems: {
get: function() {
var promises = this.items.map(VotingItem);
return Promise.all(promises);
},
default: []
}
}
});
Lastly, I needed to use: v-for="item in processedItems" in my template to render the items with image urls attached
I was able to solve it without any extra dependencies not adding elements to the array until the url is resolved:
in my template:
<div v-for="foo in foos" :key="foo.bar">
<img :src="foo.src" :alt="foo.anotherbar">
...
</div>
in my component (for example inside mounted())
const db = firebase.firestore()
const storage = firebase.storage().ref()
const _this = this
db.collection('foos').get().then((querySnapshot) => {
const foos = []
querySnapshot.forEach((doc) => {
foos.push(doc.data())
})
return Promise.all(foos.map(foo => {
return storage.child(foo.imagePath).getDownloadURL().then(url => {
foo.src = url
_this.foos.push(foo)
})
}))
}).then(() => {
console.log('all loaded')
})

(Meteor + React) Cannot access props from within subcomponent

I am passing data from one component to another (MyApplicants) via my router (FlowRouter):
FlowRouter.route('/applicants', {
name: 'Applicants',
action: function () {
var currentUser = Meteor.user();
ReactLayout.render(App, {
content: <MyApplicants institutionID={Meteor.user().profile.institutionID} />,
nav: <Nav />,
header: <Header />
});
}
});
As you can see I'm passing institutionID to the new component via a prop in the router. I know that the institutionID is being passed because I can see it in the render of the MyApplicants component.
Here is the MyApplicants component:
MyApplicants = React.createClass({
mixins: [ReactMeteorData],
pagination: new Meteor.Pagination(Applicants, {
perPage: 25,
filters: {institution_id: this.props.institutionID },
sort: {institution_id: 1, "last_name":1, "first_name":1}
}),
getMeteorData() {
return {
currentUser: Meteor.user(),
applicants: this.pagination.getPage(),
ready: this.pagination.ready()
}
},
RenderApplicantRow(applicant, key) {
return (
<div key={key}>
<p>[{applicant.institution_id}] {applicant.last_name}, {applicant.first_name}</p>
</div>
)
},
render : function () {
return (
<div>
<section className="content">
{this.data.applicants.map(this.RenderApplicantRow)}
{console.log(this.data.applicants)}
<DefaultBootstrapPaginator
pagination={this.pagination}
limit={6}
containerClass="text-center"/>
</section>
</div>
)
}
});
(FYI, I'm using krounin:pagination.) The problem is that I cannot access this.props.institutionID inside of the pagination component. I know the value is getting passed (I can see it if I'm just testing output in the render) but can't figure out why it doesn't pass into the pagination call. And I know the pagination works because I do not get an error if I hard code in a value.
Thanks for the help.
This is a simple scope problem I think, you need to bind it to the right context
Try something like this:
pagination: function() {
var self= this;
new Meteor.Pagination(Applicants, {
perPage: 25,
filters: {institution_id: self.props.institutionID },
sort: {institution_id: 1, "last_name":1, "first_name":1}
})
}
,

Nothing showing on screen. Just showing a list of items from a collection

Im holding non persistant data , calling an api (Meteor.method) Its supposed to show a list of items(text)but nothing is showing up on screen
if(Meteor.isClient) {
Searches = new Meteor.Collection('searches');
Meteor.subscribe('allSearches');
console.log(Searches.find());
}
...
renderTasks(){
return this.data.searches.map((searches, i) => {
return <SearchResultItem key={i} searches={searches} />;
});
},
...
<ul>
{this.renderTasks()}
</ul>
...
SearchResultItem = React.createClass({
render(){
return
<li >
{this.props.searches}
</li>
}
});

Meteor.renderList alway end up in [elseFunc]

I'm new to Meteor.
Trying to render items from collection but Meteor.renderList(observable, docFunc, [elseFunc]) alway go to elseFunc.
this.ComponentViewOrdersFlow = Backbone.View.extend({
template: null,
initialize: function() {
var frag;
Template.ordersFlow.events = {
"click a": function(e) {
return App.router.aReplace(e);
}
};
this.template = Meteor.render(function() {
return Template.ordersFlow();
});
console.log(Colors);
frag = Meteor.renderList(
Colors.find(),
function(color) {
console.log(color);
},
function() {
console.log('else consdition');
}
);
},
render: function() {
this.$el.html(this.template);
return this;
}
});
Initially I thought that Collection is empty, but console.log(Colors) shows that there are items in collection. Moreover if I use Meteor.render(... -> Template.colors({colors: Colors.find()}) ) it renders template end show Collection items there.
Meteor version 0.6.6.3 (Windows 7, 64bit)
Mongo - connected to MongoLab
Thank you for any help.
Jev.
Can't really explain this well in the comments, so here is a very, very simple example of using the Meteor template engine. This is a 100% functional app, showcasing basic reactivity. Note that I never call render() or renderList() anywhere.
All this app does is show a button, that when clicked, adds a number to a list. The number is reactively added to the list, even though I never do anything to make that reactivity explicit. Meteor's templates are automatically reactive! Try it out - this is all of the code.
numbers.html:
<body>
{{> numberList}}
</body>
<template name="numberList">
<ul>
{{#each numbers}}
<li>{{number}}</li>
{{/each}}
</ul>
<button>Click Me</button>
</template>
numbers.js:
var Numbers = new Meteor.Collection("numbers");
if (Meteor.isClient) {
Template.numberList.numbers = function() {
return Numbers.find();
};
var idx = 0;
Template.numberList.events({
"click button": function() {
Numbers.insert({
number: idx
});
idx++;
}
});
}

Resources