First, think of a table that has a search, sort, and pagination. Items (rows) have details, which loads a route to show the detail component. Within this detail component, there's another table with a search, sort, and pagination, that of which also has a detail view to drill down further.
I'm using vuex to retain the state of most components, at least where it makes sense. In this case, the search query, sort, and pagination are all stored in the store. Also keeping in mind, I'm trying to decouple my components as much as possible to make testing easier and provide better maintainability.
Imagine this routing structure:
/Properties Shows a list of property locations
/Properties/location/1 Shows a list of buildings within that location
/Properties/location/1/building/1 Shows a list of offices within that building
Let's say I'm on the first route, and I search for a location, that of which produces 3 records that match the search query. Great! I click on the first one, view some details... and click Go Back. My search is still intact as it was stored in vuex so my state is retained. Perfect.
I navigate away to another route, entirely separate from the locations section, and then navigate back. Yuck, my search is still there. This isn't what I would expect (intuitively), but would expect programmatically.
Here's how I solved it. Please tell me if there's a better way.
In my routes, I always use props, and in my main navigation component, I set it up to pass a clearState property.
So the component has this:
props: {
clearState: {
type: Boolean,
default() {
return false;
}
}
},
created() {
if (this.clearState) {
this.$store.dispatch("properties/setSearch", "");
}
this.$store.dispatch("fetchLocations");
}
And my route in the main navigation menu has this:
<router-link :to="{ name: "Locations", params: { clearState: true }}">Locations</router-link>
This works great. But, I have several layers of detail components as we drill down, so now I feel like I'm getting tightly coupled:
created() {
if (this.clearState) {
this.$store.dispatch("properties/setSearch", "");
this.$store.dispatch("buildings/setSearch", "");
this.$store.dispatch("offices/setSearch", "");
}
this.$store.dispatch("fetchLocations");
}
I played around with router guards, but access to this within the component isn't immediately available, which slows the request to my API. It also forced me to use meta properties to determine what to and from routes should force the clearing of state.
My Question
I've solved my issue, but I have a gut feeling there's a better design pattern out there that I haven't figured out yet.
Is my solution prone to unseen error?
Is there a better way to do this that I'm obviously missing?
I hope this makes sense.
Related
Consider the following Widget component.
<template>
<component :is="name" />
</template>
<script>
import { mapActions } from 'vuex';
export default {
props: {
name: {
type: String,
required: true
}
},
created() {
this.addWidget(this.name);
},
methods: {
...mapActions({
addWidget: 'widgets/add'
}) // adds widget name to an array
}
};
</script>
I want to have multiple components like this one all over the page.
I need to be able to gather all of the component names so I can fetch the relevant data for them.
My current idea is to add the component name to the store when the created hook fires.
That works just fine, but my problem is that I can't find a way to dispatch a Vuex action to fetch the data once all of the components have been created. I tried using various hooks to no avail since none of them seems to have Vuex access.
Am I missing something? Is there some kind of hook that fires after all of the components have been created (SSR) that has Vuex access. Or maybe there's some better way to achieve what I've been trying to do?
Why do you want to wait until all the widgets have been loaded to fetch the data? Instead, what I'd do is fetch the data for each Component as they get added in the page. Using this approach, each component would require a specific piece of data, and nothing better than to load each of them in a different request. Easier to test, easier to trace, adheres to the single responsibility principle.
If you are worried that two components may require the same data, then you can route all requests through an Object that hashes the request (endpoint + verb if using REST) and keeps a pool of active connections, so that if two requests come in one reuses the Promise of the other, and only one of them reaches the server.
Now, if you really want to do it using your original approach, make it so that each widget emits an event once it's been registered. The events are collected by the parent component (maybe the Page that loads all the widgets). The page knows which Widgets are being loaded. Every time an event is received it sets a flag on that specific widget to indicate it's been loaded. Once all widgets get a flag, the Page triggers a request to fetch the data.
I just realized that the asyncData hook is not called when hard refreshing the page. But I have important data to load to show on that page. And I want to make sure that they are always available, even when the user hard refreshes the page.
asyncData from the documentation
the promise returned by the asyncData hook is resolved during route transition
In that case, the best way is to use the fetch() hook and display a loader while you do finish your calls thanks to the $fetchState.pending helper.
Actually, I do think that fetch() is better in many ways.
Quick article on the subject: https://nuxtjs.org/blog/understanding-how-fetch-works-in-nuxt-2-12/
The one on the bottom of the page (Sergey's) is cool with some practical example of a nice implementation too.
You could also use this somewhat hacky solution in a layout to see if the initial location (the one you are when you hard refresh) is the one you want to be. Basically, if you land on a specific page (hard refresh or following a new window page for example) but want to have some custom behavior.
beforeCreate() {
if (!['some-other-page'].includes(this.$router.history._startLocation)) {
this.$router.replace({ name: 'index' }).catch(() => {})
}
},
Still, this one infinite loops if used in a middleware (or I made a mistake) and it's less clean than a fetch() hook.
I have webpack setup to bundle all of the source. I have a Vue object that is the page and multiple Vue components of my own creation. This works great!
I am now getting reference data from the database to fill in certain default options for some of these components. In my pages Mounted or Created events (there is no difference for my question) I am calling a method that will check to see if the data exists in localStorage and if not, it will extract the data from the database.
Once Extracted, I have it in localStorage so it is not an issue. However, the first time I need to gather the data (or when I need to refresh it because I have another trigger that lets me know when it has changed) the page and components have rendered (with errors because of lack of data) before the data comes back. The fetch method is in a promise, but mounted events don't seem to care if a promise exists within in before it continues to the next component.
So what is the best practice for loading/refreshing reference data in Vue? I am currently not using VueX because this is not a SPA. Sure, it is a single page that is doing things (there are many single pages that do their own thing in this site) but I have no need to make it a full SPA here. But If VueX and its store will give me some sort of guarantee that it will occur first or page/components will run AFTER VueX things, I will learn it.
Have you tried doing so:
<component v-if="page.isDataLoaded">...</component>
in your Vue-component:
data() {
return {
page: {
isDataLoaded: false,
}
}
},
mounted() {
this.fetchPageData().then(() => this.page.isDataLoaded = true);
}
You can use v-if and v-else to show, for example page loader element like so:
<PageLoader v-if="!page.isDataLoaded"></PageLoader>
<component v-else>...</component>
I have a scenario where there are two major components on a page; a frame-like component that contains common functionality for many applications (including a bookmark/tab bar) and my actual application code.
Since the frame doesn't actually own the page that it's included on, it seems like it would be incorrect for it to define any routes, however the current page may define their own routes that may match one of those links. In that case, I'd like vue-router to handle those anchor clicks and navigate appropriately rather than doing a full page reload.
Here's a simplified template of what this looks like:
Frame (an external dependency for my app):
<Frame>
<TabStrip>
</TabStrip>
<slot></slot>
<Frame>
App1:
<Frame>
<App>You're looking at: {{ pageId }}!</App>
</Frame>
So when any of the app1 domain links are clicked from that tab strip, I want my route definitions in app1 to pick that up rather than it causing a page load. Since that component is owned by the frame, I don't have access to write <router-link> since links to many different apps may co-exist there.
Any thoughts?
Whoo, this is an old one! However, since this question was high in my search results when I was researching this problem, I figured I should answer it.
My use-case was similar to the one in the comments: I needed to capture normal <a> links within rendered v-html and parse them through the router (the app is rendering Markdown with a light modification that generates internal links in some cases).
Things to note about my solution:
I'm using Vue3, not Vue2; the biggest difference is that this is the new Vue3 composition-style single page component syntax, but it should be easy to backport to Vue2, if necessary, because the actual things it's doing are standard Vue.
I stripped out the markdown logic, because it doesn't have anything to do with this question.
Note the code comment! You will very likely need to design your own conditional logic for how to identify links that need to be routed vs. other links (e.g. if the application in the original question has same-origin links that aren't handled by the Vue app, then copy/pasting my solution as-is won't work).
<script setup>
import { useRouter } from "vue-router"
const router = useRouter()
const props = defineProps({
source: {
type: String,
required: true,
},
})
function handleRouteLink(event) {
const target = event.target
// IMPORTANT! This is where you need to make a decision that's appropriate
// for your application. In my case, all links using the same origin are
// guaranteed to be internal, so I simply use duck-typing for the
// properties I need and compare the origins. Logic is inverted because I
// prefer to exit early rather than nest all logic in a conditional (pure
// style choice; works fine either way, and a non-inverted conditional is
// arguably easier to read).
if (!target.pathname || !target.origin || target.origin != window.location.origin) {
return
}
// We've determined this is a link that should be routed, so cancel
// the event and push it onto the router!
event.preventDefault()
event.stopPropagation()
router.push(target.pathname)
}
</script>
<template>
<div v-html="source" #click="handleRouteLink"></div>
</template>
I'm practically brand new to Aurelia, but over the course of a few days I've picked up the starter template and gone through some video training in Pluralsight. I have a unique vision that I can't seem to decide whether compose element, custom element, or router is best to use for this scenario - or if I need to write something completely custom.
I prefer to continue using the router because it gives you the
URLs and history state. Linking deep within the web app may be necessary.
When a view / viewmodel is initialized, I want the view appended to the DOM, NOT replaced. The <router-view> element works by replacing the view.
With each view appended to the DOM, I would like to create a set of tabs representing every view that has been opened so far. Think of any modern text editor, IDE, or even a web browser shows tabs.
Sometimes it would be necessary to detect whether a view is already rendered in the DOM (viewmodel + parameter) and just bring that view to the front -vs- appending the new one.
Do you have any suggestions, examples, etc for someone relatively new to Aurelia, SPAs, and MVVM?
Thank you.
I believe the easiest way is using the compose element. You would need an array containing all screens, and another array to hold the opened screens. Something like this:
screens = [
{ id: 1, name: 'Test 1', view: './test-1.html', viewModel: './test-1' },
{ id: 2, name: 'Test 2', view: './test-2.html', viewModel: './test-2' }
];
_activeScreens = [];
get activeScreens() {
return this.screens.filter((s) => this._activeScreens.indexOf(s.id) !== -1);
}
In the HTML you just have to use <compose></compose> for each iteration of activeScreens.
I made this example https://gist.run/?id=c32f322b1f56e6f0a83679512247af7b to show you the idea. In my case, I've used an html table. In your case, you could use a tab plugin, like Bootstrap or jQuery.
Hope this helps!