Sapper/Svelte update state inside a component after another component is done posting - sapper

I'm aware of this q&a but still not able to apply it in my situation.
I'm using sapper and have two components. Component2 is to create options or category for a menu. Component1 display the menu with all the options/categories created. Also, component1 includes the component2 in its body.
My issue is when I use component1 and hit save, everything is working(inputs data saved to db) now component1 needs to reflect the new option/category that was just created but it is not aware of the changes. How do I make my component1 (which display all the categories) aware of the changes/update/newcategory once I click save in component2?
Component2 code:
createcategoy.js is working fine and post the data to my db so it is not an issue.
<script>
...code to get locationid unrelated to my issue
function createcategory(event){
let categoryorder = event.target.categoryorder.value
let categoryname = event.target.categoryname.value
let categorydescription=
event.target.categorydescription.value
fetch('createCategory', {
method: 'POST',
credentials : 'include',
headers: {
'Accept': 'application/json',
'Content-type' : 'application/json'
},
body: JSON.stringify({
location_id : locationid,
category_order : categoryorder,
category_name : categoryname,
category_description :categorydescription
})
}).then(response => response.json())
.then(responseJson => {
console.log("All Is Well. Category created
successfully")
})
.catch(error => console.log(error));
}
</script>
html form...
Now here is component1 which display locations and each location has its own categories created by component2:
<script>
import { onMount } from 'svelte';
import { goto } from '#sapper/app';
import Createcategory from './createCategory.svelte'
let locations = []
onMount(async () => {
fetch('menubuilder', {
method: 'POST',
credentials : 'include',
headers: {
'Accept': 'application/json',
'Content-type' : 'application/json'
},
body: JSON.stringify({
})
}).then(response => response.json())
.then(responseJson => {
locations = responseJson.info
})
.catch(error => console.log(error));
}) //onMount fn
</script>
<h1> Menu Builder </h1>
<dl>
{#each locations as location}
<br>
<h1>{location.locationname} </h1>
<p>{location._id} </p>
<!-- This is component2 where I use to create new
category-->
<Createcategory locationname=
{location.locationname} locationid={location._id}/>
</dt>
<dd>
{#each location.locationcategories as item}
<div >
<p>{item.categoryname} </p>:
{item.categorydescription}
</div>
{/each}
{/each}
</dl>
Now, my only struggle is with how sapper/svelte state works. How do I make component1 update it is state/dom when I submit the component2 that creates the category.
I read the reactivity section in the documentation, the locations variable is assigned inside component1 but component2 is the one responsible for creating the new category.
So, what should I do to update component1 (locations variable and its dom) when component2 save the category to db?

There are two common ways to do this:
raise an event after posting the new category, your new workflow will be something like:
Component2 pushes to the server
Component2 raises an event categoryAdded with as payload the category.
Component1 reacts to this event by appending the given category to it's array.
Or, an in my opinion better approach:
add your categories in a store that is outside both components.
Component1 will simply display the contents of the store.
Component2 will push new items to the store.
The store itself, will be responsible for getting/pushing the data from and to the server.
This has the two extra benefits:
The components are only concerned with displaying categories and do not care where they come from or where they go.
You can easily change the way categories are stored, which endpoint it uses, ...

Related

Append components inside other component in Vue JS

I'm trying to make a simple "Load More" function for posts using Vue JS but when I try to append new posts, the previous ones are removed.
This is my PostWallComponent, which is supposed to hold all posts (<post-item-component>).
I fetch first 4 posts from the DB, store them in this.posts and then I send them using the v-for loop to <post-item-component>.
Then when someone clicks on the "More" button I call getPosts() function where I fetch another 4 posts from the DB. Here comes my problem - I store these new posts inside this.posts and I try to append them to the post container. They do append but the previous 4 get deleted from the container.
I think I know what is wrong - at line this.posts = response.data I replace old posts with new ones but I don't know how to append new ones without removing old ones. I tried to push() new posts to the array but that turned into a big mess (repetitive posts in the container).
<template>
<div class="container">
<div class="post_container">
<post-item-component v-for="post in this.posts"
v-bind:cuid="cuid"
v-bind:auid="auid"
v-bind:post="post"
v-bind:key="post.id">
</post-item-component>
<button type="button" #click="getPosts">More</button>
</div>
</div>
</template>
<script>
import PostItemComponent from "./PostItemComponent";
export default {
props: ['init_place', 'init_type', 'current_user_id', 'active_user'],
components: {
PostItemComponent
},
data() {
return {
place: this.init_place,
type: this.init_type,
cuid: this.current_user_id,
auid: this.active_user,
limit: 4,
offset: 0,
posts: [],
};
},
mounted() {
console.log('Component mounted.');
this.getPosts();
},
methods: {
getPosts() {
console.log('post');
axios.get('/p/fetch', {
params: {
place: this.place,
type: this.type,
cuid: this.cuid,
auid: this.auid,
offset: this.offset,
limit: this.limit,
}
})
.then((response) => {
console.log(response);
this.posts = response.data;
this.offset = this.limit;
this.limit += 4;
})
.catch(function (error) {
//currentObj.output = error;
});
}
}
}
</script>
In case someone wonders:
cuid is current user id = ID of user whose profile I opened
auid is active user ID = logged in user ID
<post-item-component> is just couple of divs displaying post header, body etc.
you could also use this.posts = this.posts.concat(response.data)
the problem is that the Array.push() method does not work with vue reactivity. For that you need to replace the whole array. As one proposed solution, you could use the spread operator to achieve this as so:
this.posts = [...this.posts, ...response.data];
This is replacing the whole array with a new array that is combining the old items with the fetched ones by spreading each of the array elements into the new array.
You can see an example here:
codesandbox example

Avoid fetching images everytime page load - vuejs

I'm building a page that show dynamically some photos in a feed like Instagram. I'm getting stuck trying to avoid everytime I load a page or I go into a photo's detail page and then go back, to do an API request to Laravel controller, so that means fetching data and images, losing the position of the page and starting on the top of the page.
My code:
Feed.vue
<template>
<div v-for="(image, index) in images" :key="index">
<v-img :src="image.path" class="image-masonry mini-cover" slot-scope="{ hover }"></v-img>
</div>
</template>
<script>
export default {
data() {
return {
images: []
}
},
mounted() {
this.getImagesHome();
},
methods: {
getImagesHome() {
this.axios.get('/api/images', {},
).then(response => {
this.images = response.data;
}).catch(error => {
console.log(error);
});
},
}
}
</script>
Edit:
I saw that keep-alive is primarily used to preserve component state or avoid re-rendering it. But i can't understand how to use it. I call my Feed.vue component in another Home.vue as below:
<template>
<v-app>
<Toolbar #toggle-drawer="$refs.drawer.drawer = !$refs.drawer.drawer"></Toolbar>
<Navbar ref="drawer"></Navbar>
<keep-alive>
<Feed></Feed>
</keep-alive>
</v-app>
</template>
<script>
import store from '../store';
export default {
components: {
'Toolbar' : () => import('./template/Toolbar.vue'),
'Navbar' : () => import('./template/Navbar.vue'),
'Feed' : () => import('./Feed.vue')
}
}
</script>
What i have to put more in keep-alive and what i have to change in my Feed.vue component?
mounted() should only be called once.
There seem to be multiple ways to go about this.
If you are using vue-router, then have a look at scrollBehaviour
https://router.vuejs.org/guide/advanced/scroll-behavior.html
From their documentation,
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
// return desired position
}
})
You can also try using keep-alive: https://v2.vuejs.org/v2/api/#keep-alive
It keeps the component in memory so it is not destroyed, you get activated and deactivated events to check when it comes into view.
But I don't think it saves scroll position, so you may want to use this in combination with scrollBehaviour

How to manage page titles dynamically in Vuejs?

I build an application. I have a header with the title of my page. Currently, I use view-router to define my titles.
{
path: '/events',
name: 'events',
component: Events,
meta: {
title: 'Liste des événements'
}
}
And in my blade view, in my header.blade.php I'm doing this to display the title
<h2 class="header__title title-header">
#{{ $route.meta.title }}
</h2>
It work BUT when I have a dynamic data, necessarily, it does not work. For example, when I post a post, how can I do to earn my title?
{
path: '/events/:id',
component: Event,
name: 'Event',
meta: {
title: 'my dynamic title',
page: 'event',
},
How to recover the title of the post page? I have the name of the post in my post, but I can not access it from my header ...
I do not have access to the other components. These are components coming from element-ui.
Thank you in advance
In your router, before export default router you can add
router.beforeEach((toRoute, fromRoute, next) => {
window.document.title = toRoute.meta && toRoute.meta.title ? toRoute.meta.title : 'Home';
next();
})
This will read the meta title if it exists and it will update the page title.
Or you can add it like this
const router = new Router({
beforeEach(toRoute, fromRoute, next) {
window.document.title = toRoute.meta && toRoute.meta.title ? toRoute.meta.title : 'Home';
next();
},
routes: [
...
]
})
Don't forget to change the default title value from Home to something else - maybe name of your project

broadcast to others and dont broadcast to current user are not working

In my TaskList.vue I have the code below:
<template>
<div>
<ul>
<li v-for="task in tasks" v-text="task"></li>
</ul>
<input type="text" v-model="newTask" #blur="addTask">
</div>
</template>
<script>
export default {
data(){
return{
tasks: [],
newTask: ''
}
},
created(){
axios.get('tasks')
.then(
response => (this.tasks = response.data)
);
Echo.channel('task').listen('TaskCreated', (data) => {
this.tasks.push(data.task.body);
});
},
methods:{
addTask(){
axios.post('tasks', { body: this.newTask })
this.tasks.push(this.newTask);
this.newTask = '';
}
}
}
</script>
When I hit the axios.post('tasks') end point, I got duplicate result in my current tab that i input the value and the another tab got only 1 value which is correct.
To avoid this, I tried to use
broadcast(new TaskCreated($task))->toOthers();
OR
I put $this->dontBroadcastToCurrentUser() in the construct of my TaskCreated.php
However, both methods are not working. Any idea why?
The image below is from network tab of mine. One of it is pending, is that what caused the problem?
https://ibb.co/jpnsnx (sorry I couldn't post image as it needs more reputation)
I solved this issue on my Laravel Spark project by manually sending the X-Socket-Id with the axios post.
The documentation says the header is added automatically if you're using vue and axios, but for me (because of spark?) it was not.
Below is an example to show how I manually added the X-Socket-Id header to an axios post using the active socketId from Laravel Echo:
axios({
method: 'post',
url: '/api/post/' + this.post.id + '/comment',
data: {
body: this.body
},
headers: {
"X-Socket-Id": Echo.socketId(),
}
})
Laravel looks for the header X-Socket-ID when using $this->dontBroadcastToCurrentUser(); Through this ID, Laravel identifies which user (current user) to exclude from the event.
You can add an interceptor for requests in which you can add the id of your socket instance to the headers of each of your requests:
/**
* Register an Axios HTTP interceptor to add the X-Socket-ID header.
*/
Axios.interceptors.request.use((config) => {
config.headers['X-Socket-ID'] = window.Echo.socketId() // Echo instance
// the function socketId () returns the id of the socket connection
return config
})
window.axios.defaults.headers.common['X-Socket-Id'] = window.Echo.socketId();
this one work for me..!!

Vue 2 and child component events with multiple components

I've read this question / answer, and whilst that works when the parent contains a specific component, in my scenario the parent contains this:
<component :is="currentView"></component>
and the value of currentView determines which component is 'loaded' at any particular time. So, using v-on:event="handler" in the component tag here means all child components must use the same event name :(. Is it possible in the created() function of the parent to set up specific handlers which will be called regardless of which component is currently 'in view', and when they each might send different events? For example, I have a login component which sends a 'login' event, and a data view component which sends an 'update' event. In the parent, I want something like this:
this.$on('login', doLogin)
this.$on('update', doUpdate)
However, that's listening to events from itself, not its children. I also tried giving the component a ref:
<component ref="main" :is="currentView"></component>
and using this.$refs.main.$on('login', doLogin), but the $refs object isn't created until the initial render, so that can't go in the mounted() method (too early), and in the updated() method it will be repeatedly called which I'm sure isn't a good idea...
you could set a global event and then pass the name of the action as a part of the payload, i.e
const Login = {
template: `<h1>Login</h1>`,
mounted() {
this.$emit('global-event', {
action: 'login',
data: {
user: 'foo',
},
})
},
}
const Update = {
template: `<h1>Update</h1>`,
mounted() {
this.$emit('global-event', {
action: 'update',
data: {
user: 'foo bar',
},
})
},
}
new Vue({
el: '#app',
data: {
called: {
update: 0,
login: 0,
},
currentView: 'login',
},
components: {
Login,
Update,
},
methods: {
// within doSomething you would process the various events based on their payload.action
doSomething (payload) {
this.called[payload.action]++
},
toggleView () {
this.currentView = this.currentView === 'login' ? 'update' : 'login'
},
},
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<component #global-event="doSomething" :is="currentView"></component>
<button #click="toggleView">toggle view</button>
<pre>{{ called }}</pre>
</div>
Gah! After spending ages searching, finding nothing, then posting here, I of course immediately stumble on the answer.
But this only applies if you have control over the child components (I'm building all of them). In the child, you simply:
this.$parent.$emit('login', {some: 'data'})
And then set up the event listeners with this.$on('login', doLogin) in the parent as normal in the created() method.
I'm sure there will be a scenario where I'm using a third party component that simply calls this.$emit(), and at the moment, I can't see any alternative to v-on:eventName="handler" in the parent.

Resources