From a872c27baa7bf604148639e8df941fd4aefcedd4 Mon Sep 17 00:00:00 2001 From: dobromir-hristov Date: Sat, 1 Feb 2020 19:08:52 +0200 Subject: [PATCH 01/49] feat: add async-workflow rfc --- .../0000-vtu-improve-async-workflow.md | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 active-rfcs/0000-vtu-improve-async-workflow.md diff --git a/active-rfcs/0000-vtu-improve-async-workflow.md b/active-rfcs/0000-vtu-improve-async-workflow.md new file mode 100644 index 00000000..ad6f333c --- /dev/null +++ b/active-rfcs/0000-vtu-improve-async-workflow.md @@ -0,0 +1,133 @@ +- Start Date: 2020.02.01 +- Target Major Version: VTU beta-0.3x/1.x +- Reference Issues: +- Implementation PR: + +# Summary + +Allows the Vue Test Utils APIs that trigger re-renders to be awaited. This makes asserting changes after re-renders easier. + +# Basic example +```js + const wrapper = mount(Component) + await wrapper.find('.element').trigger('click') + expect(wrapper.emitted('event')).toBeTruthy() +``` +# Motivation + +With the removal of `sync` mode in Vue Test Utils `beta-29`, we now need to `await` for things like watchers or the template to re-render, before we make assertions. + +This proposal aims to make it easier for developers to work with async tests, by allowing you to `await` calls like `trigger` or those that use it internally. + +The proposal assumes the usage of `async/await` inside tests. + +# Detailed design + +The general idea is to return a promise resolving on `nextTick`, from actions that trigger async changes. + +At this moment we need to manually `await` for the component to update. This leads to extra boilerplate, and is harder to grasp for beginners. + +```js + const wrapper = mount(Component) + wrapper.find('.element').trigger('click') + // we need to wait for the component to render + await wrapper.vm.$nextTick() + expect(wrapper.emitted('event')).toBeTruthy() +``` + +The new API should look like: + +```js + const wrapper = mount(Component) + await wrapper.find('.element').trigger('click') + expect(wrapper.emitted('event')).toBeTruthy() +``` + +With more complicated tests, the benefits are obvious: + +```js + await wrapper.find('.button').trigger('click') + expect(wrapper.emitted('event')).toBeTruthy() + + await wrapper.find('.country-option').setValue('US') + expect(wrapper.find('.state').exists()).toBe(true) + + await wrapper.find('.radio-option').setChecked() + expect(wrapper.find('.finish').attributes('disabled')).toBeFalsy() +``` + +**Methods that should return a promise, resolving on next tick:** + +- trigger +- setValue +- setChecked +- setData +- setProps +- setSelected +- setValue + +Most of the above helpers rely on trigger internally, so updating the majority of listed methods would be easy. + +### Additional helpers + +Currently we have seen 3 ways to await for changes: + +```js + import flushPromises from 'flush-promises' + await flushPromises() + // or the more preferred + await wrapper.vm.$nextTick() + await Vue.nextTick() +``` + +In Vue 3 `nextTick` will be removed from the VM instance, so users will have to migrate over to importing it from Vue directly `import { nextTick } from 'vue'`. + +A `tick` helper can be added to the VTU exports. That way users have everything in one place, and an official way to await for renders, from actions that cannot directly return a promise. + +Such a case is triggering a custom Vue event. + +```js + import { mount, tick } from '@vue/test-utils' + + it('test' => { + const wrapper = mount(Component) + wrapper.find('.input').vm.$emit('input', 'Newly added note') + await tick() + + expect(wrapper).toMatchSnapshot() + }) +``` + +Or we could be focusing an element on mounted, which is usually done on next tick + +```js + const wrapper = mount(Component) + // await data to be focused on mounted + await tick() + let input = wrapper.find('input').element + expect(input).toBe(document.activeElement) +``` + +# Drawbacks + +Each `trigger` will now call `nextTick()` , which I am not sure if it can hurt performance. + +Users may try to `await` anything that interacts with the DOM, so it is a matter of writing good DOCs and guides on the topic. + +# Alternatives + +# Adoption strategy + +### Vue Test Utils beta-30+ + +Users would have to make sure `async/await` is working in their testing env. + +Users just have to remove extra `$nextTick` and `flushPromises` calls and use the new api. + +Improve docs on the topic. + +### Vue Test Utils prior to beta-30 + +Before the removal of `sync` mode it was not necessary to await for renders. + +# Unresolved questions From 5a3a89835f19a8ab1400f57d3e5e842237136477 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 30 Apr 2020 14:18:03 +0200 Subject: [PATCH 02/49] Redesigning Navigation failures and global handlers (#150) * initial * Apply suggestions from code review Co-Authored-By: Nicolas Turlais Co-Authored-By: Patrick * clearer distinction with `redirect` * Apply suggestions from code review Co-Authored-By: Alex Van Liew * add adoption strategy Co-authored-by: Nicolas Turlais Co-authored-by: Patrick Co-authored-by: Alex Van Liew --- .../0000-router-navigation-failures.md | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 active-rfcs/0000-router-navigation-failures.md diff --git a/active-rfcs/0000-router-navigation-failures.md b/active-rfcs/0000-router-navigation-failures.md new file mode 100644 index 00000000..fa5f8686 --- /dev/null +++ b/active-rfcs/0000-router-navigation-failures.md @@ -0,0 +1,174 @@ +- Start Date: 2020-03-26 +- Target Major Version: Vue Router v4 +- Reference Issues: https://github.com/vuejs/vue-router/issues/2833, https://github.com/vuejs/vue-router/pull/3047, https://github.com/vuejs/vue-router/issues/2932, https://github.com/vuejs/vue-router/issues/2881 +- Implementation PR: + +# Summary + +- Explicitly define what a navigation failure is, and how and where we can catch them. +- Change when the Promised-based `router.push`(and `router.replace` by extension) resolves and rejects. +- Make `router.push` consistent with `router.afterEach` and `router.onError`. + +# Basic example + +## `router.push` + +If there is an unhandled error or an error is passed to `next`: + +```js +// any other navigation guard +router.beforeEach((to, from, next) => { + next(new Error()) + // or + throw new Error() + // or + return Promise.reject(new Error()) +}) +``` + +Then the promise returned by `router.push` rejects: + +```js +router.push('/url').catch(err => { + // ... +}) +``` + +In **all** other cases, the promise is resolved. We can know if the navigation failed or not by checking the resolved value: + +```js +router.push('/dashboard').then(failure => { + if (failure) { + failure instanceof Error // true + failure.type // NavigationFailure.canceled + } +}) +``` + +## `router.afterEach` + +It's the global equivalent of `router.push().then()` + +## `router.onError` + +It's the global equivalent of `router.push().catch()` + +# Motivation + +The current behavior of Vue Router regarding the promise returned by `push` is inconsistent with `router.afterEach` and `router.onError`. Ideally, we should be able to catch all succeeded and failed navigations globally and locally but we can only do it locally. + +- `onError` is only triggered on thrown errors and `next(new Error())` +- `afterEach` is only called if there is a navigation +- `redirect` should behave the same as `next('/url')` in a Navigation guard when it comes to the outcome of `router.push` and calls of `router.afterEach`/`router.onError`. The only difference being that a `redirect` would only trigger leave guards and other before guards for the redirected location but not the original one + +The differences between the Promise resolution/rejection vs `router.afterEach` and `router.onError` are inconsistent and confusing. + +# Detailed design + +One of the main points is to be able to consistently handle failed navigations globally and locally: + +- Failed Navigation: + - Triggers `router.afterEach` + - Resolves the `Promise` returned by `router.push` +- Uncaught Errors, `next(new Error())` + - Triggers `router.onError` + - Rejects the `Promise` returned by `router.push` + +It's important to note there is no overlap in these two groups: if there is an unhandled error in a Navigation Guard, it will trigger `router.onError` as well as rejecting the `Promise` returned by `router.push` but **will not trigger** `router.afterEach`. Cancelling a navigation with `next(false)` will not trigger `router.onError` but will trigger `router.afterEach` + +## Changes to the Promise resolution and rejection + +Navigation methods like `push` return a Promise, this promise **resolves** once the navigation succeeds **or** fail. It **rejects** only if there was an unhandled error. If it rejects, it will also trigger `router.onError`. + +To differentiate a Navigation that succeeded from one that failed, the `Promise` returned by `push` will resolve to either `undefined` or a `NavigationFailure`: + +```js +import { NavigationFailureType } from 'vue-router' + +router.push('/').then(failure => { + if (failure) { + // Having an Error instance allows us to have a Stacktrace and trace back + // where the navigation was cancelled. This will, in many cases, lead to a + // Navigation Guard and the corresponding `next()` call that cancelled the + // navigation + failure instanceof Error // true + if (failure.type === NavigationFailureType.canceled) { + // ... + } + } +}) + +// using async/await +let failure = await router.push('/') +if (failure) { + // ... +} +``` + +By not rejecting the `Promise` when the navigation fails, we are avoiding _Uncaught (in promise)_ errors while still keeping the possibility to check if the Navigation failed or not. + +### Navigation Failures + +There are a few different navigation failures, to be able to react differently in your code + +Navigation failures can be differentiated through a `type` property. All possible values are hold by an `enum`, `NavigationFailureType`: + +- `cancelled`: `next(false)` inside of a navigation guard or a newer navigation took place while another one was ongoing. +- `duplicated`: Navigating to the same location as the current one will cancel the navigation and not invoke any Navigation guard. + +On top of the `type` property, navigation failures also expose `from` and `to` properties, exactly like `router.afterEach` + +### Redirections + +Redirecting inside of a navigation guard with `next('/url')` is not a navigation failure by itself, as the navigation still takes place and ends up somewhere. To detect a navigation, specially during SSR, there is a `redirectedFrom` property accessible on the `currentRoute`. + +E.g.: imagine a navigation guard that redirects to `/login` when the user isn't authenticated: + +```js +router.beforeEach((to, from, next) => { + // redirect to the login page if the target location requires authentication + if (to.meta.requiresAuth && !isAuthenticated) next('/login') + else next() +}) +``` + +When navigating to a location that requires authentication, we can retrieve the original location the user was trying to access via `redirectedFrom`: + +```js +// user is not authenticated +await router.push('/profile/dashboard') + +// `redirectedFrom` is a RouteLocationNormalized, like `currentRoute` but we are omitting +// most properties to make the example readable +router.currentRoute // { path: '/login', redirectedFrom: { path: '/profile/dashboard' } } +``` + +## Changes to `router.afterEach` + +Since `router.afterEach` also triggers when a navigation fails, we need a way to know if the navigation succeeded or failed. To do that, we introduce an extra parameter that contains the same _failure_ we could find in a resolved navigation: + +```js +import { NavigationFailureType } from 'vue-router' + +router.afterEach((to, from, failure) => { + if (failure) { + if (failure.type === NavigationFailureType.canceled) { + // ... + } + } +}) +``` + +# Drawbacks + +- Breaking change although migration is relatively simple and in many cases will allow the developer to remove existing code + +# Alternatives + +- Differentiating `next(false)` from navigations that get overridden by more recent navigations by defining another Navigation Failure + +# Adoption strategy + +- Expose `NavigationFailureType` in vue-router@3 so that Navigation Failures can be told apart from regular Errors. We could also expose a function `isNavigationFailure` to tell them apart. +- `afterEach` and `onError` are relatively simple to migrate, most of the time they are not used many times either. +- `router.push` doesn't reject when navigation fails anymore. Any code relying on catching an error should await the promise result instead. From 86a2245e6d4d54dc517946fb62135994a04352e7 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 30 Apr 2020 14:22:13 +0200 Subject: [PATCH 03/49] rename files --- ....md => 0032-vtu-improve-async-workflow.md} | 0 .../0033-router-navigation-failures.md | 174 ++++++++++++++++++ 2 files changed, 174 insertions(+) rename active-rfcs/{0000-vtu-improve-async-workflow.md => 0032-vtu-improve-async-workflow.md} (100%) create mode 100644 active-rfcs/0033-router-navigation-failures.md diff --git a/active-rfcs/0000-vtu-improve-async-workflow.md b/active-rfcs/0032-vtu-improve-async-workflow.md similarity index 100% rename from active-rfcs/0000-vtu-improve-async-workflow.md rename to active-rfcs/0032-vtu-improve-async-workflow.md diff --git a/active-rfcs/0033-router-navigation-failures.md b/active-rfcs/0033-router-navigation-failures.md new file mode 100644 index 00000000..fa5f8686 --- /dev/null +++ b/active-rfcs/0033-router-navigation-failures.md @@ -0,0 +1,174 @@ +- Start Date: 2020-03-26 +- Target Major Version: Vue Router v4 +- Reference Issues: https://github.com/vuejs/vue-router/issues/2833, https://github.com/vuejs/vue-router/pull/3047, https://github.com/vuejs/vue-router/issues/2932, https://github.com/vuejs/vue-router/issues/2881 +- Implementation PR: + +# Summary + +- Explicitly define what a navigation failure is, and how and where we can catch them. +- Change when the Promised-based `router.push`(and `router.replace` by extension) resolves and rejects. +- Make `router.push` consistent with `router.afterEach` and `router.onError`. + +# Basic example + +## `router.push` + +If there is an unhandled error or an error is passed to `next`: + +```js +// any other navigation guard +router.beforeEach((to, from, next) => { + next(new Error()) + // or + throw new Error() + // or + return Promise.reject(new Error()) +}) +``` + +Then the promise returned by `router.push` rejects: + +```js +router.push('/url').catch(err => { + // ... +}) +``` + +In **all** other cases, the promise is resolved. We can know if the navigation failed or not by checking the resolved value: + +```js +router.push('/dashboard').then(failure => { + if (failure) { + failure instanceof Error // true + failure.type // NavigationFailure.canceled + } +}) +``` + +## `router.afterEach` + +It's the global equivalent of `router.push().then()` + +## `router.onError` + +It's the global equivalent of `router.push().catch()` + +# Motivation + +The current behavior of Vue Router regarding the promise returned by `push` is inconsistent with `router.afterEach` and `router.onError`. Ideally, we should be able to catch all succeeded and failed navigations globally and locally but we can only do it locally. + +- `onError` is only triggered on thrown errors and `next(new Error())` +- `afterEach` is only called if there is a navigation +- `redirect` should behave the same as `next('/url')` in a Navigation guard when it comes to the outcome of `router.push` and calls of `router.afterEach`/`router.onError`. The only difference being that a `redirect` would only trigger leave guards and other before guards for the redirected location but not the original one + +The differences between the Promise resolution/rejection vs `router.afterEach` and `router.onError` are inconsistent and confusing. + +# Detailed design + +One of the main points is to be able to consistently handle failed navigations globally and locally: + +- Failed Navigation: + - Triggers `router.afterEach` + - Resolves the `Promise` returned by `router.push` +- Uncaught Errors, `next(new Error())` + - Triggers `router.onError` + - Rejects the `Promise` returned by `router.push` + +It's important to note there is no overlap in these two groups: if there is an unhandled error in a Navigation Guard, it will trigger `router.onError` as well as rejecting the `Promise` returned by `router.push` but **will not trigger** `router.afterEach`. Cancelling a navigation with `next(false)` will not trigger `router.onError` but will trigger `router.afterEach` + +## Changes to the Promise resolution and rejection + +Navigation methods like `push` return a Promise, this promise **resolves** once the navigation succeeds **or** fail. It **rejects** only if there was an unhandled error. If it rejects, it will also trigger `router.onError`. + +To differentiate a Navigation that succeeded from one that failed, the `Promise` returned by `push` will resolve to either `undefined` or a `NavigationFailure`: + +```js +import { NavigationFailureType } from 'vue-router' + +router.push('/').then(failure => { + if (failure) { + // Having an Error instance allows us to have a Stacktrace and trace back + // where the navigation was cancelled. This will, in many cases, lead to a + // Navigation Guard and the corresponding `next()` call that cancelled the + // navigation + failure instanceof Error // true + if (failure.type === NavigationFailureType.canceled) { + // ... + } + } +}) + +// using async/await +let failure = await router.push('/') +if (failure) { + // ... +} +``` + +By not rejecting the `Promise` when the navigation fails, we are avoiding _Uncaught (in promise)_ errors while still keeping the possibility to check if the Navigation failed or not. + +### Navigation Failures + +There are a few different navigation failures, to be able to react differently in your code + +Navigation failures can be differentiated through a `type` property. All possible values are hold by an `enum`, `NavigationFailureType`: + +- `cancelled`: `next(false)` inside of a navigation guard or a newer navigation took place while another one was ongoing. +- `duplicated`: Navigating to the same location as the current one will cancel the navigation and not invoke any Navigation guard. + +On top of the `type` property, navigation failures also expose `from` and `to` properties, exactly like `router.afterEach` + +### Redirections + +Redirecting inside of a navigation guard with `next('/url')` is not a navigation failure by itself, as the navigation still takes place and ends up somewhere. To detect a navigation, specially during SSR, there is a `redirectedFrom` property accessible on the `currentRoute`. + +E.g.: imagine a navigation guard that redirects to `/login` when the user isn't authenticated: + +```js +router.beforeEach((to, from, next) => { + // redirect to the login page if the target location requires authentication + if (to.meta.requiresAuth && !isAuthenticated) next('/login') + else next() +}) +``` + +When navigating to a location that requires authentication, we can retrieve the original location the user was trying to access via `redirectedFrom`: + +```js +// user is not authenticated +await router.push('/profile/dashboard') + +// `redirectedFrom` is a RouteLocationNormalized, like `currentRoute` but we are omitting +// most properties to make the example readable +router.currentRoute // { path: '/login', redirectedFrom: { path: '/profile/dashboard' } } +``` + +## Changes to `router.afterEach` + +Since `router.afterEach` also triggers when a navigation fails, we need a way to know if the navigation succeeded or failed. To do that, we introduce an extra parameter that contains the same _failure_ we could find in a resolved navigation: + +```js +import { NavigationFailureType } from 'vue-router' + +router.afterEach((to, from, failure) => { + if (failure) { + if (failure.type === NavigationFailureType.canceled) { + // ... + } + } +}) +``` + +# Drawbacks + +- Breaking change although migration is relatively simple and in many cases will allow the developer to remove existing code + +# Alternatives + +- Differentiating `next(false)` from navigations that get overridden by more recent navigations by defining another Navigation Failure + +# Adoption strategy + +- Expose `NavigationFailureType` in vue-router@3 so that Navigation Failures can be told apart from regular Errors. We could also expose a function `isNavigationFailure` to tell them apart. +- `afterEach` and `onError` are relatively simple to migrate, most of the time they are not used many times either. +- `router.push` doesn't reject when navigation fails anymore. Any code relying on catching an error should await the promise result instead. From e07eeac5dc91d6ee64754be7ccb47ae9937cbe19 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 4 May 2020 16:38:12 +0200 Subject: [PATCH 04/49] Amendments to RouterLink scoped-slot (#152) --- active-rfcs/0021-router-link-scoped-slot.md | 97 +++++++++++++++------ 1 file changed, 70 insertions(+), 27 deletions(-) diff --git a/active-rfcs/0021-router-link-scoped-slot.md b/active-rfcs/0021-router-link-scoped-slot.md index 444d62bb..3f6141a6 100644 --- a/active-rfcs/0021-router-link-scoped-slot.md +++ b/active-rfcs/0021-router-link-scoped-slot.md @@ -9,6 +9,7 @@ - Remove `event` prop - Stop automatically assigning click events to inner anchors - Add a scoped-slot API +- Add a `custom` prop to fully customize `router-link`'s rendering # Basic example @@ -55,34 +56,12 @@ This implementation would: - no longer accepts `event` -> use the scoped slot instead - no longer works as a wrapper automatically looking for the first `a` inside -> use the scoped slot instead -## Custom `tag` prop - -I am not sure about keeping the `tag` prop if it can be replaced which a scoped slot because it wouldn't handle custom components and except for very simple cases, we will likely use custom UI components instead of the basics ones: - -```vue - - homeHome - -``` - -is equivalent to - -```vue - - - -``` - -(see below for explanation about the attributes passed to the scoped-slot) - ## Scoped slot -A scoped slot would get access to every bit of information needed to provide a custom integration and allows applying the active classes, click listener, links, etc at any level. This would allow a better integration with frameworks like Bootstrap (https://getbootstrap.com/docs/4.3/components/navbar/). The idea would be to create a Vue component to avoid the boilerplate like bootstrap-vue does (https://bootstrap-vue.js.org/docs/components/navbar/#navbar) +A scoped slot would get access to every bit of information needed to provide a custom integration and allows applying the active classes, click listener, links, etc at any level. This would allow a better integration with UI frameworks like Bootstrap (https://getbootstrap.com/docs/4.3/components/navbar/). The idea would be to create a Vue component to avoid the boilerplate like bootstrap-vue does (https://bootstrap-vue.js.org/docs/components/navbar/#navbar) ```vue - +
  • homeHome @@ -91,6 +70,30 @@ A scoped slot would get access to every bit of information needed to provide a c ``` +The `custom` prop is necessary to take full control over `router-link`'s rendering: not rendering a wrapping `a` element. + +**Why is a `custom` prop necessary**: in Vue 3, scoped slots and regular slots cannot be differentiated from each other, which means vue router is unable to make the difference between these 3 cases: + +```vue + + +Some Link +``` + +In all three cases we need to render the slot content but `router-link` needs to know if it has to render a wrapping `a` element. In Vue 2, we are able to do so by checking `$scopedSlots` but in Vue 3, only `slots` exists. This means that the behavior is slightly different in Vue Router v3 and Vue Router v4: + +- In v3, the `custom` prop is required (see [Adoption strategy](#adoption-strategy)) alongside `v-slot`. `router-link` will not wrap the slot content with an `a` element. +- In v4, the `custom` prop is **not** required alongside `v-slot`. It controls whether `router-link` should wrap its slot content with an `a` element or not: + ```vue + + + {{ href }} + + + / + / + ``` + ### Accessible variables The slot should provide values that are computed inside `router-link`: @@ -101,6 +104,28 @@ The slot should provide values that are computed inside `router-link`: - `isActive`: true whenever `router-link-active` is applied. Can be modified by `exact` prop - `isExactActive`: true whenever `router-link-exact-active` is aplied. Can be modified by `exact` prop. +## The removal of the `tag` prop + +The `tag` prop can be replaced which a scoped slot and make the code clearer while not being exposed to any caveat. Its removal will also lighten the vue-router library. + +```vue + + homeHome + +``` + +is equivalent to + +```vue + + + +``` + +(see above for explanation about the attributes passed to the scoped-slot) + # Drawbacks - Whereas it's possible to keep existing behaviour working and only expose a new behaviour with scoped slots, it will still prevent us from fixing existing issues with current implementation. That's why there are some breaking changes, to make things more consistent. @@ -109,10 +134,28 @@ The slot should provide values that are computed inside `router-link`: # Alternatives - Keeping `event` prop for convienience +- Use a different named slot instead of a `prop`: + + ```vue + + + + + + + + + + + + ``` + + The adoption strategy in this case would be similar but the warning would tell the user to use a different slot instead of a prop named `custom` + +- Create a new component like `router-link-custom` to differentiate the behavior. This solution is however heavier (in terms of size) than a prop or a different named slot. It is also less suitable than a prop because we are only changing a behavior of the component. The difference between the two components woud be too small to justify a whole new component. # Adoption strategy - Document new slot behaviour based on examples -- Deprecate `tag` and `event` with a message and link to documentation the remove in v4 - -# Unresolved questions +- Deprecate `tag` and `event` with a message in v3 and link to documentation, then remove in v4 +- In v3, if no `custom` prop is provided when using a scoped slot, warn the user to use the `custom` prop From 283eee93d659b1d1009d263033c5bf833d4a0bda Mon Sep 17 00:00:00 2001 From: lawvs Date: Tue, 5 May 2020 01:40:34 +0900 Subject: [PATCH 05/49] Fix typos (#169) --- active-rfcs/0025-teleport.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/active-rfcs/0025-teleport.md b/active-rfcs/0025-teleport.md index 788db375..512e362f 100644 --- a/active-rfcs/0025-teleport.md +++ b/active-rfcs/0025-teleport.md @@ -255,7 +255,7 @@ If the new target selector doesn't match any elements: ### Destruction -When a `` is being destroyed (e.g. because its parent component is being destroyed or because of a `v-if`), its children are removed from the DOM and any component instances destroyed just like they were still children iof the parent. +When a `` is being destroyed (e.g. because its parent component is being destroyed or because of a `v-if`), its children are removed from the DOM and any component instances destroyed just like they were still children of the parent. ## Miscellaneous @@ -272,13 +272,13 @@ Or should we keep it as the concept of what a portal is in Vue, React e.t al. is ### dev-tools -The `` should not appear in the chain of parent components (`this.$parent`), but it should be identifiable within the virtual DOM so that Vue's dee-tools can show them in their visualisation of the component tree. +The `` should not appear in the chain of parent components (`this.$parent`), but it should be identifiable within the virtual DOM so that Vue's dev-tools can show them in their visualisation of the component tree. ### Using a `` on an element within a Vue app Technically, this proposal allows to select _any_ element in the DOM , including elements that are rendered by our Vue app in some other part of the component tree. -But that puts the portal'd slot content under the control of that other component's lifecycle, which means the content can possibly be removed from the DOM if that component gets destroyed. +But that puts the portal's slot content under the control of that other component's lifecycle, which means the content can possibly be removed from the DOM if that component gets destroyed. Any component that came through a `` would effectively have its DOM removed by still be in the original virtual DOM tree, which would lead to patch errors when these components tried to update. From fe16b95d56421c34fd4da333c4d70d17989856b5 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 8 May 2020 09:03:16 +0200 Subject: [PATCH 06/49] remove old file --- .../0000-router-navigation-failures.md | 174 ------------------ 1 file changed, 174 deletions(-) delete mode 100644 active-rfcs/0000-router-navigation-failures.md diff --git a/active-rfcs/0000-router-navigation-failures.md b/active-rfcs/0000-router-navigation-failures.md deleted file mode 100644 index fa5f8686..00000000 --- a/active-rfcs/0000-router-navigation-failures.md +++ /dev/null @@ -1,174 +0,0 @@ -- Start Date: 2020-03-26 -- Target Major Version: Vue Router v4 -- Reference Issues: https://github.com/vuejs/vue-router/issues/2833, https://github.com/vuejs/vue-router/pull/3047, https://github.com/vuejs/vue-router/issues/2932, https://github.com/vuejs/vue-router/issues/2881 -- Implementation PR: - -# Summary - -- Explicitly define what a navigation failure is, and how and where we can catch them. -- Change when the Promised-based `router.push`(and `router.replace` by extension) resolves and rejects. -- Make `router.push` consistent with `router.afterEach` and `router.onError`. - -# Basic example - -## `router.push` - -If there is an unhandled error or an error is passed to `next`: - -```js -// any other navigation guard -router.beforeEach((to, from, next) => { - next(new Error()) - // or - throw new Error() - // or - return Promise.reject(new Error()) -}) -``` - -Then the promise returned by `router.push` rejects: - -```js -router.push('/url').catch(err => { - // ... -}) -``` - -In **all** other cases, the promise is resolved. We can know if the navigation failed or not by checking the resolved value: - -```js -router.push('/dashboard').then(failure => { - if (failure) { - failure instanceof Error // true - failure.type // NavigationFailure.canceled - } -}) -``` - -## `router.afterEach` - -It's the global equivalent of `router.push().then()` - -## `router.onError` - -It's the global equivalent of `router.push().catch()` - -# Motivation - -The current behavior of Vue Router regarding the promise returned by `push` is inconsistent with `router.afterEach` and `router.onError`. Ideally, we should be able to catch all succeeded and failed navigations globally and locally but we can only do it locally. - -- `onError` is only triggered on thrown errors and `next(new Error())` -- `afterEach` is only called if there is a navigation -- `redirect` should behave the same as `next('/url')` in a Navigation guard when it comes to the outcome of `router.push` and calls of `router.afterEach`/`router.onError`. The only difference being that a `redirect` would only trigger leave guards and other before guards for the redirected location but not the original one - -The differences between the Promise resolution/rejection vs `router.afterEach` and `router.onError` are inconsistent and confusing. - -# Detailed design - -One of the main points is to be able to consistently handle failed navigations globally and locally: - -- Failed Navigation: - - Triggers `router.afterEach` - - Resolves the `Promise` returned by `router.push` -- Uncaught Errors, `next(new Error())` - - Triggers `router.onError` - - Rejects the `Promise` returned by `router.push` - -It's important to note there is no overlap in these two groups: if there is an unhandled error in a Navigation Guard, it will trigger `router.onError` as well as rejecting the `Promise` returned by `router.push` but **will not trigger** `router.afterEach`. Cancelling a navigation with `next(false)` will not trigger `router.onError` but will trigger `router.afterEach` - -## Changes to the Promise resolution and rejection - -Navigation methods like `push` return a Promise, this promise **resolves** once the navigation succeeds **or** fail. It **rejects** only if there was an unhandled error. If it rejects, it will also trigger `router.onError`. - -To differentiate a Navigation that succeeded from one that failed, the `Promise` returned by `push` will resolve to either `undefined` or a `NavigationFailure`: - -```js -import { NavigationFailureType } from 'vue-router' - -router.push('/').then(failure => { - if (failure) { - // Having an Error instance allows us to have a Stacktrace and trace back - // where the navigation was cancelled. This will, in many cases, lead to a - // Navigation Guard and the corresponding `next()` call that cancelled the - // navigation - failure instanceof Error // true - if (failure.type === NavigationFailureType.canceled) { - // ... - } - } -}) - -// using async/await -let failure = await router.push('/') -if (failure) { - // ... -} -``` - -By not rejecting the `Promise` when the navigation fails, we are avoiding _Uncaught (in promise)_ errors while still keeping the possibility to check if the Navigation failed or not. - -### Navigation Failures - -There are a few different navigation failures, to be able to react differently in your code - -Navigation failures can be differentiated through a `type` property. All possible values are hold by an `enum`, `NavigationFailureType`: - -- `cancelled`: `next(false)` inside of a navigation guard or a newer navigation took place while another one was ongoing. -- `duplicated`: Navigating to the same location as the current one will cancel the navigation and not invoke any Navigation guard. - -On top of the `type` property, navigation failures also expose `from` and `to` properties, exactly like `router.afterEach` - -### Redirections - -Redirecting inside of a navigation guard with `next('/url')` is not a navigation failure by itself, as the navigation still takes place and ends up somewhere. To detect a navigation, specially during SSR, there is a `redirectedFrom` property accessible on the `currentRoute`. - -E.g.: imagine a navigation guard that redirects to `/login` when the user isn't authenticated: - -```js -router.beforeEach((to, from, next) => { - // redirect to the login page if the target location requires authentication - if (to.meta.requiresAuth && !isAuthenticated) next('/login') - else next() -}) -``` - -When navigating to a location that requires authentication, we can retrieve the original location the user was trying to access via `redirectedFrom`: - -```js -// user is not authenticated -await router.push('/profile/dashboard') - -// `redirectedFrom` is a RouteLocationNormalized, like `currentRoute` but we are omitting -// most properties to make the example readable -router.currentRoute // { path: '/login', redirectedFrom: { path: '/profile/dashboard' } } -``` - -## Changes to `router.afterEach` - -Since `router.afterEach` also triggers when a navigation fails, we need a way to know if the navigation succeeded or failed. To do that, we introduce an extra parameter that contains the same _failure_ we could find in a resolved navigation: - -```js -import { NavigationFailureType } from 'vue-router' - -router.afterEach((to, from, failure) => { - if (failure) { - if (failure.type === NavigationFailureType.canceled) { - // ... - } - } -}) -``` - -# Drawbacks - -- Breaking change although migration is relatively simple and in many cases will allow the developer to remove existing code - -# Alternatives - -- Differentiating `next(false)` from navigations that get overridden by more recent navigations by defining another Navigation Failure - -# Adoption strategy - -- Expose `NavigationFailureType` in vue-router@3 so that Navigation Failures can be told apart from regular Errors. We could also expose a function `isNavigationFailure` to tell them apart. -- `afterEach` and `onError` are relatively simple to migrate, most of the time they are not used many times either. -- `router.push` doesn't reject when navigation fails anymore. Any code relying on catching an error should await the promise result instead. From ad5111bfd069d25044bf781528769842be4e925f Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Sat, 18 Jul 2020 16:38:22 +0200 Subject: [PATCH 07/49] Support for KeepAlive and Transition in Vue Router 4 (#160) * initial version * rename props into attrs * add note about warning * review * remove v-if * add example with both keep alive and transition * prepare for merge --- ...0034-router-view-keep-alive-transitions.md | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 active-rfcs/0034-router-view-keep-alive-transitions.md diff --git a/active-rfcs/0034-router-view-keep-alive-transitions.md b/active-rfcs/0034-router-view-keep-alive-transitions.md new file mode 100644 index 00000000..256de4c9 --- /dev/null +++ b/active-rfcs/0034-router-view-keep-alive-transitions.md @@ -0,0 +1,88 @@ +- Start Date: 2020-04-20 +- Target Major Version: Vue Router 4.x +- Reference Issues: +- Implementation PR: + +# Summary + +Given the changes to functional components in Vue 3, `KeepAlive` and `Transition` usage combined with `RouterView` is no longer possible by simply wrapping `RouterView` with `KeepAlive`/`Transition`. Instead we need a way to directly provide the component rendered by `RouterView` to those components: + +```vue + + + + + +``` + +# Motivation + +As described at https://github.com/vuejs/vue-next/issues/906#issuecomment-611080663, we need a new API to allow the usage of `KeepAlive` and other components accepting slots with `RouterView`. In order to implement such behavior we need to be able to **directly** wrap the component rendered by `RouterView`. The only way to do so by having access to the component rendered by `RouterView` and the _props_ passed to it. + +# Detailed design + +We can achieve this level of control through a slot: + +```vue + + + +``` + +In the example, `Component` is the _component definition_ that can be passed to the function `h` or to the `is` prop of `component`. + +When defining a `props: true` option in the route definition: + +```js +createRouter({ + routes: [{ path: '/users/:id', component: User, props: true }], +}) +``` + +The `router-view` will automatically add the `id` prop to the rendered component with the value of the param named `id`. Note you can also do `v-bind="route.params"` like shown in the example above **instead** of using `props: true`. + +## No match case + +When the current location isn't matched by any record registered by the Router, the `matched` array of a `RouteLocation` is empty and, by default, when no _slot_ is provided, it renders nothing. When we provide a _slot_, we can decide of what to display, whether we want to display a _not found_ page or we want the default behavior, we are able to do it. `Component` will be falsy if there is no component to render: + +```vue + + +
    Not Found
    +
    +``` + +Note that this behavior is redundant with the _catch all_ route (`path: '/:pathMatch(.*)`) when it comes to displaying a not found page. + +## `v-slot` properties + +- `Component`: _Component_ that can be passed to the function `h` or to the `is` prop of `component`. +- `route`: Normalized Route location rendered by `RouterView`. Same as `$route` but allows easy and typed access in JSX. + +## Wrapping `RouterView` with `Transition` or `KeepAlive` + +If the user accidentally wraps `RouterView` with `Transition` or is migrating their application to Vue 3, we could issue a warning pointing to the documentation (this RFC in the meantime) and hinting them to use the `v-slot` api. + +## Using both `KeepAlive` and `Transition` at the same time + +When using `KeepAlive` and `Transition` at the same time, we need to do `Transition` then `KeepAlive`: + +```vue + + + + + + + +``` + + + +# Alternatives + +- Using a function like `useView` that would return `Component` and `attrs` removing the use of `v-slot`. + +# Adoption strategy + +- A codemod could rewrite v3 to v4. From 932e072c2607c9212e6e7ad034643ba3dc1bf2b1 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Sat, 18 Jul 2020 16:48:07 +0200 Subject: [PATCH 08/49] Redesign the position object returned by `scrollBehavior` (#176) * initial version * add note about promise return being supported * prepare merge --- active-rfcs/0035-router-scroll-position.md | 133 +++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 active-rfcs/0035-router-scroll-position.md diff --git a/active-rfcs/0035-router-scroll-position.md b/active-rfcs/0035-router-scroll-position.md new file mode 100644 index 00000000..2f7dbc63 --- /dev/null +++ b/active-rfcs/0035-router-scroll-position.md @@ -0,0 +1,133 @@ +- Start Date: 2020-05-31 +- Target Major Version: Vue Router v3 and v4 +- Reference Issues: https://github.com/vuejs/vue-router/issues/3008, +- Implementation PR: (leave this empty) + +# Summary + +Deprecate `selector`, `x`, `y` and `offset` and introduce instead `el`, `top`, `left` (and `behavior`) that align with native apis and are more flexible. + +# Basic example + +```js +const router = new Router({ + scrollBehavior(to, from, savedPosition) { + + // scroll to id `can~contain-special>characters` + 200px + return { + el: '#can~contain-special>characters' + // top relative offset + top: 200 + // instead of `offset: { y: 200 }` + } + } +}) +``` + +Other possible values returned by `scrollBehavior`: + +```js +// scroll smoothly (when supported by the browser) to 400px from top and 20px from left +{ + top: 400, + left: 20, + behavior: 'smooth' +} + +// scroll smoothly to selector .container +{ + el: '.container' + behavior: 'smooth' +} + +// directly pass an Element to `el` +{ + // use the fragment(to.hash) on the url but scroll to a child of it with a class `container` + el: document.getElementById(to.hash.slice(1)).querySelector('.container') +} +``` + +# Motivation + +## Deprecating `selector` + +The existing scroll behavior accepts a `selector` property that internally uses `document.querySelector`. In vue-router@3 we currently have a workaround for selectors that match `/^#\d/` (id CSS selector starting with a number). This is because such selector is invalid, so we detect that and use `document.getElementById` instead. However, this breaks when using CSS combinators like `#1one .container` (select an element with class `.container` inside of an element with id `1one`). The truth is there ary many [others characters that need to be escaped](https://mathiasbynens.be/notes/css-escapes) and that we cannot escape everything, creating cases where the scroll behavior is harder to use. On top of that, it can be confusing for the user when vue-router throws because `document.querySelector` failed. + +## Deprecating `x`, `y` and `offset` + +Currently there are two ways of specifying an offset and both use an `x`/`y` coordinate system that differs from native functions: native browser functions allow the use of a [`ScrollToOptions`](https://developer.mozilla.org/en-US/docs/Web/API/ScrollToOptions) in `scrollTo` methods. This object contains `top`, `left` and `behavior` properties. + +# Detailed design + +Even though it is not commented anywhere in the RFC, **`scrollBehavior` can still return a Promise of any of the mentioned types**. It is omitted to focus on the shape of the return type instead of it being able to _await_ inside of `scrollBehavior`. + +## Deprecating `x` and `y` + +This aligns with [`Element.scrollTo`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo) and makes it more natural passing extra options like [`behavior`](https://developer.mozilla.org/en-US/docs/Web/API/ScrollToOptions/behavior). + +```js +{ x: 0, y: 200 } +// becomes +{ left: 0, top: 200 } +// it can accept `behavior` +{ left: 0, top: 200, behavior: 'smooth' } +``` + +## Deprecating `offset` + +Instead of taking an `offset` option alongside `selector`, we could directly accept `x` and `y` alongside `selector` when specifying an offset: + +```js +{ + selector: '#getting-started', + y: -120, +} +``` + +Which, following the proposed rename, would become + +```js +{ + el: '#getting-started', + top: -120, +} +``` + +Omitting `el` will create an absolute offset as if we were providing `x` and/or `y` in vue-router@3. + +This also aligns with `new Vue`'s `el` option + +## More intuitive selectors thanks to `el` + +The goal of a new `el` property, would be to provide simple, _"it just works"_, selectors for most common use cases while still allowing advanced cases: + +- `to.hash` refers to an _id_ on the page and is directly provided to `el`. e.g. `el: to.hash` when `to.hash` equals `#about`, `#getting-started`, or `#symbols~work` +- `el` is provided a more generic selector, not necessarily coming from `to.hash`. e.g. `.container > main`, **anything not starting with `#`** +- `el` is provided an `Element` to allow any advanced use case e.g. when `to.hash` starts with `#` but contains a CSS selector rather than an id: `document.querySelector(to.hash)` + +Thanks to allowing a raw Element, this covers all possible cases and makes it easier to provide the user with feedback when things do not work. + +## Developer experience through warnings + +Another important part of this change is warning in development when vue-router fails to find an element or when `document.querySelector` throws an Error. We can divide this in two cases: + +### `el` starts with `#` + +When `el` starts with `#`, we internally use `document.getElementById(el.slice(1))`. If it doesn't find an element, **in development** mode we try using `document.querySelector`, if we find something, we tell the user to use `el: document.querySelector(${providedEl})` and explain why. If we find nothing, show the usual warning of no element was found + +### `el` doesn't start with `#` + +In development mode, _try catch_ to provide an error message pointing to this great article https://mathiasbynens.be/notes/css-escapes and to [`CSS.escape`](https://developer.mozilla.org/en-US/docs/Web/API/CSS/escape) if `document.querySelector` throws. If nothing is found, display the same warning of no element was found. + +# Drawbacks + +- Breaking change but can be introduced through a deprecation + +# Adoption strategy + +- Deprecate `selector`, `x`, `y` and `offset` with a warning in v3 +- Remove in v4 + +# Notes + +- This RFC does not concern scrolling to a different element than window or scrolling multiple elements at once From a0b68d18b38e72b4e41529bafbcb5d30513e5b78 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Sat, 18 Jul 2020 16:49:49 +0200 Subject: [PATCH 09/49] Allow displaying a View different from where the user currently is at (#153) * initial * prepare merge --- active-rfcs/0036-router-view-route-prop.md | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 active-rfcs/0036-router-view-route-prop.md diff --git a/active-rfcs/0036-router-view-route-prop.md b/active-rfcs/0036-router-view-route-prop.md new file mode 100644 index 00000000..b149d59d --- /dev/null +++ b/active-rfcs/0036-router-view-route-prop.md @@ -0,0 +1,58 @@ +- Start Date: 2020-04-01 +- Target Major Version: Vue Router v3 and v4 +- Reference Issues: +- Implementation PR: + +# Summary + +Add a `route` prop that allows customizing what is displayed by the `router-view` component. This allows users to display a view that is not the one connected to the URL. It enables patterns like modals (https://github.com/vuejs/vue-router/issues/703#issuecomment-428123334) + +# Basic example + +Imagine a `backgroundView` variable that allows us to know if we are displaying a different route. We could then compute a route location that would resolve to a different location than the one displayed on the url: + +```js +const App = { + computed: { + routeWithModal() { + if (backgroundView) { + return this.$router.resolve(backgroundView) + } else { + return this.$route + } + } + } +} +``` + +We could pass that variable to `router-view`: + +```vue + +``` + +This will in most scenarios display the component associated with the current url (`this.$route`), and in others display something different while still exposing the resolved location at `this.$route`. + +# Motivation + +By design, Vue Router associates a URL with a Component. This is sometimes a limitation, like with modals, and this allows escaping the limitation. It's very hard to implement a userland solution without hacks (like modifying `this.$route` at [this example](https://github.com/tmiame/vue-router-twitter-style-modals)) because it involves changing the current location. Exposing a way to modify what is displayed by `router-view` is straightforward from an implementation point of view. + +# Detailed design + +Vue Router already exposes the mechanism to resolve a location so users can use some global state (ideally it should be held in `window.history.state` so it is restored when navigating using the browser back and forward buttons) to generate a resolved location that can be consumed by `router-view`. + +Internally, `router-view` must make sure its children (nested views) use that same location to be consistent. In v4, using `inject`/`provide` should make this simple, in v3, we might need to look up like we do for _depth_. If a `route` prop is provided, it takes precedence against any _providen_ route on the `router-view` receiving the prop **and all its children**. + +The end user API is one single prop: + +```vue + +``` + +# Drawbacks + +- For this mechanism to work with _Lazy Loading_, it requires for the resolved route to have been navigated to before. This ensures any lazy view has been already fetched and catched. In the scenario of modals, this is automatic because we are already on the view we want to make `router-view` display. I think this limitation makes sense because I cannot think of other use cases apart from _Modals_ when it comes to displaying a view that is not associated to the URL. + +# Alternatives + +- A composition function `useView` that returns a reactive component to be used with ``. This solutions is way more complicated than a prop. From 3f9eebb0162fb18f8f9f8f22523ae4598d198740 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 20 Jul 2020 13:50:33 +0300 Subject: [PATCH 10/49] Fix dead links (#191) --- active-rfcs/0009-global-api-change.md | 2 +- active-rfcs/0011-v-model-api-change.md | 2 +- active-rfcs/0012-custom-directive-api-change.md | 2 +- active-rfcs/0030-emits-option.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/active-rfcs/0009-global-api-change.md b/active-rfcs/0009-global-api-change.md index 8cd5bd26..7ca7963e 100644 --- a/active-rfcs/0009-global-api-change.md +++ b/active-rfcs/0009-global-api-change.md @@ -9,7 +9,7 @@ Re-design app bootstrapping and global API. - Global APIs that globally mutate Vue's behavior are now moved to **app instances** created by the new `createApp` method, and their effects are now scoped to that app instance only. -- Global APIs that are do not mutate Vue's behavior (e.g. `nextTick` and the APIs proposed in [Advanced Reactivity API](https://github.com/vuejs/rfcs/pull/22)) are now named exports as specified in [the Global API Treeshaking RFC](https://github.com/vuejs/rfcs/blob/treeshaking/active-rfcs/0000-global-api-treeshaking.md). +- Global APIs that are do not mutate Vue's behavior (e.g. `nextTick` and the APIs proposed in [Advanced Reactivity API](https://github.com/vuejs/rfcs/pull/22)) are now named exports as specified in [the Global API Treeshaking RFC](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0004-global-api-treeshaking.md). # Basic example diff --git a/active-rfcs/0011-v-model-api-change.md b/active-rfcs/0011-v-model-api-change.md index 31e8f07f..abdabcd5 100644 --- a/active-rfcs/0011-v-model-api-change.md +++ b/active-rfcs/0011-v-model-api-change.md @@ -39,7 +39,7 @@ h(Comp, { }) ``` -If the component wants to support `v-model` without an argument, it should expect a prop named `modelValue`. To sync its value back to the parent, the child should emit an event named `"update:modelValue"` (see [Render Function API change](https://github.com/vuejs/rfcs/blob/render-fn-api-change/active-rfcs/0000-render-function-api-change.md) for details on the new VNode data structure). +If the component wants to support `v-model` without an argument, it should expect a prop named `modelValue`. To sync its value back to the parent, the child should emit an event named `"update:modelValue"` (see [Render Function API change](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0008-render-function-api-change.md) for details on the new VNode data structure). The default compilation output prefixes the prop and event names with `model` to avoid conflict with common prop names. diff --git a/active-rfcs/0012-custom-directive-api-change.md b/active-rfcs/0012-custom-directive-api-change.md index f0274008..e40fe633 100644 --- a/active-rfcs/0012-custom-directive-api-change.md +++ b/active-rfcs/0012-custom-directive-api-change.md @@ -76,7 +76,7 @@ return withDirectives(h('div'), [ Where `vFoo` will be the directive object written by the user, which contains hooks like `mounted` and `updated`. -`withDirectives` returns a cloned VNode with the user hooks wrapped and injected as vnode lifecycle hooks (see [Render Function API Changes](https://github.com/vuejs/rfcs/blob/render-fn-api-change/active-rfcs/0000-render-function-api-change.md#special-reserved-props) for more details): +`withDirectives` returns a cloned VNode with the user hooks wrapped and injected as vnode lifecycle hooks (see [Render Function API Changes](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0008-render-function-api-change.md#special-reserved-props) for more details): ``` js { diff --git a/active-rfcs/0030-emits-option.md b/active-rfcs/0030-emits-option.md index 4eea6677..de999832 100644 --- a/active-rfcs/0030-emits-option.md +++ b/active-rfcs/0030-emits-option.md @@ -80,7 +80,7 @@ Or it can be an object with event names as its keys. The value of each property ## Fallthrough Control -The new [Attribute Fallthrough Behavior](https://github.com/vuejs/rfcs/blob/amend-optional-props/active-rfcs/0000-attr-fallthrough.md) proposed in [#154](https://github.com/vuejs/rfcs/pull/154) now applies automatic fallthrough for `v-on` listeners used on a component: +The new [Attribute Fallthrough Behavior](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0031-attr-fallthrough.md) proposed in [#154](https://github.com/vuejs/rfcs/pull/154) now applies automatic fallthrough for `v-on` listeners used on a component: ```html From 33c48c56693499608923b41e9248e945b8bbd075 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 28 Jul 2020 16:21:33 +0200 Subject: [PATCH 11/49] add aborted and isNavigationFailure (#184) --- .../0033-router-navigation-failures.md | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/active-rfcs/0033-router-navigation-failures.md b/active-rfcs/0033-router-navigation-failures.md index fa5f8686..2aba2b74 100644 --- a/active-rfcs/0033-router-navigation-failures.md +++ b/active-rfcs/0033-router-navigation-failures.md @@ -29,7 +29,7 @@ router.beforeEach((to, from, next) => { Then the promise returned by `router.push` rejects: ```js -router.push('/url').catch(err => { +router.push('/url').catch((err) => { // ... }) ``` @@ -37,7 +37,7 @@ router.push('/url').catch(err => { In **all** other cases, the promise is resolved. We can know if the navigation failed or not by checking the resolved value: ```js -router.push('/dashboard').then(failure => { +router.push('/dashboard').then((failure) => { if (failure) { failure instanceof Error // true failure.type // NavigationFailure.canceled @@ -83,16 +83,16 @@ Navigation methods like `push` return a Promise, this promise **resolves** once To differentiate a Navigation that succeeded from one that failed, the `Promise` returned by `push` will resolve to either `undefined` or a `NavigationFailure`: ```js -import { NavigationFailureType } from 'vue-router' +import { NavigationFailureType, isNavigationFailure } from 'vue-router' -router.push('/').then(failure => { +router.push('/').then((failure) => { if (failure) { // Having an Error instance allows us to have a Stacktrace and trace back // where the navigation was cancelled. This will, in many cases, lead to a // Navigation Guard and the corresponding `next()` call that cancelled the // navigation failure instanceof Error // true - if (failure.type === NavigationFailureType.canceled) { + if (isNavigationFailure(failure, NavigationFailureType.canceled)) { // ... } } @@ -111,9 +111,10 @@ By not rejecting the `Promise` when the navigation fails, we are avoiding _Uncau There are a few different navigation failures, to be able to react differently in your code -Navigation failures can be differentiated through a `type` property. All possible values are hold by an `enum`, `NavigationFailureType`: +Navigation failures can be differentiated through a `type` property **although you don't need to directly check it**. All possible values are hold by an `enum`, `NavigationFailureType`: -- `cancelled`: `next(false)` inside of a navigation guard or a newer navigation took place while another one was ongoing. +- `aborted`: a newer navigation took place while the current one was ongoing. +- `cancelled`: `next(false)` inside of a navigation guard. - `duplicated`: Navigating to the same location as the current one will cancel the navigation and not invoke any Navigation guard. On top of the `type` property, navigation failures also expose `from` and `to` properties, exactly like `router.afterEach` @@ -148,27 +149,49 @@ router.currentRoute // { path: '/login', redirectedFrom: { path: '/profile/dashb Since `router.afterEach` also triggers when a navigation fails, we need a way to know if the navigation succeeded or failed. To do that, we introduce an extra parameter that contains the same _failure_ we could find in a resolved navigation: ```js -import { NavigationFailureType } from 'vue-router' +import { NavigationFailureType, isNavigationFailure } from 'vue-router' router.afterEach((to, from, failure) => { - if (failure) { - if (failure.type === NavigationFailureType.canceled) { - // ... - } + if (isNavigationFailure(failure)) { + // ... } }) ``` -# Drawbacks +## Differentiating Navigation failures -- Breaking change although migration is relatively simple and in many cases will allow the developer to remove existing code +Instead of directly checking the `type` property, we can use the `isNavigationFailure` helper: + +```js +import { NavigationFailureType, isNavigationFailure } from 'vue-router' + +router.afterEach((to, from, failure) => { + // Any kind of navigation failure + if (isNavigationFailure(failure)) { + // ... + } + // Only duplicated navigations + if (isNavigationFailure(failure, NavigationFailureType.duplicated)) { + // ... + } + // Aborted or canceled navigations + if ( + isNavigationFailure( + failure, + NavigationFailureType.aborted | NavigationFailureType.canceled + ) + ) { + // ... + } +}) +``` -# Alternatives +# Drawbacks -- Differentiating `next(false)` from navigations that get overridden by more recent navigations by defining another Navigation Failure +- Breaking change although migration is relatively simple and in many cases will allow the developer to remove existing code # Adoption strategy -- Expose `NavigationFailureType` in vue-router@3 so that Navigation Failures can be told apart from regular Errors. We could also expose a function `isNavigationFailure` to tell them apart. +- Expose `NavigationFailureType` and `isNavigationFailure` in vue-router@3 so that Navigation Failures can be told apart from regular Errors. - `afterEach` and `onError` are relatively simple to migrate, most of the time they are not used many times either. - `router.push` doesn't reject when navigation fails anymore. Any code relying on catching an error should await the promise result instead. From 217adc671d49a0e645927eb97dbe09ffab454e3e Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 5 Aug 2020 14:17:10 +0200 Subject: [PATCH 12/49] Allow guards to return instead of calling the `next` callback (#187) * initial version * Update active-rfcs/0000-router-return-guards.md * prepare for merge --- active-rfcs/0037-router-return-guards.md | 110 +++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 active-rfcs/0037-router-return-guards.md diff --git a/active-rfcs/0037-router-return-guards.md b/active-rfcs/0037-router-return-guards.md new file mode 100644 index 00000000..097271b6 --- /dev/null +++ b/active-rfcs/0037-router-return-guards.md @@ -0,0 +1,110 @@ +- Start Date: 2020-06-08 +- Target Major Version: Vue Router 3 and 4 +- Reference Issues: https://github.com/vuejs/rfcs/issues/177 +- Implementation PR: https://github.com/vuejs/vue-router-next/pull/343 + +# Summary + +Allow navigation guards to return the value **or a Promise** of it instead of calling `next`: + +```js +// change +router.beforeEach((to, from, next) => { + if (!isAuthenticated) next(false) + else next() +}) + +// into +router.beforeEach(() => isAuthenticated) +``` + +Since we can check the amount or arguments the navigation guard has, we can automatically know if we should look at the returned value or not. **This is not a breaking change**. + +# Motivation + +- Avoid forgetting to call `next` +- Avoid the problem of calling `next` multiple times (since we can only return once) +- `return` is more idiomatic since it can only be called once +- Avoid the need of 3 arguments in the function if they are not all used + +# Detailed design + +If 2 arguments are passed to a navigation guard, Vue Router will look at the returned value as if it was passed to `next`. + +## Validate/cancel a navigation + +To validate a navigation, you currently have to call `next()` or `next(true)`. Instead, you can not return anything (same as explicitly returning `undefined`) or return `true`. To cancel the navigation you could call `next(false)`, now you can explicitly `return false`: + +```js +router.beforeEach((to) => { + if (to.meta.requiresAuth && !isAuthenticated) return false +}) + +// with async / await +router.beforeEach(async (to) => { + return await canAccessPage(to) +}) +``` + +## Redirect to a different location + +We can redirect to a location by returning the same kind of object passed to `router.push`: + +```js +router.beforeEach((to) => { + if (to.meta.requiresAuth && !isAuthenticated) + return { + name: 'Login', + query: { + redirectTo: to.fullPath, + }, + } +}) + +// with async / await +router.beforeEach(async (to) => { + if (!(await canAccessPage(to))) { + return { + name: 'Login', + query: { + redirectTo: to.fullPath, + }, + } + } +}) +``` + +## Errors + +Unexpected errors can still be thrown synchronously or synchronously: + +```js +router.beforeEach((to) => { + throw new Error() +}) + +// with async / await +router.beforeEach(async (to) => { + throw new Error() +}) + +// with promises +router.beforeEach((to) => { + return Promise.reject(new Error()) +}) +``` + +# Drawbacks + +- Vue Router 3 might preset a higher implementation cost + +# Alternatives + +- This could be implemented with a function helper but the idea is to shift the way we write navigation guards. + +What other designs have been considered? What is the impact of not doing this? + +# Adoption strategy + +- Add both syntaxes to Vue Router 4 (https://github.com/vuejs/vue-router-next/pull/343/files) +- A codemod should be able to handle the conversion even though it's not a breaking change From de64615601c95603d1f0e13d48049ede0d7e45f0 Mon Sep 17 00:00:00 2001 From: Chris Calo Date: Sun, 16 Aug 2020 12:49:52 -0400 Subject: [PATCH 13/49] fix: sync repeated when sync / async was intended (#201) --- active-rfcs/0037-router-return-guards.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0037-router-return-guards.md b/active-rfcs/0037-router-return-guards.md index 097271b6..de2db39b 100644 --- a/active-rfcs/0037-router-return-guards.md +++ b/active-rfcs/0037-router-return-guards.md @@ -76,7 +76,7 @@ router.beforeEach(async (to) => { ## Errors -Unexpected errors can still be thrown synchronously or synchronously: +Unexpected errors can still be thrown synchronously or asynchronously: ```js router.beforeEach((to) => { From bbe7d4e3b6a866da17215539e9fe5777405bf455 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 24 Aug 2020 17:26:45 -0400 Subject: [PATCH 14/49] [Amendment] add `created` custom element hook (#203) --- active-rfcs/0012-custom-directive-api-change.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/active-rfcs/0012-custom-directive-api-change.md b/active-rfcs/0012-custom-directive-api-change.md index e40fe633..08249bcf 100644 --- a/active-rfcs/0012-custom-directive-api-change.md +++ b/active-rfcs/0012-custom-directive-api-change.md @@ -46,12 +46,13 @@ Make custom directive hook names more consistent with the component lifecycle. Existing hooks are renamed to map better to the component lifecycle, with some timing adjustments. Arguments passed to the hooks remain unchanged. -- `bind` -> `beforeMount` -- `inserted` -> `mounted` -- `beforeUpdate` *new, called before the element itself is updated* +- **new** `created` (called before vnode props are applied to DOM node) +- `bind` -> `beforeMount` (called after vnode props have been applied to DOM node) +- `inserted` -> `mounted` (called after children have been inserted into the DOM node, and the DOM node itself has been inserted into parent element) +- **new** `beforeUpdate` (called before the element itself is updated) - ~~`update`~~ *removed, use `updated` instead* -- `componentUpdated` -> `updated` -- `beforeUnmount` *new* +- `componentUpdated` -> `updated` (called after the element itself and its children have been updated) +- **new** `beforeUnmount` - `unbind` -> `unmounted` ## Usage on Components From 36dda5b82b5e725d441cfcb45c9bef6284b6339c Mon Sep 17 00:00:00 2001 From: underfin <2218301630@qq.com> Date: Tue, 25 Aug 2020 05:30:12 +0800 Subject: [PATCH 15/49] Ammendment(transition): `appear-class` -> `appear-from-class` (#172) --- active-rfcs/0018-transition-class-change.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/active-rfcs/0018-transition-class-change.md b/active-rfcs/0018-transition-class-change.md index 23321314..6eddeda3 100644 --- a/active-rfcs/0018-transition-class-change.md +++ b/active-rfcs/0018-transition-class-change.md @@ -7,6 +7,7 @@ - Rename the `v-enter` transition class to `v-enter-from` - Rename the `v-leave` transition class to `v-leave-from` +- Rename the `v-appear` transition class to `v-appear-from` # Basic example @@ -52,9 +53,11 @@ The asymmetry and lack of explicitness in `.v-enter` and `.v-leave` makes these - `.v-enter` is renamed to `.v-enter-from` - `.v-leave` is renamed to `.v-leave-from` +- `.v-appear` is renamed to `.v-appear-from` - The `` component's related prop names are also changed: - `leave-class` is renamed to `leave-from-class` (in render functions or JSX, can be written as `leaveFromClass`) - `enter-class` is renamed to `enter-from-class` (in render functions or JSX, can be written as `enterFromClass`) + - `appear-class` is renamed to `appear-from-class` (in render functions or JSX, can be written as `appearFromClass`) # Adoption strategy From a954f2e0fcc28bc50c53cea34b71ce23db82cef5 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 17 Sep 2020 21:24:52 -0400 Subject: [PATCH 16/49] [scoped styles]: add shorthand for pseudo selectors --- active-rfcs/0023-scoped-styles-changes.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/active-rfcs/0023-scoped-styles-changes.md b/active-rfcs/0023-scoped-styles-changes.md index b6f2dd66..84594b30 100644 --- a/active-rfcs/0023-scoped-styles-changes.md +++ b/active-rfcs/0023-scoped-styles-changes.md @@ -13,12 +13,18 @@ Provide more consistent custom CSS extensions in Single File Component scoped st ``` @@ -38,17 +44,19 @@ To avoid the confusion of the dropped `/deep/` combinator, we introduced yet ano The previous versions of the deep combinator are still supported for compatibility reasons in the current [Vue 2 SFC compiler](https://github.com/vuejs/component-compiler-utils), which again, can be confusing to users. In v3, we are deprecating the support for `>>>` and `/deep/`. -As we were working on the new SFC compiler for v3, we noticed that CSS pseudo elements are in fact semantically NOT [combinators](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors/Combinators). It is more consistent with idiomatic CSS for pseudo elements to accept arguments instead, so we are also making `::v-deep()` work that way. The current usage of `::v-deep` as a combinator is still supported, however it is considered deprecated and will raise a warning. +As we were working on the new SFC compiler for v3, we noticed that CSS pseudo elements are in fact semantically NOT [combinators](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors/Combinators). It is more consistent with idiomatic CSS for pseudo elements to accept arguments instead, so we are also making `::v-deep()` work that way. If you don't care about the explicit `v-` prefix, you can also use the shorter `:deep()` variant, which works exactly the same. + +The current usage of `::v-deep` as a combinator is still supported, however it is considered deprecated and will raise a warning. ## Targeting / Avoiding Slot Content Currently, slot content passed in from the parent are affected by both the parent's scoped styles AND the child's scoped styles. There is no way to author rules that explicitly target slot content only, or ones that do not affect slot content. -In v3, we intend to make child scoped styles NOT affecting slot content by default. To explicitly target slot content, the `::v-slotted()` pseudo element can be used. +In v3, we intend to make child scoped styles NOT affecting slot content by default. To explicitly target slot content, the `::v-slotted()` (shorthand: `:slotted()`) pseudo element can be used. ## One-Off Global Rules -Currently to add a global CSS rule we need to use a separate unscoped ` +``` + +# Motivation + +Vue SFC styles provide straightforward CSS collocation and encapsulation, but it is purely static - which means up to this point we have no capability of dynamically updating the styles at runtime based on the component's state. + +Now with [most modern browsers supporting native CSS variables](https://caniuse.com/#feat=css-variables), we can leverage it to easily connect the component's state and styles. + +# Detailed design + +The ` +``` + +As expected, this would bind the `color` declaration's value to the `color` property of the component's state, reactively. + +The `v-bind` function can support arbitrary JavaScript expressions inside, but since JavaScript expressions may contain characters that are not valid in CSS identifiers, they will need to be wrapped in quotes most of the time: + +```css +.text { + font-size: v-bind('theme.font.size'); +} +``` + +When such CSS variables are detected, the SFC compiler will perform the following: + +1. Rewrite the `v-bind()` to a native `var()` with a hashed variable name. The above will be rewritten to: + + ```css + .text { + color: var(--6b53742-color); + font-size: var(--6b53742-theme_font_size); + } + ``` + + Note the hashing will be applied in all cases, regardless of whether `