Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

On initial page load run init before turbo:load has been fired #646

Merged
merged 10 commits into from
Nov 26, 2024
218 changes: 121 additions & 97 deletions resources/js/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,47 @@ import useMask from './stores/useMask'
import { swatches, clear as clearSwatches } from './stores/useSwatches'
import { clear as clearAttributes } from './stores/useAttributes.js'
import './vue'
import { computed } from 'vue'
import './fetch'
import './filters'
import './mixins'
import './turbolinks'
(() => import('./turbolinks'))()
import './cookies'
import './callbacks'
import './vue-components'

if (import.meta.env.VITE_DEBUG === 'true') {
document.addEventListener('vue:loaded', () => {
window.app.$on('notification-message', function (message, type, params, link) {
switch (type) {
case 'error':
console.error(...arguments)
break
case 'warning':
console.warn(...arguments)
break
case 'success':
case 'info':
default:
console.log(...arguments)
}
})
})
}

document.addEventListener('vue:loaded', () => {
const lastStoreCode = useLocalStorage('last_store_code', window.config.store_code)
if (lastStoreCode.value !== window.config.store_code) {
clearAttributes()
clearSwatches()
lastStoreCode.value = window.config.store_code
}
})

function init() {
if (document.body.contains(window.app.$el)) {
return;
}

// https://vuejs.org/api/application.html#app-config-performance
Vue.config.performance = import.meta.env.VITE_PERFORMANCE == 'true'
Vue.prototype.window = window
Expand Down Expand Up @@ -59,101 +90,94 @@ function init() {
custom_attributes: [],
}

window.app = new Vue({
el: '#app',
data: {
custom: {},
config: window.config,
loadingCount: 0,
loading: false,
loadAutocomplete: false,
csrfToken: document.querySelector('[name=csrf-token]').content,
cart: useCart(),
order: useOrder(),
user: useUser(),
mask: useMask(),
showTax: window.config.show_tax,
swatches: swatches,
scrollLock: useScrollLock(document.body),
},
methods: {
search(value) {
if (value.length) {
Turbo.visit(window.url('/search?q=' + encodeURIComponent(value)))
}
},
setSearchParams(url) {
window.history.pushState(window.history.state, '', new URL(url))
},
toggleScroll(bool = null) {
if (bool === null) {
this.scrollLock = !this.scrollLock
} else {
this.scrollLock = bool
}
},

resizedPath(imagePath, size, store = null) {
if (!store) {
store = window.config.store
requestAnimationFrame(
() => {
window.app = new Vue({
el: '#app',
data: {
custom: {},
config: window.config,
loadingCount: 0,
loading: false,
loadAutocomplete: false,
csrfToken: document.querySelector('[name=csrf-token]').content,
cart: useCart(),
order: useOrder(),
user: useUser(),
mask: useMask(),
showTax: window.config.show_tax,
swatches: swatches,
scrollLock: useScrollLock(document.body),
},
methods: {
search(value) {
if (value.length) {
Turbo.visit(window.url('/search?q=' + encodeURIComponent(value)))
}
},

setSearchParams(url) {
window.history.pushState(window.history.state, '', new URL(url))
},

toggleScroll(bool = null) {
if (bool === null) {
this.scrollLock = !this.scrollLock
} else {
this.scrollLock = bool
}
},

resizedPath(imagePath, size, store = null) {
if (!store) {
store = window.config.store
}

let url = new URL(imagePath)
url = url.pathname.replace('/media', '')

return `/storage/${store}/resizes/${size}/magento${url}`
},
},
computed: {
// Wrap the local storage in getter and setter functions so you do not have to interact using .value
guestEmail: wrapValue(
useLocalStorage('email', window.debug ? 'wayne@enterprises.com' : '', { serializer: StorageSerializers.string }),
),

loggedIn() {
return this.user?.is_logged_in
},

hasCart() {
return this.cart?.id && this.cart.items.length
},


canOrder() {
return this.cart.items.every((item) => item.is_available)
},
},
watch: {
loadingCount: function (count) {
window.app.$data.loading = count > 0
},
},
mounted() {
setTimeout(() => {
const event = new CustomEvent('vue:mounted', { detail: { vue: window.app } })
document.dispatchEvent(event)
})
}

let url = new URL(imagePath)
url = url.pathname.replace('/media', '')

return `/storage/${store}/resizes/${size}/magento${url}`
},
},
computed: {
// Wrap the local storage in getter and setter functions so you do not have to interact using .value
guestEmail: wrapValue(
useLocalStorage('email', window.debug ? 'wayne@enterprises.com' : '', { serializer: StorageSerializers.string }),
),

loggedIn() {
return this.user?.is_logged_in
},

hasCart() {
return this.cart?.id && this.cart.items.length
},

canOrder() {
return this.cart.items.every((item) => item.is_available)
},
},
watch: {
loadingCount: function (count) {
window.app.$data.loading = count > 0
},
},
})

const lastStoreCode = useLocalStorage('last_store_code', window.config.store_code)
if (lastStoreCode.value !== window.config.store_code) {
clearAttributes()
clearSwatches()
lastStoreCode.value = window.config.store_code
}

if (window.debug) {
window.app.$on('notification-message', function (message, type, params, link) {
switch (type) {
case 'error':
console.error(...arguments)
break
case 'warning':
console.warn(...arguments)
break
case 'success':
case 'info':
default:
console.log(...arguments)
}
})
}

const event = new CustomEvent('vue:loaded', { detail: { vue: window.app } })
document.dispatchEvent(event)
})

setTimeout(() => {
const event = new CustomEvent('vue:loaded', { detail: { vue: window.app } })
document.dispatchEvent(event)
})
}
)
}

document.addEventListener('turbo:load', init)
setTimeout(init)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This specific order of setTimeout and requestAnimationFrame ensures LCP is not impacted.
And ensures small task time for the turbo:load/evaluate script, and vue:loaded events by scheduling them all in their own task.

Using a setTimeout for both, a requestAnimationFrame for both, or executing this immediately and booting Vue in a requestAnimationFrame all has impact on the LCP

2 changes: 1 addition & 1 deletion resources/views/components/productlist.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<div slot="renderNoResults"></div>

<div class="relative" slot="render" slot-scope="{ data, loading }" v-if="!loading">
<div class="relative" slot="render" slot-scope="{ data, loading }" v-if="!loading && data?.length">
<slider>
<div slot-scope="{ navigate, showLeft, showRight, currentSlide, slidesTotal }">
<div class="-mx-2 flex mt-5 overflow-x-auto snap-x scrollbar-hide scroll-smooth snap-mandatory" ref="slider">
Expand Down
3 changes: 3 additions & 0 deletions tests/Browser/CartTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public function addSimpleProduct()
$this->browse(function (Browser $browser) {
$browser->addProductToCart($this->testProduct->url)
->visit('/cart')
->waitUntilVueLoaded()
->waitUntilIdle()
->waitFor('@cart-content', 15)
->waitUntilIdle()
Expand Down Expand Up @@ -43,6 +44,7 @@ public function changeProductQty()
{
$this->browse(function (Browser $browser) {
$browser->visit('/cart')
->waitUntilVueLoaded()
->waitUntilIdle()
->waitFor('@cart-content', 15)
->waitUntilIdle()
Expand All @@ -60,6 +62,7 @@ public function removeProduct()
{
$this->browse(function (Browser $browser) {
$browser->visit('/cart')
->waitUntilVueLoaded()
->waitUntilIdle()
->waitFor('@cart-content', 15)
->waitUntilIdle()
Expand Down
2 changes: 2 additions & 0 deletions tests/Browser/CheckoutTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public function checkoutAsUser()
// Go through checkout as guest and log in.
$this->browse(function (Browser $browser) use ($email) {
$browser->waitForReload(fn ($browser) => $browser->visit('/'), 4)
->waitUntilVueLoaded()
->waitUntilIdle()
->waitFor('@account_menu')
->click('@account_menu')
Expand Down Expand Up @@ -71,6 +72,7 @@ public function doCheckoutLogin(Browser $browser, $email = false, $password = fa
{
$browser
->visit('/checkout')
->waitUntilVueLoaded()
->waitUntilIdle()
->type('@email', $email ?: 'wayne@enterprises.com')
->waitUntilIdle();
Expand Down
1 change: 1 addition & 0 deletions tests/Browser/DialogTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public function test()
{
$this->browse(function (Browser $browser) {
$browser->visit('/?show-cookie-notice')
->waitUntilVueLoaded()
->waitUntilIdle()
->assertSee('Accept cookies')
->waitForReload(function (Browser $browser) {
Expand Down
2 changes: 1 addition & 1 deletion tests/Browser/HomepageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class HomepageTest extends DuskTestCase
public function homepage()
{
$this->browse(function (Browser $browser) {
$browser->visit('/')->assertSee('All rights reserved.');
$browser->visit('/')->waitUntilVueLoaded()->assertSee('All rights reserved.');
});
}
}
1 change: 1 addition & 0 deletions tests/Browser/NewsletterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public function test()
$this->browse(function (Browser $browser) {
$email = $this->faker->email;
$browser->visit('/')
->waitUntilVueLoaded()
->scrollIntoView('@newsletter')
->waitUntilIdle()
->type('@newsletter-email', $email)
Expand Down
19 changes: 16 additions & 3 deletions tests/DuskTestCaseSetup.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ protected function setUp(): void
return $this;
});

Browser::macro('waitUntilVueLoaded', function () {
/** @var Browser $this */
$this
->waitUntilIdle()
->waitUntilTrueForDuration('document.body.contains(window.app?.$el) && window.app?._isMounted && console.log("mounted") === undefined', 10, 1)
->waitUntilIdle();

return $this;
});

Browser::macro('assertFormValid', function ($selector) {
/** @var Browser $this */
$fullSelector = $this->resolver->format($selector);
Expand All @@ -60,14 +70,17 @@ protected function setUp(): void
/** @var Browser $this */
if ($productUrl) {
$this
->visit($productUrl);
->visit($productUrl)
->waitUntilVueLoaded()
->waitUntilIdle();
}

// @phpstan-ignore-next-line
$this
->waitUntilIdle()
->pressAndWaitFor('@add-to-cart', 60)
->waitForText('Added', 60)
->waitUntilEnabled('@add-to-cart', 200)
->pressAndWaitFor('@add-to-cart', 120)
->waitForText(__('Added'), 120)
->waitUntilIdle();

return $this;
Expand Down
Loading