Navigation

    Quasar Framework

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

    beets

    @beets

    Global Moderator

    86
    Reputation
    47
    Profile views
    332
    Posts
    3
    Followers
    0
    Following
    Joined Last Online

    beets Follow
    Global Moderator

    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: sending text values on uploader

      @zeppelinexpress See the form-fields prop under the Upload section: https://quasar.dev/vue-components/uploader#QUploader-API

      You can use a function to return the values from your form

      posted in Help
      beets
      beets
    • RE: Pass class to native component

      @tiago-razera Like this:

      <q-input input-class=“my-class”></q-input>
      

      6b1726da-c577-40bb-9851-9f8c8d6c72df-image.png

      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

    Latest posts made by beets

    • RE: I need suggestions

      @incremental Correct, just a file object should do, which is what the code I posted should be doing…

      Can you post the whole function call that ends with the this.$store.dispatch(“server/changeProfile”, … ) call ?

      It will help if you post that code so we can see if there’s anything obvious going on here. A blob really shouldn’t be POSTed to the server.

      posted in Help
      beets
      beets
    • RE: Carousel load all slides

      @bozaq I don’t think you’ll need to actually do anything with the image objects. Just making the object and setting the src should be enough to load them, else the browser won’t load until the slide is mounted / navigated to. I followed the answer here: https://stackoverflow.com/a/29339033 which I’m pretty sure I’ve used before, I just had to re-google to find it.

      posted in Help
      beets
      beets
    • RE: Carousel load all slides

      @bozaq Are you using the format like this?

          <q-carousel
            animated
            v-model="slide"
            arrows
            navigation
            infinite
          >
            <q-carousel-slide :name="1" img-src="https://cdn.quasar.dev/img/mountains.jpg" />
            <q-carousel-slide :name="2" img-src="https://cdn.quasar.dev/img/parallax1.jpg" />
            <q-carousel-slide :name="3" img-src="https://cdn.quasar.dev/img/parallax2.jpg" />
            <q-carousel-slide :name="4" img-src="https://cdn.quasar.dev/img/quasar.jpg" />
          </q-carousel>
      
      <
      
      

      If so, you could try something like this in a mounted hook:

      mounted() {
        const img1 = new Image()
        img1.src = "https://cdn.quasar.dev/img/mountains.jpg"
      
        const img2 = new Image()
        img2.src = "https://cdn.quasar.dev/img/parallax1.jpg"
      
        const img3 = new Image()
        img3.src = "https://cdn.quasar.dev/img/parallax2.jpg"
      
        const img4 = new Image()
        img4.src = "https://cdn.quasar.dev/img/quasar.jpg"
      
      }
      

      Edit: Note that you can be smarter the above code if you have the slides stored in some array, so you don’t have to hardcopy img1, img2, etc.

      posted in Help
      beets
      beets
    • RE: I need suggestions

      @incremental Hi, sorry for delay in responding. This code should attach a file:

      if(this.newAvatar) {
        if(this.newAvatar.file instanceof File) {
          profile_data.append('avatar', this.newAvatar.file)
        }
      }
      

      At least it does when I try and read with Nodejs instead of PHP, but that shouldn’t make a difference. Can you post the whole function call that ends with the this.$store.dispatch("server/changeProfile", ... ) call ?

      Otherwise, maybe try and remove the headers: { 'Content-Type': 'multipart/form-data' } header? I’m not sure if it’s needed with axios since I don’t use it much.

      posted in Help
      beets
      beets
    • RE: I need suggestions

      @incremental I think you have to do the second one, but I don’t use axios very often.

      posted in Help
      beets
      beets
    • RE: I need suggestions

      @incremental Instead of this:

      const profile_data = new URLSearchParams();
      profile_data.append("email", this.Email);
      ...
      profile_data.append("avatar", this.newAvatar);
      this.$store.dispatch("server/changeProfile", {	data: profile_data }); // AXIOS call
      

      Try:

      const profile_data = new FormData()
      profile_data.append("email", this.Email);
      ...
      if(this.newAvatar) {
        if(this.newAvatar.file instanceof File) {
          profile_data.append('avatar', this.newAvatar.file)
        }
      }
      
      this.$store.dispatch("server/changeProfile", {	data: profile_data }); // AXIOS call
      
      posted in Help
      beets
      beets
    • RE: I need suggestions

      @incremental

      Instead of controlling max filesize before upload, could it be simple to myself resize and compress the uploaded file, or do you recommend a light vue component ?

      I haven’t used a crop / resize component for Vue before, but I know they exists. The other way some people do is to write the image to canvas, resize, and POST the base64 data.

      to continue, I try to send this blob to my profile route API on October CMS

      So it maybe sounds like you are already using the canvas trick I mentioned above? If so, you will need to convert the blob like so: https://stackoverflow.com/a/18650249

      If you’re just trying to send the file as is, I would not send a blob to the server, Instead see the code from post #2 here:

      const formData = new FormData()
      formData.append('somefield', 'somevalue') // append other fields
      
      if(this.newAvatar) {
        if(this.newAvatar.file instanceof File) {
          formData.append('avatar', this.newAvatar.file)
        }
      }
      
      // Submit FormData through axios
      
      

      I’m not sure if October CMS handles file uploads specially, but normally they’d be in the $_FILES global: https://www.php.net/manual/en/reserved.variables.files.php

      posted in Help
      beets
      beets
    • RE: I need suggestions

      @incremental You can put a class="col" on the second q-card-section that wraps the q-file: https://codepen.io/pianopronto/pen/bGBWbgN

      posted in Help
      beets
      beets
    • RE: Using checkbox array with objects and mark selected

      Another option, where we store selected right in the options object, and optionally use a computed variable to turn it back to what you want: https://codepen.io/pianopronto/pen/XWNMeWg?editors=101

      posted in Help
      beets
      beets
    • RE: Using checkbox array with objects and mark selected

      @alhidalgodev The tricky thing, is that the objects in selection are not equal to the objects in options. They need to pass an equality test (i.e. ==) in order to be considered selected. This will depend on how you want to define the pre-selected options, if it’s hardcoded or from vuex / api. See this example for the most basic idea:
      https://codepen.io/pianopronto/pen/wvoJqLV?editors=1010

      posted in Help
      beets
      beets