Quasar & Quasar CLI v0.17.0 are out! SSR arrived.


  • Admin

    Hi All,

    I’m announcing with great pleasure that Quasar & Quasar CLI v0.17.0 has been released. We got SSR now! Thank you all who contributed in any way.

    We would appreciate if you could take a moment of your time to also visit our Patreon campaign. Help Quasar grow even more.

    Now onwards with the Release Notes (first Quasar then Quasar CLI). And enjoy!

    Quasar v0.17.0

    Breaking Changes

    Only one, regarding QLayoutDrawer mini-width prop. It is now a Number (instead of String!) defining width in pixels (default: 60).

    SSR (Server-Side Rendering) Support

    The long awaited SSR support is here! Quasar code is now isomorphic. It might be a one-liner, but this is where 90% of the development time was spent for this release.

    Some things worth mentioning, in order to best benefit from Quasar SSR:

    • Make sure you read the SSR docs in Guide

    • Use Platform and Cookies Quasar plugins only in their $q.platform and $q.cookies form. Do not use this outside of Vue components when building with SSR mode. This restriction is required because of the singleton problem when dealing with SSR.

    • The LocalStorage and SessionStorage Quasar plugins cannot be used on server-side. The reasons are obvious since this is a client-side only browser API. DO NOT rely on web storage for your SSR website. You can use Cookies Quasar plugin instead, which does work on server-side too.

    • Make your QBtns SEO-friendly. Quasar offers this functionality out of the box, but you need to know how to enable it:

      Quasar buttons are usually rendered with a <button> HTML tag. But now they can be rendered with <a> tags too. When you want the user to navigate to another route by clicking a QBtn, you were used to add a @click Vue event handler. Now you can use <router-link>-like props on QBtn too. By doing this, Quasar can make your buttons SEO friendly.

      <!-- old way, still works, but not SEO friendly -->
      <q-btn @click="$router.push('/some/route')" ... />
      <!-- new, equivalent way -->
      <q-btn to="/some/route" ... />
      
      <!-- old way, still works, but not SEO friendly -->
      <q-btn @click="$router.replace('/some/route')" ... />
      <!-- new, equivalent way -->
      <q-btn to="/some/route" replace ... />
      
      <!-- old way, still works, but not SEO friendly -->
      <q-btn @click="$router.push({ name: 'special' })" ... />
      <!-- new, equivalent way -->
      <q-btn :to="{ name: 'special' }" ... />
      

    Quasar Meta plugin

    The Meta plugin can change page title, manage meta tags, manage <html> & <body> DOM element attributes, add/remove/change <style> tags in head (useful for CDN stylesheets, for example), or manage <noscript> tags. Note that this is not a full description of what the Meta plugin can do (that will be available in the final release notes).

    Installation:

    // quasar.conf.js
    framework: {
      // ...
      plugins: ['Meta']
    }
    

    What this does is that it enables the use of a special property in your Vue components called meta. Example below, with almost all of its features:

    // some .vue file
    
    export default {
      // ...
      meta: {
        title: 'Index Page', // sets document title
        titleTemplate: title => `${title} - My Website`, // optional; sets final title as "Index Page - My Website", useful for multiple level meta
        meta: {
          description: { name: 'description', content: 'Page 1' },
          keywords: { name: 'keywords', content: 'Quasar website' },
          equiv: { 'http-equiv': 'Content-Type' content: 'text/html; charset=UTF-8' }
        },
        link: {
          material: { rel: 'stylesheet', href: 'https://fonts.googleapis.com/icon?family=Material+Icons' }
        },
        script: {
          ldJson: {
            type: 'application/ld+json',
            innerHTML: `{ "@context": "http://schema.org" }`
          }
        },
        htmlAttr: {
          'xmlns:cc': 'http://creativecommons.org/ns#' // generates <html xmlns:cc="http://creativecommons.org/ns#">
        },
        bodyAttr: {
          'action-scope': 'xyz', // generates <body action-scope="xyz">
          empty: undefined // generates <body empty>
        },
        noscript: {
          default: 'This is content for browsers with no JS (or disabled JS)'
        }
      }
    }
    

    Metas are computed from .vue files in the order they are activated by Vue Router (let’s call this a chain for further explanations). Example: App.vue > SomeLayout.vue > IndexPage.vue > …?

    Notice that all properties (except for title and titleTemplate) are Objects; you can override meta props defined in previous Vue components in the chain by using these keys again. Example:

    // first loaded Vue component
    meta: {
      meta: {
        myKey: { name: 'description', content: 'My Website' }
      }
    }
    
    // a subsequent Vue component in the chain;
    // this will override the first definition on "myKey"
    meta: {
      meta: {
        myKey: { name: 'description', content: 'Page 1' }
      }
    }
    

    The “meta” properties can be dynamic. For example, this is how you can bind to the Vue scope with them. Think of them as a Vue computed property.

    export default {
      data () {
        return {
          title: 'Some title' // we define the "title" prop
        }
      },
      meta () {
        return {
          // this accesses the "title" property in your Vue "data";
          // whenever "title" prop changes, your meta will automatically update
          title: this.title
        }
      },
      methods: {
        setAnotherTitle () {
          this.title = 'Another title' // will automatically trigger a Meta update due to the binding
        }
      }
      // ...
    }
    

    New

    • QNoSsr component; highly optimized - takes into consideration client takeover so subsequent renders are as fast as possible.

    • QJumbotron component

    • QTable as grid

    qtable-grid

    <q-table
      grid
      hide-header
      :data="data"
      :columns="columns"
      :filter="filter"
      :selection="selection"
      :selected.sync="selected"
      :visible-columns="visibleColumns"
      row-key="name"
    >
      <template slot="top-right" slot-scope="props">
        <q-search hide-underline v-model="filter" />
      </template>
    
      <div
        slot="item"
        slot-scope="props"
        class="q-pa-xs col-xs-12 col-sm-6 col-md-4 col-lg-3 transition-generic"
        :style="props.selected ? 'transform: scale(0.95);' : ''"
      >
        <q-card class="transition-generic" :class="props.selected ? 'bg-grey-2' : ''">
          <q-card-title class="relative-position">
            <q-checkbox v-model="props.selected" :label="props.row.name" />
          </q-card-title>
          <q-card-separator />
          <q-card-main class="q-pa-none">
            <q-list no-border>
              <q-item v-for="col in props.cols.filter(col => col.name !== 'desc')" :key="col.name">
                <q-item-main>
                  <q-item-tile label>{{ col.label }}</q-item-tile>
                </q-item-main>
                <q-item-side right>
                  <q-item-tile>{{ col.value }}</q-item-tile>
                </q-item-side>
              </q-item>
            </q-list>
          </q-card-main>
        </q-card>
      </div>
    </q-table>
    
    • New quasar.conf > framework > config Object

      framework: {
        // ...
        config: {
          brand: { // this will NOT work on IE 11
            primary: '#e46262',
            // ... or all other brand colors
          },
          notify: {...}, // default set of options for Notify Quasar plugin
          loading: {...}, // default set of options for Loading Quasar plugin
          loadingBar: { ... }, // settings for LoadingBar Quasar plugin
          cordova: {
            iosStatusBarPadding: true/false, // add the dynamic top padding on iOS mobile devices
            backButtonExit: true/false // Quasar handles app exit on mobile phone back button
          }
        }
      }
      

      The Quasar UMD version will define the following before including the Quasar script tag:

      <script>
        // optional
        window.quasarConfig = { .... }
      </script>
      
    • Loading & Notify Quasar plugins now support a new method: setDefaults({...}) – use this or through quasar.conf > framework > config: { loading: {...}, notify: {...} }

    • (NEW) LoadingBar Quasar plugin – adds a loading bar a-la Youtube; no need to do anything else in your app code

      It will catch all Ajax calls and even hook into asyncData() automatically, but if you have some custom actions, you can programmatically start/stop it by calling this.$q.loadingBar.start() and this.$q.loadingBar.stop() in your Vue components. Outside of Vue components, you can import { LoadingBar } from 'quasar' then call LoadingBar.start()/stop()/increment(val)

      LoadingBar is using QAjaxBar component under the covers, so you can configure it using QAjaxBar props like this:

      // quasar.conf
      framework: {
        // ...
        plugins: [
          // ...
          'LoadingBar'
        ],
      
        config: {
          // optional:
          loadingBar: { ... } // QAjaxBar props
        }
      }
      
    • Hook AddressbarColor into cordova-plugin-statusbar when available

    • QLayoutDrawer

      • “mini-width” prop is now a Number (instead of String!) defining width in pixels (default: 60)
      • new prop: width defining width in pixels for non-mini mode (default: 300)
      • backdrop animation when in mobile mode
    • Many performance enhancements

      • layout header/footer animation
      • tweaks to make Vue render Quasar components faster by avoiding some children array normalization
    • Ability to cancel frameDebounce() from utils (call .cancel() on the debounced function)

    • QUploader: added upload-factory prop (#2093)

    • QInput: added prop lower-case (#2117)

    • New Quasar language packs: Traditional Chinese, Guarani

    • [Request] Q-Chips-Input: Pass upper-case parameter to underlying q-input #2031

    • [Request] QUploader - new prop: no-content-type #1974

    • Q-Page: ability to disable minHeight prop. (#2120)

    • Allow Dialog and ActionSheet Quasar plugins to receive a resolver token (#1869)

    • Improve out of the box SEO for QItem, QBreadcrumbs, QRouteTab – allow bots to follow links

    • Improve keyboard navigation (#1936)

      • QModal
        • fix not emmiting dismiss on ESC
        • focus after show
        • add back noRefocus
      • QPopover
        • focus after show
        • refocus after hide (noRefocus prop)
        • allow toggle by keyboard ENTER
      • QDialog
        • fix button focus
        • find button by class name (q-btn)
        • add back noRefocus
      • QFab - refocus after hide
      • QTab / QRouteTab
        • focus with keyboard
        • allow select with keyboard ENTER
      • QColor and QDatetime
        • fix tabindex lost on close
        • fix popup not closing on blur
      • QColorPicker
        • fix incorrect tabindex on disable
        • allow to select saturation on click on mobile (was working only on drag)
      • QDatetimePicker.mat - allow keyboard adjustment of all values
      • QActionsheet
        • select option with ENTER/SPACE, navigation with TAB
        • trigger selection on keyup to avoid click on original button
      • QAutocomplete - use QPopup with noFocus
      • QKnob, QSlider, QRange - keyboard updating (UP|RIGHT +, DOWN|LEFT -, CTRL+… 10*)
      • QSelect
        • remove autofocusFilter - always select filter if present
        • move keyboard navigation on QPopover
        • delay popover.reposition on filter to allow resize
      • v-back-to-top, v-close-overlay, v-go-back, v-ripple - trigger by keyboard ENTER
      • QInput - set model on ENTER
    • Flex addons – provide breakpoint aware versions for all flex (and display) related classes – needs to be enabled from quasar.conf > framework > cssAddon: true

        .flex-<bp>-(block|inline)
        .(row|column|flex)-<bp>(|-inline)
        .reverse-<bp>
        .(wrap|no-wrap|reverse-wrap)-<bp>
        .order-<bp>-(first|last|none)
        .justify-<bp>-(start|end|center|between|around)
        .items-<bp>-(start|end|center|baseline|stretch)
        .content-<bp>-(start|end|center|between|around)
        .self-<bp>-(start|end|center|baseline|stretch)
        .flex-<bp>-center
        .gutter-<bp>(|-x|-y)-(xs|sm|md|lg|xl)
        .(col|offset)-<bp>(|0..12)
      
    • QModal: Auto size based on content, can be customized through CSS with (min|max)(Width|Height) passed in :content-css. It’s also applied when using QModalLayout.

    • Flex: .col has higher grow priority than .col-auto and .col-grow, add .(col|offset)-0

    • Material ripple - centered on round buttons (as per Material guidelines)

    • [Request] debounce property in QScrollObservable component #2169

    • QTabs - desktop state hover

    • QScrollArea - added hover effect (#2189)

    • Lots of enhancements to QAjaxBar to best accommodate SSR mode

    • Use round/rounded focus helper for radio/checkbox (#2200)

    • QBtnDropdown - popover-anchor & popover-self props (to align its popover) #1665

    • Enhancements for clearable and clearValue (#2216) #1994

    • Loading plugin default delay was set to 0

    • feat: add v-scroll horizontal position #2296

    • Finalized helper-json files

    • Set xhr’s withCredentials flag in request for QUploader #2305

    • QTabs new prop: “panes-container-class”

    Fixes

    • QTab’s count and alert attributes look bad when QTabs’s align is set to justify #2032
    • [Bug] QSelect throws an error when container block disappears #2071
    • QInput shouldn’t have expanding underline animation on focus in iOS theme #2085
    • v-scroll-fire get fired when reload the page #2081
    • Q-Pagination Direction-Links Broken on Cordova #2088
    • v-model on QLayoutDrawer bug #2094 – example: having an input component breaks the model of QLayoutDrawer
    • iPhoneX - avoid toolbar/header top-padding when not in fullscreen #2118
    • [Regression] Dialog is not blurring input element in v0.15.15/v0.16 #2137
    • QBtnDropdown Popover does not scroll #2136
    • QTable: [MS-Edge] Loading indicator not displayed correctly #2122
    • QSearch: propagate upper-case and lower-case #2124
    • Force QChip pointing zIndex (when in an already positioned context) #1982
    • QPullToRefresh causing scroll issues with large tables inside QScrollArea #1884
    • QColorPicker - process unset value #1887
    • Error when navigating to other page while select component is opened #1980
    • Return dismiss function from Notify.create #1913
    • Fix material ripples on IE 11
    • QAutocomplete: Fix focus after tab, fix loop in normalizeInterval, fix reposition, select options on keyboard navigation #2149 #2155 close #2157
    • Override Material Icons CDN default size
    • Colors utils: Fix guard for color type in luminosity (#2180)
    • Japanese Quasar language pack: 2 words changed for more natural Japanese (#2182)
    • Mini layout drawer animates like normal mode when it hides automatically #2080
    • Infinite scroll delay after resume #2187
    • animScrollTo util - bug if duration < 16 after recursion (#2192)
    • Fix dom.ready() util when readyState = “interactive” (#2199)
    • QSelect: fix blur when selecting clear (#2202)
    • QBreadcrumbs and align mixin - fix align prop value check (#2217)
    • className for QSelect, QSearch before|after, QInput focus on marginalia (#2215)
    • Screen plugin doesn’t updates values after setting new sizes #2221
    • QRouterTab: fix double tabindex and hide outline (#2226)
    • QPopover: only refocus on anchorEl when anchorClick (solves refocusing on split dropdown) (#2225)
    • Fix QAutocomplete when not editable and pass readonly to QInputFrame (#2227)
    • Fix internal popups.getPosition corner-case (#2235)
    • QDatetime: use a ±50 years interval from current, use precomputed mins (#2234)
    • QDatetime: format when type is date and formatModel is string (#2231)
    • Fix not caught rejected promises when showing/hiding modals, dialogs, popovers
    • fix: QTable - sort by non visible column #1886
    • fix: QInfiniteScroll concurrency issue #2241
    • fix(QSelect): does not show placeholder text when using chips (fix #2003) #2287
    • fix: Corner case in model-toggle where component gets destroyed & not cleans up history on Cordova

    Quasar CLI v0.17.0

    New

    SSR mode (+ optional PWA takeover)

    • quasar.conf.js new prop, specific to SSR (all sub-props are optional)

      // quasar.config.js
      return {
        // ...
        ssr: {
          pwa: true/false, // should a PWA take over (default: false), or just a SPA?
          componentCache: {...} // lru-cache package options
        }
      }
      
    • Production server
      When you add SSR mode ($ quasar mode -a ssr), you will notice a new folder gets created: /src-ssr. This folder contains your production server which you can fully customize. You can learn more about it by opening /src-ssr/index.js file and reading the comments.

    • App plugins

      • Ability to tell if to get embedded only on server or only on client
        // quasar.conf.js
        return {
          // ...
          plugins: [
            'some-plugin',
            { path: 'some-other', server: false } // this plugin gets embedded only on client-side
            { path: 'third', client: false } // this plugin gets embedded only on server-side
          ]
        }
        
      • The default exported function now has one more property received on its Object parameter:
        // some app plugin
        export default ({ app, ..., ssrContext }) => {
          // ssrContext has: { url, req, res }
        
          // You can add props to the ssrContext then use them in the src/index.template.html.
          // Example - let's say we ssrContext.someProp = 'some value', then in index template we can reference it:
          // {{ someProp }}
        }
        
        When you add such references ({{ someProp }}) into your src/index.template.html, make sure you tell Quasar it’s only valid for SSR builds:
        <!-- index.template.html -->
        <% if (htmlWebpackPlugin.options.ctx.mode.ssr) { %>{{ someProp }} <% } %>
        
    • extendWebpack() & chainWebpack() now receive one more parameter (Object), currently containing isServer or isClient boolean props:

      // quasar.conf.js
      build: {
        extendWebpack(cfg, { isServer, isClient }) { ... }
      }
      

      This is because the extendWebpack() & chainWebpack() are called twice, once for the client Webpack config and once for the server Webpack config. So by looking at the two props you can make decisions.

    SSR mode (ONLY) small required changes

    These changes will be required by the Quasar CLI only when you build with SSR mode. After doing these changes you’ll still be able to build the other modes (SPA/PWA/Cordova/Electron) too.

    src/router/index.js

    You need to have a default export set to “function ({ store })” which returns a new instance of Router instead of default exporting the Router instance itself.

    // OLD WAY
      import Vue from 'vue'
      import VueRouter from 'vue-router'
      import routes from './routes'
      Vue.use(VueRouter)
    
      // in the new way, we'll wrap the instantiation into:
      // export default function ({ store }) --> store is optional
      const Router = new VueRouter({
        scrollBehavior: () => ({ y: 0 }),
        routes,
        // Leave these as they are and change from quasar.conf.js instead!
        mode: process.env.VUE_ROUTER_MODE,
        base: process.env.VUE_ROUTER_BASE,
      })
    
      // in the new way, this will be no more
      export default Router
    
    // NEW WAY
      import Vue from 'vue'
      import VueRouter from 'vue-router'
      import routes from './routes'
      Vue.use(VueRouter)
    
      // DO NOT import the store here as you will receive it as
      // parameter in the default exported function:
    
      export default function (/* { store } */) {
        // IMPORTANT! Instantiate Router inside this function
    
        const Router = new VueRouter({
          scrollBehavior: () => ({ y: 0 }),
          routes,
          // Leave these as they are and change from quasar.conf.js instead!
          mode: process.env.VUE_ROUTER_MODE,
          base: process.env.VUE_ROUTER_BASE,
        })
    
        return Router
      }
    

    src/store/index.js

    You need to have a default export set to “function ()” which returns a new instance of Vuex Store instead of default exporting the Store instance itself.

    Some of you might need the Router instance on the Store. It is accessible through this.$router inside your actions, mutations, etc.

    // OLD WAY
      import Vue from 'vue'
      import Vuex from 'vuex'
      import example from './module-example'
      Vue.use(Vuex)
    
      // in the new way, we'll wrap the instantiation into:
      // export default function ()
      const store = new Vuex.Store({
        modules: {
          example
        }
      })
    
      // in the new way, this will be no more
      export default store
    
    // NEW WAY
      import Vue from 'vue'
      import Vuex from 'vuex'
      import example from './module-example'
      Vue.use(Vuex)
    
      export default function () {
        // IMPORTANT! Instantiate Store inside this function
    
        const Store = new Vuex.Store({
          modules: {
            example
          }
        })
    
        return Store
      }
    

    Also, if you want to be able to access the Router instance from vuex actions, mutations, etc, you need to make some simple changes (in all of them):

    // OLD WAY:
    export const someAction = (context) => { ... }
    
    // NEW WAY:
    export function someAction (context) {
      // now we have access to:
      this.$router
    }
    

    App Plugins

    Here’s a full list of the parameters for app plugins:

    // ...
    export default ({
      app, // the Vue instantiation Object for the app
      router, // the instantiated Vue Router of your app
      store, // the instantiated Vuex Store (if using one)
      Vue, // so you don't need to "import Vue from 'vue'"
      ssrContext // the SSR context (when running server-side only)
    }) => {
      // ...
    }
    

    Quasar “serve” command

    “$ quasar serve <…options>” creates a customizable and optimized ad-hoc static webserver that can be used for quick testing (or on production too!).

    Type “$ quasar serve -h” for all the options.

    # example
    $ quasar serve dist/spa-mat
    

    Tree-shaking on Quasar

    This has been improved near to perfection, avoiding some webpack pitfalls. The final build of your app will include ONLY & EXACTLY what you cherry-picked from Quasar.

    New quasar.conf option

    • build > showProgress: true/false – show/hide the compilation progress

    Other new features

    • Vue component new instantiation method: preFetch() support (for all Quasar modes, not just SSR) – works great along with the new LoadingBar Quasar plugin

      // .vue component
      <template>...</template>
      <script>
        export default {
          ...,
          preFetch ({ store, currentRoute, previousRoute, redirect, ssrContext }) {
            // fetch data etc...
            // return a Promise
      
            // ssrContext is available only server-side
      
            // no access to "this" here as preFetch() is called before
            // the component gets instantiated
          }
        }
      </script>
      

      However, preFetch() is a feature on demand, so if you want to use it, you need to enable it in quasar.conf:

      // quasar.conf
      preFetch: true
      
    • In app code, you can now use new booleans process.env.SERVER and process.env.CLIENT which tell if code is run on server or on client. Useful for decision making, since for SSR apps the code must be isomorphic.

    Improvements

    • Updated deps: webpack 4.16.1, register-service-worker 1.4.1, css-loader v1.0.0, quasar-extras v2.0.4 (Material Icons CDN v38, Fontawesome 5.1.0, Ionicons 4.2.4)
    • Extract CSS to /css instead of root #141
    • Ability to specify node_module paths on quasar.conf > css (example: ‘~flag-icon-css/css/flag-icon.css’) #136
    • Improve Cordova deps install after repo clone/fork
    • Follow webpack recommendations of not using filename hashes for dev build #122
    • Detecting if Quasar CLI is run on a CI or minimal terminal and avoiding some pitfalls (like trying to open up a browser if quasar.conf > devServer: open is “true”)
    • New Webpack compile progress bar – most important feature: takes into account parallel compilations
    • Information banners on dev/build commands at startup and finish
    • Enable webpack scope hoisting by default (can be disabled by quasar.conf > build > scopeHoisting: false)
    • Bring back quasar.conf > vendor > add, remove arrays to configure what gets into the vendor bundle and what not.

    Fixes

    • Quasar commands problem on Windows Node v10.3 #130
    • quasar init -v <any version> returns error #133
    • vuex store presence is not detected if not using .js file extension #135


  • Like Andy Kaufman would say:
    https://youtu.be/2ymMyY4GNAE



  • Great job!



  • What a brilliant release. Fixed a lot of small things I’ve had issues with.
    Cheers!



  • Thank you so much for this great job.


  • Admin

    A small followup of v0.17 release. Please upgrade to v0.17.1 as soon as possible.

    Fixed some reported issues:

    • fix: Weird webpack behavior regarding css files
    • fix: Vendor chunk changing filename on every build
    • fix(PWA): service-worker.js path for production
    • [Bug] .17 - Notify defaults #2322

    Enjoy!



  • @rstoenescu Thanks for quickly fixing that notify bug! 🙂


  • Admin

    @Nicholas Thanks for reporting it! Wish this was caught during the beta stage.


  • Admin

    Another batch of fixes. If you are on v0.17, please upgrade to v0.17.2. Enjoy!

    QUASAR

    Input: outline and textarea height (#2328)

    CLI

    fix: bug with q-app div detection when using Pug #150
    fix: helper path for logger when running a command outside of a Quasar project folder (#152)
    fix: Quasar source code transpiling



  • raz on fire! thanks


  • Admin

    And well underslept!



  • So many goodies in this release. Using QTable grid will be my first refactor but I’ll probably be implementing all of the new features.

    I have a question about preFetch. How does it differ from beforeCreate?

    Thank you for this very easy and smooth upgrade.


  • Admin

    @RichAyotte It can be async (remember to return a Promise in this case), it runs before the route is validated, it has access to all of its params (redirect, currentRoute, previousRoute, ssrContext etc etc). The doc page on PreFetch Feature should explain it all.