Navigation

    Quasar Framework

    • Register
    • Login
    • Search
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Search
    1. Home
    2. beets
    3. Best
    • Profile
    • Following 0
    • Followers 1
    • Topics 2
    • Posts 264
    • Best 62
    • Groups 0

    Best posts made by beets

    • RE: How to set up a CDN for SPA (got problems with vue-router)

      @daniel Here’s some snippets on how I have it set up. This is not the normal SSR setup, but was customized pretty heavily for my needs.

      config.json

      {
        "api_url": "https://api.example.com:4443",
        "api_url_local": "http://127.0.0.1:8001",
        "media_url": "https://media.example.com:4443",
        "static_url": "https://static.example.com:4443",
        "frontend_url": "https://example.com:4443",
      }
      

      Above is the basic config file, set for the dev environment. The api_url_local is just because on SSR, we can access the loopback address instead calling the full domain, which makes it a bit faster.

      quasar.conf.js

      const fs = require('fs')
      const path = require('path')
      const zlib = require('zlib')
      
      const CompressionPlugin = require('compression-webpack-plugin')
      
      const config = require('./config.json')
      
      module.exports = function (ctx) {
        return {
          boot: [
            'init',
            'filters',
            'plugins',
            { path: 'a11y', server: false },
            { path: 'polyfills', server: false },
            { path: 'hydrate', server: false },
          ],
          css: [
            'index.sass'
          ],
          extras: [
          ],
          framework: {
            iconSet: 'svg-mdi-v5',
            lang: 'en-us',
      
            importStrategy: 'auto',
      
            plugins: [
              'Cookies',
              'AddressbarColor',
              'AppVisibility',
              'LoadingBar',
              'Dialog',
              'Screen',
              'Notify',
            ],
            config: {
              loadingBar: {
                color: 'secondary',
                size: '4px',
                skipHijack: true,
              }
            }
          },
          preFetch: true,
          build: {
            scopeHoisting: true,
            vueRouterMode: 'history',
            showProgress: true,
            gzip: false,
            // analyze: {
            //   analyzerPort: 9000,
            //   openAnalyzer: false
            // },
      
            appBase: false,
      
            beforeDev({ quasarConf })	{
              // Hook our own meta system into Quasar
              quasarConf.__meta = true
            },
      
            extendWebpack (cfg) {
      
              cfg.resolve.alias['@'] = path.resolve(__dirname, 'src/components')
              cfg.resolve.alias['mixins'] = path.resolve(__dirname, 'src/mixins')
              cfg.resolve.alias['modules'] = path.resolve(__dirname, 'src/modules')
              cfg.resolve.alias['utils'] = path.resolve(__dirname, 'src/utils')
      
              if(cfg.mode === 'production') {
                cfg.output.publicPath = config.static_url + '/'
                cfg.plugins.push(
                  new CompressionPlugin({
                    filename: '[path].gz[query]',
                    algorithm: 'gzip',
                    test: /\.(js|css|svg)$/,
                    compressionOptions: {
                      level: 9,
                    },
                    minRatio: 1,
                  }),
                  new CompressionPlugin({
                    filename: '[path].br[query]',
                    algorithm: 'brotliCompress',
                    test: /\.(js|css|svg)$/,
                    compressionOptions: {
                      [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
                      [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
                    },
                    minRatio: 1,
                  })
                )
              }
            }
          },
          devServer: {
            // https: {
            //   key: fs.readFileSync('certs/key.pem'),
            //   cert: fs.readFileSync('certs/cert.pem'),
            //   ca: fs.readFileSync('certs/minica.pem'),
            // },
            https: false,
            port: 8000,
            sockPort: 4443,
            open: false,
          },
          animations: [
            'slideInLeft',
            'slideInRight',
            'slideOutLeft',
            'slideOutRight',
            'zoomIn',
            'zoomOut',
            'pulse',
          ],
          ssr: {
            pwa: false,
            manualHydration: true,
            extendPackageJson(pkg) {
              // Default quasar SSR packages we don't use
              delete pkg.dependencies['compression']
              delete pkg.dependencies['express']
              delete pkg.dependencies['lru-cache']
            }
          }
        }
      }
      
      

      Main interesting things above are:

      • I use the webpack compress plugin (only on production) to gzip and brotli compress the assets. You can just do .gz without the plugin, but I wanted .br too.
      • I also set the publicPath there too, as shown in my other post
      • For SSR, I’m removing express and some other packages I don’t use
      • I also manually hydrate, which is pretty store specific to my case with vuex. It’s because I freeze a lot of objects stored there.
      • I rolled my own meta plugin, for various reasons, so I had to mimic how the official one works with the beforeDev hook.

      Then finally, src-ssr/index.js

      const { createServer } = require('http')
      
      const { promisify } = require('util')
      const randomBytes = promisify(require('crypto').randomBytes)
      
      const { createBundleRenderer } = require('vue-server-renderer')
      const bundle = require('./quasar.server-manifest.json')
      const clientManifest = require('./quasar.client-manifest.json')
      
      const config = require('../config.json')
      
      const renderer = createBundleRenderer(bundle, {
        clientManifest,
        runInNewContext: false,
      })
      
      const port = 8000
      
      const server = createServer(async (req, res) => {
        // Stats
        const start_time = Date.now()
        let ttfb = null, total = null
      
        // Generate CSP nonce
        const nonce = (await randomBytes(16)).toString('base64')
      
        const ctx = {
          url: req.url,
          req,
          res,
          nonce,
        }
        const stream = renderer.renderToStream(ctx)
      
        stream.once('data', () => {
      
          console.log('First chunk: ', Date.now())
      
          // Interesting note: if needed, we can access vuex with ctx.state
      
          // Custom Asset Prefetch
          // Instead of using ${ ctx.renderResourceHints() } in the template,
          // We are going to do the same here but remove preload links handled above
          // Todo: would be nice if vue exposed the render function for just these
          let prefetchLinks = ctx.renderResourceHints()
          prefetchLinks = prefetchLinks.substring(prefetchLinks.indexOf('<link rel="prefetch"'))
      
          // Custom Asset Preload
          // Instead of using ${ ctx.renderResourceHints() } in the template,
          // We are going to push the preload files in the HTTP header
          let preloadLinks = ctx.getPreloadFiles().map(f => {
            return {
              file: config.static_url + '/' + f.file,
              asType: f.asType,
              //extra: '; crossorigin',
              extra: '',
            }
          })
          preloadLinks.push(
            { file: config.media_url + '/fonts/roboto-v20-latin-300.woff2',     asType: 'font', extra: '; crossorigin; type="font/woff2"' },
            { file: config.media_url + '/fonts/roboto-v20-latin-regular.woff2', asType: 'font', extra: '; crossorigin; type="font/woff2"' },
            { file: config.media_url + '/fonts/roboto-v20-latin-500.woff2',     asType: 'font', extra: '; crossorigin; type="font/woff2"' },
          )
          preloadLinks = preloadLinks.map(f => `<${f.file}>; rel=preload; as=${f.asType}${f.extra}`).join(', ')
      
          const csp_template = {
            'default-src': [config.static_url],
            'prefetch-src': [config.static_url],
            'base-uri': ["'self'"],
            'script-src': [config.static_url, `'nonce-${nonce}'`, "'unsafe-inline'", /*"'strict-dynamic'",*/],
      	  // abbreviated
          }
      
          const csp = Object.entries(csp_template).reduce((acc, [key, values]) => {
            return acc += key + ' ' + values.join(' ') + '; '
          }, '')
      
          res.writeHead(ctx.httpCode || 200, {
            'Content-Type': 'text/html; charset=UTF-8',
            'Content-Security-Policy': csp,
            'Link': preloadLinks,
          })
      
          res.write(`<!DOCTYPE html>
      <html ${ctx.Q_HTML_ATTRS}>
        <head>
          <meta charset="utf-8">
          <meta name="format-detection" content="telephone=no">
          <meta name="msapplication-tap-highlight" content="no">
          <meta name="viewport" content="width=device-width, initial-scale=1">
          <link rel="icon" type="image/png" sizes="128x128" href="${config.media_url}/favicon/favicon-128x128.png">
          <link rel="icon" type="image/png" sizes="96x96" href="${config.media_url}/favicon/favicon-96x96.png">
          <link rel="icon" type="image/png" sizes="32x32" href="${config.media_url}/favicon/favicon-32x32.png">
          <link rel="icon" type="image/png" sizes="16x16" href="${config.media_url}/favicon/favicon-16x16.png">
          <link rel="icon" type="image/ico" href="${config.media_url}/favicon/favicon.ico">
          ${ ctx.Q_HEAD_TAGS }
          ${ ctx.renderStyles() }
          ${ prefetchLinks }
        </head>
        <body class="${ctx.Q_BODY_CLASSES}">
          <noscript>
            <div>
              Javascript disabled
            </div>
          </noscript>
          `)
          ttfb = Date.now() - start_time
        })
      
        stream.on('data', chunk => {
          res.write(chunk)
        })
      
        stream.on('end', () => {
          res.end(`
          ${ ctx.renderState() }
          ${ ctx.renderScripts() }
        </body>
      </html>
          `)
          total = Date.now() - start_time
          const ttfb_pct = Math.floor(100 * ttfb / total)
          console.log(`${req.url} TTFB: ${ttfb} Total: ${total} - ${ttfb_pct}%`)
        })
      
        stream.on('error', error => {
          console.log("ONERROR")
          console.log(error.message)
      
          if(ctx.statusCode === 307 || ctx.statusCode === 308) {
            res.writeHead(ctx.statusCode, {
              'Location': ctx.location
            })
            res.end()
      
          } else {
            // Nginx will render this error page
            // Note, statusMessage doesn't seem to be
            // available to nginx
      
            res.statusCode = 502
            res.statusMessage = error.message
            res.end()
          }
      
        })
      
      })
      
      server.listen(port, '127.0.0.1', error => {
        console.log(`Server listening at port ${port}`)
      })
      

      The above file it a bit of a doozy, I really didn’t need express for SSR mode, and I really wanted to get renderToStream to work (which it does, and saves about 50% TTFB.) Luckily, Quasar just gives you a template for the SSR server, but you don’t have to stick to it. The file generates the CSP, and also sends the preload links as HTTP headers, instead of meta tags. It also completely ignores what’s in index.template.html and writes it directly.

      All of the domains are set up through nginx, I’ll see if I can clean that up and post it here, but basic idea is that example.com goes to the SSR process (which you can see doesn’t serve any static assets) and static.example.com serves the dist/ssr/www folder, with gzip_static and brotli_static enabled (you have to manually compile the brotli plugin for that to work, but it’s worth it for me.)

      Finally, I also serve quasar’s dev mode through nginx as well, just so I can use SSL, test things like my CSP, etc. Basically if i run quasar dev, or build it and run the node process, I just access it through example.com:4443.

      Edit: an example of the TTFB savings:

      / TTFB: 188 Total: 458 - 41%
      

      That is logged when I request my homepage. With renderToStream, I get a TTFB of 188ms (minus network latency) while rederToString would take 458ms

      posted in Help
      beets
      beets
    • RE: Matomo Tracking Code

      @PeterQF Yup, that looks like it would work, basics steps would be:

      • Install with yarn add vue-matomo
      • Add a new boot file: quasar new boot matomo
      • Edit quasar.conf.js and add into the boot array:
      boot: [
        { path: 'matomo', server: false},
      ]
      
      • Edit src/boot/matomo.js and make it look like this:
      import VueMatomo from 'vue-matomo'
      
      export default ({ Vue, app, router }) => {
        Vue.use(VueMatomo, {
          router,
      
          /** Other configuration options **/
        })
      }
      

      It’s untested on my end, but sounds like it should just work out of the box.

      posted in Help
      beets
      beets
    • RE: How to achieve v-ripple effect on table rows?

      @dobbel @maptesting you can fix this by applying this css rule:

      tr{position:relative;transform: scale(1);}
      

      Note that the ripple directive requires the element have a position relative, but I guess per spec, position relative on a tr is undefined. The transform part is a hack to make it work though. I’m not sure I’d rely on this, but I don’t know if there’s another way. I also only tested in chrome, so if this is used in production, please test it on other browsers.

      posted in Help
      beets
      beets
    • RE: How to set up a CDN for SPA (got problems with vue-router)

      @daniel great, glad I could help.

      Also just for anyone interested who might read this thread, my use case is a bit different which I’ll explain below:

      • I use SSR, and have the server process running at example.com
      • It only serves server generated pages, not js, css or images. It will 404 anything not in the routes.js file.
      • I have static.example.com pointed to the dist/ssr/www folder
      • I like this better since node doesn’t need to waste time serving static assets, and additionally I pre compress (.gz and .br) all the files, so I can just use nginx’s gzip_static and brotli_static
      • Most of my images come from some other process (magento in particular), so I have media.example.com serve those. I use something similar to the boot file I showed above, except a bit nicer, i.e. this.$media_url('somefile.png') returns https://media.example.com/somefile.png
      • For favicons, I simply change index.template.html to point to media.example.com/favicons/favicon.ico, etc. And since some browsers will always try to look for example.com/favicon.ico, I just do a simple location block for that in nginx to either respond 404, or the actual file in /var/www/media/htdocs/favicon.ico.

      Overall it makes me happy, the only thing that doesn’t work is Quasar’s automagic static asset handling, which as I mentioned above I just don’t use, and instead use explicit references to the media subdomain.

      posted in Help
      beets
      beets
    • RE: Problem when use History go back

      @ernestocuzcueta I think you could even make it load all previous items in the onLoad function, but it’s been a while since I used the infinite scroll. It of course also depends on how expensive the onLoad function is, if you have them stored in vuex then it would be easy of course.

      Also, just to make sure, you can enable vue router to scroll to the previous position on history back with this in your router/index.js file:

      new Router({
        scrollBehavior: (to, from, savedPosition) => {
          if (savedPosition) {
            return savedPosition;
          } else {
            return { x: 0, y: 0 }
          }
        };
        // ...
      })
      

      But, since the page being pushed may not have that much height because of the lazy loading, you might need to solve it some other way like:

      scrollBehavior: (to, from, savedPosition) => {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
             if (savedPosition) {
               resolve(savedPosition)
             } else {
               resolve({ x: 0, y: 0 })
             } 
          }, 600) // inelegant, but may work
        })
      }
      

      Or even better, create a (throttled) scroll observer on the homepage, and save the position in the history.state object along with the index, and scrollTo on mounted / after you have loaded the items.

      Edit, there’s even another way in this thread: https://forum.vuejs.org/t/cant-get-scrollbehavior-working/29077/7

      You can create a listener that only fires once, and emit that after you load. That being said, I would probably still handle this all in the homepage component with history state if possible.

      posted in Framework
      beets
      beets
    • RE: Button Toggle to choose Div´s class

      @arqsergio Here’s the more vue way to do it, and what scott was hinting at: https://codepen.io/pianopronto/pen/wvzEoYm . What it’s doing is binding a dynamic classname to the two divs based on the model value. No DOM manipulations needed.

      posted in Help
      beets
      beets
    • RE: q-google-map - Quasar app extension to integrate google map

      Neat, I recently integrated google maps into a quasar project too. One fun thing I did was support for dark mode, which is reactive to changes:

        computed: {
          styles() {
            if(!this.$q.dark.isActive) {
              return []
            }
            return [
              {
                elementType: "geometry",
                stylers: [{ color: "#242f3e" }]
              },
              {
                elementType: "labels.text.stroke",
                stylers: [{ color: "#242f3e" }]
              },
              {
                elementType: "labels.text.fill",
                stylers: [{ color: "#746855" }]
              },
              {
                featureType: "administrative.locality",
                elementType: "labels.text.fill",
                stylers: [{ color: "#d59563" }]
              },
              {
                featureType: "poi",
                elementType: "labels.text.fill",
                stylers: [{ color: "#d59563" }]
              },
              {
                featureType: "poi.park",
                elementType: "geometry",
                stylers: [{ color: "#263c3f" }]
              },
              {
                featureType: "poi.park",
                elementType: "labels.text.fill",
                stylers: [{ color: "#6b9a76" }]
              },
              {
                featureType: "road",
                elementType: "geometry",
                stylers: [{ color: "#38414e" }]
              },
              {
                featureType: "road",
                elementType: "geometry.stroke",
                stylers: [{ color: "#212a37" }]
              },
              {
                featureType: "road",
                elementType: "labels.text.fill",
                stylers: [{ color: "#9ca5b3" }]
              },
              {
                featureType: "road.highway",
                elementType: "geometry",
                stylers: [{ color: "#746855" }]
              },
              {
                featureType: "road.highway",
                elementType: "geometry.stroke",
                stylers: [{ color: "#1f2835" }]
              },
              {
                featureType: "road.highway",
                elementType: "labels.text.fill",
                stylers: [{ color: "#f3d19c" }]
              },
              {
                featureType: "transit",
                elementType: "geometry",
                stylers: [{ color: "#2f3948" }]
              },
              {
                featureType: "transit.station",
                elementType: "labels.text.fill",
                stylers: [{ color: "#d59563" }]
              },
              {
                featureType: "water",
                elementType: "geometry",
                stylers: [{ color: "#17263c" }]
              },
              {
                featureType: "water",
                elementType: "labels.text.fill",
                stylers: [{ color: "#515c6d" }]
              },
              {
                featureType: "water",
                elementType: "labels.text.stroke",
                stylers: [{ color: "#17263c" }]
              }
            ]
          }
        },
        watch: {
          '$q.dark.isActive'() {
            this.map.setOptions({
              styles: this.styles
            })
          },
        }
      
      posted in [v1] App Extensions
      beets
      beets
    • RE: Images grid

      Agreed a pen would help, but you may just have to wrap the q-img in a div. Also notice the correct gutter class q-col-gutter-xs

      <div class="q-col-gutter-xs row">
         <div
            v-for="(value, index) in images"
            v-bind:key="index"
            class="col-4"
         >
            <q-img
              :src="value"
              :ratio="1"
              />
         </div>
      </div>
      

      Edit: also, you want col-4 for three columns. That was likely the error in combination with the wrong gutter class, so you may not even need the extra div, although it doesn’t hurt.

      posted in Framework
      beets
      beets
    • RE: remove deep element

      @zeppelinexpress
      Maybe:

      .q-uploader >>> .q-uploader__header-content > a:first-child {
        display: none !important
      }
      
      posted in Help
      beets
      beets
    • RE: Setup for Quasar with PHP api for MySQL

      @dirkhd said in Setup for Quasar with PHP api for MySQL:

      For the second I could just replace the URLs in my Ajax api calls by the remote server and manage the CORS probs, right?

      Is your site at example.com and api at example.com/api ? If so, then luckily webpack server has an api proxy built in. This will remove the cors problem without having to set up a cors-stripping proxy on your server.

      https://quasar.dev/quasar-cli/api-proxying
      https://webpack.js.org/configuration/dev-server/#devserverproxy

      If your api endpoint is api.example.com instead, I would probably just do something like this in quasar.conf.js:

      build: {
        env: {
          API: ctx.dev
            ? '/api'
            : 'https://api.example.com'
        }
      }
      

      And use process.env.API in your axios calls, and use the same devServer proxy in the first method. Note, I’ve never had to use the proxy feature, but it should work in your case.

      posted in CLI
      beets
      beets
    • RE: Infinite Scroll with images are reloading

      I think that you must provide more code for anyone to be helpful… Are you perhaps replacing the entire array in the vuex store?

      posted in Framework
      beets
      beets
    • RE: Unable to access environment variables with square bracket notation

      @jvik Maybe you need if(key.startsWith('VUE_APP') || key.startsWith('$VUE_APP')) { instead?

      Can you try putting:

      const tmp = Object.entries(process.env).reduce((acc, [key,val]) => {
        if(key.startsWith('VUE_APP') || key.startsWith('$VUE_APP')) {
          acc[key] = val
        }
        return acc
      }, {})
      console.log('ENV VARS:', tmp)
      
      

      at the top of quasar.conf.js and see what the output on the command line is during a quasar dev or quasar build

      posted in Help
      beets
      beets
    • RE: Setup for Quasar with PHP api for MySQL

      @dirkhd So is it that your web app is at example.com/app and your api is at example.com/db ? If so, then maybe this will still work?

      devServer: {
        proxy: {
          '/db': {
            target: 'http://example.com',
            changeOrigin: true,
          }
        }
      }
      

      I would probably prefer to use absolute urls instead of relative, since the automatic <base> tag quasar puts modifies the base url for ajax, I believe.

      Is your site deployed on several domains where that would be unfeasible? If not, I would recommend you use the build -> env variable in quasar.conf.js, and then in your axios boot file (I assume you have one, if not we can make one easily) add

      axios.defaults.baseURL = process.env.API

      Edit: and change all your calls to :

       this.$axios
              .post("api.php", { // or db/api.php depending on what you set process.env.API to 
      
      posted in CLI
      beets
      beets
    • RE: Render-problem in q-avatar on iPhone

      I haven’t encountered this, but maybe try to use a QBadge instead?

      posted in Framework
      beets
      beets
    • RE: Disabling import strategy

      And for visual people, here’s a before / after of webpack analyze:

      webpack

      posted in Useful Tips (NEW)
      beets
      beets
    • RE: Disable LoadingBar plugin for certain Ajax calls

      @perelin I have a mixin that I include on certain pages that I want to hide the loading bar:

      export default {
        created() {
          this.loadingBarSize = this.$q.loadingBar.size
          this.$q.loadingBar.setDefaults({
            size: '0px'
          })
        },
        destroyed() {
          this.$q.loadingBar.setDefaults({
            size: this.loadingBarSize
          })
        },
      }
      

      This disables all loading bars on a certain page, but you could just do something like:

      methods: {
        async makeAjaxCall() {
          const loadingBarSize = this.$q.loadingBar.size
          this.$q.loadingBar.setDefaults({
            size: '0px'
          })
          
          // await some axios call
      
          this.$q.loadingBar.setDefaults({
            size: loadingBarSize
          })
        },
      }
      posted in Help
      beets
      beets
    • RE: Dashboard is printing in mobile device size (undesirable viewer port) but i want to be printing in desktop size

      @Filipinjo Strange, but glad it worked anyway 🙂

      posted in Framework
      beets
      beets
    • Disabling import strategy

      Just thought I’d share this tip. Quasar/app v2+ by default uses importStrategy: 'auto' but doesn’t provide a way to disable it and manually specify what components to include like we used to be able to do pre v2.

      Today, I was checking out my bundle, and noticed a component I didn’t use was still in the bundle. Turns out quasar cli is searching with a regex for strings like q-select, q-btn, etc and including them if found. I had a comment such as // Use this instead of q-select and that caused q-select to be bundled even though I didn’t use it at all!

      Here’s how I disabled this behavior:

      // quasar.conf.js
      
      module.exports = function (ctx) {
        return {
        
          // ...
          
          framework: {
            
            // ...
            
            importStrategy: 'auto', // keep this as auto
              
            components: [ // manually specify components
              'QLayout',
              'QHeader',
              'QFooter',
              'QDrawer',
              'QPageContainer',
              'QPage',
              'QInnerLoading',
            ],
            directives: [ // manually specify directives
              'ClosePopup',
              'Ripple',
            ],
            plugins: [
            ],
          },
          
          // ...
          
          build: {
          
            // ...
      
            extendWebpack(cfg, { isServer, isClient }) {
              if(isClient) {
                // delete the auto import loader
                for(const rule of cfg.module.rules) {
                  if(rule.use.length && rule.use[0].loader.endsWith('loader.auto-import-client.js')) {
                    rule.use.splice(0, 1)
                    break
                  }
                }
              }
            },
          },
      
          // ...
      
        }
      }
      

      I hope it helps someone who is trying to reduce their bundle size.

      posted in Useful Tips (NEW)
      beets
      beets
    • RE: Select placeholder with no use-input?

      @rubs It’s actually in the docs, but as a more complicated example. I just simplified it a bit to make it more clear. https://quasar.dev/vue-components/select#Example--Selected-item-slot

      posted in Help
      beets
      beets
    • RE: How to use SASS variables in template

      @imagine You can use Quasar’s color helper:

      <template>
        <circle :fill="primary" class=“breath-circle” cx=“50%” cy=“50%” :r=“radius” />
      </template>
      <script>
      import { colors } from 'quasar'
      export default {
        data() {
          return {
            primary: colors.getBrand('primary')
          }
        }
      }
      </script>
      
      posted in Framework
      beets
      beets