Skip to content

Commit

Permalink
Merge pull request #178 from hotwired/query-param-visits
Browse files Browse the repository at this point in the history
New path properties presentation option for same-path visits with query strings
  • Loading branch information
jayohms authored Jul 21, 2021
2 parents 2050478 + a5cf43e commit 637234f
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 7 deletions.
9 changes: 9 additions & 0 deletions turbo/src/main/assets/json/test-configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@
"presentation": "clear_all"
}
},
{
"patterns": [
"/feature"
],
"properties": {
"query_string_presentation": "replace"
}
},
{
"patterns": [
"/new$",
Expand All @@ -31,6 +39,7 @@
"properties": {
"context": "modal",
"uri": "turbo://fragment/web/modal",
"query_string_presentation": "default",
"pull_to_refresh_enabled": false
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.net.Uri
import dev.hotwire.turbo.nav.TurboNavPresentation
import dev.hotwire.turbo.nav.TurboNavPresentationContext
import com.google.gson.annotations.SerializedName
import dev.hotwire.turbo.nav.TurboNavQueryStringPresentation
import java.net.URL

/**
Expand Down Expand Up @@ -110,6 +111,14 @@ val TurboPathConfigurationProperties.presentation: TurboNavPresentation
TurboNavPresentation.DEFAULT
}

val TurboPathConfigurationProperties.queryStringPresentation: TurboNavQueryStringPresentation
@SuppressLint("DefaultLocale") get() = try {
val value = get("query_string_presentation") ?: "default"
TurboNavQueryStringPresentation.valueOf(value.toUpperCase())
} catch (e: IllegalArgumentException) {
TurboNavQueryStringPresentation.DEFAULT
}

val TurboPathConfigurationProperties.context: TurboNavPresentationContext
@SuppressLint("DefaultLocale") get() = try {
val value = get("context") ?: "default"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package dev.hotwire.turbo.nav

/**
* Represents how a given navigation destination should be presented when the current
* location path on the backstack matches the new location path *and* a query string is
* present in either location.
*
* Example situation:
* current location: /feature
* new location: /feature?filter=true
*/
enum class TurboNavQueryStringPresentation {
/**
* A generic default value when no specific presentation value is provided and results in
* generally accepted "normal" behavior — replacing the root when on the start destination and
* going to the start destination again, popping when the location is in the immediate
* backstack, replacing when going to the same destination, and pushing in all other cases.
*/
DEFAULT,

/**
* Pops the current location off the nav stack and pushes the new location onto the nav stack.
* If you use query strings in your app to act as a way to filter results in a destination,
* this allows you to present the new (filtered) destination without adding onto the backstack.
*/
REPLACE
}
19 changes: 15 additions & 4 deletions turbo/src/main/kotlin/dev/hotwire/turbo/nav/TurboNavRule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ internal class TurboNavRule(
val newExtras = extras
val newProperties = pathConfiguration.properties(newLocation)
val newPresentationContext = newProperties.context
val newQueryStringPresentation = newProperties.queryStringPresentation
val newPresentation = newPresentation()
val newNavigationMode = newNavigationMode()
val newModalResult = newModalResult()
Expand All @@ -55,8 +56,8 @@ internal class TurboNavRule(
return newProperties.presentation
}

val locationIsCurrent = locationPathsAreEqual(newLocation, currentLocation)
val locationIsPrevious = locationPathsAreEqual(newLocation, previousLocation)
val locationIsCurrent = locationsAreSame(newLocation, currentLocation)
val locationIsPrevious = locationsAreSame(newLocation, previousLocation)
val replace = newVisitOptions.action == TurboVisitAction.REPLACE

return when {
Expand Down Expand Up @@ -132,11 +133,21 @@ internal class TurboNavRule(
private val NavBackStackEntry?.location: String?
get() = this?.arguments?.getString("location")

private fun locationPathsAreEqual(first: String?, second: String?): Boolean {
private fun locationsAreSame(first: String?, second: String?): Boolean {
if (first == null || second == null) {
return false
}

return Uri.parse(first).path == Uri.parse(second).path
val firstUri = Uri.parse(first)
val secondUri = Uri.parse(second)

return when (newQueryStringPresentation) {
TurboNavQueryStringPresentation.REPLACE -> {
firstUri.path == secondUri.path
}
TurboNavQueryStringPresentation.DEFAULT -> {
firstUri.path == secondUri.path && firstUri.query == secondUri.query
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class TurboPathConfigurationRepositoryTest : BaseRepositoryTest() {
assertThat(json).isNotNull()

val config = load(json)
assertThat(config?.rules?.size).isEqualTo(7)
assertThat(config?.rules?.size).isEqualTo(8)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class TurboPathConfigurationTest : BaseRepositoryTest() {

@Test
fun assetConfigurationIsLoaded() {
assertThat(pathConfiguration.rules.size).isEqualTo(7)
assertThat(pathConfiguration.rules.size).isEqualTo(8)
}

@Test
Expand Down
59 changes: 58 additions & 1 deletion turbo/src/test/kotlin/dev/hotwire/turbo/nav/TurboNavRuleTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class TurboNavRuleTest {
private val resumeUrl = "https://hotwired.dev/custom/resume"
private val modalRootUrl = "https://hotwired.dev/custom/modal"
private val filterUrl = "https://hotwired.dev/feature?filter=true"
private val customUrl = "https://hotwired.dev/custom"
private val customQueryUrl = "https://hotwired.dev/custom?id=1"

private val webDestinationId = 1
private val webModalDestinationId = 2
Expand Down Expand Up @@ -80,6 +82,7 @@ class TurboNavRuleTest {
assertThat(rule.newLocation).isEqualTo(featureUrl)
assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT)
assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.PUSH)
assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.REPLACE)
assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.IN_CONTEXT)
assertThat(rule.newModalResult).isNull()
assertThat(rule.newDestinationUri).isEqualTo(webUri)
Expand All @@ -101,6 +104,7 @@ class TurboNavRuleTest {
assertThat(rule.newLocation).isEqualTo(newUrl)
assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.MODAL)
assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.PUSH)
assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT)
assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.TO_MODAL)
assertThat(rule.newModalResult).isNull()
assertThat(rule.newDestinationUri).isEqualTo(webModalUri)
Expand Down Expand Up @@ -130,6 +134,7 @@ class TurboNavRuleTest {
assertThat(rule.newLocation).isEqualTo(homeUrl)
assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT)
assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.CLEAR_ALL)
assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT)
assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.IN_CONTEXT)
assertThat(rule.newModalResult).isNull()
assertThat(rule.newDestinationUri).isEqualTo(webHomeUri)
Expand All @@ -153,6 +158,7 @@ class TurboNavRuleTest {
assertThat(rule.newLocation).isEqualTo(featureUrl)
assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT)
assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.POP)
assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.REPLACE)
assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.DISMISS_MODAL)
assertThat(rule.newModalResult?.location).isEqualTo(featureUrl)
assertThat(rule.newDestinationUri).isEqualTo(webUri)
Expand All @@ -175,6 +181,7 @@ class TurboNavRuleTest {
assertThat(rule.newLocation).isEqualTo(newUrl)
assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.MODAL)
assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.REPLACE)
assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT)
assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.IN_CONTEXT)
assertThat(rule.newModalResult).isNull()
assertThat(rule.newDestinationUri).isEqualTo(webModalUri)
Expand All @@ -197,6 +204,7 @@ class TurboNavRuleTest {
assertThat(rule.newLocation).isEqualTo(editUrl)
assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.MODAL)
assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.PUSH)
assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT)
assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.IN_CONTEXT)
assertThat(rule.newModalResult).isNull()
assertThat(rule.newDestinationUri).isEqualTo(webModalUri)
Expand All @@ -219,6 +227,7 @@ class TurboNavRuleTest {
assertThat(rule.newLocation).isEqualTo(refreshUrl)
assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT)
assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.REFRESH)
assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT)
assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.REFRESH)
assertThat(rule.newModalResult).isNull()
assertThat(rule.newDestinationUri).isEqualTo(webUri)
Expand All @@ -242,6 +251,7 @@ class TurboNavRuleTest {
assertThat(rule.newLocation).isEqualTo(resumeUrl)
assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT)
assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.NONE)
assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT)
assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.DISMISS_MODAL)
assertThat(rule.newModalResult).isNotNull()
assertThat(rule.newModalResult?.location).isEqualTo(resumeUrl)
Expand All @@ -252,7 +262,53 @@ class TurboNavRuleTest {
}

@Test
fun `navigate to the same path with query params`() {
fun `navigate to the same path with new query string`() {
controller.navigate(webDestinationId, locationArgs(customUrl))
val rule = getNavigatorRule(customQueryUrl)

// Current destination
assertThat(rule.previousLocation).isEqualTo(homeUrl)
assertThat(rule.currentLocation).isEqualTo(customUrl)
assertThat(rule.currentPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT)
assertThat(rule.isAtStartDestination).isFalse()

// New destination
assertThat(rule.newLocation).isEqualTo(customQueryUrl)
assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT)
assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.PUSH)
assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT)
assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.IN_CONTEXT)
assertThat(rule.newModalResult).isNull()
assertThat(rule.newDestinationUri).isEqualTo(webUri)
assertThat(rule.newDestination).isNotNull()
assertThat(rule.newNavOptions).isEqualTo(navOptions)
}

@Test
fun `navigate to the same path with same query string`() {
controller.navigate(webDestinationId, locationArgs(customQueryUrl))
val rule = getNavigatorRule(customQueryUrl)

// Current destination
assertThat(rule.previousLocation).isEqualTo(homeUrl)
assertThat(rule.currentLocation).isEqualTo(customQueryUrl)
assertThat(rule.currentPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT)
assertThat(rule.isAtStartDestination).isFalse()

// New destination
assertThat(rule.newLocation).isEqualTo(customQueryUrl)
assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT)
assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.REPLACE)
assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT)
assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.IN_CONTEXT)
assertThat(rule.newModalResult).isNull()
assertThat(rule.newDestinationUri).isEqualTo(webUri)
assertThat(rule.newDestination).isNotNull()
assertThat(rule.newNavOptions).isEqualTo(navOptions)
}

@Test
fun `navigate to the same path with filterable query string`() {
controller.navigate(webDestinationId, locationArgs(featureUrl))
val rule = getNavigatorRule(filterUrl)

Expand All @@ -266,6 +322,7 @@ class TurboNavRuleTest {
assertThat(rule.newLocation).isEqualTo(filterUrl)
assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT)
assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.REPLACE)
assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.REPLACE)
assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.IN_CONTEXT)
assertThat(rule.newModalResult).isNull()
assertThat(rule.newDestinationUri).isEqualTo(webUri)
Expand Down

0 comments on commit 637234f

Please sign in to comment.