Query:
diff --git a/packages/alpinejs/src/alpine.js b/packages/alpinejs/src/alpine.js
index 09c20106d..e4d386b2f 100644
--- a/packages/alpinejs/src/alpine.js
+++ b/packages/alpinejs/src/alpine.js
@@ -1,7 +1,7 @@
import { setReactivityEngine, disableEffectScheduling, reactive, effect, release, raw } from './reactivity'
import { mapAttributes, directive, setPrefix as prefix, prefix as prefixed } from './directives'
import { start, addRootSelector, addInitSelector, closestRoot, findClosest, initTree, destroyTree, interceptInit } from './lifecycle'
-import { mutateDom, deferMutations, flushAndStopDeferringMutations, startObservingMutations, stopObservingMutations } from './mutation'
+import { onElRemoved, onAttributeRemoved, mutateDom, deferMutations, flushAndStopDeferringMutations, startObservingMutations, stopObservingMutations } from './mutation'
import { mergeProxies, closestDataStack, addScopeToNode, scope as $data } from './scope'
import { setEvaluator, evaluate, evaluateLater, dontAutoEvaluateFunctions } from './evaluator'
import { transition } from './directives/x-transition'
@@ -11,6 +11,7 @@ import { getBinding as bound, extractProp } from './utils/bind'
import { debounce } from './utils/debounce'
import { throttle } from './utils/throttle'
import { setStyles } from './utils/styles'
+import { entangle } from './entangle'
import { nextTick } from './nextTick'
import { walk } from './utils/walk'
import { plugin } from './plugin'
@@ -31,6 +32,7 @@ let Alpine = {
startObservingMutations,
stopObservingMutations,
setReactivityEngine,
+ onAttributeRemoved,
closestDataStack,
skipDuringClone,
onlyDuringClone,
@@ -45,6 +47,7 @@ let Alpine = {
mergeProxies,
extractProp,
findClosest,
+ onElRemoved,
closestRoot,
destroyTree,
interceptor, // INTERNAL: not public API and is subject to change without major release.
@@ -52,6 +55,7 @@ let Alpine = {
setStyles, // INTERNAL
mutateDom,
directive,
+ entangle,
throttle,
debounce,
evaluate,
diff --git a/packages/alpinejs/src/directives/x-model.js b/packages/alpinejs/src/directives/x-model.js
index 0e4e1fccc..f17e55e9b 100644
--- a/packages/alpinejs/src/directives/x-model.js
+++ b/packages/alpinejs/src/directives/x-model.js
@@ -106,8 +106,6 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
}
el._x_forceModelUpdate = (value) => {
- value = value === undefined ? getValue() : value
-
// If nested model key is undefined, set the default value to empty string.
if (value === undefined && typeof expression === 'string' && expression.match(/\./)) value = ''
diff --git a/packages/alpinejs/src/directives/x-on.js b/packages/alpinejs/src/directives/x-on.js
index cd997a339..210e2f17b 100644
--- a/packages/alpinejs/src/directives/x-on.js
+++ b/packages/alpinejs/src/directives/x-on.js
@@ -7,7 +7,7 @@ mapAttributes(startingWith('@', into(prefix('on:'))))
directive('on', skipDuringClone((el, { value, modifiers, expression }, { cleanup }) => {
let evaluate = expression ? evaluateLater(el, expression) : () => {}
-
+
// Forward event listeners on portals.
if (el.tagName.toLowerCase() === 'template') {
if (! el._x_forwardEvents) el._x_forwardEvents = []
diff --git a/packages/alpinejs/src/entangle.js b/packages/alpinejs/src/entangle.js
index 4d0661a34..f0621a486 100644
--- a/packages/alpinejs/src/entangle.js
+++ b/packages/alpinejs/src/entangle.js
@@ -9,7 +9,7 @@ export function entangle({ get: outerGet, set: outerSet }, { get: innerGet, set:
if (firstRun) {
outer = outerGet()
- innerSet(outer)
+ innerSet(JSON.parse(JSON.stringify(outer))) // We need to break internal references using parse/stringify...
inner = innerGet()
firstRun = false
} else {
@@ -24,7 +24,7 @@ export function entangle({ get: outerGet, set: outerSet }, { get: innerGet, set:
innerSet(outer)
inner = outer // Assign inner to outer so that it can be serialized for diffing...
} else { // If inner changed...
- outerSet(inner)
+ outerSet(JSON.parse(JSON.stringify(inner))) // We need to break internal references using parse/stringify...
outer = inner // Assign outer to inner so that it can be serialized for diffing...
}
}
diff --git a/packages/alpinejs/src/evaluator.js b/packages/alpinejs/src/evaluator.js
index 74e52e669..c123810b2 100644
--- a/packages/alpinejs/src/evaluator.js
+++ b/packages/alpinejs/src/evaluator.js
@@ -70,9 +70,9 @@ function generateFunctionFromString(expression, el) {
// calling function so that we don't throw an error AND a "return" statement can b e used.
let rightSideSafeExpression = 0
// Support expressions starting with "if" statements like: "if (...) doSomething()"
- || /^[\n\s]*if.*\(.*\)/.test(expression)
+ || /^[\n\s]*if.*\(.*\)/.test(expression.trim())
// Support expressions starting with "let/const" like: "let foo = 'bar'"
- || /^(let|const)\s/.test(expression)
+ || /^(let|const)\s/.test(expression.trim())
? `(async()=>{ ${expression} })()`
: expression
diff --git a/packages/alpinejs/src/lifecycle.js b/packages/alpinejs/src/lifecycle.js
index 6a62a61ab..cd9e59734 100644
--- a/packages/alpinejs/src/lifecycle.js
+++ b/packages/alpinejs/src/lifecycle.js
@@ -3,6 +3,7 @@ import { deferHandlingDirectives, directives } from "./directives"
import { dispatch } from './utils/dispatch'
import { walk } from "./utils/walk"
import { warn } from './utils/warn'
+import Alpine from "./alpine"
let started = false
diff --git a/packages/alpinejs/src/utils/bind.js b/packages/alpinejs/src/utils/bind.js
index a857568ca..87722bc44 100644
--- a/packages/alpinejs/src/utils/bind.js
+++ b/packages/alpinejs/src/utils/bind.js
@@ -57,7 +57,7 @@ function bindInputValue(el, value) {
// automatically.
if (Number.isInteger(value)) {
el.value = value
- } else if (! Number.isInteger(value) && ! Array.isArray(value) && typeof value !== 'boolean' && ! [null, undefined].includes(value)) {
+ } else if (! Array.isArray(value) && typeof value !== 'boolean' && ! [null, undefined].includes(value)) {
el.value = String(value)
} else {
if (Array.isArray(value)) {
@@ -71,7 +71,7 @@ function bindInputValue(el, value) {
} else {
if (el.value === value) return
- el.value = value
+ el.value = value === undefined ? '' : value
}
}
diff --git a/packages/history/builds/module.js b/packages/history/builds/module.js
index a7d9aba46..122f73ea5 100644
--- a/packages/history/builds/module.js
+++ b/packages/history/builds/module.js
@@ -1,3 +1,5 @@
import history from '../src/index.js'
+import { track } from '../src/index.js'
export default history
+export { track }
diff --git a/packages/history/src/index.js b/packages/history/src/index.js
index f230d809a..23d35ba57 100644
--- a/packages/history/src/index.js
+++ b/packages/history/src/index.js
@@ -1,62 +1,105 @@
+
export default function history(Alpine) {
Alpine.magic('queryString', (el, { interceptor }) => {
let alias
+ let alwaysShow = false
+ let usePush = false
- return interceptor((initialValue, getter, setter, path, key) => {
- let pause = false
+ return interceptor((initialSeedValue, getter, setter, path, key) => {
let queryKey = alias || path
- let value = initialValue
- let url = new URL(window.location.href)
+ let { initial, replace, push, pop } = track(queryKey, initialSeedValue, alwaysShow)
+
+ setter(initial)
+
+ if (! usePush) {
+ Alpine.effect(() => replace(getter()))
+ } else {
+ Alpine.effect(() => push(getter()))
+
+ pop(async newValue => {
+ setter(newValue)
+
+ let tillTheEndOfTheMicrotaskQueue = () => Promise.resolve()
- if (url.searchParams.has(queryKey)) {
- value = url.searchParams.get(queryKey)
+ await tillTheEndOfTheMicrotaskQueue() // ...so that we preserve the internal lock...
+ })
}
- setter(value)
+ return initial
+ }, func => {
+ func.alwaysShow = () => { alwaysShow = true; return func }
+ func.usePush = () => { usePush = true; return func }
+ func.as = key => { alias = key; return func }
+ })
+ })
- let object = { value }
+ Alpine.history = { track }
+}
- url.searchParams.set(queryKey, value)
+export function track(name, initialSeedValue, alwaysShow = false) {
+ let { has, get, set, remove } = queryStringUtils()
- replace(url.toString(), path, object)
+ let url = new URL(window.location.href)
+ let isInitiallyPresentInUrl = has(url, name)
+ let initialValue = isInitiallyPresentInUrl ? get(url, name) : initialSeedValue
+ let initialValueMemo = JSON.stringify(initialValue)
+ let hasReturnedToInitialValue = (newValue) => JSON.stringify(newValue) === initialValueMemo
- window.addEventListener('popstate', (e) => {
- if (! e.state) return
- if (! e.state.alpine) return
+ if (alwaysShow) url = set(url, name, initialValue)
- Object.entries(e.state.alpine).forEach(([newKey, { value }]) => {
- if (newKey !== key) return
+ replace(url, name, { value: initialValue })
- pause = true
+ let lock = false
- Alpine.disableEffectScheduling(() => {
- setter(value)
- })
+ let update = (strategy, newValue) => {
+ if (lock) return
- pause = false
- })
- })
+ let url = new URL(window.location.href)
- Alpine.effect(() => {
- let value = getter()
+ if (! alwaysShow && ! isInitiallyPresentInUrl && hasReturnedToInitialValue(newValue)) {
+ url = remove(url, name)
+ } else {
+ url = set(url, name, newValue)
+ }
- if (pause) return
+ strategy(url, name, { value: newValue})
+ }
- let object = { value }
+ return {
+ initial: initialValue,
- let url = new URL(window.location.href)
+ replace(newValue) { // Update via replaceState...
+ update(replace, newValue)
+ },
- url.searchParams.set(queryKey, value)
+ push(newValue) { // Update via pushState...
+ update(push, newValue)
+ },
- push(url.toString(), path, object)
- })
+ pop(receiver) { // "popstate" handler...
+ window.addEventListener('popstate', (e) => {
+ if (! e.state || ! e.state.alpine) return
- return value
- }, func => {
- func.as = key => { alias = key; return func }
- })
- })
+ Object.entries(e.state.alpine).forEach(([iName, { value: newValue }]) => {
+ if (iName !== name) return
+
+ lock = true
+
+ // Allow the "receiver" to be an async function in case a non-syncronous
+ // operation (like an ajax) requests needs to happen while preserving
+ // the "locking" mechanism ("lock = true" in this case)...
+ let result = receiver(newValue)
+
+ if (result instanceof Promise) {
+ result.finally(() => lock = false)
+ } else {
+ lock = false
+ }
+ })
+ })
+ }
+ }
}
function replace(url, key, object) {
@@ -64,13 +107,127 @@ function replace(url, key, object) {
if (! state.alpine) state.alpine = {}
- state.alpine[key] = object
+ state.alpine[key] = unwrap(object)
- window.history.replaceState(state, '', url)
+ window.history.replaceState(state, '', url.toString())
}
function push(url, key, object) {
- let state = { alpine: {...window.history.state.alpine, ...{[key]: object}} }
+ let state = { alpine: {...window.history.state.alpine, ...{[key]: unwrap(object)}} }
+
+ window.history.pushState(state, '', url.toString())
+}
+
+function unwrap(object) {
+ return JSON.parse(JSON.stringify(object))
+}
+
+function queryStringUtils() {
+ return {
+ has(url, key) {
+ let search = url.search
+
+ if (! search) return false
+
+ let data = fromQueryString(search)
+
+ return Object.keys(data).includes(key)
+ },
+ get(url, key) {
+ let search = url.search
- window.history.pushState(state, '', url)
+ if (! search) return false
+
+ let data = fromQueryString(search)
+
+ return data[key]
+ },
+ set(url, key, value) {
+ let data = fromQueryString(url.search)
+
+ data[key] = value
+
+ url.search = toQueryString(data)
+
+ return url
+ },
+ remove(url, key) {
+ let data = fromQueryString(url.search)
+
+ delete data[key]
+
+ url.search = toQueryString(data)
+
+ return url
+ },
+ }
}
+
+// This function converts JavaScript data to bracketed query string notation...
+// { items: [['foo']] } -> "items[0][0]=foo"
+function toQueryString(data) {
+ let isObjecty = (subject) => typeof subject === 'object' && subject !== null
+
+ let buildQueryStringEntries = (data, entries = {}, baseKey = '') => {
+ Object.entries(data).forEach(([iKey, iValue]) => {
+ let key = baseKey === '' ? iKey : `${baseKey}[${iKey}]`
+
+ if (! isObjecty(iValue)) {
+ entries[key] = encodeURIComponent(iValue)
+ .replaceAll('%20', '+') // Conform to RFC1738
+ } else {
+ entries = {...entries, ...buildQueryStringEntries(iValue, entries, key)}
+ }
+ })
+
+ return entries
+ }
+
+ let entries = buildQueryStringEntries(data)
+
+
+ return Object.entries(entries).map(([key, value]) => `${key}=${value}`).join('&')
+}
+
+// This function converts bracketed query string notation back to JS data...
+// "items[0][0]=foo" -> { items: [['foo']] }
+function fromQueryString(search) {
+ search = search.replace('?', '')
+
+ if (search === '') return {}
+
+ let insertDotNotatedValueIntoData = (key, value, data) => {
+ let [first, second, ...rest] = key.split('.')
+
+ // We're at a leaf node, let's make the assigment...
+ if (! second) return data[key] = value
+
+ // This is where we fill in empty arrays/objects allong the way to the assigment...
+ if (data[first] === undefined) {
+ data[first] = isNaN(second) ? {} : []
+ }
+
+ // Keep deferring assignment until the full key is built up...
+ insertDotNotatedValueIntoData([second, ...rest].join('.'), value, data[first])
+ }
+
+ let entries = search.split('&').map(i => i.split('='))
+
+ let data = {}
+
+ entries.forEach(([key, value]) => {
+ value = decodeURIComponent(value.replaceAll('+', '%20'))
+
+ if (! key.includes('[')) {
+ data[key] = value
+ } else {
+ // Convert to dot notation because it's easier...
+ let dotNotatedKey = key.replaceAll('[', '.').replaceAll(']', '')
+
+ insertDotNotatedValueIntoData(dotNotatedKey, value, data)
+ }
+ })
+
+ return data
+}
+
diff --git a/packages/morph/src/morph.js b/packages/morph/src/morph.js
index 6ab20b504..3514b9ec4 100644
--- a/packages/morph/src/morph.js
+++ b/packages/morph/src/morph.js
@@ -101,6 +101,8 @@ export function morph(from, toHtml, options) {
}
function patchAttributes(from, to) {
+ if (from._x_transitioning) return
+
if (from._x_isShown && ! to._x_isShown) {
return
}
diff --git a/packages/navigate/src/history.js b/packages/navigate/src/history.js
index cd230682a..f294c06e9 100644
--- a/packages/navigate/src/history.js
+++ b/packages/navigate/src/history.js
@@ -9,7 +9,13 @@ export function updateCurrentPageHtmlInHistoryStateForLaterBackButtonClicks() {
export function whenTheBackOrForwardButtonIsClicked(callback) {
window.addEventListener('popstate', e => {
- let { html } = fromSessionStorage(e)
+ let state = e.state || {}
+
+ let alpine = state.alpine || {}
+
+ if (! alpine._html) return
+
+ let html = fromSessionStorage(alpine._html)
callback(html)
})
@@ -30,18 +36,20 @@ export function replaceUrl(url, html) {
function updateUrl(method, url, html) {
let key = (new Date).getTime()
- tryToStoreInSession(key, JSON.stringify({ html: html }))
+ tryToStoreInSession(key, html)
- let state = Object.assign(history.state || {}, { alpine: key })
+ let state = history.state || {}
+
+ if (! state.alpine) state.alpine = {}
+
+ state.alpine._html = key
// 640k character limit:
history[method](state, document.title, url)
}
-export function fromSessionStorage(event) {
- if (! event.state.alpine) return {}
-
- let state = JSON.parse(sessionStorage.getItem('alpine:'+event.state.alpine))
+export function fromSessionStorage(timestamp) {
+ let state = JSON.parse(sessionStorage.getItem('alpine:'+timestamp))
return state
}
@@ -52,7 +60,7 @@ function tryToStoreInSession(timestamp, value) {
// (oldest first), until there's enough space to store
// the new one.
try {
- sessionStorage.setItem('alpine:'+timestamp, value)
+ sessionStorage.setItem('alpine:'+timestamp, JSON.stringify(value))
} catch (error) {
// 22 is Chrome, 1-14 is other browsers.
if (! [22, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14].includes(error.code)) return
diff --git a/packages/navigate/src/index.js b/packages/navigate/src/index.js
index cc120ad0a..8f65df72d 100644
--- a/packages/navigate/src/index.js
+++ b/packages/navigate/src/index.js
@@ -1,36 +1,56 @@
-import { transition } from "alpinejs/src/directives/x-transition"
-import { finishAndHideProgressBar, showAndStartProgressBar } from "./bar"
-import { fetchHtml } from "./fetch"
import { updateCurrentPageHtmlInHistoryStateForLaterBackButtonClicks, updateUrlAndStoreLatestHtmlForFutureBackButtons, whenTheBackOrForwardButtonIsClicked } from "./history"
-import { extractDestinationFromLink, hijackNewLinksOnThePage, whenALinkIsClicked, whenALinkIsHovered } from "./links"
-import { swapCurrentPageWithNewHtml } from "./page"
-import { putPersistantElementsBack, storePersistantElementsForLater } from "./persist"
import { getPretchedHtmlOr, prefetchHtml, storeThePrefetchedHtmlForWhenALinkIsClicked } from "./prefetch"
+import { createUrlObjectFromString, extractDestinationFromLink, whenThisLinkIsHoveredFor, whenThisLinkIsPressed } from "./links"
import { restoreScrollPosition, storeScrollInformationInHtmlBeforeNavigatingAway } from "./scroll"
+import { putPersistantElementsBack, storePersistantElementsForLater } from "./persist"
+import { finishAndHideProgressBar, showAndStartProgressBar } from "./bar"
+import { transition } from "alpinejs/src/directives/x-transition"
+import { swapCurrentPageWithNewHtml } from "./page"
+import { fetchHtml } from "./fetch"
+import { prefix } from "alpinejs/src/directives"
-let enablePrefetch = true
let enablePersist = true
let showProgressBar = true
let restoreScroll = true
let autofocus = false
export default function (Alpine) {
- updateCurrentPageHtmlInHistoryStateForLaterBackButtonClicks()
+ Alpine.navigate = (url) => {
+ navigateTo(
+ createUrlObjectFromString(url)
+ )
+ }
+
+ Alpine.addInitSelector(() => `[${prefix('navigate')}]`)
+
+ Alpine.directive('navigate', (el, { value, expression, modifiers }, { evaluateLater, cleanup }) => {
+ let shouldPrefetchOnHover = modifiers.includes('hover')
+
+ shouldPrefetchOnHover && whenThisLinkIsHoveredFor(el, 60, () => {
+ let destination = extractDestinationFromLink(el)
- enablePrefetch && whenALinkIsHovered((el) => {
- let forDestination = extractDestinationFromLink(el)
+ prefetchHtml(destination, html => {
+ storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination)
+ })
+ })
- prefetchHtml(forDestination, html => {
- storeThePrefetchedHtmlForWhenALinkIsClicked(html, forDestination)
+ whenThisLinkIsPressed(el, (whenItIsReleased) => {
+ let destination = extractDestinationFromLink(el)
+
+ prefetchHtml(destination, html => {
+ storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination)
+ })
+
+ whenItIsReleased(() => {
+ navigateTo(destination)
+ })
})
})
- whenALinkIsClicked((el) => {
+ function navigateTo(destination) {
showProgressBar && showAndStartProgressBar()
- let fromDestination = extractDestinationFromLink(el)
-
- fetchHtmlOrUsePrefetchedHtml(fromDestination, html => {
+ fetchHtmlOrUsePrefetchedHtml(destination, html => {
restoreScroll && storeScrollInformationInHtmlBeforeNavigatingAway()
showProgressBar && finishAndHideProgressBar()
@@ -43,15 +63,11 @@ export default function (Alpine) {
swapCurrentPageWithNewHtml(html, () => {
enablePersist && putPersistantElementsBack()
- // Added setTimeout here to detect a currently hovered prefetch link...
- // (hack for laracon)
- setTimeout(() => hijackNewLinksOnThePage())
-
restoreScroll && restoreScrollPosition()
fireEventForOtherLibariesToHookInto()
- updateUrlAndStoreLatestHtmlForFutureBackButtons(html, fromDestination)
+ updateUrlAndStoreLatestHtmlForFutureBackButtons(html, destination)
andAfterAllThis(() => {
autofocus && autofocusElementsWithTheAutofocusAttribute()
@@ -59,10 +75,9 @@ export default function (Alpine) {
nowInitializeAlpineOnTheNewPage(Alpine)
})
})
-
})
})
- })
+ }
whenTheBackOrForwardButtonIsClicked((html) => {
// @todo: see if there's a way to update the current HTML BEFORE
@@ -76,8 +91,6 @@ export default function (Alpine) {
swapCurrentPageWithNewHtml(html, andThen => {
enablePersist && putPersistantElementsBack()
- hijackNewLinksOnThePage()
-
restoreScroll && restoreScrollPosition()
fireEventForOtherLibariesToHookInto()
@@ -91,6 +104,12 @@ export default function (Alpine) {
})
})
+
+ // Because DOMContentLoaded is fired on first load,
+ // we should fire alpine:navigated as a replacement as well...
+ setTimeout(() => {
+ fireEventForOtherLibariesToHookInto()
+ })
}
function fetchHtmlOrUsePrefetchedHtml(fromDestination, callback) {
diff --git a/packages/navigate/src/links.js b/packages/navigate/src/links.js
index 1273a2443..6322fcf76 100644
--- a/packages/navigate/src/links.js
+++ b/packages/navigate/src/links.js
@@ -1,51 +1,52 @@
-let handleLinkClick = () => {}
-let handleLinkHover = () => {}
+export function whenThisLinkIsClicked(el, callback) {
+ el.addEventListener('click', e => {
+ e.preventDefault()
-export function whenALinkIsClicked(callback) {
- handleLinkClick = callback
-
- initializeLinksForClicking()
+ callback(el)
+ })
}
-export function whenALinkIsHovered(callback) {
- handleLinkHover = callback
+export function whenThisLinkIsPressed(el, callback) {
+ el.addEventListener('click', e => e.preventDefault())
- initializeLinksForHovering()
-}
+ el.addEventListener('mousedown', e => {
+ e.preventDefault()
-export function extractDestinationFromLink(linkEl) {
- return new URL(linkEl.getAttribute('href'), document.baseURI)
-}
+ callback((whenReleased) => {
+ let handler = e => {
+ e.preventDefault()
-export function hijackNewLinksOnThePage() {
- initializeLinksForClicking()
- initializeLinksForHovering()
-}
+ whenReleased()
-function initializeLinksForClicking() {
- getLinks().forEach(el => {
- el.addEventListener('click', e => {
- e.preventDefault()
+ el.removeEventListener('mouseup', handler)
+ }
- handleLinkClick(el)
+ el.addEventListener('mouseup', handler)
})
})
}
-function initializeLinksForHovering() {
- getLinks()
- .filter(i => i.hasAttribute('wire:navigate.prefetch'))
- .forEach(el => {
- el.addEventListener('mouseenter', e => {
- handleLinkHover(el)
- })
- })
+export function whenThisLinkIsHoveredFor(el, ms = 60, callback) {
+ el.addEventListener('mouseenter', e => {
+ let timeout = setTimeout(() => {
+
+ }, ms)
+
+ let handler = () => {
+ clear
+ el.removeEventListener('mouseleave', handler)
+ }
+
+ el.addEventListener('mouseleave', handler)
+ callback(e)
+ })
}
-function getLinks() {
- return Array.from(document.links)
- .filter(i => i.hasAttribute('wire:navigate')
- || i.hasAttribute('wire:navigate.prefetch'))
+export function extractDestinationFromLink(linkEl) {
+ return createUrlObjectFromString(linkEl.getAttribute('href'))
}
+export function createUrlObjectFromString(urlString) {
+ return new URL(urlString, document.baseURI)
+}
diff --git a/packages/navigate/src/page.js b/packages/navigate/src/page.js
index 8c68156ee..ec41265d1 100644
--- a/packages/navigate/src/page.js
+++ b/packages/navigate/src/page.js
@@ -6,6 +6,9 @@ export function swapCurrentPageWithNewHtml(html, andThen) {
let newHead = document.adoptNode(newDocument.head)
mergeNewHead(newHead)
+
+ // mergeNewHead(newHead)
+
prepNewScriptTagsToRun(newBody)
transitionOut(document.body)
@@ -42,7 +45,7 @@ function transitionIn(body) {
function prepNewScriptTagsToRun(newBody) {
newBody.querySelectorAll('script').forEach(i => {
- if (i.hasAttribute('x-navigate:ignore')) return
+ if (i.hasAttribute('data-navigate-once')) return
i.replaceWith(cloneScriptTag(i))
})
@@ -54,9 +57,15 @@ function mergeNewHead(newHead) {
// Only add scripts and styles that aren't already loaded on the page.
let garbageCollector = document.createDocumentFragment()
- for (child of Array.from(newHead.children)) {
+ for (let child of Array.from(newHead.children)) {
if (isAsset(child)) {
if (! headChildrenHtmlLookup.includes(child.outerHTML)) {
+ if (isTracked(child)) {
+ setTimeout(() => window.location.reload())
+
+ return
+ }
+
if (isScript(child)) {
document.head.appendChild(cloneScriptTag(child))
} else {
@@ -71,12 +80,12 @@ function mergeNewHead(newHead) {
// How to free up the garbage collector?
// Remove existing non-asset elements like meta, base, title, template.
- for (child of Array.from(document.head.children)) {
+ for (let child of Array.from(document.head.children)) {
if (! isAsset(child)) child.remove()
}
// Add new non-asset elements left over in the new head element.
- for (child of Array.from(newHead.children)) {
+ for (let child of Array.from(newHead.children)) {
document.head.appendChild(child)
}
}
@@ -94,13 +103,17 @@ function cloneScriptTag(el) {
return script
}
-function isAsset (el) {
+function isTracked(el) {
+ return el.hasAttribute('data-navigate-track')
+}
+
+function isAsset(el) {
return (el.tagName.toLowerCase() === 'link' && el.getAttribute('rel').toLowerCase() === 'stylesheet')
|| el.tagName.toLowerCase() === 'style'
|| el.tagName.toLowerCase() === 'script'
}
-function isScript (el) {
+function isScript(el) {
return el.tagName.toLowerCase() === 'script'
}
diff --git a/packages/navigate/src/persist.js b/packages/navigate/src/persist.js
index 79bb0d4b3..e56d99fa3 100644
--- a/packages/navigate/src/persist.js
+++ b/packages/navigate/src/persist.js
@@ -5,8 +5,8 @@ let els = {}
export function storePersistantElementsForLater() {
els = {}
- document.querySelectorAll('[x-navigate\\:persist]').forEach(i => {
- els[i.getAttribute('x-navigate:persist')] = i
+ document.querySelectorAll('[x-persist]').forEach(i => {
+ els[i.getAttribute('x-persist')] = i
Alpine.mutateDom(() => {
i.remove()
@@ -15,8 +15,8 @@ export function storePersistantElementsForLater() {
}
export function putPersistantElementsBack() {
- document.querySelectorAll('[x-navigate\\:persist]').forEach(i => {
- let old = els[i.getAttribute('x-navigate:persist')]
+ document.querySelectorAll('[x-persist]').forEach(i => {
+ let old = els[i.getAttribute('x-persist')]
old._x_wasPersisted = true
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 0d73a701a..1f7afe09e 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -13,10 +13,5 @@
"main": "dist/module.cjs.js",
"module": "dist/module.esm.js",
"unpkg": "dist/cdn.min.js",
- "devDependencies": {
- "alpinejs": "file:../alpinejs"
- },
- "peerDependencies": {
- "alpinejs": "^3.10.0"
- }
+ "devDependencies": {}
}
diff --git a/packages/ui/src/combobox.js b/packages/ui/src/combobox.js
index 595d851af..b0ac15b72 100644
--- a/packages/ui/src/combobox.js
+++ b/packages/ui/src/combobox.js
@@ -96,7 +96,7 @@ function handleRoot(el, Alpine) {
this.__nullable = Alpine.extractProp(el, 'nullable', false)
this.__compareBy = Alpine.extractProp(el, 'by')
- this.__context = generateContext(this.__isMultiple, 'vertical', () => this.__activateSelectedOrFirst())
+ this.__context = generateContext(Alpine, this.__isMultiple, 'vertical', () => this.__activateSelectedOrFirst())
let defaultValue = Alpine.extractProp(el, 'default-value', this.__isMultiple ? [] : null)
@@ -109,7 +109,7 @@ function handleRoot(el, Alpine) {
Alpine.effect(() => {
// Everytime the value changes, we need to re-render the hidden inputs,
// if a user passed the "name" prop...
- this.__inputName && renderHiddenInputs(this.$el, this.__inputName, this.__value)
+ this.__inputName && renderHiddenInputs(Alpine, this.$el, this.__inputName, this.__value)
})
})
},
diff --git a/packages/ui/src/list-context.js b/packages/ui/src/list-context.js
index 46f0ba464..fd51f8450 100644
--- a/packages/ui/src/list-context.js
+++ b/packages/ui/src/list-context.js
@@ -1,6 +1,5 @@
-import Alpine from "alpinejs";
-export function generateContext(multiple, orientation, activateSelectedOrFirst) {
+export function generateContext(Alpine, multiple, orientation, activateSelectedOrFirst) {
return {
/**
* Main state...
@@ -326,7 +325,7 @@ function keyByValue(object, value) {
return Object.keys(object).find(key => object[key] === value)
}
-export function renderHiddenInputs(el, name, value) {
+export function renderHiddenInputs(Alpine, el, name, value) {
// Create input elements...
let newInputs = generateInputs(name, value)
diff --git a/packages/ui/src/listbox.js b/packages/ui/src/listbox.js
index 24e20fc52..72f099cd9 100644
--- a/packages/ui/src/listbox.js
+++ b/packages/ui/src/listbox.js
@@ -100,7 +100,7 @@ function handleRoot(el, Alpine) {
this.__compareBy = Alpine.extractProp(el, 'by')
this.__orientation = Alpine.extractProp(el, 'horizontal', false) ? 'horizontal' : 'vertical'
- this.__context = generateContext(this.__isMultiple, this.__orientation, () => this.$data.__activateSelectedOrFirst())
+ this.__context = generateContext(Alpine, this.__isMultiple, this.__orientation, () => this.$data.__activateSelectedOrFirst())
let defaultValue = Alpine.extractProp(el, 'default-value', this.__isMultiple ? [] : null)
diff --git a/tests/cypress/integration/plugins/history.spec.js b/tests/cypress/integration/plugins/history.spec.js
index 7fd7820c7..8c1bc9524 100644
--- a/tests/cypress/integration/plugins/history.spec.js
+++ b/tests/cypress/integration/plugins/history.spec.js
@@ -1,84 +1,129 @@
import { haveText, html, test } from '../../utils'
-test('can go back and forth',
+test('value is reflected in query string upon changing',
[html`
+
Dec
`],
({ get, url, go }) => {
get('span').should(haveText('1'))
- url().should('include', '?count=1')
+ url().should('not.include', '?count=1')
get('button').click()
get('span').should(haveText('2'))
url().should('include', '?count=2')
- go('back')
+ get('button').click()
+ get('span').should(haveText('3'))
+ url().should('include', '?count=3')
+ get('h1').click()
+ get('h1').click()
+ get('span').should(haveText('1'))
+ url().should('not.include', '?count=1')
+ },
+)
+
+test('can configure always making the query string value present',
+ [html`
+
+
+
Dec
+
+
+ `],
+ ({ get, url, go }) => {
get('span').should(haveText('1'))
url().should('include', '?count=1')
- go('forward')
+ get('button').click()
get('span').should(haveText('2'))
url().should('include', '?count=2')
+ get('h1').click()
+ get('span').should(haveText('1'))
+ url().should('include', '?count=1')
},
)
-test('property is set from the query string on load',
+test('value is persisted across requests',
[html`
`],
- ({ get, url }, reload) => {
+ ({ get, url, go }, reload) => {
get('span').should(haveText('1'))
- url().should('include', '?count=1')
+ url().should('not.include', '?count=1')
get('button').click()
get('span').should(haveText('2'))
url().should('include', '?count=2')
+
reload()
+
+ url().should('include', '?count=2')
get('span').should(haveText('2'))
},
)
-test('can use a query string key alias',
+test('can provide an alias',
[html`
-
+
`],
- ({ get, url }, reload) => {
+ ({ get, url, go }) => {
get('span').should(haveText('1'))
- url().should('include', '?foo=1')
+ url().should('not.include', '?tnuoc=1')
get('button').click()
get('span').should(haveText('2'))
- url().should('include', '?foo=2')
- reload()
+ url().should('include', '?tnuoc=2')
+ },
+)
+
+test('can use pushState',
+ [html`
+
+
+
+
+ `],
+ ({ get, url, go }) => {
+ get('span').should(haveText('1'))
+ url().should('not.include', '?count=1')
+ get('button').click()
get('span').should(haveText('2'))
+ url().should('include', '?count=2')
+ go('back')
+ get('span').should(haveText('1'))
+ url().should('not.include', '?count=1')
+ go('forward')
+ get('span').should(haveText('2'))
+ url().should('include', '?count=2')
},
)
test('can go back and forth with multiple components',
[html`
-
+
-
+
`],
({ get, url, go }) => {
get('#foo span').should(haveText('1'))
- url().should('include', 'foo=1')
+ url().should('not.include', 'foo=1')
get('#foo button').click()
get('#foo span').should(haveText('2'))
url().should('include', 'foo=2')
get('#bar span').should(haveText('1'))
- url().should('include', 'bar=1')
+ url().should('not.include', 'bar=1')
get('#bar button').click()
get('#bar span').should(haveText('2'))
url().should('include', 'bar=2')
@@ -86,15 +131,87 @@ test('can go back and forth with multiple components',
go('back')
get('#bar span').should(haveText('1'))
- url().should('include', 'bar=1')
+ url().should('not.include', 'bar=1')
get('#foo span').should(haveText('2'))
url().should('include', 'foo=2')
go('back')
get('#bar span').should(haveText('1'))
- url().should('include', 'bar=1')
+ url().should('not.include', 'bar=1')
get('#foo span').should(haveText('1'))
- url().should('include', 'foo=1')
+ url().should('not.include', 'foo=1')
+ },
+)
+
+test('supports arrays',
+ [html`
+
+
+
+
+ `],
+ ({ get, url, go }, reload) => {
+ get('span').should(haveText('["foo"]'))
+ url().should('not.include', '?items')
+ get('button').click()
+ get('span').should(haveText('["foo","bar"]'))
+ url().should('include', '?items[0]=foo&items[1]=bar')
+ reload()
+ url().should('include', '?items[0]=foo&items[1]=bar')
+ get('span').should(haveText('["foo","bar"]'))
+ },
+)
+
+test('supports deep arrays',
+ [html`
+
+
+
+
+ `],
+ ({ get, url, go }, reload) => {
+ get('span').should(haveText('["foo",["bar","baz"]]'))
+ url().should('not.include', '?items')
+ get('button').click()
+ get('span').should(haveText('["foo",["bar","baz","bob"]]'))
+ url().should('include', '?items[0]=foo&items[1][0]=bar&items[1][1]=baz&items[1][2]=bob')
+ reload()
+ url().should('include', '?items[0]=foo&items[1][0]=bar&items[1][1]=baz&items[1][2]=bob')
+ get('span').should(haveText('["foo",["bar","baz","bob"]]'))
+ },
+)
+
+test('supports objects',
+ [html`
+
+
+
+
+ `],
+ ({ get, url, go }, reload) => {
+ get('span').should(haveText('{"foo":"bar"}'))
+ url().should('not.include', '?items')
+ get('button').click()
+ get('span').should(haveText('{"foo":"bar","bob":"lob"}'))
+ url().should('include', '?items[foo]=bar&items[bob]=lob')
+ reload()
+ url().should('include', '?items[foo]=bar&items[bob]=lob')
+ get('span').should(haveText('{"foo":"bar","bob":"lob"}'))
+ },
+)
+
+test('encodes values according to RFC 1738 (plus signs for spaces)',
+ [html`
+
+
+
+ `],
+ ({ get, url, go }, reload) => {
+ url().should('include', '?foo=hey%26there&bar=hey+there')
+ get('span').should(haveText('"hey&there""hey there"'))
+ reload()
+ url().should('include', '?foo=hey%26there&bar=hey+there')
+ get('span').should(haveText('"hey&there""hey there"'))
},
)