Posting data to a specific path from multiple components - javascript

Ok I'll try to explain this the best I can. I have a ResourceInfo component that posts data to the /resources/ path and /users/ + uid + /created-resources path using newPostKeyand update.
I also have a QuizBuilder component. I want to post data from this component to a /resources/ + newPostKey + /quiz/ path. However, I don't know how to get the newPostKeyor key from that particular path I created in ResourceInfo from the QuizBuilder component.
Here are the two components. First the user adds info using the ResourceInfo component. Once they hit submit they go to the QuizBuilder component where they create the quiz.
ResourceInfo.vue
export default {
name: 'resource-info',
data () {
return {
header: 'Before you build your quiz we just need some quick info.',
sharedState: store.state,
resource: {
type: '',
title: '',
url: '',
desc: '',
timesPassed: 0,
authorId: store.state.userInfo.uid,
authorName: store.state.userInfo.displayName,
authorImage: store.state.userInfo.photoURL
},
}
},
methods: {
saveToFirebase () {
var newPostKey = firebase.database().ref().child('resources').push().key;
var updates = {};
updates['/resources/' + newPostKey] = this.resource;
updates['/users/' + store.state.userInfo.uid + '/created-resources/' + newPostKey] = this.resource;
// Clear inputs
this.resource.title = '',
this.resource.type = '',
this.resource.desc = '',
this.resource.url = ''
console.log("Saving resource data...")
return firebase.database().ref().update(updates);
}
}
}
QuizBuilder.vue
export default {
name: "quiz-builder",
data () {
return {
questions: [createNewQuestion()],
showQuestions: false
}
},
methods: {
addQuestion () {
this.questions.push(createNewQuestion())
},
addOption (question) {
question.options.push(createNewOption())
},
saveToFirebase (e) {
e.preventDefault();
var questions = this.questions;
this.firebaseRef = db.ref('a/path/here'); // /resources/ + that resources id + /quiz/
this.firebaseRef.push({ // Should I use set or push here?
questions
})
console.log('Saving quiz data...')
}
}
}

The answer depends on how the transition between the components/pages are made.
If you're building a single page app with vue-router or something, then the transition is replacing the former component with the latter, which all happens on the index.html, with no request sent(simplest situation). To still keep the generated key within our grasp after the first component is gone, you need to save it on a common parent of the two components. To be specific, add a key in the parent's data, and let the ResourceInfo emit a custom event with the generated key to notify the parent to set its key. See http://vuejs.org/guide/components.html#Using-v-on-with-Custom-Events .
If you refreshes the page when jumping from ResourceInfo to to Quiz, with server-side rendering (which should be really rare, since it requires more effort compared to the single-page way, and has an inferior performance), then it's irrelavent to vue and rather simple: redirect the user to Quiz after ResourceInfo is saved, with the key as a url param.
Edit upon OP's using store.js:
Just store the key in LocalStorage(store.js) and retrive it from another component should work since LocalStorage is available globally and even across pages/sessions.
Some thought: main.js just be the parent is in some sense right. There's no real parent vue component here, but our main.js is evaled by the browser in the global scope, so it's true that main.js is the root entry of our app, aka parent.

Related

Vue.js access component method or parent method?

I'm new to Vue and managed to make my first app with some glitches but I'm really enjoying it so far. I used a video tutorial which jump started with vue-cli project creation which as turns out is a litte different due to webpack.
I've created the project, the project does mostly what it should right now I'm trying to do some refactoring which includes DRYing out the code.
On each page I would like to access a variable stored in the cookie file I've done the saving and reading on the HomeComponent in the script section which works as promised.
<script>
import MenuComponent from '#/components/MenuComponent.vue'
import Typewriter from '#/components/vue-type-writer.vue'
export default {
name: 'HomeComponent',
components: {
MenuComponent,
Typewriter
},
prop:{
isPlaying: Boolean,
username: String,
currentSound: Object
},
data() {
return{
currentSound: null,
isPlaying: false,
username: ''
}
},
methods:{
clickButton() {
this.msg= 'test 2'
},
toggleSound(){
var a = this.currentSound;
if (a.paused) {
a.play();
this.isPlaying = true;
} else {
a.pause();
this.isPlaying = false;
}
},
getCookieInfo(){
var value = "; " + document.cookie;
var parts = value.split("; weegreename=");
if (parts.length == 2)
this.username = parts.pop().split(";").shift();
else this.username = '';
},
seveFormValues (submitEvent) {
this.username = submitEvent.target.elements.name.value;
this.$refs.audio1.pause();
this.$refs.audio2.play();
var expires = "";
var days = 31;
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days*24*60*60*1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = "weegreename=" + (this.username || "") + expires + "; path=/";
}
},
mounted(){
this.isPlaying = true;
this.getCookieInfo();
if (this.username) this.currentSound = this.$refs.audio2;
else this.currentSound = this.$refs.audio1;
this.currentSound.play();
}
}
</script>
Now on every sub page I would like to access the getCookieInfo() method to check id the username is set.
I've tried to add this in the main App.vue script section, in the main.js
new Vue({
router,
render: h => h(App),
methods: {
//here the getCookieInfo code from above
}
}).$mount('#app')
created a new component whit the methods and then tried to access them in the main app via componentname.method as below.
import CookieComponent from '#/components/CookieComponent.vue'
export default {
// prop:{
// isToggled: Boolean
// },
components: {
MenuComponent,
CookieComponent
},
data() {
return{
isToggled: false
}
},
methods:{
clickToggle() {
this.isToggled = !this.isToggled;
},
},
mounted(){
CookieComponent.getCookieInfo();
}
}
I don't know right now the best approach and I will learn more in the future but this project is time sensitive - I decided to learn vue by making a simple site for my client :)
If you need it on every page it can be put into your App.vue. From there you have three options:
Pass the data as a prop to child components.
Create an event bus and emit the data to whichever component needs it.
Use Vuex to store the data and access it from your components.
If you really want to keep your cookie data inside the component you need to emit it up your component chain.
https://v2.vuejs.org/v2/guide/components.html#Emitting-a-Value-With-an-Event
Depending on how deep your chain goes and how many sibling components you have this can get really messy and in those cases Vuex or an event bus might be a better idea.
Do not try to do things like:
CookieComponent.getCookieInfo();
Please review the documentation to see good example on how to do component communication.
For that kind of stuff, the best practice is to use a state. It will save data of your application and will allow you to access them accross all components/pages.
You can see a simple state management in the Vue doc, or directly use VueX, the official state management library for Vue.
To sum up how it works (with VueX):
You create a cookieStore:
// Where data will be saved
const state = { cookie: {} }
// Getters allow you to access data
const getters = { cookie: state => state.cookie }
// Mutations allow you to modify the state
const mutations = {
// Set cookie data
saveCookie (state, cookieData) {
state.cookie = cookieData
}
}
In your HomeComponent, you will get the cookie info, and save it in
the store: this.$store.commit('saveCookie', cookieData)
In all other components, instead of getting the cookie info from the cookie, you can access the saved data from the store and do what you want with it: this.$store.getters.cookie

How to get the latest data from parent to child components after page refresh

I am working on a project and using Vue.js for the frontend. I have following code in the main.js file.
new Vue({ // eslint-disable-line no-new
//el: '#app',
router,
data () {
return {
friends: []
}
},
methods: {
getFriends: function () {
return this.friends;
}
},
created: function () {
this.$http.get('/user/' + this.getUserIDCookie('userID') +
'/friends').then(function (response) {
this.friends = response.data;
});
},
components: {
'nav-bar': require('./components/Navigation.vue')
},
template: `
<div id="app">
<nav-bar></nav-bar>
<router-view class="router-view"></router-view>
</div>`
}).$mount('#app');
In one of the pages(for ex. when the page is redirected to localhost/#/user/1/details, I am retrieving the friends' list from main.js like below:
<script type="text/babel">
export default {
name: 'profile',
data: function () {
return {
user: {},
friends: []
}
},
methods: {
// Some methods
},
created: function () {
this.friends = this.$root.getFriends();
}
}
</script>
The problem arises when I refresh the current page. After page refresh, this.friends is null/undefined because this.$root.getFriends() is returning null/undefined. I can move it to user component, but I want to keep it in main.js so that GET call is used once and data will be available to the whole application.
Any input regarding how to solve this issue would be great. I am using Vue 2.0.1
Really, what you want to do, is pass the data the component needs as props.
The dirt simple easiest way to do it is this.
<router-view class="router-view" :friends="friends"></router-view>
And in your profile component,
export default {
props:["friends"],
name: 'profile',
data: function () {
return {
user: {},
friends: []
}
},
methods: {
// Some methods
}
}
If you want to get more sophisticated, the later versions of VueRouter allow you to pass properties to routes in several ways.
Finally, there's always Vuex or some other state management tool if your application gets complex enough.
The problem is that when you refresh the page, the whole app reloads, which includes the get, which is asynchronous. The router figures out that it needs to render details, so that component loads, and calls getFriends, but the asynchronous get hasn't finished.
You could work around this by saving and pulling the Promise from the get, but Bert's answer is correct: the Vue Way is to send data as props, not to have children pull it from parents.

Firebase React Binding

I'm somewhat new to React, and using the re-base library to work with Firebase.
I'm currently trying to render a table, but because of the way my data is structured in firebase, I need to get a list of keys from two locations- the first one being a list of user keys that are a member of a team, and the second being the full user information.
The team node is structured like this: /teams/team_id/userkeys, and the user info is stored like this: /Users/userkey/{email, name, etc.}
My table consists of two react components: a table component and a row component.
My table component has props teamid passed to it, and I'm using re-base's bindToState functionality to get the associated user keys in componentWillMount(). Then, I use bindToState again to get the full user node, like so:
componentWillMount() {
this.ref = base.bindToState(`/teams/${this.props.data}/members`, {
context: this,
state: 'members',
asArray: true,
then() {
this.secondref = base.bindToState('/Users', {
context: this,
state: 'users',
asArray: true,
then() {
let membersKeys = this.state.members.map(function(item) {
return item.key;
});
let usersKeys = this.state.members.map(function(item) {
return item.key;
});
let onlyCorrectMembersKeys = intersection(membersKeys, usersKeys);
this.setState({
loading: false
});
}
});
}
});
}
As you can see, I create membersKeys and usersKeys and then use underscore.js's intersection function to get all the member keys that are in my users node (note: I do this because there are some cases where a user will be a member of a team, but not be under /Users).
The part I'm struggling with is adding an additional rebase call to create the full members array (ie. the user data from /Users for the keys in onlyCorrectMembersKeys.
Edit: I've tried
let allKeys = [];
onlyCorrectMembersKeys.forEach(function(element) {
base.fetch(`/Users/${element}`, {
asArray: true,
then(data) {
allKeys.prototype.concat(data);
}
});
});
But I'm receiving the error Error: REBASE: The options argument must contain a context property of type object. Instead, got undefined
I'm assuming that's because onlyCorrectMembersKeys hasn't been fully computed yet, but I'm struggling with how to figure out the best way to solve this..
For anyone dealing with this issue as well, I seemed to have found (somewhat) of a solution:
onlyCorrectMembersKeys.map(function(item) {
base.fetch(`/Users/${item}`, {
context: this,
asObject: true,
then(data) {
if (data) {
allKeyss.push({item,data});
this.setState({allKeys: allKeyss});
}
this.setState({loading: false});
},
onFailure(err) {
console.log(err);
this.setState({loading: false});
}
})
}, this);
}
This works fine, but when users and members state is updated, it doesn't update the allkeys state. I'm sure this is just due to my level of react knowledge, so when I figure that out I'll post the solution.
Edit: using listenTo instead of bindToState is the correct approach as bindToState's callback is only fired once.

Vue 2 - Communication between components (sending and receiving data)

So I working on app in Vue. I have problem with sending and receiving data between components. Already tried with $dispatch/$broadcast, $emit/$on but still now working. I want to send selected active_club from ClubSelection.vue to vue_main.js.
Vue version: 2.0.3
Structure of my app:
vue_main - main Vue file
HeaderElement.vue (child of vue_main)
ClubSelection.vue (child of HeaderElement)
Need to send active_club from ClubSelection to vue_main.
ClubSelection.vue
<script>
export default{
props: [
'club', 'title'
],
created(){
//Get club list
this.$http.get('/api/clubs', function(data) {
this.clubs = data;
console.log(data);
//read active club from parent
this.selected = this.$parent.$parent.active_club;
});
},
data(){
return{
clubs: [],
selected: null,
}
},
watch: {
selected: function(v) {
this.club = v;
//Post to database selected club
this.$http.post('/api/clubs/' + v + '/active')
},
club: function(v) {
this.selected = v;
//Change active_club at parent (THIS NOT WORKING)
// this.$emit('active_club', v);
// this.$parent.active_club = v;
club.$emit('active_club', v);
},
}
}
</script>
vue_main.js
const app = new Vue({
router,
data() {
return {
user: [],
active_club: null,
ranking: null
}
},
created: function() {
var self = this;
this.$http.get('/api/users/me', function(data) {
this.user = data;
self.active_club = data.active_club;
})
}
}).$mount('#app');
const club = new Vue();
//THIS NOT WORKING
club.$on('active_club', function (id) {
alert(id)
this.active_club = id;
});
Errors:
Vue warn]: Error in watcher "club" (found in component
)
vue_main.js:16924 Uncaught (in promise) ReferenceError: club is not
defined
I have tried many set ups, this is one of them. How to make this working?
$dispatch and $broadcast are deprecated in Vue 2.0.
In your case, what you need is communication between a parent component and child component. When a child $emits an event, parent can listen to it by providing a method in template markup itself, using v-on:parentMethod() as follows:
<child-component v-on:child-event="handlerMethod"></child-component>
The above markup is done inside parent component's template. And the parent component needs to have that handlerMethod in its methods.
Here is a sample "parent-child communication" question on Stackoverflow, which has a jsFiddle example also: Delete a Vue child component
You may use the above answer as reference to implement $emit in your app.
Edit: Additional Notes
I forgot to mention the note about three level hierarchy you have. In your app, you have the following hierarchy:
parent: vue_main
child 1: HeaderElement
child 1.1: ClubSelection
For sending events from ClubSelection to vue_main, you may either use non parent-child communication method or you can relay the event using the intermediate HeaderElement.
Here is how the event relay can work:
Step 1: ClubSelection sends a $emit, which is received by HeaderElement using v-on.
Step 2: The handlerMethod in HeaderElement does a this.$emit, which can be received by your main template using another v-on.
While the above may look a bit convoluted, it is much more efficient than broadcasting to every single component in the app, as it is generally done in Angualr 1.x or other frameworks.

Ember Loading Template with Liquid Fire

I have been doing a lot of tinkering with this and can't seem to get it working. I am looking to show my loading template while waiting for my model promise to return.
My understanding is, by default, if I have app/templates/loading.hbs, this template will be rendered across all routes. However, even with that template in place whenever I switch between routes the old route remains displayed until the model returns, at which point my liquid fire transition occurs and you're taken to the next route.
I have tried various version of creating nested loading templates for each route, tried creating subroutes for each route for the loading template, and have even messed with the beforeModel/afterModel methods that are available but I am making no progress. This is the last hurdle I want to cross before launching and am perplexed as to why I can't get it working. Here is a bunch of my code I feel is relevant.
Note: I am using Ember CLI and Liquid Fire. My data is also being returned to the model from am Ember Service for the time being.
Router
Router.map(function() {
this.route('reviews', function() {
this.route('index', {path: '/'});
this.route('review', {path: '/:review_id'});
});
this.route('movies');
this.route('about');
});
app/template/loading.hbs
<div class="content-container">
<h1>Ish be loading</h1>
</div>
Slowest Model Route
export default Ember.Route.extend({
activate() {
this._super();
$("html,body").animate({scrollTop:0},"fast");
$("body").addClass('movies');
},
deactivate() {
$("body").removeClass('movies');
},
model() {
const movies = this.get('movies');
return movies.getMoviesInAlphOrder();
},
afterModel: function() {
$(document).attr('title', 'Slasher Obscura - Movie Database');
},
movies: Ember.inject.service()
});
app.js
App = Ember.Application.extend({
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
Resolver,
...
});
loadInitializers(App, config.modulePrefix);
Service Methods
sortReviewsByDateDesc(arr) {
return arr.slice().sort(function (a, b) {
return a.review.date > b.review.date ? -1 : 1;
});
},
getSetAmountOfMovies(num, arr) {
const movieList = arr ? null : this.getMovies();
const trimmedList = arr ? arr.slice(0, num) : movieList.slice(0, num);
return trimmedList;
},
setFirstReviewToFeatured(arr) {
arr[0].isFeatured = true;
return arr;
},
getLatestReviews(num) {
const movieList = this.getMovies();
const reviewList = movieList.filterBy('review');
const indexList = this.sortReviewsByDateDesc(reviewList);
const latestList = this.getSetAmountOfMovies(num, indexList);
return this.setFirstReviewToFeatured(latestList);
},
getMoviesInAlphOrder() {
const movieList = this.getMovies();
let lowerCaseA, lowerCaseB;
return movieList.sort(function(a, b) {
lowerCaseA = a.title.toLocaleLowerCase();
lowerCaseB = b.title.toLocaleLowerCase();
return lowerCaseA.localeCompare(lowerCaseB);
});
},
getMovies() {
return [{
id: 1,
title: '303 Fear Faith Revenge',
year: "1999",
imdb: "tt0219682",
youtube: "iFV1qaUWemA"
}
...
]
I have read the docs on Ember's site along with various other Google resources and can't seem to figure out why my loading template isn't rendering at all. Any help would be awesome! Thanks!
Loading templates trigger when your model hook returns a promise that takes a long time to resolve, however, your model hook is not returning a promise.
model() {
const movies = this.get('movies');
return movies.getMoviesInAlphOrder();
}
getMoviesInAlphOrder is returning a synchronous array. After talking with you further, it turns out that you've pre-filled this array client side with 540 items, so the issue here is that the loading template not only doesn't have a promise to wait for, but even if it did it would resolve immediately anyway.
Your time delay is very likely a performance issue stemming from rendering a long list of items. There are several Ember addons to help with this including one of my own: https://github.com/runspired/smoke-and-mirrors
Alternatively/ In addition you may want to consider "chunking" your array into smaller bits and render it 25-50 at a time, or setup some pagination.

Categories