<?xml version="1.0" encoding="utf-8"?> <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"> <channel> <title>VueDose</title> <link>https://vuedose.tips</link> <description>Learn the best tips and tricks about Vue.js, Nuxt and JavaScript and find the best articles about Web Development</description> <lastBuildDate>Sun, 23 Apr 2023 04:11:38 GMT</lastBuildDate> <docs>http://blogs.law.harvard.edu/tech/rss</docs> <generator>https://github.com/nuxt-community/feed-module</generator> <category>3D</category> <category>Accessibility</category> <category>Advanced</category> <category>API</category> <category>Best Practices</category> <category>Components</category> <category>Composition API</category> <category>CSS</category> <category>Deploy</category> <category>Devtools</category> <category>DX</category> <category>Gaming</category> <category>Headless CMS</category> <category>i18n</category> <category>JAM Stack</category> <category>Library</category> <category>Nuxt</category> <category>SEO</category> <category>Testing</category> <category>UI</category> <category>Use Cases</category> <category>Video</category> <category>Vue 3</category> <category>Vue.js</category> <category>Vuex</category> <category>Web Performance</category> <item> <title><![CDATA[The new Provide and Inject in Vue 3]]></title> <link>https://vuedose.tips/the-new-provide-inject-in-vue-3/</link> <guid>https://vuedose.tips/the-new-provide-inject-in-vue-3/</guid> <pubDate>Mon, 18 Jul 2022 02:00:00 GMT</pubDate> <description><![CDATA[Getting stuck into the prop drilling? Learn how provide/inject can make your components more flexible and independent in this short tutorial.]]></description> <content:encoded><![CDATA[<p>In Vue we use <code>props</code> to pass data between components.</p> <p>However, in more complex applications, we may need to pass data across multiple layers to a lower level component. In such cases, components pass certain props down without needing them themselves. This pattern is known as <strong>prop drilling</strong>.</p> <p>For developers of plugins or component libraries, problems also arise with providing data. These are even more severe because of the lack of access to the consuming application.</p> <p>These problems can be bypassed by the two methods <code>provide</code> and <code>inject</code>. Through so-called dependency providers, data can be passed to any lower-level component, no matter how deep it is. Completely without prop drilling.</p> <pre><code class="language-js">import { provide } from 'vue' export default { setup() { provide(/* key */ 'count', /* value */ 0) } } </code></pre> <pre><code class="language-vue"><Parent> <Child> <Grandchild><Grandchild> </Child> </Parent> </code></pre> <pre><code class="language-js">import { inject } from 'vue' export default { setup() { const count = inject(/* key */ 'count') console.log(count) // 0 } } </code></pre> <p>However, this becomes particularly useful when working with reactive data and methods. For example, consider a table component whose columns can be defined by components rather than <code>props</code>:</p> <pre><code class="language-vue"><MyTable> <MyColumn key="id" label="Identifier" /> <MyColumn key="name" label="First name" :sortable="true" /> <MyColumn key="email" label="Email address" :sortable="true" /> </MyTable> </code></pre> <p>We could create such functionality by using <code>provide</code>and <code>inject</code>:</p> <pre><code class="language-js">import { provide, ref } from 'vue' export default { setup() { const columns = ref<Column[]>([]) provide('TableKey', { columns, }) } } </code></pre> <pre><code class="language-js">import { inject } from 'vue' export default { props: ['key', 'label', 'sortable'] setup(props) { const table = inject('TableKey') table?.columns.push(props) } } </code></pre> <p>If we take a closer look at this example, we can see several pitfalls that should be avoided right from the start. For this, Vue offers some recommendations and optimization options.</p> <h2><code>Symbol</code> and relocation of keys</h2> <p>The key used is specified as an inline string and therefore does not scale well. We cannot ensure that nobody else uses this key or that we do not mistype. Therefore it is a good idea to store and export the keys in a separate file and make them more collision-proof by using a <code>symbol</code> declaration (see <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol">MDN</a>).</p> <pre><code class="language-ts">// keys.ts export const MY_TABLE_KEY = Symbol('MY_TABLE_KEY') // in provider component import { MY_TABLE_KEY } from '@/keys' provide(MY_TABLE_KEY, {...}) // in injector component import { MY_TABLE_KEY } from '@/keys' const injected = inject(MY_TABLE_KEY) </code></pre> <h2>Data typing</h2> <p>However, the use of constants does not help with proper typing. <code>provide</code> and <code>inject</code> are usually defined in different components, so we can never really be sure in both places that we are not mistyping and using the data correctly. Did we really name the variable in <code>MyTable</code> <code>columns</code> or was it <code>cols</code> after all?</p> <p>Therefore the type <code>InjectionKey</code> should be used. This will ensure synchronization of the type between the provider and consumer.</p> <p>Translated with <a href="http://www.deepl.com/Translator">www.DeepL.com/Translator</a> (free version)</p> <pre><code class="language-ts">// keys.ts import { InjectionKey } from 'vue'; import { TableConfig } from '@/types'; export const MY_TABLE_KEY: InjectionKey<TableConfig> = Symbol('MY_TABLE_KEY'); // in provider component... Type Error provide(MY_TABLE_KEY, {}); // in injector component const injected = inject(MY_TABLE_KEY) // typed as TableConfig | undefined injected.columns // safe access through autocompletion </code></pre> <h2>Default values and guaranteed provision of data</h2> <p>Since there is no guarantee that <code>provide</code> was actually called further up the tree, the return value of <code>inject</code> is always nullable. <code>inject</code> allows a second parameter, which makes it possible to specify a default value, but this does not help us to map the desired functionality and is only useful for static data.</p> <p>Therefore it is recommended to write a helper function for <code>inject</code> which checks if the data was really provided and throws an exception otherwise.</p> <pre><code class="language-ts">function requireInjection<T>(key: InjectionKey<T>, defaultValue?: T) { const resolved = inject(key, defaultValue); if (!resolved) { throw new Error(`${key} was not provided.`); } return resolved; } </code></pre> <p>If we now use this helper function in <code>MyColumn</code>, we can access the data completely safely.</p> <pre><code class="language-ts">import { inject } from 'vue' import { MY_TABLE_KEY } from '@/keys' import { requireInjection } from '@/utils' export default { props: ['key', 'label', 'sortable'] setup(props) { const table = requireInjection(MY_TABLE_KEY) // autocompletion and without optional chaining, since it is safe to use table.register(props) } } </code></pre> <h2>Immutability</h2> <p>Finally, we turn our attention to the reactive property <code>columns</code>. The provided data should always be protected from direct manipulation to ensure traceability and a consistent data flow. For this we use the <code>readonly</code> function. Functions (see <code>register</code>) should be provided by which a controlled manipulation in the provider can be guaranteed.</p> <pre><code class="language-ts">import { provide, ref, readonly } from 'vue' import { MY_TABLE_KEY } from '@/keys' export default { setup() { const columns = ref([]) function register(column: Column) { columns.value.push(column) } provide('TableKey', { columns: readonly(columns), register }) } } </code></pre> <h2>Wrapping Up</h2> <p>At this point, you already know how you can make your components more flexible and independent by using provide/inject. Tell me, where do you plan to use them?</p> <p>In <a href="https://conf.vuejs.de/">Vue.js Conf</a> we'll cover this topic in depth and many others, from Pinia by its author Eduardo (Posva) to testing and sharing all news on the Vue ecosystem.</p> <p>Will I see you in Berlin? 😉 As a VueDose reader, grab <strong>your 20% <a href="https://conf.vuejs.de/tickets/?voucher=community-vuejs-vuedose-NEI6-REDUCED">discounted ticket</a></strong> if you don't have it yet!</p> ]]></content:encoded> </item> <item> <title><![CDATA[The 101 guide to Script Setup in Vue 3]]></title> <link>https://vuedose.tips/the-101-guide-to-script-setup-in-vue-3/</link> <guid>https://vuedose.tips/the-101-guide-to-script-setup-in-vue-3/</guid> <pubDate>Mon, 20 Jun 2022 08:01:00 GMT</pubDate> <description><![CDATA[Don't you know about Script Setup yet? Check out this short article now and learn the nicest way to define a Vue component right now!]]></description> <content:encoded><![CDATA[<p>With the release of the Composition API, the new component option <code>setup</code> has also made its way into Vue. With this, we no longer need other options in most cases and write all our code within this function. The idea of <code><script setup</code> is that you want to get rid of the unnecessary wrapper and the other old component options so you can write components simpler and focused.</p> <pre><code class="language-vue"><script lang="ts"> import { defineComponent, ref } from 'vue' export default defineComponent({ setup() { const count = ref<number>(0) function increment() { count.value++ } return { count, increment } } }) </script> </code></pre> <pre><code class="language-vue"><script setup lang="ts"> import { ref } from 'vue' const count = ref<number>(0) function increment() { count.value++ } </script> </code></pre> <p>With the <code><script setup></code> syntax we can write our component logic more compactly. Even a <code>return</code> statement is no longer needed. Every variable and method we define is automatically provided in the template.</p> <p>We can also use imported data like functions and even components directly. Components don`t even have to be registered separately anymore:</p> <pre><code class="language-vue"><script setup lang="ts"> import { ref } from 'vue' import { format } from '@/utils/currency' import MyComponent from '@/components/MyComponent.vue' const count = ref<number>(0) function increment() { count.value++ } </script> <template> <button @click="increment">{{ format(count) }}</button> <MyComponent /> </template> </code></pre> <h2>Props and Events</h2> <p>To declare options like <code>props</code> and <code>emits</code> we have to use so called compiler macros which are automatically available inside <code><script setup></code>.</p> <pre><code class="language-vue"><script setup lang="ts"> const props = defineProps({ msg: String }) const emit = defineEmits(['update', 'remove']) </script> </code></pre> <p>The two functions do not need to be imported and are compiled away when <code><script setup></code> is preprocessed.</p> <p><code>defineProps</code> takes the same value as the <code>props</code> option, while <code>defineEmits</code> takes the same value as the <code>emits</code> option.</p> <p>If we are already using TypeScript in our application anyway, then we can declare <code>props</code> and <code>emits</code> with a plain type syntax as well:</p> <pre><code class="language-ts">const props = defineProps<{ msg: string count?: number }>() const emit = defineEmits<{ (e: 'update', id: number): void (e: 'remove'): void }>() </code></pre> <p>Both approaches cannot be combined, i.e. either we use the generic or the function brackets syntax, but never both at the same time. If we decide to go the TypeScript way, it is important to mention that we cannot currently use imported types or interfaces, but only type literals (see example) or types/interfaces defined in the same file.</p> <p>In the case of events, we can specify the parameters more precisely ourselves and thus write type-safe code.</p> <h3>Default values for <code>props</code></h3> <p>Above in the example we have marked the prop <code>count</code> as optional, but here we lack a possibility to specify a default value. For this reason there is another compiler macro called <code>withDefaults</code>.</p> <pre><code class="language-ts">interface Props { msg: string count?: number } const props = withDefaults(defineProps<Props>(), { count: 0 }) </code></pre> <p>If <code>withDefaults</code> is not used, but a prop is marked as optional in the type, Vue treats this <code>prop</code> as required.</p> <h2>Using Slots and Attributes</h2> <p>Normally, slots and attributes in components are used directly in the template via <code>$slots</code> and <code>$attrs</code> and rarely if ever needed in <code>script</code>. If it should be necessary nevertheless, Vue provides appropriate functions with <code>useSlots</code> and <code>useAttrs</code>.</p> <pre><code class="language-vue"><script setup lang="ts"> import { useSlots, useAttrs } from 'vue' const slots = useSlots() const attrs = useAttrs() </script> </code></pre> <h2>Use additional options</h2> <p>In some cases it is necessary that we declare custom options in our components. Vuelidate's <code>validations</code> option is just one popular example. These other options, which include setting a component name via <code>name</code>, are not possible with <code><script setup></code>.</p> <p>The solution to these problems is to use an additional <code>script</code> tag. The two blocks are then automatically merged when the <code>*.vue</code> file is compiled.</p> <pre><code class="language-vue"><script> // executed only once export const componentName = 'MyComponent'; export default { name: componentName inheritAttrs: false, customOptions: {} } </script> <script setup> // executed for each component instance </script> </code></pre> <h2>Access from the outside?</h2> <p>If we use <code><script setup></code> every variable and method we define is automatically provided in the template, but the component itself is closed to the outside. So we cannot access the properties and functions declared inside <code><script setup></code> via the instance of a component, for example when using <code>$parent</code> or template refs.</p> <p>To explicitly expose something, we need to use another compiler macro: <code>defineExpose</code>.</p> <pre><code class="language-vue"><script setup lang="ts"> import { ref } from 'vue' const count = ref<number>(0) function increment() { count.value++ } defineExpose({ count, increment }) </script> </code></pre> <h2>Top-level <code>await</code></h2> <p>In components we often need to request data asynchronously from a service. In <code><script setup></code> we have the <code>await</code> keyword available for this at the top level.</p> <p>A component defined in this way must be used with the <code>suspense</code> component so that Vue can take care of resolving the asynchrony and load the component properly.</p> <pre><code class="language-vue"><script setup> const user = await fetch(`/users/1`).then((d) => d.json()) </script> </code></pre> <pre><code class="language-vue"><template> <Suspense> <AsyncComponent /> </Suspense> </template> </code></pre> <p>Warning: Suspense is currently still an experimental feature, which is not recommended for productive applications.</p> <h2>Availability outside of Vue 3</h2> <p>If you are not lucky enough to be working with Vue 3 already, you will be happy to know that there is a fantastic plugin that makes <code><script setup></code> available in Vue 2 as well as Nuxt, Vite or the Vue CLI: <a href="https://github.com/antfu/unplugin-vue2-script-setup">https://github.com/antfu/unplugin-vue2-script-setup</a></p> ]]></content:encoded> </item> <item> <title><![CDATA[Hybrid Rendering: the secret way to smoothly test Vue.js components]]></title> <link>https://vuedose.tips/hybrid-rendering-the-secret-way-to-test-components-in-vuejs/</link> <guid>https://vuedose.tips/hybrid-rendering-the-secret-way-to-test-components-in-vuejs/</guid> <pubDate>Mon, 07 Mar 2022 07:00:00 GMT</pubDate> <description><![CDATA[Find out how to combine Deep and Shallow Rendering in order to achieve a flexible solution to test your Vue.js combining the best of both worlds.]]></description> <content:encoded><![CDATA[<p>In the article <a href="https://vuedose.tips/tips/deep-vs-shallow-rendering-in-vuejs-tests">Deep vs Shallow Rendering in Vue.js Tests</a> I showed you how deep and shallow rendering works to create Vue.js tests.</p> <p>Each of them have their own use case and it could depend in your way of testing and architecting as well.</p> <p>But… why not having the best of both worlds? Let’s get into Hybrid Rendering.</p> <p>The <strong>Hybrid Rendering</strong> consists on stubbing only part of the child components on a test.</p> <p>For instance, taking back the example from the tip, let's add another component called <code>FoodList</code> to the <code>App</code> component:</p> <pre><code class="language-vue"><template> <div> <h3>User List</h3> <UserList :users="['Rob Wesley']" /> <FoodList :foods="['Tomatoes', 'Carrots']" /> </div> </template> <script setup> import UserList from "./UserList"; import FoodList from "./FoodList"; </script> </code></pre> <p>Assuming <code>FoodList</code> has a similar implementation to <code>UserList</code>, and we're using deep rendering, this test:</p> <pre><code class="language-js">import { mount } from "@vue/test-utils"; import App from "@/App"; describe("App.vue", () => { it("Deep renders the App component", () => { const wrapper = mount(App); expect(wrapper.html()).toMatchSnapshot(); }); }); </code></pre> <p>will result in the following snapshot:</p> <pre><code class="language-html"><div> <h3>User List</h3> <ul> <li> Rob Wesley </li> </ul> <ul> <li> Tomatoes </li> <li> Carrots </li> </ul> </div> </code></pre> <p>Now, let's say that for whatsoever reason you want only to deep render <code>UserList</code>, but not <code>FoodList</code>.</p> <p>In that case, you can use the <code>stubs</code> option of the <code>mount</code> method in order to indicate which components must be stubbed:</p> <pre><code class="language-js">import { mount } from "@vue/test-utils"; import App from "@/App"; describe("App.vue", () => { it("Hybrid renders the app component", () => { const wrapper = mount(App, { stubs: ["FoodList"] }); expect(wrapper.html()).toMatchSnapshot(); }); }); </code></pre> <p>Notice I'm stubbing <code>FoodList</code>. That's the way to apply hybrid rendering in a test, or partial shallow/deep rendering if you're more comfortable with that name.</p> <p>The generated snapshot in this case will be like:</p> <pre><code class="language-html"><div> <h3>User List</h3> <ul> <li> Rob Wesley </li> </ul> <foodlist-stub foods="Tomatoes,Carrots"></foodlist-stub> </div> </code></pre> <p>Keep in mind that this won't work with <code>shallowMount</code>, since it already stubs all child components. So make sure to use <code>mount</code>. Learn more in the <a href="https://test-utils.vuejs.org/guide/advanced/stubs-shallow-mount.html#mount-shallow-and-stubs-which-one-and-when">Stubs and Shallow Mount section on the official docs</a></p> <p>Do you have any case in mind where this technique can be useful in your projects? Let me know on <a href="https://twitter.com/vuedose">Twitter</a>!</p> <p>That's it for today's tip!</p> <p>Stay cool 🦄</p> ]]></content:encoded> </item> <item> <title><![CDATA[How to use script setup in Nuxt 2]]></title> <link>https://vuedose.tips/how-to-use-script-setup-in-nuxt-2/</link> <guid>https://vuedose.tips/how-to-use-script-setup-in-nuxt-2/</guid> <pubDate>Mon, 14 Feb 2022 15:00:00 GMT</pubDate> <description><![CDATA[If you love Vue script setup and want to use it in your current Nuxt 2 project, the Nuxt team has made it possible to start using it today!]]></description> <content:encoded><![CDATA[<p>If you’re like me and love ❤️ the new “script setup” syntax, you’ve probably been wondering when we could start using it on our current Nuxt (2) projects. Well, the wait is over! Thanks to the Nuxt team, now it’s possible. Let’s see how, but first...</p> <h2>What is <code>script setup</code>?</h2> <p><a href="https://v3.vuejs.org/api/sfc-script-setup.html"><code><script setup></code></a> refers to the latest Vue API for authoring components. It is the <strong>recommended syntax</strong> if you're using the Composition API in Single File Components (SFC). This is what it looks like:</p> <pre><code class="language-html"><script setup> const msg = 'Hello world' </script> <template> <p>{{ msg }}</p> </template> </code></pre> <h2>How to use it in a Nuxt 2 app:</h2> <p>If you want to use it in an existing Nuxt (v2) application, you have two options:</p> <ul> <li>Use the Composition API Nuxt module, <a href="https://composition-api.nuxtjs.org/getting-started/introduction"><code>@nuxtjs/composition-api</code></a>.</li> <li>Or start migrating Nuxt with Nuxt <a href="https://v3.nuxtjs.org/getting-started/bridge/">Bridge</a>.</li> </ul> <h2>Option 1: Composition API Nuxt module</h2> <p>Starting in <strong>v0.28.0</strong>, the Composition API Nuxt module includes <a href="https://github.com/antfu/unplugin-vue2-script-setup"><code>unplugin-vue2-script-setup</code></a> which enables the <code><script setup></code> syntax <em>out of the box</em>.</p> <p>First, you will need to <strong>install</strong> the Composition API Nuxt module if you don’t have it yet (or update it to <code>v0.28.0</code> or higher). You can see the <a href="https://composition-api.nuxtjs.org/getting-started/setup">docs here</a>.</p> <pre><code class="language-bash">npm i @nuxtjs/composition-api </code></pre> <p>Then, <strong>add it</strong> in <code>nuxt.config.js</code>, under <code>buildModules</code>:</p> <pre><code class="language-js">{ buildModules: [ // https://composition-api.nuxtjs.org/getting-started '@nuxtjs/composition-api/module', ], } </code></pre> <p>After that, you can <strong>start using</strong> <code><script setup></code> in Nuxt component and pages:</p> <pre><code class="language-vue"><template> <p>{{ awesome }}</p> </template> <script setup> // *In Nuxt 3 apps you can omit importing Composition API methods* import { ref } from '@nuxtjs/composition-api'; const awesome = ref('Look, ma, script setup!!') </script> </code></pre> <p><strong>That’s it!</strong></p> <p>Only downside I see is that adding a JavaScript library makes your JS bundle bigger. So, it needs to be justified. More on that later.</p> <h2>Option 2: Nuxt Bridge</h2> <p>Nuxt Bridge is a Nuxt module that allows you to use the Nuxt 3 features of <em>tomorrow</em> into your Nuxt 2 application <em>today</em>. It sits between Nuxt v2 and v3, <em>backporting</em> Nuxt v3 features into Nuxt v2. Straight from the <a href="https://v3.nuxtjs.org/getting-started/bridge/">docs</a>:</p> <blockquote> <p><em>Using Nuxt Bridge, you can make sure your project is (almost) ready for Nuxt 3 and have the best developer experience without needing a major rewrite or risk breaking changes.</em></p> </blockquote> <p>Migrating your Nuxt app to Nuxt Bridge also requires changing your <code>nuxt</code> dependency into <code>nuxt-edge@latest</code>. Fortunately, the Nuxt team has put together an <a href="https://v3.nuxtjs.org/getting-started/bridge/#install-nuxt-bridge">installation guide</a>. I recommend you follow all the steps described in that link.</p> <p>Regarding the current status of the different Nuxt builds –Nuxt 2, Nuxt Bridge, Nuxt 3– you can refer to this <a href="https://v3.nuxtjs.org/getting-started/introduction#comparison">table</a> for a comparison between them. At least, at the current state, the Nuxt team agrees that:</p> <blockquote> <p><em><strong>Nuxt Bridge is more stable than Nuxt 3</strong> at the moment (– Feb 2022)</em></p> </blockquote> <h2>Conclusion</h2> <p>At least for me, <code><script setup></code> is *the way* of writing Vue components from now on.</p> <ul> <li>For me, it’s more fun to follow function composition patterns. Basing your logic on pure functions makes the app easier to test and maintain.</li> <li>Generally, you will need fewer lines of code to achieve the same results as we used to with the Options API.</li> <li>Everything comes up nicely organised if you group your logic by features.</li> <li>Script setup also brings a better developer experience because it has a better TypeScript IDE integration.</li> <li>One of the biggest benefits for me is that I no longer need to remember to add things in the <strong>returned object of the <code>setup()</code> function</strong> with the traditional Composition API. 😅</li> </ul> <p>Being able to use this modern syntax today in a Nuxt 2 app is a complete bliss. ✨🚀</p> <h2>Demo</h2> <p>I’ve prepared a <a href="https://stackblitz.com/edit/nuxt-starter-qymsh4?ctl=1&devtoolsheight=33&embed=1&file=pages/index.vue">live demo</a> so you can play, watch, and even break the code all by yourself.</p> ]]></content:encoded> </item> <item> <title><![CDATA[When to use a Vue Render Function]]></title> <link>https://vuedose.tips/when-to-use-a-vue-render-function/</link> <guid>https://vuedose.tips/when-to-use-a-vue-render-function/</guid> <pubDate>Thu, 27 Jan 2022 12:25:00 GMT</pubDate> <description><![CDATA[When should I use a render function in Vue.js? We’re going to cover some advanced Vue.js patterns when you need to use render functions.]]></description> <content:encoded><![CDATA[<p>There are <strong>very few</strong> cases where you should be using a render function instead of regular templates to create components in Vue.js. And the Render Functions API got slimmer in Vue 3. In this article, we’re going to see a few cases where the use of render functions is justified. By the way, if you need an introduction to render functions, make sure you read my previous post on <a href="https://vuedose.tips/introduction-to-render-functions">Render Functions</a>.</p> <p>This article is going to show you some advanced patterns, so strap in and bear with me!</p> <p><strong>TL;DR</strong>: In my opinion, you should only use render functions when you want to intercept the data passed to the component and change it, and only if you cannot do it with regular template functions and props or similar approaches.</p> <p>So, please, if you can use a template, use it. You and anyone reading the code will thank me later!</p> <p>Let’s see a few render functions use-cases. And stay until the end to see my favourite one!</p> <h2>Intercepting HTML attributes</h2> <p>One of the use-cases I’ve found while developing Design Systems based on CSS classes (ahem, Tailwind) is that you want to make certain classes appear alongside other classes in your DS. And if you want to validate that the classes that are passed are consistent with what you expect, you cannot do that with templates, unless you access the DOM or pass classes via props...</p> <p>Let me show you the principle with a live example: <br/><br/> <iframe loading="lazy" style="border:0; border-radius: 4px; overflow:hidden;" src="https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCB7IHJlZiwgZGVmaW5lQ29tcG9uZW50LCBoLCB1c2VBdHRycywgdXNlU2xvdHMgfSBmcm9tICd2dWUnXG5cbmNvbnN0IE15Q29tcG9uZW50ID0gZGVmaW5lQ29tcG9uZW50KHtcbiAgcmVuZGVyKCkge1xuICAgIHJldHVybiBoKCdkaXYnLCB7fSwgdGhpcy4kc2xvdHMuZGVmYXVsdCgpKVxuICB9XG59KVxuY29uc3QgTXlDc3NDbGFzc0ludGVyY2VwdG9yQ29tcG9uZW50ID0gZGVmaW5lQ29tcG9uZW50KGZ1bmN0aW9uIE15Q3NzQ2xhc3NJbnRlcmNlcHRvckNvbXBvbmVudCgpIHtcbiBjb25zdCBhdHRycyA9ICB1c2VBdHRycygpXG4gY29uc3Qgc2xvdHMgPSB1c2VTbG90cygpXG4gXG4gcmV0dXJuICgpID0+IGgoXG4gICAnc3BhbicsXG4gICB7XG4gICAgIC4uLmF0dHJzLFxuICAgICBjbGFzczogYXR0cnMuY2xhc3MuaW5jbHVkZXMoJ3JlZCcpID8gJ2JsdWUnIDogJydcbiAgIH0sXG4gICBzbG90cy5kZWZhdWx0KClcbiApXG5cbn0pXG48L3NjcmlwdD5cblxuPHRlbXBsYXRlPlxuICA8bXktY29tcG9uZW50IGNsYXNzPVwicmVkXCI+aGVsbG88L215LWNvbXBvbmVudD5cbiAgPE15Q3NzQ2xhc3NJbnRlcmNlcHRvckNvbXBvbmVudCBjbGFzcz1cImdyZWVuXCI+aGVsbG88L015Q3NzQ2xhc3NJbnRlcmNlcHRvckNvbXBvbmVudD5cbiAgPGJyIC8+XG4gIDxNeUNzc0NsYXNzSW50ZXJjZXB0b3JDb21wb25lbnQgY2xhc3M9XCJyZWRcIj5oZWxsbzwvTXlDc3NDbGFzc0ludGVyY2VwdG9yQ29tcG9uZW50PiAmbHQ7LS0gcGFzc2luZyBjbGFzcyByZWQ/IE5haCwgYWgsIGJldHRlciBtYWtlIGl0IGJsdWUgKHRoYW5rcyB0byB0aGUgY2FzY2FkZSkhXG4gIDxiciAvPlxuPC90ZW1wbGF0ZT5cblxuPHN0eWxlPlxuICAucmVkIHtcbiAgICBjb2xvcjogcmVkO1xuICB9XG4gIC5ncmVlbiB7XG4gICAgY29sb3I6IGdyZWVuO1xuICB9XG4gIC5ibHVlIHtcbiAgICBjb2xvcjogYmx1ZTtcbiAgfVxuPC9zdHlsZT4iLCJpbXBvcnQtbWFwLmpzb24iOiJ7XG4gIFwiaW1wb3J0c1wiOiB7XG4gICAgXCJ2dWVcIjogXCJodHRwczovL3VucGtnLmNvbS9AdnVlL3J1bnRpbWUtZG9tQDMuMi4yOS9kaXN0L3J1bnRpbWUtZG9tLmVzbS1icm93c2VyLmpzXCJcbiAgfVxufSJ9" width="100%" height="400px"></iframe> <br/><br/></p> <p>As you can see, we’re “reading" the classes passed to the component via <code>attrs</code>, and if the class passed is <code>red</code>, we make add <code>blue</code>. When would you want to do such a thing? To enforce consistency or to add your own logic. Imagine you want to have something like this:</p> <ul> <li>Every time someone passes the <code>.x</code> class, you want to make sure <code>.y</code> class is also present.</li> </ul> <p>So, this pattern is probably only interesting for components library authors or Design System authors, and I recognise it, it’s an edge-case probably you won’t even encounter.</p> <p><strong>Note</strong>: did you notice how we’re creating components inside <code><script setup></code> and <code>defineComponent</code>? Also, the second component is returning a function. This way, we can use <a href="https://v3.vuejs.org/api/sfc-script-setup.html#useslots-and-useattrs"><code>useAttrs</code> and <code>useSlots</code></a> inside of it (instead of <code>this.slots</code>). And then, by returning a function, it’s treated as a render function.</p> <p>Unfortunately for us, in Vue 3, the class object is <em>read-only</em>, so you can only add classes and not remove existing ones. But it’s an example of what you can do with render functions. And the same way we have intercepted the <code>class</code> attribute, you could intercept other <strong>HTML attributes</strong>.</p> <h2>Validating <code>slots</code> for Accessibility</h2> <p>Again, let’s consider you’re creating you’re own Design System. And you want to provide <em>a11y</em> and usability hints when someone does something funky in the template. For example:</p> <ul> <li><a href="https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element">By spec</a>, the <code>a</code> element cannot contain any interactive (clickable) elements.</li> </ul> <p>So, what if we try to warn our library users of an incorrect markup, in development, before they even need to check it? <br/><br/> <iframe loading="lazy" style="border:0; border-radius: 4px; overflow:hidden;" src="https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCB7IHJlZiwgZGVmaW5lQ29tcG9uZW50LCBoLCB1c2VBdHRycywgdXNlU2xvdHMgfSBmcm9tICd2dWUnXG5cbmNvbnN0IE15Q29tcG9uZW50ID0gZGVmaW5lQ29tcG9uZW50KHtcbiAgcmVuZGVyKCkge1xuICAgIHJldHVybiBoKCdkaXYnLCB7fSwgdGhpcy4kc2xvdHMuZGVmYXVsdCgpKVxuICB9XG59KVxuY29uc3QgTXlDc3NDbGFzc0ludGVyY2VwdG9yQ29tcG9uZW50ID0gZGVmaW5lQ29tcG9uZW50KGZ1bmN0aW9uIE15Q3NzQ2xhc3NJbnRlcmNlcHRvckNvbXBvbmVudCgpIHtcbiBjb25zdCBhdHRycyA9ICB1c2VBdHRycygpXG4gY29uc3Qgc2xvdHMgPSB1c2VTbG90cygpXG4gXG4gcmV0dXJuICgpID0+IGgoXG4gICAnc3BhbicsXG4gICB7XG4gICAgIC4uLmF0dHJzLFxuICAgICBjbGFzczogYXR0cnMuY2xhc3MuaW5jbHVkZXMoJ3JlZCcpID8gJ2JsdWUnIDogJydcbiAgIH0sXG4gICBzbG90cy5kZWZhdWx0KClcbiApXG5cbn0pXG48L3NjcmlwdD5cblxuPHRlbXBsYXRlPlxuICA8bXktY29tcG9uZW50IGNsYXNzPVwicmVkXCI+aGVsbG88L215LWNvbXBvbmVudD5cbiAgPE15Q3NzQ2xhc3NJbnRlcmNlcHRvckNvbXBvbmVudCBjbGFzcz1cImdyZWVuXCI+aGVsbG88L015Q3NzQ2xhc3NJbnRlcmNlcHRvckNvbXBvbmVudD5cbiAgPGJyIC8+XG4gIDxNeUNzc0NsYXNzSW50ZXJjZXB0b3JDb21wb25lbnQgY2xhc3M9XCJyZWRcIj5oZWxsbzwvTXlDc3NDbGFzc0ludGVyY2VwdG9yQ29tcG9uZW50PiAmbHQ7LS0gcGFzc2luZyBjbGFzcyByZWQ/IE5haCwgYWgsIGJldHRlciBtYWtlIGl0IGJsdWUgKHRoYW5rcyB0byB0aGUgY2FzY2FkZSkhXG4gIDxiciAvPlxuPC90ZW1wbGF0ZT5cblxuPHN0eWxlPlxuICAucmVkIHtcbiAgICBjb2xvcjogcmVkO1xuICB9XG4gIC5ncmVlbiB7XG4gICAgY29sb3I6IGdyZWVuO1xuICB9XG4gIC5ibHVlIHtcbiAgICBjb2xvcjogYmx1ZTtcbiAgfVxuPC9zdHlsZT4iLCJpbXBvcnQtbWFwLmpzb24iOiJ7XG4gIFwiaW1wb3J0c1wiOiB7XG4gICAgXCJ2dWVcIjogXCJodHRwczovL3VucGtnLmNvbS9AdnVlL3J1bnRpbWUtZG9tQDMuMi4yOS9kaXN0L3J1bnRpbWUtZG9tLmVzbS1icm93c2VyLmpzXCJcbiAgfVxufSJ9" width="100%" height="400px" ></iframe> <br/><br/> I can see how this kind of message could be useful in a big Design System that focuses on <strong>accessibility</strong> (and why shouldn’t you, right?).</p> <h2>Rendering dynamic templates</h2> <p>You’ve probably encountered this problem while developing an app. You receive a string from some Backend or API that contains a template or some HTML that needs to interpolate some data. For example, it’s commonly needed for <strong>internationalisation</strong> (i18n) or rendering dynamic <strong>dashboards</strong>.</p> <p>Note: As you might have thought, it is an alternative API to <a href="https://vuejs.org/v2/api/#Vue-compile"><code>Vue.compile</code></a>. And for any compilation during runtime, you would need to load the Vue “build" that includes the <a href="https://v3.vuejs.org/guide/installation.html#runtime-compiler-vs-runtime-only"><strong>compiler</strong></a>, so bear in mind it produces a heavier JS bundle and you’ll need to add it into your configuration manually.</p> <p>For this example, I will showcase an example I had up my sleeve, using Vue 2: <br/><br/> <iframe loading="lazy" src="https://codesandbox.io/embed/vue-2-vue-compiled-templates-on-the-fly-rvbvj?file=/src/App.vue&fontsize=14&hidenavigation=1&theme=dark" style="width:100%; height:400px; border:0; border-radius: 4px; overflow:hidden;" title="vue-2-vue-compiled-templates-on-the-fly" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe></p> <p>As you can see, it could be a replacement for <code>v-html</code> if you need to <em>transpile</em> during runtime some expressions or if you need to compile at runtime components. For example, the <code><router-view></code> is also a very common case. Notice how in my example I'm not implementing methods/events. I just wanted to showcase the main idea.</p> <p>This pattern is actually the principle behind Alex Jover's <a href="https://github.com/alexjoverm/v-runtime-template">v-runtime-template</a> package and <a href="https://github.com/mattelen/vue3-runtime-template">many</a> <a href="https://www.npmjs.com/package/vue-dynamic">other</a> <a href="https://www.npmjs.com/package/vue-runtime-template-compiler">similar</a> <a href="https://www.npmjs.com/package/@elisiondesign/v-runtime-template">ones</a>. And now you know, it’s not magic at all 😉!</p> <p><em>Mandatory note</em>: ❗️ Be careful about rendering user inputs (avoid it or sanitize them), as it can lead to an <a href="https://v3.vuejs.org/guide/security.html#security">XSS attack</a>.</p> <h2>Conclusion</h2> <p>These are some of the use-cases of render functions. I hope you have seen something new or you have learned something! 😊 Let me know other use cases of render functions you know about!</p> <p>Also, remember, when possible, use templates instead!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Nuxt 3 + Storyblok for a Sushi Recipes Website]]></title> <link>https://vuedose.tips/nuxt-3-storyblok-sushi-website/</link> <guid>https://vuedose.tips/nuxt-3-storyblok-sushi-website/</guid> <pubDate>Wed, 15 Dec 2021 18:55:00 GMT</pubDate> <description><![CDATA[Have you used Nuxt 3 with Storyblok? Learn how to build a real-world recipes website from scratch (includes a video so you can see all the process)]]></description> <content:encoded><![CDATA[<p>What happens when you combine the versatility of the <a href="https://v3.nuxtjs.org/">NuxtJS</a> Framework with a top-notch headless CMS solution like <a href="https://www.storyblok.com/">Storyblok</a>?</p> <p>The result is a sparkling website with a ridiculously easy content editing experience that deploys seamlessly on your cloud infrastructure of choice. Sounds amazing right?</p> <p>In this article, we are going to learn how to combine both technologies using the <strong>Storyblok Nuxt module.</strong> The format of this post is going to be different than usual, we will go section by section checking the content of my last video along with the repository <a href="https://github.com/alvarosaburido/sushi-wuut-storyblok-nuxt3">here</a>.</p> <p>@<a href="https://www.youtube.com/embed/UClPbNm4lX4">video</a></p> <h2>What the... is headless?</h2> <p>"Off with their heads!" said the Queen of Hearts! This phrase could come to your mind when thinking of headless, but it has a slightly different meaning (kinda) when we are talking about CMS solutions.</p> <p>Do you remember those old, monolithic, self-hosted websites solutions where the BE (Back-end), the FE (Front-end), and the content were tightly coupled?</p> <blockquote> <p>A Headless CMS is when the presentational layer or Front-end (a.k.a the "head") and it's entirely decoupled from the content repository and the BE system (a.k.a the "body")</p> </blockquote> <p>With a content-first approach, users can author their content through an API and deliver it to any client app, could be a website, a mobile app, you name it.</p> <p>Check the complete section here <a href="https://youtu.be/UClPbNm4lX4?t=38">00:38</a></p> <h2>Storyblok</h2> <p>It's a leading technology in terms of headless CMS. What I personally like the most, is how easy is to change content, preview it and then deploy to production independently from tech stack you choose.</p> <p>The developer and content experience are beautiful, something that is missing in most of the CMS out there.</p> <p><img src="https://res.cloudinary.com/alvarosaburido/image/upload/v1639302628/blog/Storyblok%20%2B%20Nuxt3/storyblok_sjjtz7.gif" alt="https://res.cloudinary.com/alvarosaburido/image/upload/v1639302628/blog/Storyblok%20%2B%20Nuxt3/storyblok_sjjtz7.gif"></p> <p>For more info check here <a href="https://youtu.be/UClPbNm4lX4?t=82">01:22</a></p> <h2>Content structures</h2> <p><img src="https://res.cloudinary.com/alvarosaburido/image/upload/v1639303735/blog/Storyblok%20%2B%20Nuxt3/storyblok-diagram_e2fzrd.png" alt="https://res.cloudinary.com/alvarosaburido/image/upload/v1639303735/blog/Storyblok%20%2B%20Nuxt3/storyblok-diagram_e2fzrd.png"></p> <p><strong>Storyblok's</strong> way of creating content objects is quite cool. Have you ever played with Legos? They use <strong>bloks</strong> to create bigger <strong>pieces and</strong> each blok has its own set of <strong>properties</strong> (like color, width, height, specific form). This CMS uses <strong>bloks</strong> that represent components (hero, features section etc) to create bigger <strong>pieces (Stories),</strong> were ****each <em>blok</em> has their own set of <strong>fields</strong> (called <strong>Schema</strong>) holding your content.</p> <p><img src="https://res.cloudinary.com/alvarosaburido/image/upload/v1639303735/blog/Storyblok%20%2B%20Nuxt3/storyblok-diagram-2_qosd5n.png" alt="https://res.cloudinary.com/alvarosaburido/image/upload/v1639303735/blog/Storyblok%20%2B%20Nuxt3/storyblok-diagram-2_qosd5n.png"></p> <p>There is also another important concept, <a href="https://www.storyblok.com/docs/guide/essentials/content-structures#content-type">Content Types</a>, which is another type of component that represents templates for your stories. Examples of common Content Types are <strong>Post</strong>, <strong>Product</strong>, <strong>Page</strong> etc**.**</p> <p>For more detailed explanation, check here <a href="https://youtu.be/UClPbNm4lX4?t=1402">23:22</a></p> <h2>Installation</h2> <p>Let's create a new Nuxt project:</p> <pre><code class="language-bash">npx nuxi init name-of-your-project </code></pre> <p>Then we install the module:</p> <pre><code class="language-bash">yarn add -D @storyblok/nuxt@next axios </code></pre> <p>Check complete process here <a href="https://youtu.be/UClPbNm4lX4?t=308">05:08</a>.</p> <h2>Setting up your Storyblok space</h2> <p>First you will need to create your user if you haven't already at <a href="https://www.storyblok.com/">Storyblok</a>, then login to enter the dashboard and you will see a call to action next to the welcome hero. Click on it "Create a new space"</p> <p>I will not cover the whole Dashboard explanation, if you are interested, you can check it on the video here <a href="https://youtu.be/UClPbNm4lX4?t=176">02:56</a>.</p> <p><img src="https://res.cloudinary.com/alvarosaburido/image/upload/v1639307394/blog/Storyblok%20%2B%20Nuxt3/Setting_up_location_c9puvp.png" alt="https://res.cloudinary.com/alvarosaburido/image/upload/v1639307394/blog/Storyblok%20%2B%20Nuxt3/Setting_up_location_c9puvp.png"></p> <p>Before we continue, please go to <strong>Settings</strong> on the left sidebar, then <strong>General</strong> and where it says <strong>Location (default environment)</strong> add your <code>localhost</code> url, which on Nuxt is <code>http://localhost:3000</code></p> <p><img src="https://res.cloudinary.com/alvarosaburido/image/upload/v1639306333/blog/Storyblok%20%2B%20Nuxt3/Screenshot_2021-12-12_at_11.50.06_wl3uvp.png" alt="https://res.cloudinary.com/alvarosaburido/image/upload/v1639306333/blog/Storyblok%20%2B%20Nuxt3/Screenshot_2021-12-12_at_11.50.06_wl3uvp.png"></p> <p>Now to link our module we will need a key, so click on <strong>API-Keys</strong> and copy your personal key from the list. ⚠️ Please be careful not to share or save this in a public place or in the repository.</p> <h2>Configuring the Nuxt module</h2> <p>Add the following code to the modules section of <code>nuxt.config.ts</code> and replace the <code>accessToken</code> with API token from <strong>Storyblok</strong> space.</p> <pre><code class="language-js">import { defineNuxtConfig } from "nuxt3"; export default defineNuxtConfig({ modules: [ ["@storyblok/nuxt", { accessToken: "process.env.STORYBLOK_API_TOKEN" }] // ... ] }); </code></pre> <p>Safe tip here: you can create and <code>.env</code>file and add there your key and the API Url</p> <pre><code class="language-bash">STORYBLOK_API_URL=https://api.storyblok.com/v2 STORYBLOK_API_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx </code></pre> <p>Make sure you <strong>don't commit</strong> the file to your repository.</p> <p>As a final step, run <code>yarn dev</code> on the project to serve the Nuxt application and if all goes smooth you should see the <code><NuxtWelcome /></code> component.</p> <p>For the complete explanation refer to this section of the video <a href="https://youtu.be/UClPbNm4lX4?t=618">10:18</a>.</p> <h2>Creating our first page</h2> <p>Now is when the magic begins! Open <strong>Content</strong> on the sidebar and it will display a list of all the pages available. By default, there is a <strong>Home</strong> page already included. Do you notice that the <strong>Content Type</strong> is <code>Page</code>? We're going need this on a moment., but for now click on the <strong>Home</strong> item.</p> <p>Do you remember when we add the default environment to <code>http://localhost:3000</code>? Here is why: <strong>Storyblok</strong> allows you to see the preview of your page directly for the CMS. On the right-hand sidebar you will see all the configuration for the current page, including the <strong>Bloks.</strong></p> <h3>Page component on Nuxt</h3> <p>On your Nuxt application, create a component under <code>/components</code> named <code>ThePage</code> , which is responsible to render all our Stories and Bloks. This is connected to</p> <pre><code class="language-vue"><script lang="ts" setup> const props = defineProps({ blok: { type: Object, required: true, }, }) </script> <template> <div v-editable="blok"> <component :is="blok.component" v-for="blok in blok.body" :key="blok._id" :blok="blok" /> </div> </template> </code></pre> <p>Check here for the complete process <a href="https://youtu.be/UClPbNm4lX4?t=1669">27:49</a></p> <h3>Nuxt home route</h3> <p>Since Nuxt has already a Router, just create an <code>pages/index.vue</code> to edit your Home page, this will correspond with the <code>/</code> path.</p> <pre><code class="language-vue"><script lang="ts" setup> ... </script> <template> <component :is="state.story.content.component" :key="state.story.content._uid" :blok="state.story.content" /> </template> </code></pre> <p>Since <strong>Storyblok</strong> uses a JSON data structure like this on the request</p> <pre><code class="language-js">{ "story": { "name": "Home", "slug": "home", "full_slug": "home", "id": 10718480, "uuid": "74a92c75-dbb6-4cb6-8c2f-b4582017c087", "content": { "_uid": "c0c5fd8d-6913-4ebf-bbed-a60d5e83dc1f", "component": "the-page", "body": [ ... ], ... }, ... } } </code></pre> <p>We will use <a href="https://v3.vuejs.org/guide/component-dynamic-async.html">dynamic components</a> from Vue3 a lot. Why? You see that <code>story.content.component: 'the-page'</code> on the response? We will pass exactly that field on our component tag <code><component :is="story.content.component" /></code> so Nuxt knows which components are associated to the story and be able to render it. Genius I know.</p> <p>Then we need to fetch that JSON right? For that we're going to add some composable functions on our <code><script></code>.</p> <pre><code class="language-js">import { useStoryApi, useStoryBridge } from '@storyblok/nuxt/composables' const storyapi = useStoryApi() const { data } = await storyapi.get('cdn/stories/home', { version: 'draft', }) const state = reactive({ story: data.story, }) </code></pre> <p>We use the <code>useStoryApi</code> to fetch directly to <strong>Storyblok cdn</strong>, and get the content of our Story (Home). For now let's fetch the content on draft instance (check content versions <a href="https://www.storyblok.com/docs/guide/essentials/accessing-data#content-versions">here</a>)</p> <p>Now we need to tell our Nuxt app to listen to changes made on the CMS dashboard and update the content, for that we are gonna use <code>useStoryBridge</code> on the mounted hook.</p> <pre><code class="language-js">onMounted(() => { useStoryBridge(state.story.id, event => { state.story = event }) }) </code></pre> <p>Check here <a href="https://youtu.be/UClPbNm4lX4?t=1901">31:41</a></p> <h3></h3> <h2>Creating Bloks (components)</h2> <p>On the left sidebar go to <strong>Components,</strong> you will see a list of available ones (Storyblok gives you some default ones). Remember that we talk about the <strong>Page component</strong> which is a <code>Content Type</code> on the previous section? If you click on it you will be able to check it's <code>Schema</code>, which contains a field called <code>body</code> (check template of <code>/components/ThePage.vue</code> 😜 ) .</p> <p>I will rename it to <code>the-page</code> to align with the name of the component on Nuxt, but by default is <code>page</code>.</p> <p><img src="https://res.cloudinary.com/alvarosaburido/image/upload/v1639311385/blog/Storyblok%20%2B%20Nuxt3/Screenshot_2021-12-12_at_13.15.59_ge5ynr.png" alt="https://res.cloudinary.com/alvarosaburido/image/upload/v1639311385/blog/Storyblok%20%2B%20Nuxt3/Screenshot_2021-12-12_at_13.15.59_ge5ynr.png"></p> <p>Now click con <strong>New</strong> to create a Blok**,** give it a name (This name need to be k<em>ebab-case</em> and the same as your Nuxt component to be able to render), I will call it <code>the-hero</code> 🦸🏻♀️ , make sure you check the <strong>Nestable</strong> checkbox before hitting <strong>Next</strong>.</p> <p>On the tab <strong>Schema</strong> is where we are gonna define the properties to hold our content. since it's a Hero Component we will need:</p> <ul> <li>Headline</li> <li>Description</li> <li>CTA's</li> <li>Image</li> </ul> <p>To add a new field just enter a key value and click the button <code>+ Add</code>. By default it will create a simple <strong>Text Field</strong>, but if you click the field you can change its type (For more info <a href="https://www.storyblok.com/docs/terminology/field-type">check the Field Types docs</a>).</p> <p><img src="https://res.cloudinary.com/alvarosaburido/image/upload/v1639323102/blog/Storyblok%20%2B%20Nuxt3/storyblok-field-types.png" alt="https://res.cloudinary.com/alvarosaburido/image/upload/v1639323102/blog/Storyblok%20%2B%20Nuxt3/storyblok-field-types.png"></p> <h3>Hero component on Nuxt</h3> <p>Similar to our <code>ThePage</code>, we are gonna create another component called <code>TheHero</code> with the following code.</p> <pre><code class="language-html"><script lang="ts" setup> const props = defineProps({ blok: { type: Object, required: true, }, }) </script> <template> <section class="hero w-full py-16"> <h1 class="font-display text-shrimp-400 font-extrabold text-3xl mb-12"> {{ blok.headline }} </h1> ... </section> </template> </code></pre> <p>All components need to have a prop called <code>blok</code> which is going to contain the actual component <strong>Schema</strong> from the JSON response done at page level.</p> <p>To see the complete explanation + markup check the video at minute <a href="https://youtu.be/UClPbNm4lX4?t=2070">34:30</a>.</p> <h2>Final result</h2> <p>Go back to your <strong>Home Story,</strong> on the right sidebar you will see a <code>+ Add block</code> under the field label <strong>Body,</strong> click there and add your recently created component, once you do, click in it and add the content of each field you created before on the <strong>Schema</strong>.</p> <p>If all went well (if not, you can always download the <a href="https://github.com/alvarosaburido/sushi-wuut-storyblok-nuxt3">repo</a> and that's it 😜) and you create your markup and style like in the video you should be able to see something like this:</p> <p><img src="https://res.cloudinary.com/alvarosaburido/image/upload/v1639324044/blog/Storyblok%20%2B%20Nuxt3/Screenshot_2021-12-12_at_16.47.16_cklinu.png" alt="https://res.cloudinary.com/alvarosaburido/image/upload/v1639324044/blog/Storyblok%20%2B%20Nuxt3/Screenshot_2021-12-12_at_16.47.16_cklinu.png"></p> <p>Try to edit something and see how it updates automatically on the Preview on the left. That's what I call real-time editing 😱.</p> <h3>Wrap up</h3> <p><strong>Storyblok + Nuxt3</strong> is a powerful combination to deliver a modern and flexible CMS experience to your clients and developers. Some people say it's impossible to have them both happy at the same time. Well, now you can 😉</p> <p>I hope you enjoyed the article + <a href="https://www.youtube.com/watch?v=UClPbNm4lX4&t">video</a>. See you around</p> <p>Happy coding!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Going 3D with Trois.js and Vue 3]]></title> <link>https://vuedose.tips/going-3d-with-trois-js-and-vue-3/</link> <guid>https://vuedose.tips/going-3d-with-trois-js-and-vue-3/</guid> <pubDate>Tue, 16 Nov 2021 11:00:00 GMT</pubDate> <description><![CDATA[Learn about Trois.js, a JS library to render 3D scenes in Vue. In this article, we're learning the basics of using Trois.js in a Vite + Vue 3 app]]></description> <content:encoded><![CDATA[<p><a href="https://troisjs.github.io">Trois.js</a> allows you to bring the 3D magic of <a href="https://threejs.org/">Three.js</a> into the modern world of <a href="https://vitejs.dev/">Vite.js</a> ⚡️ and <a href="https://v3.vuejs.org/">Vue3</a> 💚.</p> <p>In this article, we are going to learn the basics. I will assume you have some notion about 3D before continuing. You can find the complete repo <a href="https://github.dev/alvarosaburido/troisjs-example">here</a>.</p> <pre><code>yarn create vite troisjs-example --template vue </code></pre> <p>Add <strong>Trois.js</strong> to the project:</p> <pre><code>yarn add three@0.127 troisjs </code></pre> <p>You can install it as a plugin:</p> <pre><code class="language-js">import { createApp } from 'vue'; import { TroisJSVuePlugin } from 'troisjs'; const app = createApp(App); app.use(TroisJSVuePlugin); </code></pre> <p>Or importing the resources directly into your component (This enable tree shaking and typescript support):</p> <pre><code class="language-js">import { defineComponent } from 'vue'; import { Box, Camera, LambertMaterial, PointLight, Renderer, Scene } from 'troisjs'; export default defineComponent({ components: { Box, Camera, Renderer, Scene, PointLight, LambertMaterial }, }); </code></pre> <h2>Core components</h2> <p>The more basic example const of <strong>3 core components</strong>:</p> <ul> <li><a href="https://troisjs.github.io/guide/core/renderer.html">Render</a>: enables to perform 3D rendering in an HTML canvas.</li> <li><a href="https://troisjs.github.io/guide/core/camera.html">Camera</a>: that uses perspective projection. This projection mode is designed to mimic the way the human eye sees.</li> <li><a href="https://troisjs.github.io/guide/core/scene.html">Scene</a>: Scenes allow you to set up what and where is to be rendered by three.js. This is where you place objects, lights and cameras.</li> </ul> <pre><code class="language-vue"><script lang="ts"> import { PointLight, Box, Camera, Renderer, Scene, LambertMaterial, } from 'troisjs'; import { defineComponent } from 'vue'; export default defineComponent({ components: { Box, Camera, Renderer, Scene, PointLight, LambertMaterial }, }); </script> <template> <Renderer resize="window" orbit-ctrl ref="renderer"> <Camera :position="{ z: 10 }" /> <Scene background="#4DBA87"> <PointLight :position="{ y: 50, z: 50 }" /> <Box ref="box"> <LambertMaterial /> </Box> </Scene> </Renderer> </template> </code></pre> <p>To break down a little bit the code above, we set a <code>Renderer</code> component as a wrapper, we add <code>resize="window"</code> to make the canvas responsive, <code>orbit-ctrl</code> allows you to use the mouse to change the camera position (Initially on <code>z: 10</code>) dynamically, so you can rotate the object as you wish.</p> <p>Inside <code>Scene</code>, we have <code>PointLight</code> which is a component that enables a light that gets emitted from a single point in all directions (think of it as a bare lightbulb 💡) and a <code>Box</code> mesh with a <code>LambertMaterial</code> inside as a slot (non-shiny surface without specular highlights).</p> <p>Use <code>yarn dev</code> to see the result on the browser:</p> <p><img src="https://res.cloudinary.com/alvarosaburido/image/upload/v1633253957/blog/Going%203d%20with%20Troisjs/troisjs-cube-example.png" alt="https://res.cloudinary.com/alvarosaburido/image/upload/v1633253957/blog/Going%203d%20with%20Troisjs/troisjs-cube-example.png"></p> <h2>Render loop</h2> <p>Trois.js uses the <code>requestAnimationFrame</code> loop under the hood to render the scene. Usually runs at <code>60fps</code>.</p> <p>Let's use it to give some life to the box once the scene is rendered, first of all, make sure you are referencing the <strong>renderer</strong> and the <strong>box</strong> with <a href="https://v3.vuejs.org/guide/composition-api-template-refs.html">template refs</a>:</p> <pre><code class="language-vue"><script lang="ts"> import { PointLight, Box, Camera, Renderer, Scene, LambertMaterial, } from 'troisjs'; import { defineComponent, onMounted, ref } from 'vue'; export default defineComponent({ setup() { const renderer = ref(null); const box = ref(null); onMounted(() => { renderer?.value?.onBeforeRender(() => { box.value.mesh.rotation.x += 0.01; }); }) return { renderer, box } } }; </script> </code></pre> <p>Once we <code>mount</code> our host component, we will listen to the <code>onBeforeRender</code> event on the <code>renderer</code> object instance and wait for it to start rotating the box by accessing the <code>rotation</code> property inside the <code>mesh</code>.</p> <h2>Conclusion</h2> <p><strong>Trois.js</strong> allows you to quickly set up a 3D project with top-notch modern frontend technologies. It's pretty performant and easy to use.</p> <ul> <li>To learn how to use more features, check out <a href="https://troisjs.github.io/guide/lights/">trois.js features</a>.</li> <li>For the extended video tutorial of this article, check <a href="https://youtu.be/yMYjb5O45hI">Going 3D with Trois.js (Three.js + Vite)</a></li> </ul> <p>Happy coding!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Introduction to Render Functions]]></title> <link>https://vuedose.tips/introduction-to-render-functions/</link> <guid>https://vuedose.tips/introduction-to-render-functions/</guid> <pubDate>Tue, 28 Sep 2021 00:00:00 GMT</pubDate> <description><![CDATA[What is a render function? Let's see a simple example and the comparison with Vue compiled code and its counterpart template compiled code.]]></description> <content:encoded><![CDATA[<h2>What is a Vue render function?</h2> <p>You're probably used to write Vue components in Single File Components (SFC) format:</p> <pre><code class="language-vue"><template> <div>Hello world</div> </template> </code></pre> <p>…where you put all your HTML inside the <code>template</code> block.</p> <p>Vue has an HTML-based template engine, and what it does is transform these templates into “render functions”.</p> <blockquote> <p>Render functions are the JavaScript representation of a Vue template.</p> </blockquote> <p>Just so you know, Vue compiles the previous example into something like:</p> <pre><code class="language-js">function render( ) { with(this){ return _c('div',[_v("Hello World")]) } } </code></pre> <p>But more generally, when we talk about <strong>render functions</strong>, we talk about the way of authoring templates with the following syntax:</p> <pre><code class="language-vue"><script> // Vue 2 syntax export default { render(h) { // <- Render Function return h('div', ['hello world']) // <- This is our template } } </script> </code></pre> <p>So, for components defined with render functions, there's no need to have a <code>template</code> block at all, because the template is defined in the <code>render</code> function.</p> <p>To get to know this syntax better, please refer to the official docs: https://vuejs.org/v2/guide/render-function.html</p> <h2>Vue 3 syntax</h2> <p>Now let's write the previous example with Vue 3 syntax:</p> <pre><code class="language-vue"><script> // Vue 3 syntax import { h } from 'vue' export default { render() { return h('div', ['hello world']) } } </script> </code></pre> <p>What changed is that now we need to import the <code>h</code> function from <code>Vue</code> manually. It's no longer passed as a parameter to the <code>render</code> function by default.</p> <p>Here's a link to the Vue (3) SFC Playground to see this in action:</p> <p><iframe loading="lazy" src="https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdD5cbiAgaW1wb3J0IHsgaCB9IGZyb20gJ3Z1ZSdcbiAgZXhwb3J0IGRlZmF1bHQge1xuICAgIHJlbmRlcigpIHtcbiAgICAgIHJldHVybiBoKCdkaXYnLCBbJ2hlbGxvJ10pXG4gICAgfVxuICB9XG48L3NjcmlwdD5cbiIsImltcG9ydC1tYXAuanNvbiI6IntcbiAgXCJpbXBvcnRzXCI6IHtcbiAgICBcInZ1ZVwiOiBcImh0dHBzOi8vc2ZjLnZ1ZWpzLm9yZy92dWUucnVudGltZS5lc20tYnJvd3Nlci5qc1wiXG4gIH1cbn0ifQ==" width="100%" height="400px"></iframe></p> <h2>Differences with regular templates</h2> <ul> <li>With render functions, performance is not increased or even degraded.</li> <li>File size differences are marginal.</li> <li>Readability is worse in render function components.</li> <li>Therefore, maintainability is harder.</li> <li>And it makes it more difficult for folks not used to JavaScript (or Vue render functions) to contribute.</li> </ul> <p>So, if it makes them harder to maintain and the performance is not significantly improved, why do we want them, why do we need them? In the next article, we're going to see a few cases where render functions are actually needed and recommended. <strong>Stay tuned</strong>!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Achieve Max Performance loading your images with v-lazy-image]]></title> <link>https://vuedose.tips/achieve-max-performance-loading-your-images-with-v-lazy-image/</link> <guid>https://vuedose.tips/achieve-max-performance-loading-your-images-with-v-lazy-image/</guid> <pubDate>Mon, 30 Aug 2021 09:00:00 GMT</pubDate> <description><![CDATA[Don't you know what to do to improve your web performance? Learn Progressive Image Loading, a technique used by Spotify, Medium and Netflix.]]></description> <content:encoded><![CDATA[<p>Most likely you already know how to <a href="/lazy-loading-images-with-v-lazy-image">use v-lazy-image</a>, even <a href="/use-responsive-images-with-v-lazy-image">using responsive images</a> to load the best image size based on the viewport's size. If you don't, I suggest you to do it now.</p> <p>Have you ever been reading a blog on Medium and see this cool loading image effect as you scroll down an article?</p> <p><img src="https://a.storyblok.com/f/83078/584x268/7dc4dacd6a/lazy-load-effect.gif" alt="Img"></p> <p>That's Progressive Image Loading, a performance technique that Medium, Spotify and Netflix (among others) use to improve the user experience when lazy loading an image.</p> <p>Note that it's a perceived performance technique, meaning that the image won't be loaded quicker. Instead, you'll load a tiny version of the image and make the switch to the real one.</p> <p>That improves considerably the UX (User eXperience). The user will perceive the image load is smooth and you'll avoid the user to wait seeing a blank space until the image is loaded, accompanied by a CSS transition.</p> <p>If you're more interested on the different kind of performances, I talk in more detail in my <a href="https://youtu.be/rzYoM2jVJr0?t=1351">Vue Day's talk</a>, but let's go to the point.</p> <h2>Progressive Image Loading in v-lazy-image</h2> <p>As simple as using the <code>src-placeholder</code> property to define an image that is shown until the <code>src</code> image is loaded.</p> <p>Make sure the placeholder image is a tiny version of the original one. If you host your images in Storyblok, that's pretty easy to achieve with the <a href="https://www.storyblok.com/docs/image-service">Image Service</a> since it allows you to resize images on the fly.</p> <p>When the <code>src</code> image is loaded, a <code>v-lazy-image-loaded</code> class is added, so you can use it to perform animations. For example, a blur effect:</p> <pre><code class="language-html"><template> <v-lazy-image src="https://cdn-images-1.medium.com/max/1600/1*xjGrvQSXvj72W4zD6IWzfg.jpeg" src-placeholder="https://cdn-images-1.medium.com/max/80/1*xjGrvQSXvj72W4zD6IWzfg.jpeg" /> </template> <style scoped> .v-lazy-image { filter: blur(10px); transition: filter 0.7s; } .v-lazy-image-loaded { filter: blur(0); } </style> </code></pre> <p>You can listen to the <code>intersect</code> and <code>load</code> events for more complex animations and state handling:</p> <pre><code class="language-html"><template> <v-lazy-image src="https://cdn-images-1.medium.com/max/1600/1*xjGrvQSXvj72W4zD6IWzfg.jpeg" src-placeholder="https://cdn-images-1.medium.com/max/80/1*xjGrvQSXvj72W4zD6IWzfg.jpeg" @intersect="..." @load="..." /> </template> </code></pre> <h2>Demo with CSS Animations</h2> <p>Look at this demo and see different CSS Animations you can apply. It's aim is that you can get inspired by playing with it.</p> <p><iframe src="https://codesandbox.io/embed/9l3n6j5944?fontsize=14&hidenavigation=1&module=%2Fsrc%2FApp.vue&theme=dark" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="v-lazy-image with placeholder" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" loading="lazy"</p> <blockquote> <p></iframe></p> </blockquote> <h2>Follow up</h2> <p>Now you're ready to provide the best user experience using <code>v-lazy-image</code>! You can always go to its <a href="https://github.com/alexjoverm/v-lazy-image">Gihub repo</a> to see the full component API.</p> <p>If you want to learn more about Progressive Image Loading, my friend <a href="https://twitter.com/jmperezperez">@jmperezperez</a> has written about the <a href="https://jmperezperez.com/more-progressive-image-loading/">progressive loading technique</a> on his blog with a deeper explanation.</p> ]]></content:encoded> </item> <item> <title><![CDATA[Use Responsive Images with v-lazy-image]]></title> <link>https://vuedose.tips/use-responsive-images-with-v-lazy-image/</link> <guid>https://vuedose.tips/use-responsive-images-with-v-lazy-image/</guid> <pubDate>Mon, 23 Aug 2021 09:00:00 GMT</pubDate> <description><![CDATA[Is Web Performance a priority for you? Then read this article! You'll learn how to lazy load images with srcset and picture tag using v-lazy-image.]]></description> <content:encoded><![CDATA[<p>Yes I know. Web Performance is a must nowadays, and image loading plays a big part on it.</p> <p>I've already told you how to <a href="/lazy-loading-images-with-v-lazy-image">lazy load an image easily</a>. But... Can you image how the performance would improve if you can load different sizes of an image depending on the viewport?</p> <p><a href="https://github.com/alexjoverm/v-lazy-image">v-lazy-image</a> allows you to do that using Web Standards: <code>srcset</code> and the <code><picture></code> tag.</p> <p>Let me show you the how-to.</p> <h2>Srcset</h2> <p>Using the <code>srcset</code> property you can set images for different resolutions:</p> <pre><code class="language-html"><template> <v-lazy-image srcset="image.jpg 1x, image_2x.jpg 2x" /> </template> </code></pre> <p>When using the <code>srcset</code> attribute is recommended to use also <code>src</code> as a fallback for browsers that don't support the <code>srcset</code> and <code>sizes</code> attributes:</p> <pre><code class="language-html"><template> <v-lazy-image srcset="image-320w.jpg 320w, image-480w.jpg 480w" sizes="(max-width: 320px) 280px, 440px" src="image-480w.jpg" /> </template> </code></pre> <p>The <code>srcset</code> prop is combinable with <code>src-placeholder</code> in order to apply <a href="/achieve-max-performance-loading-your-images-with-v-lazy-image">progressive loading</a>.</p> <h2>Picture</h2> <p>If you want to wrap the <code>img</code> in a <code><picture></code> tag, use the prop <code>usePicture</code>. You can then use slots to add additional elements above the <code>img</code> element`.</p> <pre><code class="language-html"><v-lazy-image srcset="image-320w.jpg 320w, image-480w.jpg 480w" alt="Fallback" use-picture > <source srcset="image-320w.jpg 320w, image-480w.jpg 480w" /> </v-lazy-image> </code></pre> <p>Renders as:</p> <pre><code class="language-html"><picture> <source srcset="image-320w.jpg 320w, image-480w.jpg 480w" /> <img srcset="image-320w.jpg 320w, image-480w.jpg 480w" alt="Fallback" /> </picture> </code></pre> <p>Note you can use the <a href="https://github.com/scottjehl/picturefill">picture polyfill</a>.</p> <h2>Demo</h2> <p>Better shown than said. See how it works in this demo and feel free to play with it:</p> <p><iframe src="https://codesandbox.io/embed/v-lazy-image-responsive-images-forked-kq48f?fontsize=14&hidenavigation=1&module=%2Fsrc%2FApp.vue&theme=dark" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="v-lazy-image responsive images (forked)" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"</p> <blockquote> <p></iframe></p> </blockquote> <h2>Follow Up</h2> <p>I wanted to go straight, so now you know how to optimize image loading in your website and improve performance by loading different sizes of an image, improving the UX indirectly.</p> <p>However, if you didn't know about <code>srcset</code> and <code><picture></code>, you probably need to understand that better. You can read a deeper explanation on the article <a href="https://www.smashingmagazine.com/2014/05/responsive-images-done-right-guide-picture-srcset/">Responsive Images Done Right: A Guide To <picture> And srcset</a> from Smashing Magazine.</p> <p>Do you want to go one step further on achieving max performance? Then you must learn <strong><a href="/achieve-max-performance-loading-your-images-with-v-lazy-image">Progressive Image Loading</a></strong>, one of the techniques I find more fascinating.</p> ]]></content:encoded> </item> <item> <title><![CDATA[How to test Web Workers with Jest]]></title> <link>https://vuedose.tips/how-to-test-web-workers-with-jest/</link> <guid>https://vuedose.tips/how-to-test-web-workers-with-jest/</guid> <pubDate>Tue, 20 Oct 2020 02:56:00 GMT</pubDate> <description><![CDATA[Do you know the best part of Web Workers? They can speed up heavy tasks on your UI. But, have you tried testing them? Is not that easy... until now.]]></description> <content:encoded><![CDATA[<p>As you could read in Alex's article, <a href="https://vuedose.tips/use-web-workers-in-your-vuejs-component-for-max-performance/">Use Web Workers in your Vue.js Components for Max Performance</a>, you can use Web Workers to maximize performance in your Vue.js app instead of running heavy tasks in the main thread which is UI-blocking.</p> <p>But how can we test Web Workers? Webpack-bundled Web Workers are not supported by Jest so we have to mock the worker in order to test it! Let's see how to do in 3 simple steps, starting from a simple Vue app to calculate the Fibonacci number, where <code>fibonacci</code> function is the heavy task performed by the Web worker (you can follow the code <a href="https://github.com/astagi/webworker-test">here</a>)</p> <p><img src="https://github.com/astagi/webworker-test/blob/master/public/article/fibonacci_demo.gif?raw=true&v=1" style="width: 300px; height: auto;"></p> <p>First of all we need to isolate the main functionality of our worker, in this case is really straightforward because it's just our <code>fibonacci</code> function (<code>src/fibonacci.js</code>)</p> <pre><code class="language-js">let fibonacci = (num) => { if (num <= 1) return 1; return fibonacci(num - 1) + fibonacci(num - 2); } export default fibonacci </code></pre> <p>and keep the worker minimal (<code>src/fibonacci.worker.js</code>):</p> <pre><code class="language-js">import fibonacci from "./fibonacci"; self.onmessage = async function (e) { self.postMessage(fibonacci(e.data)); }; </code></pre> <p>This way we can mock just the Web Worker part of our implementation (<code>src/__mocks__/fibonacci.worker.js</code>)</p> <pre><code class="language-js">import fibonacci from "../fibonacci"; export default class fibonacciWorker { constructor() { // Note that `onmessage` should be overwritten by the code using the worker. this.onmessage = () => { }; } postMessage(data) { this.onmessage({ data: fibonacci(data) }); } } </code></pre> <p>and easily test the main functionality</p> <pre><code class="language-js">import { shallowMount } from '@vue/test-utils' import App from '@/App.vue' jest.mock("@/fibonacci.worker") describe('Fibonacci App.vue', () => { it('should calculate Fibonacci number', async () => { const wrapper = shallowMount(App) await wrapper.find('input').setValue('10') await wrapper.find('button').trigger('click') expect(wrapper.find('.result').element.innerHTML).toBe('Result: 89') }) }) </code></pre> <p>I created <code>workerloader-jest-transformer</code> to generalize this solution so that all workers are mocked at once. This Jest transformer helps you to test Web Workers loaded with Webpack worker-loader module in Jest. It's easy to use, install it with</p> <pre><code class="language-sh">yarn add workerloader-jest-transformer --dev </code></pre> <p>and add the tranformation rule to your Jest configuration:</p> <pre><code class="language-js">transform: { "^.+\\.worker.[t|j]sx?$": "workerloader-jest-transformer" } </code></pre> <p>This transformer is inspired by <a href="https://github.com/developit/jsdom-worker">jsdom-worker</a> and implements Web Worker API for JSDOM, so you can remove any mocking code as you can see <a href="https://github.com/astagi/webworker-test/tree/feature/jesttransformer">here</a>.</p> <p>Workerloader-jest-transformer is highly experimental and code is available on <a href="https://github.com/astagi/workerloader-jest-transformer">Github</a>, any contribution and advice would be greatly appreciated!</p> ]]></content:encoded> </item> <item> <title><![CDATA[How to use the new Fetch in Nuxt.js]]></title> <link>https://vuedose.tips/how-to-use-the-new-fetch-in-nuxt-js/</link> <guid>https://vuedose.tips/how-to-use-the-new-fetch-in-nuxt-js/</guid> <pubDate>Mon, 07 Sep 2020 03:57:00 GMT</pubDate> <description><![CDATA[The Nuxt team is on fire, releasing new stuff every week! In this tip Samuel shows you a feature that might've gone unnoticed... till now.]]></description> <content:encoded><![CDATA[<p>It was back in the March (March 17th to be exact), when the Nuxt.js team introduced version 2.12 and the new Fetch hook. This step made it possible to create better architecture for our Nuxt projects.</p> <p>You may wonder what I am talking about. The fetch was there the whole time... or was it? Yes it was, but in version 2.12 you can now define the fetch hook on the component, which is much cleaner than before. I will cover the difference between the old and new fetch in another article.</p> <p>Imagine you have a simple component which should show multiple teasers of the articles. In the headless CMS (Storyblok in my case) you could select the articles using the multiselect, like in this example.</p> <p><img src="https://paper-attachments.dropbox.com/s_F71899AF079BA5A6DB5D27935BC60CC06C07E8F87294187045ED27DB5F163713_1597841497368_image.png" alt=""></p> <p>The articles are usually content types on their own. They used to live in a separate folder and have a pretty rich content. Because of this the JSON response of this story will not contain the content of the articles, but the IDs of the articles. You can see this in the following code sample, where the property <code>articles</code> is array of IDs.</p> <pre><code class="language-json">{ "_uid": "47d9a0eb-fccd-463a-bfd1-5393afce3ff2", "title": "Selected Articles", "articles": [ "7a9efb98-f8e2-4482-b77e-b7d12fd7297c", "c3a03db7-b8f8-48ab-abaf-9d84c477a071", "b429a8b0-668c-42b3-b043-f4a3de4966c4" ], "component": "featured-articles", "_editable": "<!--#storyblok#{\"name\": \"featured-articles\", \"space\": \"81302\", \"uid\": \"47d9a0eb-fccd-463a-bfd1-5393afce3ff2\", \"id\": \"9879809\"}-->" } </code></pre> <p>You could also include whole content of the articles in the response, but that is another topic covered by this article from Storyblok.</p> <p>This situation brings up a good question: Where should we request and load our data? Back in the old days, I would do it in the page (in the page folder of Nuxt). The new fetch allows us to do it directly in the component, which then will also render our data. This means, that if we don't use that component, these data will be not requested. That's awesome!</p> <p>It also means that all of our logic and styling will live in one file. I named this component <code>FeaturedArticles.vue</code> and it looks like this (with a little bit of TailwindCSS styling):</p> <pre><code class="language-vue"><template> <div v-editable="blok"> <h2 class="pt-2 pl-6 text-lg text-gray-700 italic">{{ blok.title }}</h2> <p v-if="$fetchState.error"> Error occured during the article loading </p> <p v-else-if="$fetchState.pending"> Loading articles... </p> <ul v-else class="flex py-6 mb-6"> <li v-for="article in articles" :key="article._uid" class="flex-auto px-6" style="min-width: 33%"> <article-teaser v-if="article.content" :article-content="article.content"/> </li> </ul> </div> </template> <script> export default { props: { blok: { type: Object, required: true } }, async fetch() { this.articles = await this.$storyapi.get(`cdn/stories/`, { starts_with: 'articles/', version: 'draft', by_uuids: this.blok.articles } ).then((res) => { return res.data.stories }).catch((res) => { console.error(res) }) }, fetchOnServer: true, data() { return { articles: [] } } } </script> </code></pre> <p>You can see in the template that we can end up with three different situations:</p> <ul> <li>$fetchState.pending - If the request takes a long time we will inform the user that we are loading articles.</li> <li>$fetchState.error - If we get an error from our end-point, we will let the user know about it. Then we could show a button to renew request.</li> <li>!$fetchState.pending - Finally if the request was succesful, we will render the teasers on the page.</li> </ul> <p>Why do it like this?</p> <ul> <li>You can provide better UX to your user.</li> <li>You don't have to use $store.</li> <li>You will load data only when you need them.</li> <li>Your logic lives in one place and is not spread through multiple files.</li> <li>If you decide to remove the component, you will remove all of it's code with it.</li> </ul> <p>You can read more about the new Fetch hook and its option in the official Nuxt.js documentation. This was just the tip of the iceberg. 😏</p> <p>In the next tip I will compare the difference between the old fetch and the new fetch. Stay tuned and see you soon!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Generate and deploy the blog as a full static Nuxt site]]></title> <link>https://vuedose.tips/generate-and-deploy-the-blog-as-a-full-static-nuxt-site/</link> <guid>https://vuedose.tips/generate-and-deploy-the-blog-as-a-full-static-nuxt-site/</guid> <pubDate>Tue, 25 Aug 2020 01:55:00 GMT</pubDate> <description><![CDATA[Your site is ready for the world. Wait, where and how do I publish it? You only need 5 minutes to learn how to deploy a Nuxt.js site in Netlify.]]></description> <content:encoded><![CDATA[<p>Congrats if you reach this point because that means you should have now a fully running blog with articles, authors and topics, as well as search functionality thanks to <a href="https://www.storyblok.com/">Storyblok</a>. All organized in UI components with <a href="https://tailwindcss.com/">TailwindCSS</a> and basic SEO and social media sharing covered.</p> <p>Now it's the time to show it to the world!</p> <h2>Generating a Nuxt.js full static site</h2> <p>Since version 2.14, you're able to <a href="https://nuxtjs.org/blog/going-full-static/">generate full static sites</a> with Nuxt.js. That was indeed one of the releases that I've been waiting for so badly.</p> <p>If you go back to the first chapter where <a href="https://vuedose.tips/setting-up-a-full-static-nuxt-site">we set up a full static Nuxt.js site</a>, remember that we prepared the Nuxt.js project already to be full static.</p> <p>You can double check it by looking at the <em>nuxt.config.js</em> file:</p> <pre><code class="language-js">export default async () => { const routes = await fetchSitemapRoutes() return { mode: 'universal', target: 'static', // ... } } </code></pre> <p>The <code>target: static</code> part is the one in charge of making that happen.</p> <p>Here's the cool part, to generate the full static project you just need to run <code>npm run generate</code> and Nuxt will create the HTML files for everything:</p> <p><img src="https://a.storyblok.com/f/83078/770x804/530d96422b/07-01-run-generate.png" alt="https://a.storyblok.com/f/83078/770x804/530d96422b/07-01-run-generate.png"> {.img-shadow}</p> <p>As you can see, each route we created has its own folder with its <code>index.html</code> file, all pre-rendered.</p> <p>You can also run <code>npm run start</code> after being generated to see that it's working correctly.</p> <p>But in order to deploy it, in the way I'm going to show you, you don't need to run these commands locally.</p> <p>Hang on a second and continue reading 😉.</p> <h2>Deploying the full static website</h2> <p>By nature, a static site, also known as a <a href="https://jamstack.org/">JAM Stack</a> site, it's fast and secure because of the fact that it doesn't render on the server.</p> <p>Just like ready-cooked meals, a static site is pre-rendered and there is no rendering nor processing effort on the server side.</p> <p>At the end, it's all HTML, CSS and JavaScript, and to run that you just need a basic static server since all the rest happens on the client side.</p> <p>Normally, a static site is served pre-rendered and from that point becomes an SPA when it reaches the browser. It'd perform API calls or whatever is needed to get the data for a page.</p> <p>But when we specify that the website is a <strong>full-static</strong> site, it means that literally all API calls are performed only once at compilation.</p> <p>In order to host and deploy the site, we have several options in the market and it might be hard to choose one.</p> <p>I'm going to suggest to you my top 4, knowing that my preferences are:</p> <ul> <li>As zero-config as possible</li> <li>Good pricing with a free-tier option</li> <li>Friendly for frontend devs</li> </ul> <h3>Hosting options</h3> <p>It comes without saying that probably <strong><a href="https://www.netlify.com/">Netlify</a></strong> is the top here, and the one we'll use. It's focused on static sites. It's performant with a generous free-tier and with many features covering most of the cases for static sites and SPA's.</p> <p>I love using <a href="https://surge.sh/">Surge</a> for demos, because to deploy it you just need to literally run one command: <code>surge</code>. It's for frontend code as well and quite easy to use.</p> <p>In the land of cloud servers that are not only-frontend, my favorite is by no doubt <a href="https://vercel.com/">Vercel</a>. It's zero-config, focused on DX and performance and very easy to use for one that is not used to deal with servers. It's probably one of the very few that has a free tier as well.</p> <p>The last I'd suggest is Digital Ocean. You'll choose this one when you want to have more control on your server. The prices are very convenient and it's indeed a quality product. Alba Silvente has a nice guide on <a href="https://dev.to/dawntraoz/how-to-deploy-a-nuxt-full-static-site-in-digitalocean-4l73">how to deploy a Nuxt static site</a>.</p> <h3>Deploying the site to Netlify</h3> <p>Netlify is where VueDose is hosted, and I'm completely happy and satisfied by it.</p> <p>I have no knowledge about DevOps or system administration, so configuring servers and stuff is always daunting for me.</p> <p>But Netlify makes it very easy. As easy as pushing some buttons, so let's get started.</p> <p>Assuming your project is hosted on Gihub or Gitlab, you can <strong>automatically</strong> <strong>connect it to Netlify</strong>.</p> <p>For that, log into Netlify and you'll see this button to create a project from Git directly:</p> <p><img src="https://a.storyblok.com/f/83078/1688x690/f1d3c90ab8/07-02-netlify-new-site.png" alt="https://a.storyblok.com/f/83078/1688x690/f1d3c90ab8/07-02-netlify-new-site.png"> {.img-shadow}</p> <p>You'll go through a step-by-step process where you'll connect to Github, Gitlab or Bitbucket and choose your repository.</p> <p>At some point, you'll reach a step where you have to <strong>configure the build commands</strong>. Here's where you need to set <code>npm run generate</code> and the <code>dist</code> folder to be published:</p> <p><img src="https://a.storyblok.com/f/83078/1680x1182/1dd5477e9a/07-03-netlify-commands.png" alt="https://a.storyblok.com/f/83078/1680x1182/1dd5477e9a/07-03-netlify-commands.png"> {.img-shadow}</p> <p>You can also set the branch where you want to be <em>"listened"</em> instead of the master branch by default, so it can fit your personal workflow.</p> <p>And... that's basically it! Let it run for a minute or two and you'll see...</p> <p><img src="https://a.storyblok.com/f/83078/1646x698/b094e89105/07-04-netlify-error.png" alt="https://a.storyblok.com/f/83078/1646x698/b094e89105/07-04-netlify-error.png"> {.img-shadow}</p> <p>An error!!!??? WTF?</p> <p>Wait, I just forgot something important: we haven't set the environment variables.</p> <p>We have them locally in the <em>.env</em> file, but that's not published to the repository, and it shouldn't for security reasons.</p> <p>Instead, we need to do it on Netlify itself. Go to the <em>Site settings > Build and deploy > Environment</em> page and set the environment variables as you need:</p> <p><img src="https://a.storyblok.com/f/83078/2468x1168/caa9a00918/07-05-netlify-env.png" alt="https://a.storyblok.com/f/83078/2468x1168/caa9a00918/07-05-netlify-env.png"> {.img-shadow}</p> <p>:::blockquote</p> <p>Don't copy this image values, but instead use the ones you need to.</p> <p>:::</p> <p>After fixing that, let's trigger a new deploy. Go to the deploy page and you'll find this button to do it:</p> <p><img src="https://a.storyblok.com/f/83078/1998x1188/93e9e187de/07-06-netlify-trigger-deploy.png" alt="https://a.storyblok.com/f/83078/1998x1188/93e9e187de/07-06-netlify-trigger-deploy.png"> {.img-shadow}</p> <p>This time, all should work properly and your site will be live! Yay \o/</p> <p>You'll see the default url that Netlify gives you on the overview page:</p> <p><img src="https://a.storyblok.com/f/83078/1712x782/a3d21a2aa4/07-07-netlify-success.png" alt="https://a.storyblok.com/f/83078/1712x782/a3d21a2aa4/07-07-netlify-success.png"> {.img-shadow}</p> <p>What I love from Netlify is that you can have your custom domain, HTTPS, redirects and much more for free! Check out <a href="https://docs.netlify.com/domains-https/custom-domains/">their docs</a> to know-how!</p> <h2>Wrapping Up</h2> <p>Yes, this is the end.</p> <p>It was an incredible journey to guide you through how to create, step-by-step, a full-static blog with Nuxt.js and Storyblok following the same process that we used for VueDose.</p> <p>You've learnt how to create the project from scratch and how to define components based on a design system, how to build articles overview and detail pages.</p> <p>We dug deep into the Storyblok and learn how to transform our/your design into the components, how to organize your content and create queries to get it. At the end we learnt how to generate and deploy it statically into the world wide web.</p> <p>I feel very proud to say this was done thanks to the help and collaboration of <a href="https://www.storyblok.com/developers?utm_source=newsletter&utm_medium=logo&utm_campaign=vuedose">Storyblok</a>, a beloved sponsor of ours 💚.</p> <p>If you have any questions, feel free to reach <a href="https://twitter.com/vuedose">VueDose</a> or <a href="https://twitter.com/storyblok">Storyblok</a> on Twitter and we'll answer everything for you!</p> <p>Stay awesome.</p> ]]></content:encoded> </item> <item> <title><![CDATA[Optimize SEO and Social Media Sharing in a Nuxt blog]]></title> <link>https://vuedose.tips/optimize-seo-and-social-media-sharing-in-a-nuxt-blog/</link> <guid>https://vuedose.tips/optimize-seo-and-social-media-sharing-in-a-nuxt-blog/</guid> <pubDate>Tue, 18 Aug 2020 00:01:00 GMT</pubDate> <description><![CDATA[Do you want your blog to reach an audience? Then you should have SEO in your mind. Learn two techniques you can easily apply to your Nuxt blog.]]></description> <content:encoded><![CDATA[<p>So far in this guide, the blog is full-featured since we added <a href="https://vuedose.tips/tags-and-search-functionality-in-nuxt-using-storyblok-api">tags and search functionality</a>. But still, SEO is important in a blog so we cannot overlook it.</p> <p>SEO is a big thing and there are people working almost exclusively in SEO, especially those working on copywriting.</p> <p>That's why I'm not covering everything about SEO here. I'm not an expert on it, but at least we can make sure the blog has the minimum stuff for having the basics sorted.</p> <p>We'll make sure the pages have the correct metadata for search engines and social sharing, as well as generating a sitemap so we make easier the job for search engines.</p> <h2>Meta Tags and Social Media</h2> <p>One of the basic things we can do is to set up the meta tags for our blog pages.</p> <p>They're important for giving some extra info to the search engines, so they can show better information on the <a href="https://www.wordstream.com/serp">SERP</a> (Search Engine Result Pages).</p> <p>For instance, if you search <em>"vuedose"</em> on Google, you'll see this result thanks to the meta tags and the sitemap:</p> <p><img src="https://a.storyblok.com/f/83078/1638x842/fe5b30cf36/06-01-serp-explanation.png" alt="https://a.storyblok.com/f/83078/1638x842/fe5b30cf36/06-01-serp-explanation.png"> {.img-shadow}</p> <p>I'll cover the sitemap topic later, but for now you can see at least we have a <code>title</code> and <code>description</code> meta tags.</p> <p>First, set them in <em>nuxt.config.js</em> using the <code>head</code> property, just like this:</p> <pre><code class="language-js">export default { // ... head: { title: "NarutoDose", meta: [ { hid: "description", name: "description", content: "Get to know all about Naruto and its characters in tiny bits of info.", }, // ... ], }, }; </code></pre> <p>That will be translated to <code><meta name="description" content="Get to know..."></code> and inserted by Nuxt within the <code><head></code> tag. It's necessary that we set <code>hid</code>, otherwise the tags will be duplicated.</p> <p>Since later we'll add more tags in different pages, I'll suggest to create a util function for it. Create the file <em>utils/seo.js</em> with the following function:</p> <pre><code class="language-js">export const createSEOMeta = (data) => [ { hid: 'description', name: 'description', content: data.description }, ] </code></pre> <p>And update it on <em>nuxt.config.js:</em></p> <pre><code class="language-js">import { createSEOMeta } from "./utils/seo"; export default { // ... head: { title: "NarutoDose", meta: [ ...createSEOMeta({ description: "Get to know all about Naruto and its characters in tiny bits of info.", }), // ... ], }, }; </code></pre> <p>Now we need to do it on each page as well. If a tag is not set on a page, Nuxt will fallback to the one in <em>nuxt.config.js</em> if it exists there, so for the home page located at <em>pages/index.vue</em>.</p> <p>Let's go to <em>pages/_slug.vue</em> and set it there. In this case, it's a dynamic page, but that's no problem because the <code>head</code> method can access the component instance properties:</p> <pre><code class="language-js">export default { // ... head() { const { title, description } = this.article.content return { title, meta: createSEOMeta({ description }), } }, } </code></pre> <p>:::blockquote In case you're wondering where <code>this.article</code> comes from, I explained in another article how to <a href="https://www.storyblok.com/tp/show-blog-content-in-nuxt">show the blog content in Nuxt using Storyblok API</a>. :::</p> <p>In <em>pages/topics/_slug.vue</em> can be more simple, since I'd just set a title and let take the rest from the default one from <em>nuxt.config.js.</em></p> <p>Since it's a category, I'd like it to show up in the SERP and the browser tab like <code>CATEGORY_NAME - NarutoDose</code>:</p> <pre><code class="language-js">head() { return { title: `${this.topic.name} - NarutoDose`, } }, </code></pre> <h3>Social Media Sharing</h3> <p>Imagine you have your blog published and you want to share the link of an article on Twitter, Facebook or LinkedIn.</p> <p>By default, that'd be shown as just a link. Wouldn't be cool that instead of showing just a link, it can appear in a more visual way, like this?</p> <p><img src="https://a.storyblok.com/f/83078/1192x1246/116d779bc0/06-02-social-media-tweet.png" alt="https://a.storyblok.com/f/83078/1192x1246/116d779bc0/06-02-social-media-tweet.png"> {.img-shadow}</p> <p>They are called Social Cards, and they don't show up automatically. We need to add some extra tags. In particular, we need the ones for the <a href="https://ogp.me/">Open Graph</a>, a protocol coined first by Facebook but now also Twitter and LinkedIn (I can imagine Instagram as well).</p> <p>We need at least the tags:</p> <ul> <li><code>og:title</code></li> <li><code>og:description</code></li> <li><code>og:url</code>: the absolute URL that points to the article.</li> <li><code>og:image</code>: make sure it has the right resolution</li> <li><code>twitter:card</code>: the type of card you want to show on Twitter. We'll use <code>summary_large</code>, give it a look at <a href="https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/summary-card-with-large-image">Twitter docs</a>.</li> </ul> <p>You could use some extra ones if you want to polish it even more, but like these we're ok.</p> <p>Let's add these tags to the <code>createSEOTags</code> util function we defined before:</p> <pre><code class="language-js">export const createSEOMeta = (data) => [ { hid: 'og:title', property: 'og:title', content: data.title }, { hid: 'description', name: 'description', content: data.description }, { hid: 'og:description', property: 'og:description', content: data.description, }, { hid: 'og:image', property: 'og:image', content: data.image }, { hid: 'og:url', property: 'og:url', content: process.env.HOST_NAME + '/' + data.url, }, { hid: 'twitter:card', name: 'twitter:card', content: data.cardType || 'summary_large_image', }, ] </code></pre> <p>Since the url must be absolute, probably it's a good time for you to add a <code>HOST_NAME</code> variable on you <em>.env</em> file.</p> <p>We have all the data we need to set these tags, except the <code>image</code>. So first of all let's go to our Storyblok space and add an <em>asset field</em> to the Article schema.</p> <p><img src="https://a.storyblok.com/f/83078/1280x1164/a910dc55b2/06-03-storyblok-asset.png" alt="https://a.storyblok.com/f/83078/1280x1164/a910dc55b2/06-03-storyblok-asset.png"> {.img-shadow}</p> <p>:::blockquote If you don't know how to do it, take a look at <a href="https://www.storyblok.com/tp/setting-up-blog-content-structure">how to set up your blog structure on Storyblok</a>, I've explained how to add fields to a schema there 😉. :::</p> <p>Then, go through each article and update them by adding an image to each one.</p> <p>Once you do that, go to <em>pages/_slug.vue</em> and update the <code>head</code> function:</p> <pre><code class="language-js">head() { const url = this.article.slug const { title, description, image } = this.article.content return { title, meta: createSEOMeta({ title, description, image: image.filename, url }), } }, </code></pre> <p>Notice the image url is located on <code>image.filename</code>.</p> <p>Do the same in <em>nuxt.config.js</em>:</p> <pre><code class="language-js">import { createSEOMeta } from "./utils/seo"; export default async () => { const routes = await fetchSitemapRoutes(); return { head: { title: "NarutoDose", meta: [ ...createSEOMeta({ title: "NarutoDose", description: "Get to know all about Naruto and its characters in tiny bits of info.", image: "[Insert_NarutoDose_Image_URL]", url: process.env.HOST_NAME, }), ], }, }; }; </code></pre> <p>That'd be basically it. If you need to set specific tags on <em>pages/topics/_slug.vue</em> you're free to do it.</p> <h2>Sitemap</h2> <p>A sitemap tells search engines how's the page structured. That not only makes your pages indexable, but it makes the job easier to search engines and will be able to show the different sections of your site as you saw on the first image above.</p> <p>To generate a sitemap, let's use <a href="https://www.npmjs.com/package/@nuxtjs/sitemap">@nuxtjs/sitemap</a>: a Nuxt module that makes the job easy. Just install it and add it to the <code>modules</code> section on <em>nuxt.config.js</em>. Then, in the same file, you need to add this minimal configuration:</p> <pre><code class="language-js">sitemap: { hostname: process.env.HOST_NAME, routes: [] // all the dynamic routes }, </code></pre> <p>The module will pick up by default the static routes, but it's not able to pick up the dynamic routes. So we have to fetch them and add it to it.</p> <p>I'll suggest to do that in a utility function, so create a <em>utils/sitemap.js</em> file with the following content:</p> <pre><code class="language-js">import StoryblokClient from 'storyblok-js-client' import kebabCase from 'lodash/kebabCase' export const fetchSitemapRoutes = async () => { const routes = [] const client = new StoryblokClient({ accessToken: process.env.STORYBLOK_KEY }) const { data: articlesData } = await client.get('cdn/links', { starts_with: 'articles/', }) const { data: tagsData } = await client.get('cdn/tags') Object.values(articlesData.links).forEach((article) => routes.push(`/${article.slug}`) ) tagsData.tags.forEach((tag) => routes.push(`/topics/${kebabCase(tag.name)}`)) return routes } </code></pre> <p>We use <code>storyblok-js-client</code> to fetch the data. You don't need to install it since it comes by default with <code>storyblok-nuxt</code> module.</p> <p>Then we fetch all the articles and tags and form an array of strings with all the routes. Notice that for the articles we use the <a href="https://www.storyblok.com/docs/api/content-delivery#core-resources/links/retrieve-multiple-links">Links Endpoint</a>, which is much faster and convenient for cases when you don't need the stories content. You don't even need to paginate them.</p> <p>Finally, we need to use that function to fill up the routes. By default, we're exporting an object from <em>nuxt.config.js,</em> but for cases like this where we need to perform an async operation like fetching the routes, we can export a function instead:</p> <pre><code class="language-js">import { fetchSitemapRoutes } from "./utils/sitemap"; export default async () => { const routes = await fetchSitemapRoutes(); return { // ... sitemap: { hostname: process.env.HOST_NAME, routes, }, // ... }; }; </code></pre> <p>That's it. To test if it works, make sure you have a <code>generate</code> script in your <em>package.json</em> and run <code>npm run generate</code>.</p> <p>A <em>dist</em> folder must've been generated, containing the right <em>sitemap.xml</em> file on it:</p> <p><img src="https://a.storyblok.com/f/83078/1766x1114/ef6d98be96/06-04-sitemap.png" alt="https://a.storyblok.com/f/83078/1766x1114/ef6d98be96/06-04-sitemap.png"> {.img-shadow}</p> <h2>Wrapping Up</h2> <p>It's important not to have just a blog, but to make sure your blog it's minimally prepared for SEO and being positioned.</p> <p>It's true that you can do much more to improve SEO, mostly dependent on the quality of the content you write. But at least with the right meta tags and a sitemap you cover the basics of it.</p> <p>Now that the blog it's ready, what's next?</p> <p>The last step: deploy it 😉</p> <p>Coming next week!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Advanced i18n in Nuxt using Interpolations]]></title> <link>https://vuedose.tips/advanced-i18n-in-nuxt-using-interpolations/</link> <guid>https://vuedose.tips/advanced-i18n-in-nuxt-using-interpolations/</guid> <pubDate>Mon, 17 Aug 2020 00:57:00 GMT</pubDate> <description><![CDATA[Internationalization it's necessary in a multi-language site. Learn how to do i18n the right way in Nuxt and Vue using the nuxt-i18n module.]]></description> <content:encoded><![CDATA[<p>In <a href="https://nuxtjs.org/">NuxtJS</a> on our home page we have a title which has the letters <strong>JS</strong> as a green color, a different color to the rest of the text. Obviously this is done by adding a span with a color of light-green. This span is then added to all the other languages we have. We then import this using <code>v-html</code>. So far so good. It works. Why change this?</p> <pre><code class="language-js">title: 'The Intuitive <span class="text-nuxt-lightgreen">Vue</span> Framework', </code></pre> <pre><code class="language-vue"><p v-html="$t('title')"></p> </code></pre> <p>So recently we added <a href="https://eslint.org/">eslint</a> to our main website. Obviously this didn't pass the rule where <code>v-html</code> is considered a bad practice. But with eslint we can just turn off the rule. Problem solved. OK but what happens if we need to change the light-green to a dark-green. Well we need to change it in the i18n file of each language. Tedious? Well we could just do a find and replace. Problem solved.</p> <p>So basically it means people can translate our text and change the text and color to anything they like. But surely they wouldn't do that. But that's not the point. Should we really have this in our translations? Is it really necessary? Isn't there a better way?</p> <p>That's where <a href="https://kazupon.github.io/vue-i18n/guide/interpolation.html">i18n interpolation</a> comes into practice. With i18n, instead of using <code>v-html</code> we can instead use interpolation. How?</p> <p>i18n gives us an <code><i18n></code> component that we can use on any of our pages or inside our components. In this component we can set up the tag to be any html element we want such as <code>h2</code>, <code>div</code>, <code>a</code> or whatever we want to use for that particular text.</p> <pre><code class="language-vue"><i18n tag="h1"></i18n> </code></pre> <p>We then allocate it to our translation. If for example in our <em>en-EN.js</em> file we have <code>homepage.title</code>:</p> <pre><code class="language-js">module.exports = { homepage: { title: 'The Intuitive <span class="text-nuxt-lightgreen">Vue</span> Framework' } } </code></pre> <p>Then this is what we will use as the path in our <code><i18n></code> component.</p> <pre><code class="language-vue"><i18n tag="h1" path="homepage.title" > </i18n> </code></pre> <p>We can also add classes to our component just like any other component</p> <pre><code class="language-vue"><i18n tag="h1" path="homepage.welcome.title" class="title" > </i18n> </code></pre> <p>We can now use slots inside our component to render what we want, which in our case is our span class. We give the slot a name so we can then refer to it in our translation file. And then we add our span.</p> <pre><code class="language-vue"><i18n tag="h1" path="homepage.welcome.title" class="title" > <template v-slot:frameworkType> <span class="text-nuxt-lightgreen"> Vue </span> </template> </i18n> </code></pre> <p>Now in our translation file we can add the name of our slot enclosed in curly brackets where we want the actual span to appear.</p> <pre><code class="language-js">module.exports = { homepage: { title: 'The Intuitive {frameworkType} Framework' } } </code></pre> <p>This will now give us the translated text with a placeholder for our span class. And in our <em>fr-FR.js</em> file we can add the French translation where our placeholder will go at the end of the sentence instead of after the word Intuitive like in English.</p> <pre><code class="language-js">module.exports = { homepage: { title: 'Le Framework Intuitif basé sur {frameworkType}' } } </code></pre> <p>Now if we want to modify our span to be a different colour or add more styling we only have to do it in one place, and our translators only have to worry about translating the texts without having to deal with unnecessary html.</p> <p>It is also possible to add more than one slot to your <code><i18n></code> component. For example, for styling purposes, you might want to add a break in the sentence. Just make sure you give them unique names and then you can use the placeholders in your translation files.</p> <pre><code class="language-vue"><i18n tag="h1" path="homepage.welcome.title" class="title" > <template v-slot:br> <br /> </template> <template v-slot:frameworkType> <span class="text-nuxt-lightgreen"> Vue </span> </template> </i18n> </code></pre> <pre><code class="language-js">title: 'The Intuitive {br} {frameworkType} Framework', </code></pre> <pre><code class="language-js">title: 'Le Framework Intuitif {br} basé sur {frameworkType}', </code></pre> <p>As you an see it is much better to use i18n interpolation rather than v-html in your code as it allows you to keep your styling and html separate from your translations. This not only makes it easier for translators but it also makes it much easier to modify as the html code is not repeated throughout the translation files.</p> <p>If you have never worked with i18n then check out these links to help you get started:</p> <ul> <li>For more info on using i18n with Nuxt.js check out the <a href="https://i18n.nuxtjs.org/">nuxt-i18n module</a></li> <li>For more info on component interpolation checkout the <a href="http://kazupon.github.io/vue-i18n/guide/interpolation.html">vue-i18n docs</a></li> <li>To learn more about i18n checkout the <a href="https://vueschool.io/courses/internationalization-with-vue-i18n">i18n course on Vue School</a></li> </ul> ]]></content:encoded> </item> <item> <title><![CDATA[Tags and Search Functionality in Nuxt Using Storyblok API]]></title> <link>https://vuedose.tips/tags-and-search-functionality-in-nuxt-using-storyblok-api/</link> <guid>https://vuedose.tips/tags-and-search-functionality-in-nuxt-using-storyblok-api/</guid> <pubDate>Tue, 11 Aug 2020 01:57:00 GMT</pubDate> <description><![CDATA[Don't confuse your users! Learn how to provide them with great navigation using categories, tags and search so they can find your content easily.]]></description> <content:encoded><![CDATA[<p>I want you to think for a moment: when you get into the role of a blog reader, what do you really want?</p> <p>In most cases, you probably want to easily find content relevant and interesting to you.</p> <p>You might be interested in apples, and I am only interested in pineapples. Maybe you're just interested in sweet fruit, or you want to find only the purple fruit.</p> <p>What you're looking to do is to narrow down the content and navigate through it. We can do this in the following two ways:</p> <ul> <li><strong>Categorization</strong>: use of tags, topics, taxonomies or any type of category.</li> <li><strong>Search</strong>: unstructured way to find content more easily</li> </ul> <p>Let's see how to implement this in our blog.</p> <h2>Content Topics in the Nuxt Blog</h2> <p>In the article <em><a href="https://www.storyblok.com/tp/setting-up-blog-content-structure">Setting up the blog content structure in Storyblok</a></em> we've already added some tags to the articles we've created.</p> <p>At this point I encourage you to create some more articles so you'll better understand this section.</p> <p>The categorization strategy we're going to implement is simple: an article can belong to different topics and we want to have a page for each topic, listing all the articles related to that topic.</p> <p>You don't need to do anything in Storyblok. As you've seen before, we'll be using tags for it, and the rest of the functionality is there for us.</p> <p>Let's start by creating the routing for the topics pages.</p> <p>They will have a url following the form of <code>/topics/:slug</code> so we have to create the page at <em>pages/topics/_slug.vue</em>. It'll have a similar shape to the home page, so for now copy/paste the template from <em>pages/index.vue</em>:</p> <pre><code class="language-vue"><template> <div class="mt-4"> <section> <div class="container mx-auto px-4"> <h1 class="text-4xl font-bold">Articles</h1> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 mt-4"> <ArticleCard v-for="article in articles" :key="article.content.title" :slug="`/${article.slug}`" :title="article.content.title" :description="article.content.description" :author="article.content.author" :date="article.content.date.toLocaleDateString()" /> </div> </div> </section> </div> </template> </code></pre> <p>We'll link to the topic page from the article detail, located at <em>pages/_slug.vue</em>. A story in Storyblok includes a <code>tag_list</code> field:</p> <pre><code class="language-js">{ id: 17434028, // ... tag_list: ['Beast', 'Hokage'] } </code></pre> <p>Let's use it in order to show the tags just below the <code><header></code> tag in <em>pages/_slug.vue</em>:</p> <pre><code class="language-vue"><header>...</header> <div class="mt-4"> <nuxt-link v-for="tag in article.tag_list" :key="tag" :to="`/topics/${tagSlug(tag)}`" class="rounded-full text-white bg-main uppercase text-sm mr-2 px-2 py-1" >{{ tag }}</nuxt-link> </div> <div v-html="$md.render(article.content.content)" class="prose mt-8"></div> </code></pre> <p>Since the tags can have any string shape, you have to make sure it gets converted to a slug so it can be a valid link. That's why you see <code>tagSlug(tag)</code> in there.</p> <p>You can implement it with <a href="https://lodash.com/">lodash</a> easily:</p> <pre><code class="language-js">import kebabCase from 'lodash/kebabCase' export default { // ... methods: { tagSlug(tag) { return kebabCase(tag) }, } } </code></pre> <p>The result should look like this:</p> <p><img src="https://a.storyblok.com/f/83078/1558x822/f69586ee71/05-01-tags.png" alt="https://a.storyblok.com/f/83078/1558x822/f69586ee71/05-01-tags.png"> {.img-shadow}</p> <p><em>Note: Don't worry about the search box, I'll get into that later</em> 😉.</p> <p>Going back to <em>pages/topics/slug.vue</em>, we have the opposite problem here: we have access to the **topic slug, but not to the topic itself.</p> <p>What we can do is to fetch the topics using the <a href="https://www.storyblok.com/docs/api/content-delivery#core-resources/tags/tags">tags resource</a> and find the tag from the response.</p> <p>That call returns an object with the following shape:</p> <pre><code class="language-json">{ "tags": [ { "name": "Beasts", "taggings_count": 2 }, { "name": "Techniques", "taggings_count": 1 } ] } </code></pre> <p>Let's use the <code>asyncData</code> function on <em>pages/topics/slug.vue</em> to perform the call and search for the right topic name*:*</p> <pre><code class="language-js">import kebabCase from 'lodash/kebabCase' export default { async asyncData({ app, params }) { // Find tag based on the slug const { data: tagsData } = await app.$storyapi.get('cdn/tags/') const topic = tagsData.tags.find((t) => kebabCase(t.name) === params.slug) } } </code></pre> <p>Notice that we're calling <code>cdn/tags/</code> and the <code>kebabCase</code> operation to compare the slugs. Using <code>params.slug</code> you're accessing the slug from the route url parameter.</p> <p>Now that we have the right topic, let's retrieve the articles for that topic. You need to use the <code>with_tag</code> <a href="https://www.storyblok.com/docs/api/content-delivery#core-resources/stories/stories">stories option</a>:</p> <pre><code class="language-js">async asyncData({ app, params }) { // Find tag based on the slug const { data: tagsData } = await app.$storyapi.get('cdn/tags/') const topic = tagsData.tags.find((t) => kebabCase(t.name) === params.slug) // Fetch articles const { data: articlesData } = await app.$storyapi.get('cdn/stories', { starts_with: 'articles/', resolve_relations: 'author', with_tag: topic.name, }) const articles = articlesData.stories.map((story) => { story.content.date = new Date(story.content.date) return story }) return { topic, articles } }, </code></pre> <p>Finally, to differentiate it from the home page, update the page <code><h1></code> tag to show as well the topic name and count:</p> <pre><code class="language-vue"><h1 class="text-4xl font-bold"> {{ topic.taggings_count }} articles on <i>#{{ topic.name }}</i> </h1> </code></pre> <p>The page will look similar to this:</p> <p><img src="https://a.storyblok.com/f/83078/1692x910/f85bef9975/05-02-topics-page.png" alt="https://a.storyblok.com/f/83078/1692x910/f85bef9975/05-02-topics-page.png"> {.img-shadow}</p> <h2>Search Content Using Storyblok API</h2> <p>Having a way to navigate through different topics is great, but sometimes we want to search for something specific more easily.</p> <p>That's where search comes into place, and you will rarely see a decent blog or content-based website without a search box.</p> <p>Let's start by creating a <em>SearchBox</em> component. It must have a search input, and when the user types in it, it will perform a call to get the stories that match the criteria.</p> <p>An example of that <em>SearchBox</em> could be:</p> <pre><code class="language-html"><template> <div class="relative"> <!-- Magnifying glass icon --> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="block absolute text-gray-600 right-0 z-10 h-4 fill-current mr-3" style="top: 9px;" > <path d="M508.5 468.9L387.1 347.5c-2.3-2.3-5.3-3.5-8.5-3.5h-13.2c31.5-36.5 50.6-84 50.6-136C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c52 0 99.5-19.1 136-50.6v13.2c0 3.2 1.3 6.2 3.5 8.5l121.4 121.4c4.7 4.7 12.3 4.7 17 0l22.6-22.6c4.7-4.7 4.7-12.3 0-17zM208 368c-88.4 0-160-71.6-160-160S119.6 48 208 48s160 71.6 160 160-71.6 160-160 160z" ></path> </svg> <!-- Search Input --> <input v-model="searchInput" @input="onInputChange" @blur="onInputBlur" placeholder="Search..." class="w-full bg-white text-gray-700 rounded border-2 border-transparent outline-none focus:border-purple-500 px-4 py-1" /> <!-- Suggestions list --> <div class="relative"> <div class="absolute z-30 bg-white top-0 inset-x-0 rounded shadow-lg mt-1" > <nuxt-link v-for="suggestion in suggestions" :key="suggestion.id" :to="`/${suggestion.slug}`" class="block truncate text-gray-700 hover:text-main hover:bg-gray-100 px-4 py-2" > {{ suggestion.content.title }} </nuxt-link> </div> </div> </div> </template> <script> import debounce from 'lodash/debounce' export default { props: { search: { type: Function, required: true, }, }, data: () => ({ searchInput: '', suggestions: [], }), methods: { onInputChange: debounce(async function () { this.suggestions = await this.search(this.searchInput) }, 300), onInputBlur() { setTimeout(() => (this.suggestions = []), 300) }, }, } </script> </code></pre> <p>I won't explain this component in detail, since it's not relevant. You're free to code your own or to use any existing libraries like the finely done <a href="https://vue-multiselect.js.org/">vue-multiselect</a>.</p> <p>What's important on this <em>SearchBox</em> component is:</p> <ul> <li>It keeps a <code>searchInput</code> and <code>suggestions</code> state.</li> <li>When the search input changes, it performs a call to retrieve the results. We do this by listening to the <code>@input</code> event and delegating the responsibility for how the call must be done to the parent component by receiving a <code>search</code> property. It's convenient as well to <code>debounce</code> the function, so it doesn't perform a call for every keystroke, but instead it'll do that when the user stops typing for 300ms.</li> <li>When the user <em>blurs</em> the input, we empty the suggestions to close the suggestion list. But we do it 300ms later, otherwise it won't be possible to click on a suggestion since the list closes immediately on blur.</li> </ul> <p>Now you can use it on <em>components/layout/AppHeader.vue</em> to show the SearchBox in the header. But before you do that, let me explain something.</p> <p>When we talk about a full-text search, it means that given a registry of data (in this case the article stories) the system performs a search on all of the fields you need. Usually, this can be quite complex, and you have technologies specifically built for it (like <a href="https://www.elastic.co/start">ElasticSearch</a>) and services (like <a href="https://www.algolia.com/">Algolia</a>).</p> <p>Here comes one of my favorite parts of Storyblok: it gives you a free full-text search, and it's actually quite easy to use.</p> <p>All you need to do is to use the <code>search_term</code> parameter on your API query. Use it on your <em>AppHeader.vue</em> component and make use of the <em>SearchBox</em>:</p> <pre><code class="language-html"><template> <header class="w-full bg-main py-2"> <div class="container mx-auto px-4"> <div class="flex"> <nuxt-link to="/" class="text-2xl text-white font-semibold"> NarutoDose </nuxt-link> <SearchBox :search="fetchSuggestions" class="flex-1 max-w-sm ml-12" /> </div> </div> </header> </template> <script> export default { methods: { async fetchSuggestions(searchInput) { const { data } = await this.$storyapi.get('cdn/stories', { starts_with: 'articles/', resolve_relations: 'author', search_term: searchInput, per_page: 5, }) return data.stories }, }, } </script> </code></pre> <p>I've also added a <code>per_page</code> so we don't have a bunch of results, but the 5 most relevant.</p> <p>When you run it, you should see it working like this:</p> <p><img src="https://a.storyblok.com/f/83078/1634x910/feeb466f9a/05-03-search.png" alt="https://a.storyblok.com/f/83078/1634x910/feeb466f9a/05-03-search.png"> {.img-shadow}</p> <h2>To Sum Up</h2> <p>Making it easy for your readers to find the content that they want doesn't need to be complicated nor complex. Providing them with categorization, tagging and search functionality is a good way to achieve it.</p> <p>You've seen how easy it is to do that in a <a href="https://nuxtjs.org/">Nuxt</a> blog thanks to <a href="https://www.storyblok.com/">Storyblok</a> and all the great capabilities its API has.</p> <p>But the job is not done...</p> <p>Is the blog discoverable by the search engines yet?</p> <p>Probably we still have a lot to do about SEO... stay tuned and we'll solve that next week in part 6!</p> <p>Do you want to play with the code? Find it <a href="https://github.com/alexjoverm/narutodose/tree/05-tags-and-search-functionality-in-nuxt-using-storyblok-api">on Github</a>.</p> ]]></content:encoded> </item> <item> <title><![CDATA[Creating UI components based on a Design System in Vue.js]]></title> <link>https://vuedose.tips/creating-ui-components-based-on-a-design-system-in-vue-js/</link> <guid>https://vuedose.tips/creating-ui-components-based-on-a-design-system-in-vue-js/</guid> <pubDate>Tue, 21 Jul 2020 01:52:00 GMT</pubDate> <description><![CDATA[Don't you know the benefits of using a Design System? Not sure how to structure your Vue.js components in an app? Read the article and find it out.]]></description> <content:encoded><![CDATA[<p><em>In <a href="https://vuedose.tips/setting-up-a-full-static-nuxt-site">the previous part of this guide</a> you created a basic Nuxt project, full-static ready including TailwindCSS. In this one you'll take it from that to start designing some components, but before you get to that, I want to tell you how we structured VueDose Design system.</em></p> <p>Organizing Vue.js components can be quite difficult if you're not very experienced with it. It took me a while to learn how to do it in a decent way, and that was through a lot of try and fail approaches.</p> <p>Nowadays, frontend technologies, paradigms and ecosystem are changing so fast, that many best practices have not yet been established.</p> <p>What's even worse is that often designers and frontend developers don't understand each other very well!</p> <p>Have you felt the pain of any of these points?</p> <p>I want to make this easier for you by sharing this article about how <a href="https://twitter.com/aarongarciah">Aarón Garcia</a> (UI designer) and I worked together to build VueDose 2.0. We created a Design System using <a href="https://tailwindcss.com/">TailwindCSS</a> to easily implement the design system and structuring in Vue.js components.</p> <p>Then, you'll apply these concepts to Naruto Dose, the project we're building to better understand the things you learn.</p> <p><em>Note: I'll focus more on the implementation side, but expect in the future an article where</em> <a href="https://twitter.com/aarongarciah">Aarón Garcia</a> <em>describes in depth the creative process of designing VueDose Design System.</em></p> <h2>Designing a Design System</h2> <p>When you decide to create a software product with a top quality design, you should consider creating a <a href="https://uxdesign.cc/everything-you-need-to-know-about-design-systems-54b109851969">Design System</a>.</p> <p>Why? Because it gives you the design structure, patterns and elements to make sure your design is consistent, harmonious and balanced throughout the product and people working on it. It's the language all designers and frontend devs are going to use to talk about design.</p> <p>We structured the VueDose design system by separating the elements into 4 different layers: <strong>style guide, components, modules and pages</strong>.</p> <p>In fact, this is similar to how <a href="https://bradfrost.com/blog/post/atomic-web-design/">Atomic Design</a> structures elements.</p> <p>:::blockquote success</p> <p>Give a read to the amazing article "<a href="https://vuedose.tips/how-to-structure-a-vue-js-app-using-atomic-design-and-tailwindcss">How to structure a Vue.js app using Atomic Design and TailwindCSS</a>" by <a href="https://vuedose.tips/authors/alba-silvente/">Alba Silvente</a> to learn more about this.</p> <p>:::</p> <h3>1. Style Guide</h3> <p>The style guide contains the basic rules, constraints and elements that your design will be built upon.</p> <p>In VueDose style guide we define:</p> <ul> <li>Screen breakpoints (mobile, tablet, desktop...)</li> <li>Colors</li> <li>Typography</li> <li>Spacing</li> <li>Grid and Container</li> <li>Icons</li> <li>Shapes</li> <li>Borders</li> <li>Shadows</li> <li>...</li> </ul> <p>Aarón designed it in <a href="http://figma.com">Figma</a>, and looks like this:</p> <p><img src="https://a.storyblok.com/f/83078/1797x1359/fa3e63ec33/02-style-guide.png" alt="https://a.storyblok.com/f/83078/1797x1359/fa3e63ec33/02-style-guide.png"></p> <p>The question still remains, why is TailwindCSS a good fit for creating a style guide and how does it work?</p> <p>Easy: TailwindCSS is style guide oriented. In fact, by default it gives you a standard and well-thought out style guide.</p> <p>Being a utility based CSS framework, it gives you the flexibility to define your own styling rules and uses them as classes.</p> <p>You can better understand it with this comparison:</p> <p>If you don't follow a style guide, and you have to set a margin top, you probably just play with some sizes, like 12px, 15px, 18px... until it fits the design, supposing the design is not following a style guide.</p> <p>But when you have a style guide, your elements are calculated coherently without guessing. For example, we can apply an 8-pt spacing scale in the VueDose style guide:</p> <p><img src="https://a.storyblok.com/f/83078/1451x860/15e704d339/02-sizing.png" alt="https://a.storyblok.com/f/83078/1451x860/15e704d339/02-sizing.png"></p> <p>The spacing is always multiples of 8, giving you a consistent, calibrated and aligned design. The end result is that you'll have an optimized <a href="https://builttoadapt.io/8-point-grid-vertical-rhythm-90d05ad95032">vertical rythm</a> and better reading experience.</p> <p>Same for typography, colors, borders, icons... They're all calculated to bring balance to the design.</p> <h3>2. Components</h3> <p>Once you have a style guide defined, you use it to build components.</p> <p>A component is considered an atomic element. This means that a component, in general, doesn't include other components.</p> <p>Some examples of components are buttons, links, form elements, alerts, logos...</p> <p>This is how we defined some components in the design system:</p> <p><img src="https://a.storyblok.com/f/83078/1394x1497/4a07723232/02-components.png" alt="https://a.storyblok.com/f/83078/1394x1497/4a07723232/02-components.png"></p> <h3>3. Modules</h3> <p>We consider a module a combination of components to create a "bigger component". For example, for the article cards we use a button, an avatar, a title and tags, which you can see in this image:</p> <p><img src="https://a.storyblok.com/f/83078/1644x1358/9c4037b510/02-modules.png" alt="https://a.storyblok.com/f/83078/1644x1358/9c4037b510/02-modules.png"></p> <p>Examples of modules are your website header and footer, a card or a form.</p> <h3>4. Pages</h3> <p>Finally, we use a combination of modules and components that follow our style guide to create the pages.</p> <p>This is an example of the Home page in VueDose:</p> <p><img src="https://a.storyblok.com/f/83078/1847x1335/2bda3c8b80/02-pages.png" alt="https://a.storyblok.com/f/83078/1847x1335/2bda3c8b80/02-pages.png"></p> <h2>Creating your own design system</h2> <p>You probably realize that building a design system makes design much easier, since you start building the smaller elements that you later use for bigger ones.</p> <p>That not only improves the gap between a designer and a developer, since they now have a common language measured and calculated to work on. But, it also makes your design more consistent.</p> <p>Is this all that complex to implement?</p> <p>Not at all. Lucky for us, today you have good libraries to make this easily happen.</p> <p>TailwindCSS covers very well the style guide part, while Vue.js, by itself, is component oriented. The combination of both: extremely powerful.</p> <p>Let's see how to apply this kind of design to NarutoDose.</p> <h3>Style Guide</h3> <p>To create a style guide you start by defining the colors, spacing and typography. You can create your fully customized style guide as we did in VueDose.</p> <p>In this case, let's start from the tailwind default utilities set which is very well thought out and has everything you need:</p> <ul> <li><strong>Colors:</strong> tailwind includes a nice <a href="https://tailwindcss.com/docs/customizing-colors/#default-color-palette">color palette</a> you can use</li> <li><strong>Spacing:</strong> an <a href="https://tailwindcss.com/docs/customizing-spacing/">8-pt spacing system</a> is also there for you to use on width, padding and margin.</li> <li><strong>Typography:</strong> you can use a combination of the <a href="https://tailwindcss.com/docs/font-size">font size</a> and <a href="https://tailwindcss.com/docs/line-height">line height</a> utility classes for custom fonts, and the recently released <a href="https://github.com/tailwindcss/typography">typography plugin</a> for the blog contents formatting.</li> </ul> <p>First of all, open the file <em>tailwind.config.js</em> in your Nuxt project.</p> <p>Let's add a main color that we want to use in our application, based on the color Teal 500. For that, let's extend the default colors:</p> <pre><code class="language-js">const { theme } = require('tailwindcss/defaultConfig') module.exports = { theme: { extend: { colors: { main: theme.colors.teal['500'], // #38b2ac }, }, }, // ... } </code></pre> <p>You could have written <code>main: '#38b2ac'</code> directly, but you can reuse the default TailwindCSS theme like that to build yours.</p> <p>I'm adding the main color under the <code>extend</code> key. That allows you to add your customizations while keeping TailwindCSS defaults, and apply that to all theme customizations. In case you want to fully override TailwindCSS colors, you have to write your settings just below the <code>theme</code> key without <code>extend</code>.</p> <p>You will use the <code>theme</code> key to override or extend any <a href="https://tailwindcss.com/docs/theme">configuration from TailwindCSS</a>.</p> <h2>Creating Components</h2> <p>First of all, I want to tell you how I <strong>organize components</strong> in the world of Vue.js where everything is a component.</p> <p>I do that by categorizing them depending on their responsibilities. In particular, I organize it by creating these folders under <code>components</code>:</p> <ul> <li><code>layout</code>: components that are related to the main application layout, such as the navbar, main header, footer, etc.</li> <li><code>ui</code>: visual components that have no application logic and are usually reusable across the app. The obvious example are any components from a UI library like <a href="https://vuetifyjs.com/en/">Vuetify</a>.</li> <li><code>app</code>: they have application logic and they usually use ui components and are generally a bit more complex. They can either be reusable or not. Some examples could be: a login form, a user table, a filtered product list... or anything that has some business logic</li> <li><code>page-sections</code>: page components can sometimes get too big. In those cases, I start splitting a page component into page section components.</li> </ul> <p>Combined with the special <a href="https://nuxtjs.org/guide/views#layouts">layout</a> and <a href="https://nuxtjs.org/guide/views#pages">pages</a> directories, this structure is complete.</p> <h3>Layout</h3> <p>First of all, let's define a simple layout with a navbar. Create the file <code>components/layout/AppHeader</code> with the following markup:</p> <pre><code class="language-vue"><template> <header class="w-full bg-main py-2"> <div class="container mx-auto px-4"> <span class="text-2xl text-white font-semibold">NarutoDose</span> </div> </header> </template> </code></pre> <p>Notice we use <code>bg-main</code>. That's the custom color we added before, and we can use that for background, text and borders. The cool part is that just by changing its value in the tailwind configuration, we change how the whole app looks 😉.</p> <p>Then add it to <code>layouts/default.vue</code>:</p> <pre><code class="language-vue"><template> <div> <AppHeader /> <main> <Nuxt /> </main> </div> </template> </code></pre> <p>In case you find it difficult to understand TailwindCSS and its classes, I suggest you take a look at these <a href="https://tailwindcss.com/docs/installation/">beautiful docs</a> and play around a bit. It's pretty easy once you have some practice.</p> <h3>Design the ArticleCard component</h3> <p>You already created <code>components/ArticleCard.vue</code>. That's a good example of a UI component, so let's follow the structure proposed above and move it into <code>components/ui/ArticleCard.vue</code>.</p> <p>Then add some tailwind magic to make it look nice:</p> <pre><code class="language-vue"><template> <nuxt-link class="block text-gray-800 rounded-lg shadow-lg p-6" :to="`/${slug}`" > <header class="text-2xl font-bold">{{ title }}</header> <p class="mt-4">{{ description }}</p> <footer class="flex items-center mt-6"> <img class="w-20 rounded-full border-4 border-main" :src="author.image" :alt="author.name" /> <div class="ml-6"> <p class="text-xl font-bold">{{ author.name }}</p> <p class="text-sm mt-1">{{ date }}</p> </div> </footer> </nuxt-link> </template> <script> export default { props: { title: String, slug: String, description: String, author: Object, date: String, }, } </script> </code></pre> <p>Notice I've also added a <code>date</code> prop.</p> <p>Now let's add some styling to the home page, located under <code>pages/index.vue</code>:</p> <pre><code class="language-vue"><template> <div class="mt-4"> <section> <div class="container mx-auto px-4"> <h1 class="text-4xl font-bold">Articles</h1> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 mt-4"> <ArticleCard v-for="article in articles" :key="article.title" :title="article.title" :description="article.description" :author="article.author" :date="article.date.toLocaleDateString()" /> </div> </div> </section> </div> </template> </code></pre> <p>I'm using a <a href="https://tailwindcss.com/docs/grid-template-columns/">grid</a> to show the articles in 1, 2 or 3 columns, depending on the window size.</p> <p>For now, translate the date simply by using <code>date.toLocaleDateString()</code>.</p> <p>In order to see several article cards displayed, let's repeat the article object that we have on <code>asyncData</code> several times, and add the <code>date</code> property as well:</p> <pre><code class="language-js">export default { asyncData() { const articles = [ { title: 'How to make your articles Vuetiful', description: 'This article guides you through all the steps to make an article shine with your favourite framework, Vue', author: { name: 'Naruto Uzumaki', image: 'https://a.storyblok.com/f/83078/500x500/cb27fcd15a/naruto-avatar.jpg', }, date: new Date(), }, { ... }, // more articles repeated { ... }, ] return { articles } }, } </code></pre> <p>That's it. If you followed the steps correctly, you should see an interface like this:</p> <p><img src="https://a.storyblok.com/f/83078/1355x873/c4290c8129/02-final-result.png" alt="https://a.storyblok.com/f/83078/1355x873/c4290c8129/02-final-result.png"></p> <h3>Layout key points</h3> <p>There are some layout techniques and best practices I've applied that might have gone unnoticed. Let me emphasize them:</p> <ul> <li>Use of <strong><a href="https://tailwindcss.com/docs/container/#app">container</a></strong>. You can see in AppHeader and the home page that I've used the classes <code>container mx-auto px-4</code>. A container is used to limit the width of the content, and we use it inside the AppHeader or the sections (instead of outside) because that way we can have a background color in full width (like AppHeader) while having the inner content width adjusted. You can center it and add a padding <a href="https://tailwindcss.com/docs/container/#centering-by-default">by default in tailwind config</a>. Then you only need the <code>container</code> class, without <code>mx-auto px-4</code>.</li> <li>Set vertical <strong>separation with margin top.</strong> For consistency, always use margin-top to separate elements, and no margin-bottom or padding-top. Margin is for separation, and padding is for inner spacing.</li> <li>Margins and widths are set <strong>outside the elements</strong>. If you check <em>ArticleCard</em>, it doesn't have any width or margins because it's not its job to know how it's going to be displayed in different places. <em>ArticleCard</em> has its inner dimensions (padding, and separation between inner elements), but how it is displayed is decided in <code>pages/index.vue</code> using the grid classes.</li> </ul> <h2>Recap</h2> <p>This is the end of the article, but not the end of the guide!</p> <p>In this article I focused on showing you the way <a href="https://twitter.com/aarongarciah">Aarón</a> and I decided to architecture VueDose 2.0 design, explaining all the techniques and methodologies we used to ensure a <strong>consistent design</strong> for the best viewing and reading experience.</p> <p>In particular, you've seen how to create a design system and to structure it in different layers.</p> <p>You've also learned how to apply these concepts to NarutoDose, the pet project you're creating to integrate all of the lessons, and how TailwindCSS fits perfectly for this way of styling web apps.</p> <p>Did you like it so far? Great, because the best is yet to come.</p> <p>In the next article, you'll bring the articles to life! We'll set up the content structure in <a href="https://www.storyblok.com/developers?utm_source=newsletter&utm_medium=logo&utm_campaign=vuedose">Storyblok</a> and use its API to take the data from there.</p> <p>Are you ready for it? 😏</p> <p><strong>Pssst</strong>: find <a href="https://github.com/alexjoverm/narutodose/tree/02-creating-ui-components-based-on-a-design-system-in-vuejs">this lesson's code on Github</a>.</p> ]]></content:encoded> </item> <item> <title><![CDATA[Setting up a full static Nuxt site]]></title> <link>https://vuedose.tips/setting-up-a-full-static-nuxt-site/</link> <guid>https://vuedose.tips/setting-up-a-full-static-nuxt-site/</guid> <pubDate>Tue, 14 Jul 2020 03:56:00 GMT</pubDate> <description><![CDATA[Overwhelmed by too many options to create a blog? Chill and use Nuxt, Storyblok and TailwindCSS to make it simple, beautiful and incredibly performant]]></description> <content:encoded><![CDATA[<p>When I start a project these days I usually choose <a href="https://nuxtjs.org/">Nuxt</a> for it. Why? Because I can do all the things I do with plain <a href="https://vuejs.org/">Vue.js</a> in less time, thanks to its conventions and the amazing Nuxt modules.</p> <p>Some people think because of these conventions you lose flexibility. That's far from the truth: I've been using Nuxt for the last 2 years and I haven't found a case where I can't do what I wanted to do.</p> <p>I've been using it even in <a href="https://www.sprintersports.com/">Sprinter</a>, one of the biggest Spanish sports E-Commerce. I spoke about the challenges we faced in <a href="https://www.youtube.com/watch?v=rzYoM2jVJr0">this talk on Vue Day 2019</a>.</p> <p>But what I love the most about Nuxt is that you can produce 3 types of applications mostly with the same code:</p> <ul> <li><strong>SSR</strong> (Server Side Rendered): they're rendered on the server for every request. Sprinter works like that.</li> <li><strong>SPA</strong> (Single Page Application): client-only javascript based apps. Nowadays, they're quite well-known.</li> <li><strong>Static Generated</strong>: a.k.a. <a href="https://jamstack.org/">Jam Stack</a>. Similar to SSR, but the html is rendered at compile time, so they don't need a server while also being SEO friendly.</li> </ul> <p><a href="https://vuedose.tips/">VueDose</a> is built as a static generated site, for different reasons:</p> <ul> <li>SEO friendly</li> <li>Very fast: HTML is already rendered</li> <li>Saving on resources: it doesn't need a server, so you're saving money and <a href="https://www.websitecarbon.com/">the planet</a></li> </ul> <p>Since Nuxt 2.13, we can create <strong>full static</strong> websites. In other words: you can generate a pure HTML + CSS + JavaScript site. All the Ajax calls to any API's are made at compile time, and then stored in json files locally.</p> <p>What does that mean? Easy: huge <strong>performance boost.</strong></p> <p>But hey that's too much introduction, right? Let's start building NarutoDose!</p> <h2>The Stack</h2> <h3>Nuxt.js</h3> <p>I think I showed enough love to Nuxt, right? Ok fine let's show Nuxt love once more 💚💚💚</p> <h3>Storyblok (vs Nuxt Content Module)</h3> <p>Nuxt's recent release, <a href="https://content.nuxtjs.org/">@nuxt/content</a>: an amazing module for creating your contents under a <code>content/</code> folder by default. Your contents can be in Markdown, Yaml, JSON and other formats and it exposes an API where you can query those contents. Honestly, this was the missing part in the Nuxt ecosystem for indie tech bloggers.</p> <p>You might think, couldn't you create VueDose with @nuxt/content? Yeah, I definitely could. But I didn't.</p> <p>Why? Because, even though they both have things in common such as:</p> <ul> <li>A flexible API to query contents, including full-text search</li> <li>Ability to schedule content</li> <li>Ability to use rich text format, such Markdown</li> </ul> <p>There are definitely different:</p> <ul> <li><strong>@nuxt/content:</strong> it acts as a git-based CMS, meaning that people need to be familiar with git. That's ok for most tech blog cases, just something to be aware of.</li> <li><strong><a href="https://www.storyblok.com/developers?utm_source=newsletter&utm_medium=logo&utm_campaign=vuedose">Storyblok</a></strong>: it's much more than that. Although easy to use, it's a fully cloud-based CMS product that has more features for working with content such as, being able to define a schema, relationships and even a visual editor. It can be extended with plugins, although by default you also have roles, on-the-fly image resizing service, assets managements, and more. It can be managed by a non-dev user.</li> </ul> <p>Since I wanted to leverage the power of Storyblok on VueDose, that was an easy decision to make. No worries, soon you'll know how to do it too 😉.</p> <h3>TailwindCSS</h3> <p>You know that there are many CSS frameworks out there... the well-known <a href="https://vuetifyjs.com/en/">Vuetify</a>, the new exciting <a href="https://vue.chakra-ui.com/">Chakra UI</a> for Vue, <a href="https://buefy.org/">Buefy</a>, <a href="https://bootstrap-vue.org/">Bootstrap Vue</a> and of course the marvelous <em>[INSERT YOUR NEW UI FRAMEWORK HERE]</em>.</p> <p>But <a href="https://twitter.com/aarongarciah">Aarón</a> and I decided to design VueDose from scratch, creating our own Design System, carefully thought out for giving the best reading experience and a beautiful UI.</p> <p><a href="https://tailwindcss.com/">TailwindCSS</a> is not opinionated and is perfect for this case. Check out the next article to find out all the whys and how-tos 😏</p> <h2>Create a Nuxt project</h2> <p>The easiest way is to use npx (install it if you haven't) to run create-nuxt-app:</p> <pre><code class="language-bash">npx create-nuxt-app mini-vuedose </code></pre> <p>That will open a prompt where you can select the features you want. For this project we'll choose TailwindCSS and it's important to choose <em>Universal (SSR / SSG)</em> as a rendering mode and <em>Static (Static/JAMStack hosting)</em> as a deployment target:</p> <p><img src="https://a.storyblok.com/f/83078/837x438/878133bee7/01-npx-create-nuxt.png" alt="https://a.storyblok.com/f/83078/837x438/878133bee7/01-npx-create-nuxt.png"></p> <p>Now let's clean up some things:</p> <ul> <li>Remove the folders <code>store</code> and <code>middleware</code>. You don't need them for now.</li> <li>Remove <code>components/Logo.vue</code></li> <li>In <code>pages/index.vue</code>, remove the <code><style></code> tag and empty the <code><template></code>.</li> </ul> <p>Starting clean, now is a good time to create the <code>ArticleCard.vue</code> component. Let's temporarily place it under the <code>components</code> folder.</p> <p>As you can guess by the name, the ArticleCard component is used for displaying the list (or grid) of articles in the home page. Create it with the following structure:</p> <pre><code class="language-vue"><template> <div> <header> <nuxt-link :to="`/${slug}`">{{ title }}</nuxt-link> </header> <p>{{ description }}</p> <footer> <img :src="author.image" :alt="author.name" /> <span>{{ author.name }}</span> </footer> </div> </template> <script> export default { props: { title: String, slug: String, description: String, author: Object, }, } </script> </code></pre> <p>It's simple: it has a title, description, slug and an author.</p> <p>Now go to <code>pages/index.vue</code> and create an articles array in the <code>asyncData</code> function. For now it will be static, but later we'll use <a href="https://www.storyblok.com/developers?utm_source=newsletter&utm_medium=logo&utm_campaign=vuedose">Storyblok</a> to get the real data. Use the <code>ArticleCard</code> component we created to display the article data:</p> <pre><code class="language-vue"><template> <div> <h1>Articles</h1> <div> <ArticleCard v-for="article in articles" :key="article.title" :title="article.title" :description="article.description" :author="article.author" /> </div> </div> </template> <script> export default { asyncData() { const articles = [ { title: 'How to make your articles Vuetiful', description: 'This article guides you through all the steps to make an article shine with your favourite framework, Vue', author: { name: 'Naruto Uzumaki', image: 'https://wallpaperaccess.com/full/2866246.jpg', }, }, ] return { articles } }, } </script> </code></pre> <p>:::blockquote info Do you see the ArticleCard being imported? No, because from <a href="https://nuxtjs.org/blog/improve-your-developer-experience-with-nuxt-components/">Nuxt 2.13 the @nuxt/components</a> modules is built-in Nuxt and activated by default, meaning... no more component imports! :::</p> <p>Let's make sure things work. Run <code>npm run dev</code> in your terminal and open <a href="http://localhost:3000/">http://localhost:3000</a>. The page should look like this:</p> <p><img src="https://a.storyblok.com/f/83078/1270x874/6d7d5309f8/01-first-article.png" alt="https://a.storyblok.com/f/83078/1270x874/6d7d5309f8/01-first-article.png"></p> <p>Don't worry too much about the card design right now, you will see that in the next article!</p> <h2>Recap</h2> <p>Up to this point, you have set up Nuxt with TailwindCSS in full static mode. Easy right? Yes, Nuxt does a lot of things for you out of the box, and based on the choices you made using the installer, things are configured out of the box. Later in this guide you'll see how to generate the production page but hold on for now.</p> <p>This was just an introduction to get you started, but things are about to get fun 😎</p> <p>Hold on there! In the next article you'll learn to design the app UI based on a Design System while following a simple yet solid component structure that I use on my projects.</p> <p>You'll also learn why TailwindCSS fits so well for that purpose.</p> <p><strong>Pssst</strong>: you can find <a href="https://github.com/alexjoverm/narutodose/tree/01-setting-up-a-full-static-nuxt-site">this article's code on Github</a>.</p> ]]></content:encoded> </item> <item> <title><![CDATA[Use Composition API to easily handle API requests in Vue.js]]></title> <link>https://vuedose.tips/use-composition-api-to-easily-handle-api-requests-in-vue-js/</link> <guid>https://vuedose.tips/use-composition-api-to-easily-handle-api-requests-in-vue-js/</guid> <pubDate>Mon, 06 Jul 2020 01:04:00 GMT</pubDate> <description><![CDATA[Not sure how to organize your API client in Vue.js? Learn this simple technique on how to do it using the new Composition API and make it easy.]]></description> <content:encoded><![CDATA[<p>When building a web-app most of the times we need to fetch from the server or execute some action.</p> <p>Handling the request status can be tedious and we often write the same code over and over.</p> <p>With the composition API we can write a composable that handles the request status and exposes reactive objects</p> <p>Let start by defining some requirements:</p> <ul> <li>Needs to provide reactive <code>isLoading</code>, <code>result</code>, <code>error</code> and <code>execute</code> <ul> <li><code>isLoading</code>: <code>true</code> is we are waiting for a request</li> <li><code>result</code>: Success response from the server</li> <li><code>error</code>: Error object form the server</li> <li><code>execute</code>: Execute the request</li> </ul> </li> <li>Need to provide the base request and the <code>execute</code> should allow sending <code>params</code> or <code>body</code> to the request.</li> </ul> <p>The expected usage will be like:</p> <pre><code class="language-ts">setup(){ const userList = useApi(()=>({url:`https://swapi.dev/api/people/1`}), (r)=> r.json()); userList.execute(); // we can await for result // ... return { ...userList } } </code></pre> <p>This factory based usage allows great flexibility, the first parameter is a <code>factory</code> that returns the <code>fetchRequest</code>, the second parameter is just a <code>transformer</code> of the <code>fetch</code> result value.</p> <p>The return <code>execute()</code> will pass all the parameters to the <code>factory</code>, allowing to greater customization on building requests. Allowing you to do:</p> <pre><code class="language-ts">setup(){ const userList = useApi((page: number)=>({url:`https://swapi.dev/api/people/${page}`}), (r)=> r.json()); userList.execute(1); // we can await for result // ... return { ...userList } } </code></pre> <p>Allowing greater <strong>composability</strong> on having <strong>automagically</strong> having a paginated list:</p> <pre><code class="language-ts">setup(){ // ... const page = ref(1); // watch for `page` changes watch(page, p=> userList.execute(p)); // ... return { ...userList, page } } </code></pre> <p>Simplifying the pagination to changing variable <code>page.value</code> instead of calling the <code>userList.execute(page.value)</code>.</p> <h3>Implementation</h3> <p>A simple javascript implementation (<a href="https://gist.github.com/pikax/b13fc50e46668659e9d2dd3851e83d9f">with typings</a>)</p> <pre><code class="language-ts">function useApi( factory, handleResponse ){ const isLoading = ref(false); const result = ref(null); const error = ref(null); const execute = async (...args) => { const request = factory(...args); isLoading.value = true; error.value = null; try { const response = await fetch(request); const valueResponse = await handleResponse(response) result.value = valueResponse; return valueResponse; } catch (e) { error.value = e; result.value = null; } finally { isLoading.value = false; } }; return { isLoading, result, error, execute, }; } </code></pre> <blockquote> <p>This implementation might not yield correct results, if the <code>execute</code> is called many times before the first finishes, because the <code>result</code> will always be the latest server response.</p> </blockquote> <p>I'm the creator of a composable library compatible with <code>vue2 + composition-api</code> and <code>vue3</code> called <a href="https://github.com/pikax/vue-composable/">vue-composable</a>, where you can do something similar by using <a href="https://pikax.me/vue-composable/composable/promise/promise.html">usePromise</a>, using it will make sure the <code>result.value</code> will hold the value of the last valid <code>exec()</code> response value.</p> <pre><code class="language-ts">// pass `true` on the second argument to make it lazy const userList = usePromise((id)=>fetch(`https://swapi.dev/api/people/${id}`).then(r=>r.json(), true) userList.exec(1) </code></pre> <p>I've been using this pattern on my projects, from action buttons to fetching the next paginated page, and has been working great so far. It makes it trivial to disable a button if a request is happening or to compose it into other composables.</p> ]]></content:encoded> </item> <item> <title><![CDATA[Build a Game in Vuejs with Phaser]]></title> <link>https://vuedose.tips/build-a-game-in-vuejs-with-phaser/</link> <guid>https://vuedose.tips/build-a-game-in-vuejs-with-phaser/</guid> <pubDate>Mon, 22 Jun 2020 00:00:00 GMT</pubDate> <description><![CDATA[Love video games and Vue.js? Then check this tutorial and learn how to build a game using Phaser, the javascript game engine for the web.]]></description> <content:encoded><![CDATA[<p>Nowadays you can create almost everything with JavaScript, also games 🎮!!! The cool thing about this is that you don’t need to be an expert making games to start, and there are a lot of tools, for example <a href="https://phaser.io/">Phaser</a>.</p> <p><img src="https://cdn-images-1.medium.com/max/1270/1*B8WRf-Yp1cSW1-VfptkmGQ.png" alt=""></p> <p><a href="https://phaser.io/">Phaser</a> let you create games with only JavaScript, I find this exciting and is the reason why I’m playing with it in some of my <a href="https://twitch.tv/ckgrafico">Twitch</a> sessions.</p> <p>One of the weaknesses that you can find in this kind of libraries is that are not in the same level of evolution than others like <a href="https://vuejs.org/">Vue </a>and <a href="https://reactjs.org/">React</a> (if we talk about UI elements). To create a button in <a href="https://phaser.io/">Phaser</a> is more complex than in <a href="https://vuejs.org/">Vue</a>.</p> <p>This is why some developers started to use <a href="https://reactjs.org/">React</a> and <a href="https://vuejs.org/">Vue</a> in combination with <a href="https://phaser.io/">Phaser</a>, in this tip you will learn how to integrate <a href="https://phaser.io/">Phaser</a> and <a href="https://vuejs.org/">Vue</a>.</p> <p>First create a new project with the CLI:</p> <pre><code class="language-bash">vue create game </code></pre> <p>Navigate to the project and install phaser and <a href="https://github.com/proyecto26/ion-phaser">ion-phaser</a></p> <pre><code class="language-bash">npm install --save phaser @ion-phaser/core </code></pre> <p>This package will help ups to easily integrate Phaser with libraries and frameworks like Angular, React or Vue, you will find more <a href="https://market.ionicframework.com/plugins/ionphaser">information in their website.</a></p> <p>The next step is to add ion-phaser to our <code>main.js</code> file:</p> <pre><code class="language-js">import { defineCustomElements as defineIonPhaser } from '@ion-phaser/core/loader'; import Vue from 'vue'; import App from './App.vue'; Vue.config.productionTip = false; Vue.config.ignoredElements = [/ion-\w*/]; defineIonPhaser(window); new Vue({ render: (h) => h(App), }).$mount('#app'); </code></pre> <p>Ok, now its time to open our example component, ‘HelloWorld.vue’ and add the ion-phaser component.</p> <p>The layout will look like:</p> <pre><code class="language-vue"><template> <div class='hello'> <div @click="initializeGame" class="flex" > <a href="#1" class="btn">Initialize</a> </div> <ion-phaser v-bind:game.prop='game' v-bind:initialize.prop='initialize' /> </div> </template> </code></pre> <p>And in the code we simply add the game object, for this example a really simple game object:</p> <pre><code class="language-js">import Phaser from "phaser"; export default { data() { return { initialize: false, game: { width: "100%", height: "100%", type: Phaser.AUTO, scene: { init() { this.cameras.main.setBackgroundColor("#24252A"); }, create() { this.helloWorld = this.add.text( this.cameras.main.centerX, this.cameras.main.centerY, "Hello World", { font: "40px Arial", fill: "#ffffff" } ); this.helloWorld.setOrigin(0.5); }, update() { this.helloWorld.angle += 1; } } } }; }, methods: { initializeGame() { this.initialize = true; } } }; </code></pre> <p>And yeah! both are integrated!</p> <p><img src="https://cdn-images-1.medium.com/max/1572/1*1BQUK52qV8IFYTj6aLgvuA.png" alt=""></p> <p>In a real world example what I recommend to you is to have a separated file game.js to have the game configuration and split the login in different scenes.</p> <p>I also like to have a Vue component called ‘<Hud>’ where I show the data about my character.</p> <p>I’m working in a game using this kind of technologies, just to train myself and improve my coding skills.</p> <p>Do you want to start to practice too? C’mon lets play and make games 🤗🤩</p> <p><img src="https://cdn-images-1.medium.com/max/1708/1*qTzpLxX49Lcim_woxJ4w1w.gif" alt=""></p> <p>(Image of my game :D)</p> ]]></content:encoded> </item> <item> <title><![CDATA[Create Dynamic Titles and Favicons with Nuxt]]></title> <link>https://vuedose.tips/create-dynamic-titles-and-favicons-with-nuxt/</link> <guid>https://vuedose.tips/create-dynamic-titles-and-favicons-with-nuxt/</guid> <pubDate>Thu, 18 Jun 2020 01:55:00 GMT</pubDate> <description><![CDATA[Thinking on showing a timer on the browser tab? Or different favicons depending on a state? Learn to use vue-meta in your Vue.js apps in this tutorial]]></description> <content:encoded><![CDATA[<p>According to a study by the University of Myself, 98.87% of developers don't pay attention to the <code>title</code> or the <code>favicon</code> of their webapps. That <em>browser tab</em> is where your app is going to live since the user opens it, goes to Twitter on another tab, spends 30 minutes liking dog memes and then comes back to you.</p> <p>There are a lot of states that happen in your application in the background: Incoming messages, Notifications, Deploys, you name it... Sometimes the best place to put an indicator to that state is just the <em>browser tab</em>.</p> <p>In our case, with <a href="https://remate.io">remate.io</a>, we have Projects the team can work on. A project has a unique color that identifies it. You can hop from one project to another, go to github, open issues, review that extra semicolon and then get back to the app (or not). When we are tracking our network tab looks like</p> <p><div style="max-width: 300px;" class="mx-auto"> <img src="http://g.recordit.co/5ZAKShtsiq.gif" alt="Counter" /> </div></p> <p>This seems rocket science, but in fact is quite easy with <code>Nuxt</code> and its head method.</p> <p>Nuxt.js uses <code>vue-meta</code> under the hood to update the headers and html attributes of your application. So we can do something like</p> <pre><code class="language-js">export default { head () { return { title: 'Vue Dose is awesome!', } } } </code></pre> <p>The fun fact is that <code>head</code> function acts like a computed, so it's reevaluated each time a dependency inside of it changes!</p> <p>With that in mind, let's create a simple example: A dynamic page that gets a slug by route param with some categories in it</p> <pre><code class="language-js">// _slug.vue export default { data() { return { slug: this.$route.params.slug, categories: [ { slug: 'geography', name: 'Geography', color: 'rgb(66, 153, 225)' }, { slug: 'history', name: 'History', color: 'rgb(236, 201, 75)' }, { slug: 'sports', name: 'Sports', color: 'rgb(237, 137, 54)' }, ] } } } </code></pre> <p>Now, lets update the title of the page using the name of the category</p> <pre><code class="language-js">export default { data() { return { slug: this.$route.params.slug, categories: [ { slug: 'geography', name: 'Geography', color: 'rgb(66, 153, 225)' }, { slug: 'history', name: 'History', color: 'rgb(236, 201, 75)' }, { slug: 'sports', name: 'Sports', color: 'rgb(237, 137, 54)' }, ] } }, computed: { category() { return this.categories.find(c => c.slug === this.slug) || {} } }, head() { return { title: this.category.name, } } } </code></pre> <p>Easy right? Let's go crazy and update the favicon too! As you might know, you can use SVGs as favicons. This feature allows you to be more dynamic. For this example let's just use an inline SVG with a circle.</p> <pre><code class="language-js">export default { data() { return { slug: this.$route.params.slug, categories: [ { slug: 'geography', name: 'Geography', color: 'rgb(66, 153, 225)' }, { slug: 'history', name: 'History', color: 'rgb(236, 201, 75)' }, { slug: 'sports', name: 'Sports', color: 'rgb(237, 137, 54)' }, ] } }, computed: { category() { return this.categories.find(c => c.slug === this.slug) || {} } }, head() { return { title: this.category.name, link: [ { rel: 'icon', type: 'image/svg+xml', href: `data:image/svg+xml, <svg width="32" height="32" viewBox="0 0 32 32" fill="none" style="color: ${this.category.color};" xmlns="http://www.w3.org/2000/svg" ><circle cx="16" cy="16" r="15.0049" fill="currentColor" /></svg>`, }, ], } } } </code></pre> <p>What we are doing here is simple. We are drawing an SVG and styling it with the color of the category. Inside, there is a circle that takes the <code>currentColor</code> and uses it in the <code>fill</code> property. The result for the <code>Sport</code> category will be an orange circle!</p> <p><img src="https://user-images.githubusercontent.com/12644599/83921635-0b455600-a77f-11ea-991e-4b32436ade63.png" alt="image"></p> <p>You can checkout this <a href="https://github.com/beliolfa/vue-dose-favicon">repo</a> and fork it out if you want to see it in action.</p> ]]></content:encoded> </item> <item> <title><![CDATA[Auto Load Vuejs Components in Nuxt]]></title> <link>https://vuedose.tips/auto-load-vuejs-components-in-nuxt/</link> <guid>https://vuedose.tips/auto-load-vuejs-components-in-nuxt/</guid> <pubDate>Wed, 17 Jun 2020 01:55:00 GMT</pubDate> <description><![CDATA[Tired of writing imports all over your code? Read this tutorial to learn how to use Nuxt components module to automatically import your components]]></description> <content:encoded><![CDATA[<p>With <a href="https://github.com/nuxt/components">Nuxt components</a> you can auto import your components really easily and even comes with support for dynamic imports otherwise known as lazy loading. That means you can just add your component in the template without having to add it to the script tag. This makes development much faster.</p> <h3>How does it work?</h3> <p>This module parses your template and automatically includes the component in the file where you are using it such as a page, layout or even a component. Because Nuxt.js uses automatic code splitting to split your pages by default this module works perfect as it will only contain the components that are used on that page. Also, if you use a component in more than 2 pages, Nuxt.js will automatically create a shared chunk for them thanks to the magic of webpack.</p> <p>@<a href="https://www.youtube.com/embed/lQ8OBrgVVr8">video</a></p> <h3>Installation</h3> <p>If you are using Nuxt v2.13+ then this module is included by default but it is not activated by default so you don't have to install it but you do have to set components to true in your nuxt config file.</p> <pre><code class="language-js">export default { components: true } </code></pre> <p>OR</p> <p>If you are using Nuxt 2.10+ then you will need to install the module.</p> <pre><code class="language-bash">yarn add --dev @nuxt/components # or npm install --save-dev @nuxt/components </code></pre> <p>Then you need to add the <a href="https://github.com/nuxt/components">@nuxt/components</a> to your buildModules section of your nuxt.config.js file.</p> <pre><code class="language-js">export default { buildModules: [ '@nuxt/components' ] } </code></pre> <h3>Auto Import your components</h3> <p>Once you have the module activated or installed you can now simply add your components in your template. The module will read the components directory and automatically look for the component in there.</p> <ol> <li>Create your components</li> </ol> <pre><code class="language-bash">components/ TheHeader.vue TheFooter.vue </code></pre> <ol start="2"> <li>Use your components in any .vue file (components, pages or layouts) without having to add a script tag or an import.</li> </ol> <pre><code class="language-vue"><template> <TheHeader /> <!-- or <the-header /> --> <TheFooter /> <!-- or <the-footer /> --> </template> </code></pre> <h3>Dynamic Imports</h3> <p>You can also dynamically import your components or lazy load your components by prefixing the world Lazy in your templates.</p> <pre><code class="language-vue"><template> <TheHeader /> <LazyTheFooter /> <!-- lazy loaded --> </template> </code></pre> <p>This is great for when you want to load a component only on a click event for example. Here we have a component called MountainsList in our components folder and we add it to our template. We only show it if show is true. The button will appear if show is false and on click it will call the showList method which will change the value of show to true and this will therefore lazy load your component.</p> <pre><code class="language-vue"><template> <div> <h1>Mountains</h1> <LazyMountainsList v-if="show" /> <button v-if="!show" @click="showList">Show List</button> </div> </template> <script> export default { data () { return { show: false } }, methods: { showList() { this.show = true }, } } </script> </code></pre> <p>You can check how this works by opening your browsers network tab and clicking on JS. Then if you refresh the page you will see the javaScripts that are loaded and when you click the button you will then see the MountainsList.js is loaded.</p> <p><a href="https://github.com/nuxt/components#features">For more options or to see a live CodeSandBox check out this link</a></p> <p>ℹ️ Nuxt components will not work with dynamic components. For these components you will have to use the script tag to import your component.</p> <pre><code class="language-vue"><component :is="componentName" /> </code></pre> ]]></content:encoded> </item> <item> <title><![CDATA[How to structure a Vue.js app using Atomic Design and TailwindCSS]]></title> <link>https://vuedose.tips/how-to-structure-a-vue-js-app-using-atomic-design-and-tailwindcss/</link> <guid>https://vuedose.tips/how-to-structure-a-vue-js-app-using-atomic-design-and-tailwindcss/</guid> <pubDate>Tue, 16 Jun 2020 01:40:00 GMT</pubDate> <description><![CDATA[Unsure about how to organize your Vue.js components? Learn in this tutorial how to do it using Atomic Design methodology. Examples and demos included!]]></description> <content:encoded><![CDATA[<p>In this article I will explain what the Atomic Design methodology is about and how we can apply it to our Vue.js project.</p> <p>To give style to the example I created in <a href="https://codesandbox.io/s/atomic-design-in-a-vuejs-app-lr64m">this codesandbox</a>, I have used <a href="https://tailwindcss.com/">TailwindCSS</a> with its default configuration.</p> <p><img src="https://a.storyblok.com/f/83078/1600x1000/a5db8f6087/atomic-design-tailwind.jpeg" alt="Img"></p> <h2>What is Atomic Design</h2> <p>:::blockquote All the theory we are going to see below is in depth in the book <a href="https://atomicdesign.bradfrost.com/">Atomic Design by Brad Frost</a> :::</p> <p>As Brad Frost says ‘Atomic design is like mental model to help us think of our user interfaces as both a cohesive whole and a collection of parts at the same time’.</p> <p>Atomic design is a methodology for creating design systems chemistry-based and there are five distinct levels in atomic design: Atoms, Molecules, Organisms, Templates and Pages. Let’s see them in depth.</p> <h3>Atoms</h3> <p>It is the smallest unit that composes our application, it is not useful by itself but it allows us to have more control over the application elements.</p> <p>Imagine that you want to create a basic layout, in it there will be a header, a body and a footer. But, in addition, each of them is made up of smaller elements such as the links you can see in the header and also in the footer.</p> <p>Well, we’re talking about the atoms being the HTML tags that will be reused throughout the application, as link, heading and svg, among others.</p> <p>For this example we have defined five atoms that will be useful to us:</p> <p><strong>AtomButton.vue</strong></p> <p>As you can see, it's simply a button-style link that expects a property that will contain an object with the name and url of our link.</p> <pre><code class="language-vue"><a :href="link.url" class="block text-center py-1 px-4 bg-blue-100 text-blue-500 font-semibold rounded" >{{ link.name }}</a> </code></pre> <p><strong>AtomLink.vue</strong></p> <p>The atom that represents our link will be the same as the previous one but with the base style.</p> <pre><code class="language-vue"><a :href="link.url">{{ link.name }}</a> </code></pre> <p><strong>AtomLogo.vue</strong> & <strong>AtomText.vue</strong></p> <p>For the logo we will add the SVG that represents it and for the text we will add the tag <p>.</p> <pre><code class="language-vue"><!-- AtomLogo --> <svg width="48" height="48" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"> … </svg> <!-- AtomText --> <p class="pb-6">{{ text }}</p> </code></pre> <p><strong>AtomTitle.vue</strong></p> <p>In this case, as the title can have more than one type of tag, h1, h2, ..., we have created a <a href="https://vuejs.org/v2/guide/components-dynamic-async.html">dynamic component</a> to be able to represent the corresponding title introduced by the prop: tag.</p> <pre><code class="language-vue"><template> <component :is="tag" class="text-3xl font-serif pb-2">{{ content }}</component> </template> <script> export default { name: "AtomTitle", props: ["tag", "content"] }; </script> </code></pre> <p>Now that we have reviewed all the atoms needed for our template, let's get on with the molecules!</p> <h3>Molecules</h3> <p>Molecules, as in nature, are groups of atoms linked together. In our application, molecules are the smallest components composed of one or more repeated atoms, always simple combinations built for reuse.</p> <p>Having composed a molecule by several atoms already made, make us work with "do one thing and do it well" mentality. Both atoms and molecules encourage the creation of independent and reusable components.</p> <p>Now, let's apply it to a case study. Imagine what we could do with the atoms we have created, what combinations without too much complexity we can create and reuse? Well, a list of links would be good for both the header and the footer and, also, we can create a card combining title and text atoms.</p> <p>Let's take a look at the examples we have created:</p> <p><strong>MoleculeLinks.vue</strong></p> <p>In this case, we have created a list with the tags ul, li and, to represent the link, we have imported the AtomLink and used it as a component.</p> <pre><code class="language-vue"><template> <ul> <li v-for="(link, index) in links" :key="link.name + index"> <AtomLink :link="link"/> </li> </ul> </template> <script> import AtomLink from "@/components/AtomLink"; export default { name: "MoleculeLinks", props: ["links"], components: { AtomLink } }; </script> </code></pre> <p><strong>MoleculeCard.vue</strong></p> <p>In this molecule, to form the card, we have used the <strong>AtomTitle</strong> and <strong>AtomText</strong>, both added to the div that gives the style to the card.</p> <pre><code class="language-vue"><template> <div class="p-6 border border-blue-200 hover:shadow-lg rounded-sm bg-blue-100 text-blue-800 transition ease-in-out duration-500" > <AtomTitle tag="h2" :content="card.title"/> <AtomText :text="card.text"/> </div> </template> <script> import AtomTitle from "@/components/AtomTitle"; import AtomText from "@/components/AtomText"; export default { name: "MoleculeCard", props: ["card"], components: { AtomTitle, AtomText } }; </script> </code></pre> <p><strong>MoleculeBanner.vue</strong></p> <p>For the Banner that will appear under the header we have needed both <strong>AtomTitle</strong> and <strong>AtomText</strong>, and also the <strong>AtomButton</strong>.</p> <pre><code class="language-vue"><template> <div class="px-4 pt-40 pb-12 md:py-32 bg-blue-900 text-gray-100"> <header class="max-w-sm md:max-w-xl mx-auto"> <AtomTitle tag="h1" content="Atomic Design in Vue.js"/> <AtomText text="Etiam in arcu lectus. Nulla facilisi. Nunc viverra vehicula nunc eu tristique. Sed gravida libero sem, sed ornare arcu venenatis at." /> <AtomButton class="md:max-w-xs" :link="{ name: 'Button', url: '#' }"/> </header> </div> </template> <script> import AtomTitle from "@/components/AtomTitle"; import AtomText from "@/components/AtomText"; import AtomButton from "@/components/AtomButton"; export default { name: "MoleculeBanner", components: { AtomTitle, AtomText, AtomButton } }; </script> </code></pre> <h3>Organisms</h3> <p>Organisms are groups of molecules joined together to form a relatively complex, distinct section of an interface, as the header and the footer.</p> <p>Now, we can see the final interface beginning to take shape. As in molecules with atoms, organisms can consist of similar and/or different molecule types.</p> <p>As in the previous sections, we will see the possibilities we have to create the organisms. Let's have a look directly on each one:</p> <p><strong>OrganismHeader.vue</strong></p> <p>In order to create the <strong>Header</strong> organism we need the logo, a list of links, to navigate through the application, and a button for registration, for example. In that case we need the <strong>Logo</strong> and <strong>Button</strong> atoms and the Links molecule.</p> <pre><code class="language-vue"> <template> <header class="absolute inset-x-0 text-gray-100 py-4 flex flex-wrap items-center"> <div class="w-full md:w-3/4 px-4 pb-4 md:pb-0 flex items-center justify-between"> <AtomLogo/> <MoleculeLinks class="flex" :links="links"/> </div> <div class="w-full md:w-1/4 px-4"> <AtomButton :link="{ name: 'Button', url: '#' }"/> </div> </header> </template> <script> import AtomLogo from "@/components/AtomLogo"; import AtomButton from "@/components/AtomButton"; import MoleculeLinks from "@/components/MoleculeLinks"; export default { name: “OrganismHeader", props: ["links"], components: { AtomLogo, AtomButton, MoleculeLinks } }; </script> </code></pre> <p><strong>OrganismGrid.vue</strong></p> <p>To represent several cards on the same grid we create the <strong>OrganismGrid</strong>, which will contain <strong>MoleculeCard</strong> inside a v-for and that will be shape it with flex. Let’s check the code:</p> <pre><code class="language-vue"><template> <div class="flex flex-wrap items-stretch md:-mr-4"> <div class="w-full md:w-1/3 md:pr-4 pb-4" v-for="card in cards" :key="card.name"> <MoleculeCard class="h-full" :card="card"/> </div> </div> </template> <script> import MoleculeCard from "@/components/MoleculeCard"; export default { name: “OrganismGrid", props: ["cards"], components: { MoleculeCard } }; </script> </code></pre> <p><strong>OrganismFooter.vue</strong></p> <p>As the previous cases, we just need the molecule we want to use. But, in this case, applying a class to the molecule we are using we can represent it vertically, now <strong>MoleculeLinks</strong> is using flex-col (flex-direction: column).</p> <pre><code class="language-vue"><template> <footer class="flex flex-wrap text-white"> <div class="w-full md:w-1/4 bg-gray-900 p-4 md:p-12"> <MoleculeLinks class="flex flex-col" :links="columnOneLinks"/> </div> <div class="w-full md:w-3/4 bg-blue-900 p-4 md:p-12"> <MoleculeLinks class="flex flex-col" :links="columnTwoLinks"/> </div> </footer> </template> <script> import MoleculeLinks from "@/components/MoleculeLinks"; export default { name: “OrganismFooter", props: ["columnOneLinks", "columnTwoLinks"], components: { MoleculeLinks } }; </script> </code></pre> <h3>Templates</h3> <p>Now, we left behind the chemistry-based theory to get into common web language. Templates consist mostly of groups of organisms and/or molecules to form the common structure of a page, what we used to call layout.</p> <p>At this stage, we already have created every piece of our template, so let's add them together to see how it looks.</p> <blockquote> <p>We are hardcoding the data in the Template component, but the section below explains that we need to do it in the page stage.</p> </blockquote> <pre><code class="language-vue"><template> <section class="flex flex-col min-h-screen"> <OrganismHeader :links="links"/> <div class="flex-auto flex flex-wrap bg-white"> <MoleculeBanner class="w-full md:w-3/4 text-center md:text-left"/> <div class="container mx-auto px-4 py-8 md:py-16"> <OrganismGrid :cards="cards"/> </div> </div> <OrganismFooter :columnOneLinks="columnOneLinks" :columnTwoLinks="columnTwoLinks"/> </section> </template> <script> import OrganismHeader from "@/components/OrganismHeader"; import OrganismFooter from "@/components/OrganismFooter"; import MoleculeBanner from "@/components/MoleculeBanner"; import OrganismGrid from "@/components/OrganismGrid"; export default { name: "TemplateLanding", components: { MoleculeBanner, OrganismHeader, OrganismFooter, OrganismGrid }, data: () => { return { links: […], cards: […], columnOneLinks: […], columnTwoLinks: […] }; } }; </script> </code></pre> <h3>Pages</h3> <p>Pages are specific instances of templates, where our components are replaced with real data to give an accurate depiction of what a user will see.</p> <p>This stage help us a lot testing the effectiveness of the design system we've created and check that everything in context is built as we expect. To better understand this part, imagine that you have a heading with three lines instead of one, in this stage you will decide how to change your atom to look as you want in this specific case.</p> <h2>Conclusion</h2> <p>Now that we know the possibilities offered by structuring our design system with this methodology, we can be sure that the changes will not be a headache and that we will be able to build huge applications.</p> ]]></content:encoded> </item> <item> <title><![CDATA[Go async in Vue 3 with Suspense]]></title> <link>https://vuedose.tips/go-async-in-vue-3-with-suspense/</link> <guid>https://vuedose.tips/go-async-in-vue-3-with-suspense/</guid> <pubDate>Mon, 15 Jun 2020 01:55:00 GMT</pubDate> <description><![CDATA[Learn Suspense, one of the most exciting Vue 3 features used to make async request easy and interactive. Don't miss this tutorial with code snippets!]]></description> <content:encoded><![CDATA[<p>Vue 3 is coming with a some exciting new features. Composition API is the hottest one at the moment but there are others that excite me as much as it.</p> <p>One of those new features is called <code>Suspense</code> and it really excites me about the benefits it brings. You might have heard about it already but I will try to show some examples of the usage of <code>Suspense</code> and where it can be beneficial.</p> <h2>What is Suspense?</h2> <p>::: blockquote Suspense is a state of mental uncertainty, anxiety, of being undecided, or of being doubtful. In a dramatic work, suspense is the anticipation of the outcome of a plot or of the solution to an uncertainty, puzzle, or mystery, particularly as it affects a character for whom one has sympathy.</p> <p>- <a href="https://en.wikipedia.org/wiki/Suspense">Wikipedia</a> :::</p> <p>Back to Vue, <code>Suspense</code> is a component, that you don't need to import or do any kind of setup, with two <code><slot></code> that allows you to render a <code>#fallback</code> while the main component you want to load is not ready.</p> <p>Ok, it seems vague. I will try to give you an example of how it is used. I also recommend you to take a look into its test cases, especially the <a href="https://github.com/vuejs/vue-next/blob/8b85aaeea9b2ed343e2ae19958abbd9e5d223a77/packages/runtime-core/__tests__/components/Suspense.spec.ts#L45-L69">first one</a> to get familiar with it.</p> <pre><code class="language-vue"><Suspense> <template #default> <!-- The component I want to render --> </template> <template #fallback> <!-- Fallback component shown while my component is not ready --> </template> </Suspense> </code></pre> <p>That is the basic blueprint of it and it tackles a really common use case: the <code>v-if</code> loading condition.</p> <p>I consider it the first benefit of <code>Suspense</code>, as now we've some standard way of dealing with this scenario. Before <code>Suspense</code> each developer could choose the way they want to implement it, they still can, and it was kind of a nightmare in situations where multiple components were loaded, so you would have <code>loadingHeader</code>, <code>loadingFooter</code>, <code>loadingMain</code>, and so on.</p> <p>At the beginning I wrote "while the main component you want to load is not ready", what it means is that the main component has some kind of async work, which plays nicely with an <code>async setup()</code> from the new Composition API.</p> <p>Let's say we have the following component with some async work to be done in <code>setup</code>:</p> <pre><code class="language-vue"><template> <h1>Ive some async work to do before I can render</h1> </template> <script> export default { name: 'MyAsyncComponent', async setup() { await someAsyncWork(); } } </script> </code></pre> <p>Now we want to use this component somewhere but we want to have a proper loading while it is not ready.</p> <p><code>Suspense</code> makes it more intuitive how it works and it really helps readability, check it:</p> <pre><code class="language-vue"><template> <Suspense> <template #default> <MyAsyncComponent /> </template> <template #fallback> <span>Loading... Please wait.</span> </template> </Suspense> </template> <script> import MyAsyncComponent from '@/components/MyAsyncComponent.vue'; export default { name: 'App', components: { MyAsyncComponent } } </script> </code></pre> <p>Another cool thing about it is that you can have multiple <code>Suspense</code> components defined and have different fallbacks for each of them.</p> <h2>How do I handle errors?</h2> <p>Imagine the following: the call to <code>someAsyncWork</code> threw an exception. How do we handle it with <code>Suspense</code>?</p> <p>We can use the <code>errorCapture</code> hook to listen to errors and conditionally render our <code>Suspense</code>. The component will look like the following:</p> <pre><code class="language-vue"><template> // Here we conditionally render based on error <h1 v-if="error">I failed to load</h1> <Suspense v-else> <template #default> <MyAsyncComponent /> </template> <template #fallback> <span>Loading... Please wait.</span> </template> </Suspense> </template> <script> import { ref, onErrorCaptured } from 'vue' import MyAsyncComponent from '@/components/MyAsyncComponent.vue'; export default { name: 'App', components: { MyAsyncComponent }, setup() { const error = ref(null); onErrorCaptured((e) => { error.value = e return true; }); return { error }; } } </script> </code></pre> <p>To be honest it is quite a boilerplate if you're doing it in multiple places and can be a bit cumbersome if you've multiple <code>Suspenses</code>.</p> <p>I do encourage you to wrap this logic, and even improve it to your use case, in a new component. The following example shows a simple wrapper on top of it:</p> <pre><code class="language-vue"><template> <slot v-if="error" name="error"></slot> <Suspense v-else> <template #default> <slot name="default"></slot> </template> <template #fallback> <slot name="fallback"></slot> </template> </Suspense> </template> <script> import { ref, onErrorCaptured } from 'vue' export default { name: 'SuspenseWithError', setup() { const error = ref(null); onErrorCaptured((e) => { error.value = e return true; }); return { error }; } } </script> </code></pre> <p>So you can use it like this:</p> <pre><code class="language-vue"><template> <SuspenseWithError> <template #default> <MyAsyncComponent /> </template> <template #fallback> <span>Loading... Please wait.</span> </template> <template #error> <h1>I failed to load</h1> </template> </SuspenseWithError> </template> <script> import MyAsyncComponent from '@/components/MyAsyncComponent.vue'; import SuspenseWithError from '@/components/SuspenseWithError.vue'; export default { name: 'App', components: { MyAsyncComponent, SuspenseWithError }, } </script> </code></pre> <p>Bear in mind that it is a simple and compact implementation that has not been tested in a real application. It also does not distinguished errors which might not be the ideal scenario for you.</p> <h2>Suspense with Vue Router</h2> <p>The main goal of this Dose is to show how to use the <code>Suspense</code> with Vue Router. All the other examples above were made to introduce <code>Suspense</code> and its power.</p> <p><code>Suspense</code> plays nicely with Vue Router. What I mean is that you can <code>Suspense</code> your <code><router-view></code> and in case the view has an async setup, you can show a fallback.</p> <p>To be more specific: You can create your loading component that will be shown whenever your view is not ready due some async work that has to be performed.</p> <p>You can achieve the same behavior with the navigation guards but for most of the cases, which do not involve a complex setup, the combination <code><router-view></code>, <code>Suspense</code> and async setup do a nice job!</p> <p>The example below shows how it can be implemented:</p> <pre><code class="language-vue"><Suspense> <template #default> <router-view /> </template> <template #fallback> <span>I'm a loading screen, I'm waiting the view to be ready!</span> </template> </Suspense> </code></pre> <h2>All in all</h2> <ul> <li><code>Suspense</code> can be used to show a fallback element when an async work is needed in the main component</li> <li>One component can have multiple suspended components inside</li> <li>Error handling can be done with <code>onErrorCaptured</code> hook</li> <li>A wrapper can be created to extract the error logic</li> <li><code>Suspense</code> plays nicely with Vue Router once we want to show a loading screen</li> </ul> <p>The final result is shown below and you can also check the sample code here: <a href="https://github.com/viniciuskneves/vue-3-suspense">vue-3-suspense</a>.</p> <p><img src="https://a.storyblok.com/f/83078/896x428/6e7b1515e8/go-async-with-suspense__demo.gif" alt="Final example"></p> ]]></content:encoded> </item> <item> <title><![CDATA[Use Web Workers in your Vue.js Components for Max Performance]]></title> <link>https://vuedose.tips/use-web-workers-in-your-vuejs-component-for-max-performance/</link> <guid>https://vuedose.tips/use-web-workers-in-your-vuejs-component-for-max-performance/</guid> <pubDate>Tue, 31 Mar 2020 00:00:00 GMT</pubDate> <description><![CDATA[Learn how to get up to 20x performance improvement of Vue.js components that rely on heavy tasks so they render and load faster]]></description> <content:encoded><![CDATA[<p>In the first vuedose VueDose tip ever I revealed a trick to <a href="https://vuedose.tips/tips/improve-performance-on-large-lists-in-vue-js">improve performance on large lists</a>. Yay, that was a great start!.</p> <p>However not all comes into that case.</p> <p>Sometimes you need to use a component that is heavy to create and render, usually because they perform complex tasks. I came up into that case yesterday.</p> <p>I was creating a page using <a href="https://www.storyblok.com/developers?utm_source=newsletter&utm_medium=logo&utm_campaign=vuedose">StoryBlok</a>. They have the amazing feature of creating a rich-text field that content managers can use in order to enter any kind of formatted text such as lists, images, block quotes, bold, italics...</p> <p>When you get the rich-text content from the StoryBlok API, it has its own structure. In order to render that data into HTML, you must use the <code>richTextResolver.render(content)</code> method of <code>storyblok-js-client</code>.</p> <p>We can encapsulated this functionality into a <code>RichText.vue</code> component. A basic implementation would be:</p> <pre><code class="language-vue"><template> <div v-html="contentHtml"></div> </template> <script> export default { props: ["content"], computed: { contentHtml() { return this.$storyapi.richTextResolver.render(content); } } }; </script> </code></pre> <p><em>Note: <code>$storyapi</code> it's an instance of the StoryBlok Client that comes from StoryBlok Nuxt.js module, which is what I'm using, but that's not relevant for the purpose of this article</em></p> <p>Nothing too fancy so far. Just... here comes the surprise.</p> <p>It seems like that render it's a heavy task, which was starting to be noticable when rendering several of this components with a decent amount of content.</p> <p>Now imagine this situation: you have a list of text-rich components in your page, and a dropdown to filter by. When you change your dropdown filter, you re-fetch all content given that filter, and the list re-renders.</p> <p>Here's where you could notice the heavyness of <code>richTextResolver.render</code>: the dropdown was lagging to close after selecting it's value.</p> <p>The reason is that the default JavaScript execution runs in the main thread, which is <strong>UI-blocking</strong>.</p> <p>Problem understood. So... how can we fix it?</p> <p>Easy: using a Web Worker for the rich-text rendering task.</p> <p><em>Note: I'm not diving into Web Workers, but check <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers">its docs</a> for more info.</em></p> <p>Web Workers run in a separate thread and they're not UI-blocking, perfect for our case. We're not diving into web workers, but check its docs for more info.</p> <p>Keep in mind that web workers run in their own context, and by default we cannot access external contexts. But we need to access the <code>storyblok-js-client</code> npm module. For that, Webpack comes to the rescue with <code>worker-loader</code>.</p> <p>First, install it by running `npm install -D worker-loader. Then you'd need to configure it. In Nuxt.js you'd do it in <em>nuxt.config.js</em> in this way:</p> <pre><code class="language-js">build: { extend(config, { isDev, isClient }) { config.module.rules.push({ test: /\.worker\.js$/, use: { loader: "worker-loader" } }); } } </code></pre> <p>With this configuration, all <em>*.worker.js</em> files will be processed by <code>worker-loader</code>.</p> <p>Let's create a <code>render-html.worker.js</code>:</p> <pre><code class="language-js">import StoryblokClient from "storyblok-js-client"; let storyClient = new StoryblokClient({}); // When the parent theard requires it, render the HTML self.addEventListener("message", ({ data }) => { const result = storyClient.richTextResolver.render(data); self.postMessage(result); }); </code></pre> <p>That's a basic implementation of a worker. You need to listen to <code>message</code> event, which is the way you'll communicate to it from your Vue.js app. Then you can get the <code>data</code> of the event, render it with <code>storyblok-js-client</code> and send up the result <code>self.postMessage</code>.</p> <p>Let's update the <code>RichText.vue</code> component to use the service worker:</p> <pre><code class="language-vue"><template> <div v-html="contentHtml"></div> </template> <script> import Worker from "./render-html.worker.js"; // Create the worker instance const worker = new Worker(); export default { props: ["content"], data: () => ({ contentHtml: "" }), mounted() { // Update the state with the processed HTML content worker.onmessage = ({ data }) => { this.contentHtml = data; }; // Call the worker to render the content worker.postMessage(this.content); } }; </script> </code></pre> <h3>The result</h3> <p>Do you want to know how much performance improvement we achieved by this? Sure, Web Performance doesn't make sense if you don't measure it.</p> <p>In fact, I have an article for you to <a href="/tips/measure-runtime-performance-in-vue-js-apps">learn and undestand how to meassure performance in Vue.js components</a>. So make sure to read it to better understand the following test.</p> <p>I've meassured this component with a medium-size on my mac, 6x throttle.</p> <p>The results are: <strong>20.65x</strong> faster on component's <em>render</em> and <strong>1.39x</strong> on <em>patch</em>.</p> <p><img src="https://i.ibb.co/sVgt3Pj/featured.png" alt=""> <img src="https://i.ibb.co/YNmqrM3/patch.png" alt=""></p> <p>If you don't know what <em>render</em> and <em>patch</em> mean, it's explained in <a href="/tips/measure-runtime-performance-in-vue-js-apps">this article</a></p> <p>That's it for today's tip!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Deep vs Shallow Rendering in Vue.js Tests]]></title> <link>https://vuedose.tips/deep-vs-shallow-rendering-in-vuejs-tests/</link> <guid>https://vuedose.tips/deep-vs-shallow-rendering-in-vuejs-tests/</guid> <pubDate>Tue, 03 Mar 2020 00:00:00 GMT</pubDate> <description><![CDATA[A short article on how to use deep and shallow rendering in Vue.js and what I suggest to use most of the cases using vue test utils.]]></description> <content:encoded><![CDATA[<p>What a nice feeling to get to write a tip right after the Vue.js Amsterdam! It was a boost, thanks to everyone that were so kind, friendly and curious about VueDose. Just so you know, if you'd like to write a tip here, just let me know!</p> <p>To give you an impression on how Vue Amsterdam went, just see this picture taken by my friend <a href="https://twitter.com/gustojs">GustoJS</a> and his amazing camera.</p> <p><img src="https://i.ibb.co/myxwdL6/alex-adrian-vueamsterdam.jpg" alt="Alex Adrian VueAmsterdam"></p> <p>Now let's get serious and start with the tip!</p> <p>Testing is still one of the most controversial dev topics, and <em>deep vs shallow rendering</em> is no exception. In this tip I want to make my point on why and when to use each of them.</p> <h3>Deep Rendering</h3> <p>Deep rendering, as the name states, renders all component tree given a root component.</p> <p>In order to illustrate it, given this <code>UserList.vue</code> component:</p> <pre><code class="language-vue"><template> <ul> <li v-for="user in users" :key="user"> {{ user }} </li> </ul> </template> <script> export default { props: { users: Array } }; </script> </code></pre> <p>That you use in an <code>App.vue</code> component like this:</p> <pre><code class="language-vue"><template> <div> <h3>User List</h3> <UserList :users="['Rob Wesley']" /> </div> </template> <script> import UserList from "./UserList"; export default { components: { UserList } }; </script> </code></pre> <p>Then it will give us the combined DOM Tree of both components:</p> <pre><code class="language-vue"><div> <h3>User List</h3> <ul> <li> Rob Wesley </li> </ul> </div> </code></pre> <p>In order to check that, you can use Jest Snapshots. If you don't know much about Snapshots, check the article <a href="/tips/the-power-of-snapshot-testing/">The power of Snapshot Testing</a> that goes deeper into them.</p> <p>When using <a href="https://vue-test-utils.vuejs.org/">@vue/test-utils</a>, the official Vue.js testing library, you can perform deep rendering by using the <code>mount</code> method.</p> <p>With these clues, you can imagine the previous snapshot was taken from a test with a shape like this:</p> <pre><code class="language-js">import { mount } from "@vue/test-utils"; import App from "@/App"; describe("App.vue", () => { it("Deeply renders the App component", () => { const wrapper = mount(App); expect(wrapper.html()).toMatchSnapshot(); }); }); </code></pre> <h3>Shallow Rendering</h3> <p>Opposite to deep rendering, shallow rendering only renders the component that you're testing without going into deeper levels.</p> <p>To implemente shallow rendering, you can use the <code>shallowMount</code> method from @vue/test-utils.</p> <p>If you rewrite the previous test to use shallow rendering:</p> <pre><code class="language-js">import { shallowMount } from "@vue/test-utils"; import App from "@/App"; describe("App.vue", () => { it("Shallow renders the App component", () => { const wrapper = shallowMount(App); expect(wrapper.html()).toMatchSnapshot(); }); }); </code></pre> <p>You'll see that the generated snapshot is:</p> <pre><code class="language-vue"><div> <h3>User List</h3> <userlist-stub users="Rob Wesley"></userlist-stub> </div> </code></pre> <p>What happened? Basically Jest has created the <code><userlist-stub></code> tag automatically instead of creating and rendering the <em>UserList</em> component.</p> <p>Notice one point: the <em>UserList</em> component wasn't even created, meaning the component isn't used at all. Not their props, not their lifecycle hooks... nothing. You'll see now why that's important.</p> <h3>What to Use and When</h3> <p>Taking back some testing theory, a test should be:</p> <ul> <li>Independant from others</li> <li>Focus on testing one thing</li> <li>Easy to maintain and stable over the time</li> <li>Valuable</li> </ul> <p>And as you may see already, when you use deep rendering you're already violating the first three points, since every time a child component changes, you'll test will fail.</p> <p>Additionally, imagine a child component performs side effects, like interacting with the store or calling an API. That could be even worse since the results might be unexpected.</p> <p>That's why, my take on this is: <strong>Use shallowMount most of the time</strong>.</p> <p>The question is: <em>When not to use it?</em>. I'll use <code>mount</code> just in those cases that you're testing a group of components as a unit, treating them as molecules.</p> <p>Think of <code>UserList</code> and <code>UserListItem</code>, or <code>Tab</code>, <code>TabGroup</code> and <code>TabItem</code>. For those cases, it makes sense to use deep rendering when you want to test the interaction of the whole group working together.</p> <h3>Follow Up</h3> <p>To end up I wanted to share that I'll be giving the workshop <a href="https://www.eventbrite.it/e/vueday-2020-tickets-60309252598">Vue.js Testing for Everyone</a> in Vue Day Italy, Verona, on 4th April.</p> <p>In that workshop you'll be able to learn all the techniques and best practices to test a Vue.js App, step by step and in the most practical way. Definitely we'll cover this content in a much deeper way.</p> <p>That's it for today's tip!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Create a i18n Plugin with Composition API in Vue.js 3]]></title> <link>https://vuedose.tips/create-a-i18n-plugin-with-composition-api-in-vuejs-3/</link> <guid>https://vuedose.tips/create-a-i18n-plugin-with-composition-api-in-vuejs-3/</guid> <pubDate>Mon, 17 Feb 2020 03:00:00 GMT</pubDate> <description><![CDATA[A example on how to use the inject and provide functions to create a i18n plugin using Composition API in Vue.js 3.]]></description> <content:encoded><![CDATA[<p>The way plugins are coded in Vue.js 3 with Composition API differ from traditional plugins. Traditional are used via a <code>install</code> function and added using <code>Vue.use(plugin)</code>. They usually manipulate/extend the Vue prototype.</p> <p>However, in Composition API plugins are non-manipulative and coded using an inject-provide pattern. For instance, you can create a <em>i18n</em> plugin like this:</p> <pre><code class="language-js">// i18nPlugin.js import { ref, provide, inject } from "@vue/composition-api"; const createI18n = config => ({ locale: ref(config.locale), messages: config.messages, $t(key) { return this.messages[this.locale.value][key]; } }); const i18nSymbol = Symbol(); export function provideI18n(i18nConfig) { const i18n = createI18n(i18nConfig); provide(i18nSymbol, i18n); } export function useI18n() { const i18n = inject(i18nSymbol); if (!i18n) throw new Error("No i18n provided!!!"); return i18n; } </code></pre> <p>As you can see, the functions <code>provide</code> and <code>inject</code> are used to create the plugin instance and hold it in a dependency injection mechanism.</p> <p>Check that we use <code>ref</code> for the locales, since we need the to be reactive.</p> <p>If you're not very familiar yet with Composition API, please read the tip to <a href="https://vuedose.tips/tips/easily-switch-to-composition-api-in-vuejs-3">easily migrate to Composition API</a> and how to <a href="https://vuedose.tips/tips/use-old-instance-properties-in-composition-api-in-vuejs-3/">use old instance properties</a> to get a bit more in detail about it.</p> <p>Then, once in the app you must initialize the plugin with the right configuration by using the <code>provideI18n</code> function. That's usually done in the root <em>App.vue</em> component:</p> <pre><code class="language-vue"><script> import { provideI18n } from "./i18nPlugin"; import HelloWorld from "./HelloWorld"; export default { components: { HelloWorld }, setup() { provideI18n({ locale: "en", messages: { en: { hello_world: "Hello world" }, es: { hello_world: "Hola mundo" } } }); } }; </script> </code></pre> <p>Finally, in any component we want to use the plugin, we must inject it by using the <code>useI18n</code> function in the <code>setup</code> function. Create a <em>HelloWorld.vue</em> component with:</p> <pre><code class="language-vue"><template> <div> <h2>{{ i18n.$t('hello_world') }}</h2> </div> </template> <script> import { useI18n } from "./i18nPlugin"; export default { setup() { const i18n = useI18n(); return { i18n }; } }; </script> </code></pre> <p>But hey... what's the fun of it if we cannot change the language? Let's add to the previous code the functionality to change the language:</p> <pre><code class="language-vue"><template> <div> <h2>{{ i18n.$t('hello_world') }}</h2> <button @click="switchLanguage">Switch language</button> </div> </template> <script> import { useI18n } from "./i18nPlugin"; export default { setup() { const i18n = useI18n(); const switchLanguage = () => { const locale = i18n.locale.value === "en" ? "es" : "en"; i18n.locale.value = locale; }; return { i18n, switchLanguage }; } }; </script> </code></pre> <p>Just by adding a button and the <code>switchLanguage</code> function we already have the feature.</p> <p>That's all. What I like the most about Composition API is the easiness to develop code that is predictable and maintainable by providing clean patterns.</p> <p>How do you like it?</p> <p>If you want to see with your own eyes that this code truly works, go and check it in <a href="https://codesandbox.io/s/composition-api-simple-demo-lp0z5">this</a> <a href="https://codesandbox.io/s/i18n-plugin-composition-api-mbe0b">CodeSandbox</a>!</p> <p>That's it for today's tip!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Access template refs in Composition API in Vue.js 3]]></title> <link>https://vuedose.tips/access-template-refs-in-composition-api-in-vuejs-3/</link> <guid>https://vuedose.tips/access-template-refs-in-composition-api-in-vuejs-3/</guid> <pubDate>Mon, 09 Dec 2019 00:00:00 GMT</pubDate> <description><![CDATA[Quick tip on how to access the old this.$refs by using ref() in the new Composition API in Vue.js 3 components.]]></description> <content:encoded><![CDATA[<p>You've seen in the last tip <em><a href="https://vuedose.tips/tips/use-old-instance-properties-in-composition-api-in-vuejs-3">"Use old instance properties in Composition API in Vue.js 3"</a></em> how to access instance properties in the new syntax.</p> <p>However, our beloved <code>this.$refs</code> wasn't included in the <strong>setup context object</strong> as you realized if you read the tip.</p> <p>So, how can you work with template refs in Composition API?</p> <p>It might be more simple than you think! The thing is, Vue.js <strong>unifies the concept of refs</strong>, meaning that you just need to use the <code>ref()</code> function you already know for declaring reactive state variables in order to declare a template ref as well.</p> <p>Only keep in mind that the ref name must be the same as the variable's one. Let me illustrate it. For the template:</p> <pre><code class="language-vue"><template> <div> <h2 ref="titleRef">{{ formattedMoney }}</h2> <input v-model="delta" type="number" /> <button @click="add">Add</button> </div> </template> </code></pre> <p>I've set <code>titleRef</code> on the <code><h2></code> tag. That's all in the template level. Now in the <code>setup</code> function, you need to declare a ref with the same <code>titleRef</code> name, initialized to <code>null</code> for instance:</p> <pre><code class="language-js">export default { setup(props) { // Refs const titleRef = ref(null); // Hooks onMounted(() => { console.log("titleRef", titleRef.value); }); return { titleRef // ... }; } }; </code></pre> <p>You access the ref value just like any other reactive ref, by accessing the <code>.value</code> property. If you do so as shown in the example you should see in the console the result <em>titleRef <h2>10.00</h2></em></p> <p>Don't believe me? Check it out with your own eyes <a href="https://codesandbox.io/s/template-refs-in-composition-api-w8rux">this CodeSandbox</a>!</p> <p>That's it for today's tip!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Use old instance properties in Composition API in Vue.js 3]]></title> <link>https://vuedose.tips/use-old-instance-properties-in-composition-api-in-vuejs-3/</link> <guid>https://vuedose.tips/use-old-instance-properties-in-composition-api-in-vuejs-3/</guid> <pubDate>Tue, 26 Nov 2019 00:00:00 GMT</pubDate> <description><![CDATA[Learn how to use this.$emit, this.$attrs and more in the new Composition API, where you have no this instance in your Vue.js Components.]]></description> <content:encoded><![CDATA[<p>In the last tip <em><a href="https://vuedose.tips/tips/easily-switch-to-composition-api-in-vuejs-3">"Easily switch to Composition API in Vue.js 3"</a></em> I explained how to migrate the basic parts of a Vue.js Object API component to the new Composition API.</p> <p>However, that's not all. What happens with all instance properties we used to have, such as <code>this.$emit</code>, <code>this.$slots</code>, <code>this.$attrs</code> and so? They were on the <code>this</code> component instance, but <strong>there is no <code>this</code> in Composition API.</strong></p> <p>In the same line, in the last tip I didn't use props, and you used to access them via <code>this</code> in the component instance. How the heck can you access it now?</p> <p>The thing is, I haven't explained the arguments of the <code>setup</code> function when using Composition API.</p> <p>Effectively, the first parameter of the <code>setup</code> function receives all the component properties. Following the example from <a href="https://vuedose.tips/tips/easily-switch-to-composition-api-in-vuejs-3/">the last tip</a>, let's add two properties to use as the initial values for the <code>money</code> and <code>delta</code> local state variables:</p> <pre><code class="language-js">export default { props: { money: { type: Number, default: 10 }, delta: { type: Number, default: 1 } }, setup(props) { const money = ref(props.money); const delta = ref(props.delta); // ... } }; </code></pre> <p>Easy-peasy. Nothing else changes about props management aside from this in Vue.js components.</p> <p>Now, what about all other instance properties and methods, such as <code>$emit</code>? You can find them in the second argument of the <code>setup</code> function: <strong>the setup context object.</strong></p> <p>The <em>setup context</em> has the following shape:</p> <pre><code class="language-typescript">interface SetupContext { readonly attrs: Record<string, string>; readonly slots: { [key: string]: (...args: any[]) => VNode[] }; readonly parent: ComponentInstance | null; readonly root: ComponentInstance; readonly listeners: { [key: string]: Function }; emit(event: string, ...args: any[]): void; } </code></pre> <p>And what's even cooler, it's that we can use object destructuring on the setup context and the reactivity is not lost!</p> <p>To illustrate it, I'm going to modify the last example and add some logging in the <code>onMounted</code> hook as well as emitting a <code>money-changed</code> event when that changes:</p> <pre><code class="language-js">setup(props, { emit, attrs, slots }) { // State const money = ref(props.money); const delta = ref(props.delta); // Hooks onMounted(() => { console.log("Money Counter (attrs): ", attrs); console.log("Money Counter (slots): ", slots); }); // Watchers const moneyWatch = watch(money, (newVal, oldVal) => emit("money-changed", newVal) ); } </code></pre> <p>That's it! Now you're already able to use most of Vue.js component instance properties and methods.</p> <p>But probably you've realized <strong>not all are in the render context</strong>... What about <code>this.$refs</code>? And plugins that inject stuff such as <code>this.$store</code>?</p> <p>No worries, I'll cover that in the next tips! No spoilers, stay with me and you'll stay cool!</p> <p>By the way, if you're a live-coding peep you should check the code from this tip working in <a href="https://codesandbox.io/s/composition-context-yq2s8">this CodeSandbox</a>!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Easily switch to Composition API in Vue.js 3]]></title> <link>https://vuedose.tips/easily-switch-to-composition-api-in-vuejs-3/</link> <guid>https://vuedose.tips/easily-switch-to-composition-api-in-vuejs-3/</guid> <pubDate>Tue, 19 Nov 2019 00:00:00 GMT</pubDate> <description><![CDATA[A step by step guide on how to migrate a Vue.js component from the traditional Object API to the modern Composition API, easy and in a cheatsheet format.]]></description> <content:encoded><![CDATA[<p>The fact that Vue.js 3 already reached the alpha version made me think that... it's time already for some Vue.js 3 tips!</p> <p>The idea is to give you some tips related to new features you can find on Vue.js 3 as they get available. For now, we'll focus on <strong>Composition API</strong>, one of the most game-changing features!</p> <p>This first one is specially focused on showing you a basic step-by-step guide or cheatsheet to migrate object-api Vue.js components to use Composition API. In future tips you'll also see how to apply certain new techniques this API allow us to do as well 😉.</p> <p>I'll do that by showing you how to convert an Object-API-based component to use Composition API.</p> <p>For that, let's create a <em>MoneyCounter.vue</em> component that basically shows a money amount and allow us to add/substract quantities to it, implemented using the following code:</p> <pre><code class="language-vue"><template> <div> <h2>{{ formattedMoney }}</h2> <input v-model="delta" type="number"> <button @click="add">Add</button> </div> </template> <script> export default { data: () => ({ money: 10, delta: 1 }), computed: { formattedMoney() { return this.money.toFixed(2); } }, mounted() { console.log("Clock Object mounted!"); }, methods: { add() { this.money += Number(this.delta); } }, watch: { money(newVal, oldVal) { console.log("Money changed", newVal, oldVal); } } }; </script> </code></pre> <p>This component has a <code>money</code> state which holds the quantity, the <code>delta</code> that is bound using <code>v-model</code> to the input and later used in the <code>add</code> method to add that quantity to money. The computed property <code>formattedMoney</code> correctly displays the decimal values of <code>money</code>. Finally, I also included a dummy <code>watch</code> and <code>mounted</code> with a <code>console.log</code> statement for the purpose of showing how to migrate it to Composition API.</p> <p>Take some time to understand this component if you need it.</p> <p>Right away, create a <em>MoneyCounterComposition.vue</em> component. This component shares the same <code><template></code> than the previous, since <strong>template is not affected by API's</strong>, so copy it from the previous component.</p> <p>The change is in the <code><script></code> part. Let's check first how it'd be all code:</p> <pre><code class="language-js">// In Vue 3, you'll import from "vue" import { ref, computed, watch, onMounted } from "@vue/composition-api"; export default { setup() { // State const money = ref(10); const delta = ref(1); // Computed props const formattedMoney = computed(() => money.value.toFixed(2)); // Hooks onMounted(() => console.log("Clock Object mounted")); // Methods const add = () => (money.value += Number(delta.value)); // Watchers const moneyWatch = watch(money, (newVal, oldVal) => console.log("Money changed", newVal, oldVal) ); return { delta, money, formattedMoney, add, moneyWatch }; } }; </code></pre> <p>First of all, we're exporting an object with the <code>setup</code> function. This is required and here's where all happens. Now let's focus part by part:</p> <p><strong>State:</strong> it's implemented using <code>ref</code>, and as you can see you can have as many as you want. To access them you don't need to access "<code>this</code>", since it's just a variable not a "magic" instance thing (yay!), although to change its value you need to access the <code>.value</code> property. You could use <code>reactive</code> as well, but I encourage use to read <a href="https://dev.to/ycmjason/thought-on-vue-3-composition-api-reactive-considered-harmful-j8c">this article</a> from <a href="https://twitter.com/ycmjason">Jason Yu</a> and use <code>ref</code> as a convention.</p> <p><strong>Computed:</strong> you need to use <code>computed</code> for it. It's basic: it takes a function as its first argument.</p> <p><strong>Hooks:</strong> every hook has its own utility. In the case of <code>mounted</code> hook its <code>onMounted</code>. Same shape as <code>computed</code>: they take a function as their first argument.</p> <p><strong>Methods:</strong> they're just functions, like any other. Nothing special here.</p> <p><strong>Watch:</strong> there are different signatures. That's the one equivalent to watch <code>money</code>, but you can check all shapes in the <a href="https://vue-composition-api-rfc.netlify.com/api.html#watch">RFC docs</a>.</p> <p>Finally, the <code>setup</code> function <strong>must return an object containing everything you want to use in the template</strong>. Anything that is not there, it won't be accesible from the template.</p> <p><em>A little note: you may notice that I'm importing from "@vue/composition-api". That's because I'm using a plugin since Vue 3 is not there yet, but with the plugin we can already use it.</em></p> <p>I hope the following image helps you understand how to migrate this component by this side-by-side perspective:</p> <p><img src="/tips/easily-switch-to-composition-api-in-vuejs-3/side-to-side.png" alt="Side to side components"></p> <p>If you want to see with your own eyes that this code truly works, go and check it in <a href="https://codesandbox.io/s/composition-api-simple-demo-lp0z5">this CodeSandbox</a>!</p> <p>That's it for today's tip!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Theming using CSS Custom Properties in Vue.js Components]]></title> <link>https://vuedose.tips/theming-using-custom-properties-in-vuejs-components/</link> <guid>https://vuedose.tips/theming-using-custom-properties-in-vuejs-components/</guid> <pubDate>Tue, 05 Nov 2019 00:00:00 GMT</pubDate> <description><![CDATA[Learn in this article how to theme your applications and components by leveraging the power of CSS Custom Properties in your Vue.js components]]></description> <content:encoded><![CDATA[<p>Hello there! The topic of this VueDose tip is to create <strong>super generic,</strong> <strong>flexible</strong> and yet robust components. Ready? Let's go!</p> <p>Let's start this tip with a simple button:</p> <pre><code class="language-vue"><template> <button><slot/></button> </template> <script> export default { name: "AppButton", }; </script> <style scoped> button { border: 4px solid #41b883; padding: 12px 24px; transition: 0.25s ease-in-out all; } button:hover { color: white; background-color: #41b883; } </style> </code></pre> <p>We want a component to be as generic as possible, to the point that we could reuse it in different websites with different brands. But how can we do that?</p> <p>Well, first of all, we are not going to assume that we'll always get text as content. What if we want to pass different markup like <code><strong></strong></code> or pass an icon? The best way to overcome this problem? Make use of <a href="https://vuejs.org/v2/guide/components-slots.html">slots</a>.</p> <p>Let's work on the styling now. As always, a good rule of thumb is to use <a href="https://vue-loader.vuejs.org/guide/scoped-css.html">scoped</a> which makes our CSS local. However, we want some things global, right? For instance the brand colors. Right now we are breaking the <a href="https://www.notion.so/%5B%3Chttps://en.wikipedia.org/wiki/Don%27t_repeat_yourself%3E%5D(%3Chttps://en.wikipedia.org/wiki/Don't_repeat_yourself%3E)">DRY</a> principle by duplicating the concept of a primary color. If we needed to change the primary color we'll have to change it in every place it's hardcoded. The solution? Use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/--*">custom properties</a>.</p> <pre><code class="language-vue"><style scoped> :root { --primary-color: #41b883; --on-primary-color: white; --small-spacing: 12px; --normal-spacing: calc(var(--small-spacing) * 2); } button { border: 4px solid var(--primary-color); padding: var(--small-spacing) var(--normal-spacing); transition: 0.25s ease-in-out all; } button:hover { color: var(--on-primary-color); background-color: var(--primary-color); } </style> </code></pre> <p>We would normally define custom properties in another file and target the <code>:root</code> selector and not inside the component, but for the sake of this example we are going to do it inside the component.</p> <p>Colors and spacing is usually what changes the most in web design, so we have to make sure we protect ourselves from these changes.</p> <p>This makes the component very flexible as we can change these properties and it will affect all the elements that make use of them.</p> <p>However, how could we tackle theming? Well we could use non-declared custom properties. Wait. What? Let me explain with code:</p> <pre><code class="language-vue"><style scoped> button { border: 4px solid var(--button-border-color, var(--primary-color)); padding: var(--small-spacing) var(--normal-spacing); transition: 0.25s ease-in-out all; } button:hover { color: var(--button-hover-text-color, var(--on-primary-color)); background-color: var(--button-hover-background-color, var(--primary-color)); } </style> </code></pre> <p>Where are we declaring <code>--button-border-color</code>, <code>--button-hover-text-color</code> and <code>--button-hover-background-color</code>? That's the trick, we are not.</p> <p>We are using an undefined custom property but giving it a default value. So, if one of these properties does not exist at runtime it will fallback to the default value.</p> <p>This means that from the outside we can do something as follows:</p> <pre><code class="language-vue"><template> <AppButton class="custom-theme">Hello VueDose!</AppButton> </template> <style scoped> .custom-theme { --button-border-color: pink; --button-hover-background-color: rgb(206, 161, 195); } </style> </code></pre> <p>And this is super flexible, but maybe too flexible. We don't want to expose so much knowledge to the client. Maybe the client just wants to set instead of a primary colored button a secondary theme. And the button should be able to know what things it has to set in order to have the look and feel of a secondary button. So let's mix <strong>custom properties and themes</strong>!</p> <pre><code class="language-vue"><template> <button :style="getTheme"><slot/></button> </template> <script> export default { name: "AppButton6", props: { theme: String, validator: (theme) => ['primary', 'secondary'].includes(theme) }, computed: { getTheme() { const createButtonTheme = ({ borderColor, hoverTextColor, hoverBackgroundColor }) => ({ '--button-border-color': borderColor, '--button-hover-text-color': hoverTextColor, '--button-hover-background-color': hoverBackgroundColor }) const primary = createButtonTheme({ borderColor: 'var(--primary-color)', hoverTextColor: 'var(--on-primary-color)', hoverBackgroundColor: 'var(--primary-color)' }) const secondary = createButtonTheme({ borderColor: 'var(--secondary-color)', hoverTextColor: 'var(--on-secondary-color)', hoverBackgroundColor: 'var(--secondary-color)' }) const themes = { primary, secondary } return themes[this.theme] } } }; </script> </code></pre> <p>So we can do this easily:</p> <pre><code class="language-vue"><AppButton theme="secondary">Hello VueDose!</AppButton> </code></pre> <p>And finally. All the cool kids nowadays are doing dark themes right?</p> <pre><code class="language-vue"><template> <main class="wrapper" :class="mode"> <AppButton @click.native="toggleTheme" theme="secondary"> Click me to change to dark mode! </AppButton> </main> </template> <script> import AppButton from "./components/AppButton"; export default { name: "App", data: () => ({ mode: 'light' }), components: { AppButton }, methods: { toggleTheme() { this.mode = this.mode === 'light' ? 'dark' : 'light' } } }; </script> <style scoped> .light { --background-color: white; --on-background-color: #222; } .dark { --background-color: #222; --on-background-color: white; } .wrapper { transition: 1s ease-in-out background-color; background-color: var(--background-color); color: var(--on-background-color); } </style> </code></pre> <p>We can switch <code>--background-color</code> and <code>--on-background-color</code> values to create new themes! And that will be all. Thanks for reading through all!</p> <p>Just like that, it works! Check it out yourself in <a href="https://codesandbox.io/s/vuedose-84yg5">this CodeSandbox</a>!</p> <p>Here it goes today's tip!</p> ]]></content:encoded> </item> <item> <title><![CDATA[The most modern Pie Chart component using CSS Conic Gradient and Vue.js]]></title> <link>https://vuedose.tips/the-most-modern-pie-chart-component-using-css-conic-gradient-and-vue-js/</link> <guid>https://vuedose.tips/the-most-modern-pie-chart-component-using-css-conic-gradient-and-vue-js/</guid> <pubDate>Mon, 21 Oct 2019 22:00:00 GMT</pubDate> <description><![CDATA[Build a Pie Chart component using one of the modern CSS features Conic Gradient]]></description> <content:encoded><![CDATA[<p>In the last tip a show you how to build <a href="https://vuedose.tips/tips/the-most-modern-carousel-component-using-css-scroll-snap-and-vue-js">The most modern Carousel component using CSS Scroll Snap</a>. This time we'll follow the same line to build a Pie Chart!</p> <p>But before starting, just wanted to mention that if you're a Packt user, this week my book on <a href="https://www.packtpub.com/programming/testing-vue-js-components-with-jest">Testing Vue.js components with Jest</a> has been published there too!</p> <p>Back to the topic, this time I'm showing you another new CSS feature: <strong>Conic Gradient</strong>.</p> <p>With it, we can easily build a Pie Chart component. In fact, it'd be very easy to build a color palette component, but that's another topic!</p> <p>In order to construct a pie chart, we need to define every section of the conic gradient, which is used on the <code>background</code> css property.</p> <p>For instance, the following pie chart:</p> <p><img style="max-width: 270px" src="/tips/the-most-modern-pie-chart-component-using-css-conic-gradient-and-vue-js/piechart.png" alt="Pie Chart" /></p> <p>Is defined with the following CSS rule:</p> <pre><code class="language-css">background: conic-gradient( #00A37A 0 40%, #365164 0 70%, #A54f93 0 100% ); </code></pre> <p>As you see, in each section you must define the color, the position from the center (in this case always 0) and how much angle that section must take.</p> <p>The angle of that section must be absolute: they act like different layers that overlap each other in a Z index. However you probably want to pass to the Pie Chart component the "piece" of the cake that a section must take. Something like:</p> <pre><code class="language-js">{ pieData: [ { color: "#00A37A", value: 40 }, { color: "#365164", value: 30 }, { color: "#a54f93", value: 30 } ] } </code></pre> <p>So that when you sum up all pieData values, they total 100.</p> <p>Given this introduction, let's build a <code>PieChart.vue</code> component. This component must take the above <code>pieData</code> structure and build the right <code>background: conic-gradient(...)</code> given that data:</p> <pre><code class="language-vue"><template> <div class="pie-chart" :style="pieStyles"></div> </template> <script> export default { props: ["pieData"], computed: { pieStyles() { let acum = 0; let styles = this.pieData.map( segment => `${segment.color} 0 ${(acum += segment.value)}%` ); return { background: `conic-gradient( ${styles.join(",")} )` }; } } }; </script> <style scoped> .pie-chart { border-radius: 50%; } </style> </code></pre> <p>First, notice we gave it a 50% <code>border-radius</code> in order to make it rounded.</p> <p>The main part of this component is in the <code>pieStyles</code> computed prop. Basically there we map the <code>pieData</code> property to build an array of conic sections with the shape of <code>"#aeaeae 0 30%"</code> for instance, and finally build it in the <code>background</code> CSS property.</p> <p>In that way, for the following data:</p> <pre><code class="language-js">{ pieData: [ { color: "#00A37A", value: 40 }, { color: "#365164", value: 30 }, { color: "#a54f93", value: 30 } ] } </code></pre> <p>The <code>pieStyles</code> computed property will return:</p> <pre><code class="language-css">background: conic-gradient( #00A37A 0 40%, #365164 0 70%, #a54f93 0 100% ); </code></pre> <p>Just like that, it works! Don't trust me? Check it out yourself in <a href="https://codesandbox.io/s/piechart-component-example-iv1yu">this CodeSandbox</a>!</p> <p>Here it goes today's tip!</p> ]]></content:encoded> </item> <item> <title><![CDATA[The most modern Carousel component using CSS Scroll Snap and Vue.js]]></title> <link>https://vuedose.tips/the-most-modern-carousel-component-using-css-scroll-snap-and-vue-js/</link> <guid>https://vuedose.tips/the-most-modern-carousel-component-using-css-scroll-snap-and-vue-js/</guid> <pubDate>Sun, 06 Oct 2019 22:00:00 GMT</pubDate> <description><![CDATA[Build a Carousel by using one of the latest CSS features called scroll-snap and bundle it into a Vue.js component]]></description> <content:encoded><![CDATA[<p>Last week I went to Codemotion Madrid with my beloved friend <a href="https://twitter.com/laurabonmati">Laura Bonmati</a>. It's one of the biggest conferences about coding in Spain.</p> <p>One of our favourite talks was <em>"The latest features in CSS"</em> (in Spanish) by <a href="https://twitter.com/Yune__vk">Sonia Ruiz</a>, and this tip comes from some ideas I took from her talk, so let's give her a shoutout on twitter for the inspiration!</p> <p>Among the features she showed, I was amazed by <code>scroll-snap</code> and how easy is to create a carousel by using it, so I thought... let's make it a component!</p> <p>First, create a <code>Carousel.vue</code> component with the following structure:</p> <pre><code class="language-vue"><template> <div class="carousel-wrapper"> <div class="carousel" :style="{ width, height }"> <slot/> </div> </div> </template> <script> export default { props: ["width", "height"] }; </script> <style scoped> .carousel { display: flex; overflow: scroll; scroll-snap-type: x mandatory; } .carousel > * { flex: 1 0 100%; scroll-snap-align: start; } </style> </code></pre> <p>The important parts here are the use of <code>scroll-snap-type</code>. Using it we're forcing the scroll to snap to every item inside the <code>.carousel</code> element, in the <em>x</em> axis.</p> <p>Then you need to make every item in the carousel to take the full space of the container. You do that using <code>flex: 1 0 100%</code> and defining the items alignment with <code>scroll-snap-align: start</code>.</p> <p>Notice that I'm also passing by a <code>width</code> and <code>height</code> properties so you can easily set the carousel dimensions from outside the component.</p> <p>By the way, if you're not sure about how <code><slot/></code> work, you can check the articles <a href="https://vuedose.tips/tips/using-scoped-slots-in-vue-js/">Using scoped slots in Vue.js</a> and <a href="https://vuedose.tips/tips/new-v-slot-directive-in-vue-js-2-6-0/">New v-slot directive in Vue.js 2.6.0</a> and know more about them (including scoped slots).</p> <p>Just like that, you have a carousel component that works smoothly using the mouse scroll!</p> <p>But hey... let's add some functionality right? What if you want to change the carousel slides manually?</p> <p>As you might have thought, the carousel "sliding" works according to the scroll, so you must play with the scroll position.</p> <p>In fact, each slide scroll position is exactly the width of the carousel multiplied per the slide position in the carousel.</p> <p>Let's add a couple of buttons that allow us to go back and forth in the carousel, so we can navigate through every item:</p> <pre><code class="language-vue"><template> <div class="carousel-wrapper"> <div class="carousel" :style="{ width, height }"> <slot/> </div> <button @click="changeSlide(-1)">Prev Slide</button> <button @click="changeSlide(1)">Next Slide</button> </div> </template> <script> export default { props: ["width", "height"], methods: { changeSlide(delta) { const carousel = this.$el.querySelector(".carousel"); const width = carousel.offsetWidth; carousel.scrollTo(carousel.scrollLeft + width * delta, 0); } } }; </script> </code></pre> <p>These buttons call the <code>changeSlide</code> method passing by the number of positions you want to navigate back or forth.</p> <p>The method basically takes the carousel current width, and calculates where should scroll by checking the current scroll position and adding the amount of pixels where it should navigate. That calculation is defined by <code>carousel.scrollLeft + width * delta</code>.</p> <p>If you try the component now, you'll see that it works as expected, but the scroll is not smooth. Instead, it changes the slide immediately.</p> <p>Luckily, modern CSS comes to the rescue again!</p> <p>You can use the property <code>scroll-behavior: smooth</code> on the carousel element and the scrolling will be smooth again!</p> <pre><code class="language-css">.carousel { display: flex; overflow: scroll; scroll-behavior: smooth; scroll-snap-type: x mandatory; } </code></pre> <p>Finally, the way to use the component is as simple as this:</p> <pre><code class="language-vue"><template> <Carousel width="400px" height="250px"> <div class="blue">Blue</div> <div class="green">Green</div> <div class="red">Red</div> </Carousel> </template> </code></pre> <p>Do you realize how easy was to build this component? Modern CSS is rad!</p> <p>I'm pretty sure you're now thinking on more ways to improve it: change slides automatically every X seconds, loop scrolling, go to a specific slide... please, tell me what you come up with and show it to me! I'll be more than happy to see what you accomplish!</p> <p>Of course, keep in mind that this component uses very new CSS properties, so it's only supported in the most modern browsers.</p> <p>But hey, you must be building the Carousel of the future!</p> <p>Do you want to see this carousel working? Go to this <a href="https://codesandbox.io/s/carousel-component-5zkbe">CodeSandbox</a>!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Data Provider component in Vue.js]]></title> <link>https://vuedose.tips/data-provider-component-in-vue-js/</link> <guid>https://vuedose.tips/data-provider-component-in-vue-js/</guid> <pubDate>Tue, 24 Sep 2019 00:00:00 GMT</pubDate> <description><![CDATA[Use scoped slots to create a data provider in Vue.js]]></description> <content:encoded><![CDATA[<p>By using component-based technologies such as Vue.js, doesn't mean that all components must be UI based.</p> <p>In fact, my favourite way to apply advanced reusability in large applications is by using component composition.</p> <p>With <em>scoped slots</em>, as you haven seen in <em><a href="https://vuedose.tips/tips/using-scoped-slots-in-vue-js">"Using scoped slots in Vue.js"</a></em>, you can encapsulate logic in another component and pass it to a slot via props. That allows you to compose the UI and behaviour of a new component by combining multiple of them.</p> <p>Today, I'm going to show you a <strong>Data provider Component</strong> example.</p> <p>Data Provider is a <strong>renderless component</strong>, meaning that it doesn't need to render anything.</p> <p>The base to create a <em>renderless component</em> is to create a <em>scoped slot</em> from a render function and pass any prop to it:</p> <pre><code class="language-js">render() { return this.$scopedSlots.default({ loading: !this.loaded, data: this.data }); } </code></pre> <p>Due to an <a href="https://github.com/vuejs/vue/issues/8056">inconsistency on scoped slots</a> (fixed by version 2.6), you can better do it like this to make it work in any case:</p> <pre><code class="language-js">render() { const slot = this.$scopedSlots.default({ loading: !this.loaded, data: this.data }); return Array.isArray(slot) ? slot[0] : slot; } </code></pre> <p>Knowing that, to create a Data Provider component you must perform an ajax/fetch call when the component is created, and then pass that data to the scoped slot.</p> <p>A final version of <code>DataProvider.js</code> could be:</p> <pre><code class="language-js">import axios from "axios"; export default { props: ["url"], data: () => ({ data: null, loaded: false }), created() { axios.get(this.url).then(({ data }) => { this.data = data; this.loaded = true; }); }, render() { const slot = this.$scopedSlots.default({ loading: !this.loaded, data: this.data }); return Array.isArray(slot) ? slot[0] : slot; } }; </code></pre> <p>Notice that it's a <code>.js</code> file. Since it just has the script part of the component, it doesn't need to be a <code>.vue</code> file.</p> <p>The Data Provider component also is holding a loading state, so you can use that to render different UI depending on that state. An example could be:</p> <pre><code class="language-html"><template> <DataProvider url="https://jsonplaceholder.typicode.com/users"> <div v-slot="{ data, loading }"> <div v-if="loading">Loading...</div> <div v-else> <h2>Result: {{ data.length }} users</h2> <p v-for="user in data" :key="user.id">{{ user.name }}</p> </div> </div> </DataProvider> </template> </code></pre> <p>That's it!</p> <p>If you want to check this example running, go to this <a href="https://codesandbox.io/s/2w6zp30kjy">CodeSandbox</a>!</p> <p>Don't forget to share <a href="https://vuedose.tips">VueDose</a> with your colleagues, so they also know about these tips as well!</p> <p>See you next week.</p> <p>Alex</p> ]]></content:encoded> </item> <item> <title><![CDATA[Using Scoped Slots in Vue.js]]></title> <link>https://vuedose.tips/using-scoped-slots-in-vue-js/</link> <guid>https://vuedose.tips/using-scoped-slots-in-vue-js/</guid> <pubDate>Sun, 15 Sep 2019 00:00:00 GMT</pubDate> <description><![CDATA[Quick example on how to use scoped slots for component reusability in vuejs]]></description> <content:encoded><![CDATA[<p>Multiple times I get to know companies that want to improve their productivity when coding in Vue.js.</p> <p>That's a too open question, but at least some part can be achieved that by identifying what functionality they build pretty often in their apps, and then having a toolkit of reusable components that allow you to put that common logic in there, while being flexible enough to adapt to other apps.</p> <p>Vue.js has <em>slots</em> in order to make components have a redefinable structure, but by themselves they're pretty limited. Sometimes you need some data or state in order to define how a component should render.</p> <p>If you don't know slots, I suggest you learn them first on <a href="https://vuejs.org/v2/guide/components-slots.html">the Vue.js docs</a>.</p> <p>Scoped slots is an advanced feature in Vue.js that allows you to do that. They're like normal slots, but they can receive properties as well.</p> <p>Let's build a <code>Clock.vue</code> component in order to illustrate that. Basically it must be a time counter:</p> <pre><code class="language-vue"><template> <div class="clock"> <slot :time="time"> <p>Default slot</p> <p>Time: {{ time.toLocaleTimeString() }}</p> </slot> </div> </template> <script> export default { data: () => ({ time: new Date() }), created() { setInterval(() => { this.time = new Date(this.time.getTime() + 1000); }, 1000); } }; </script> </code></pre> <p>You might have noticed the <code><slot :time="time"></code> line, that's the slot receiving the <code>time</code> property. That's the way this <code>Clock.vue</code> component can send the time data to the component that uses it, while encapsulating the timer logic itself.</p> <p>You also might have realised that this component already renders the time by itself, as it has some default content within the slot.</p> <p>But what if we want to redefine what it renders, while holding the timer logic in it?</p> <p>Well, since we're passing the <code>time</code> property to the slot, we can do that simply by using the <code>v-slot</code> directive on the root element of the <code>Clock</code> slot. So, wherever you want to render the <code>Clock</code> component, you'll write something like:</p> <pre><code class="language-vue"><template> <Clock> <template v-slot="{ time }"> <p><b>Slot override!</b></p> <p>Date: {{ time.toLocaleDateString() }}</p> <p>Time: {{ time.toLocaleTimeString() }}</p> </template> </Clock> </template> </code></pre> <p><code>v-slot</code> receives all the props passed from within the <code>Clock</code> component. Since that's JavaScript object, we can use the Object Spread Operator to already grab the time prop as <code>{ time }</code>.</p> <p>Do you see the power of Scoped Slots? You can do more powerful stuff, like render-less components, that you'll see in a future tip 😜.</p> <p>If you want to run the code yourself, you can find it in this <a href="https://codesandbox.io/s/yjjq04vn91">CodeSandbox</a>!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Quick Content Testing using Snapshots in Vue.js]]></title> <link>https://vuedose.tips/quick-content-testing-using-snapshots-in-vue-js/</link> <guid>https://vuedose.tips/quick-content-testing-using-snapshots-in-vue-js/</guid> <pubDate>Tue, 28 May 2019 11:00:00 GMT</pubDate> <description><![CDATA[Useful tip about how to use snapshot testing in Vue.js the right way and have consistent and coherent tests in your applications]]></description> <content:encoded><![CDATA[<p>Have you ever heard about Snapshots being <em>evil</em>? About how <strong>fragile</strong> they are and how you should avoid them? It's true! You should be extremely careful about them because they do <em>exact</em> comparison of content as text, that is if you are snapshoting a component, you are actually snapshoting its HTML content, so anything changing the HTML will break the snapshot and if this is repeated too often, you may end up accidentally accepting snapshots updates and missing regressions in your application 😱.</p> <p>But you don't have to snapshot the whole HTML! You can even provide a <em>hint</em> to recognize the snapshot and this can be used to generate tests fixtures on the flight in a very convening way, <strong>specially for very large content sets</strong></p> <p>Imagine you have a very big table and you want to test that given some props, the table renders the right content:</p> <pre><code class="language-vue"><table> <thead> <tr> <th v-for="column in columns">{{ column.name }}</th> </tr> </thead> <tbody> <tr v-for="item in items"> <td v-for="column in columns"> <span class="label">{{ colum.name }}: </span> <span class="value">{{ item[colum.key] }}</span> </td> </tr> </tbody> </table> </code></pre> <p>Here <code>columns</code> is an array of all the columns in the table and <code>items</code> in an array of all the rows displayed. You could say both are props. If you want to test the content of the table given those props, you will have to test each row:</p> <pre><code class="language-js">test('contains the right information', () => { // columns and items are defined above const wrapper = shallowMount(MyTable { props: { columns, items }}) // first cell in the header expect(wrapper.find('thead th:nth-of-type(1)').text()).toBe('Product') // first row in the tbody expect(wrapper.find('tbody tr:nth-of-type(1) .value').text()).toBe('Dinner plates set of 8') // repeat for EVERY row 🤯 }) </code></pre> <p>There are multiple approaches to select the table cell like <a href="https://github.com/LinusBorg/vue-cli-plugin-test-attrs">using a <code>data-test</code> attribute</a> but that's not the issue here. Can we go faster when writing this kind of test? What if we wrote the component, check manually and then add a test that snapshots the current state?</p> <pre><code class="language-js">test('contains the right information', () => { // columns and items are defined above const wrapper = shallowMount(MyTable { props: { columns, items }}) const cells = wrapper.findAll("td"); for (let i = 0; i < cells.length; ++i) { const cell = cells.at(i); // use label as the hint for the snapshot const label = cell.find(".label"); if (!label.exists()) continue; // filter out cells that do not have a label expect(cell.find(".value").text()).toMatchSnapshot(label.text()); } }) </code></pre> <p>Writing this test will generate a snapshot the first time it is run:</p> <pre><code class="language-js">// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`MyTable contains the right information: Product Name 1`] = `"Dinner plates set of 8"`; exports[`MyTable contains the right information: Sells 1`] = `"23"`; exports[`MyTable contains the right information: Stock 1`] = `"3"`; // more and more cells </code></pre> <p>The advantages of this solution is that adding a new column will create a single new snapshots without invalidating the others while removing existing columns will make some snapshot <strong>obsoletes</strong> and changing any of the cells <code>.value</code> content will make the snapshot test <strong>fail</strong>.</p> <p>If you don't like the idea of creating dozens of snapshots like this, you can create some custom text value and create <strong>one single snapshot</strong>:</p> <pre><code class="language-js">test('contains the right information', () => { // columns and items are defined above const wrapper = shallowMount(MyTable { props: { columns, items }}) const cells = wrapper.findAll("td"); let content = '' for (let i = 0; i < cells.length; ++i) { const cell = cells.at(i); // use label as the hint for the snapshot const label = cell.find(".label"); if (!label.exists()) continue; // filter out cells that do not have a label content += `${label.text()}: ${cell.find(".value").text()} \n` } expect(content).toMatchSnapshot() }) </code></pre> <p>You will end up with one single snapshot:</p> <pre><code class="language-js">// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`MyTable contains the right information 1`] = ` Product Name: Dinner plates set of 8 Sells: 23 Stock: 3 etc. `; </code></pre> <p>So remember: Snapshots can also be used to generate tests fixtures with text!</p> <p>Happy Testing!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Style inner elements in scoped CSS using /deep/ selector in Vue.js]]></title> <link>https://vuedose.tips/style-inner-elements-in-scoped-css-using-deep-selector-in-vue-js/</link> <guid>https://vuedose.tips/style-inner-elements-in-scoped-css-using-deep-selector-in-vue-js/</guid> <pubDate>Sun, 12 May 2019 20:00:00 GMT</pubDate> <description><![CDATA[Learn how you can use /deep/ to deeply style your Vue.js components using scoped CSS.]]></description> <content:encoded><![CDATA[<p>In the <a href="https://vuedose.tips/tips/the-importance-of-scoped-css-in-vue-js/">The importance of scoped CSS in Vue.js</a> you saw why scoped CSS is important when we want to achieve style encapsulation in components. If you haven't read that tip, I strongly suggest you to do so to understand this one.</p> <p>But when we tried to turn the style of <a href="https://codesandbox.io/s/zwkj000z7p">that example</a> into scoped CSS, the styles were missing.</p> <p>Here's the thing: when you use scoped CSS, you can modify the root element of the component you want to customise.</p> <p>In other words, from <code>BlueList.vue</code> and <code>RedList.vue</code> of the example we can only modify the <code>.list</code> class of <code>BaseList.vue</code> since it's the root element of that component.</p> <p>But what about inner elements? We want to style the <code>.list-item</code> class to change the color of the items.</p> <p>For that, we have the <code>/deep/</code> selector, and you can use it to access inner elements of components as follows:</p> <pre><code class="language-vue"><style scoped> .list /deep/ .list-item { color: white; background: #42a5f5; } </style> </code></pre> <p>Take a look at the <a href="https://codesandbox.io/s/40y6v5w3w0">updated example</a> and see how now it works as expected and each of them have a different color.</p> ]]></content:encoded> </item> <item> <title><![CDATA[The importance of scoped CSS in Vue.js]]></title> <link>https://vuedose.tips/the-importance-of-scoped-css-in-vue-js/</link> <guid>https://vuedose.tips/the-importance-of-scoped-css-in-vue-js/</guid> <pubDate>Tue, 07 May 2019 22:00:00 GMT</pubDate> <description><![CDATA[Vue.js tutorial on how to us scoped CSS to avoid style collision in your Vue.js and Nuxt.js applications.]]></description> <content:encoded><![CDATA[<p>Sometimes I see new devs coming to Vue.js getting confused about scoped CSS in Vue.js. Some using it without really knowing how does it work.</p> <p>If you are there, I hope this tip helps you understand why and when to use them (and when not) 😉.</p> <p>I'm not going to dive deep into the theory as you have it already in the <a href="https://vue-loader.vuejs.org/guide/scoped-css.html#mixing-local-and-global-styles">vue-loader docs</a>. Just to know, it's a vue-loader's feature to avoid style collisions and encapsulate styles by emulating Shadow DOM functionality.</p> <p>Let's better see an example of a problematic situation when you don't use scoped CSS.</p> <p>Imagine we have a <code>BaseList.vue</code> component with the following structure:</p> <pre><code class="language-vue"><template> <ul class="list"> <li class="list-item" v-for="item in items" :key="item"> {{ item }} </li> </ul> </template> </code></pre> <p>Then create two components with the same code. Call them <code>RedList.vue</code> and <code>BlueList.vue</code>:</p> <pre><code class="language-vue"><template> <BaseList :items="items" /> </template> <script> import BaseList from "./BaseList"; export default { props: ["items"], components: { BaseList } }; </script> </code></pre> <p>Now add two different styles to each of them, according to the colors. For instance, for <code>BlueList.vue</code>:</p> <pre><code class="language-vue"><style> .list-item { color: white; background: #42a5f5; } </style> </code></pre> <p>Put them together like I did in <a href="https://codesandbox.io/s/zwkj000z7p">this CodeSandbox</a>, and... surprise! You'll see that even though both components have a different color defined, they show the same color:</p> <p><a href="/tips/the-importance-of-scoped-css-in-vue-js/ListExample.png"></a></p> <p>That's because when we don't use scoped CSS, they're global, even though they are in different components. So, in this case one of the styles is overriding the other.</p> <p>That's why scoped CSS are that important: they avoid these collisions to happen out of the box.</p> <p>Here's the joke: if you try to put the <code>scoped</code> word into the <code>style</code> tag in the example above, you'll see that the styles of the list item won't apply 😅.</p> <p>But that's normal, <strong>next week you'll see how to style inner elements</strong> when using scoped CSS, so stay tuned!</p> <p>To end up, I want to mention that you can mix scoped (thus local) styles with global styles (not scoped). You want to use global styles in these situations:</p> <ul> <li>When applying cross-component styles (utility styles).</li> <li>When writing a third party library. If you apply scoped CSS, you'd be making your library impossible to customise styles.</li> </ul> <p>Keep in mind scoped CSS is not the only solution. You might be comfortable as well using CSS modules or methodologies such as BEM.</p> ]]></content:encoded> </item> <item> <title><![CDATA[How to create a Geolocated Currency with MaxMind in Vue.js]]></title> <link>https://vuedose.tips/geolocated-currency-with-max-mind/</link> <guid>https://vuedose.tips/geolocated-currency-with-max-mind/</guid> <pubDate>Mon, 22 Apr 2019 21:00:00 GMT</pubDate> <description><![CDATA[Learn in this tutorial how to use content that depends on the language and location of the user and apply true i18n in Vue.js]]></description> <content:encoded><![CDATA[<p>Sometimes you need to integrate a geolocated content on your website. In this little tip I will show you how easy could it be using VueJS, MaxMind GeoIP and Autonumeric. This sample is written in NuxtJS for the special case of a geolocated currency, but the same principles apply on the vanilla VueJS applications.</p> <p>The geolocated currency means, that even if you have a page only in the one language you want to show a different currency according to the country of the visitor. The visitor from the USA will see the dollar sign <code>$10,000</code> and the visitor from the Germany would see in the same sentence Euro sign <code>10.000€</code>. You could also notice the different punctuation in these two cases.</p> <p>Ok, I assume your content is reachable trough some API of headless CMS like Storyblok, like in my case. Remember, the currency could be use in any string. That's why I defined unique pattern that could be used in any string to mark the geolocated currency. I used this pattern <code>#{number}#</code>, which means that string <code>Price: #{10000}#</code> will be replaced in the USA with string <code>Price: $10,000</code> and in the Germany with <code>Price: 10.000€</code>.</p> <pre><code class="language-js">// API returns: { description: "Price: #{10000}#"; } </code></pre> <p>First you have to find and transform your patterns into HTML tags. I wanted to do it using the global filter, but then I found out that the filter is forbidden in <code>v-html</code> and <code>v-text</code>. So, I did it with the global mixin, which will transform string <code>#{number}#</code> into string <code><span data-currency>number</span></code>.</p> <pre><code class="language-js">// mixins.js import Vue from "vue"; // Adds global currency method to transfor #{}# string to span Vue.mixin({ methods: { extractCurrency(string) { if (!string.includes("#{")) { return string; } const openTag = `<span data-currency>`; const closeTag = `</span>`; const openRegex = /#{/g; const closeRegex = /}#/g; string = string.replace(openRegex, openTag); string = string.replace(closeRegex, closeTag); return string; } } }); </code></pre> <p>As this is the global mixin you can use the function <code>extractCurrency</code> in every component. Eg.</p> <pre><code class="language-vue"><template> <p v-html="extractCurrency(description)" /> </template> </code></pre> <p>If you will generate static page using <code>nuxt generate</code> as I did, you will end with a static page containing this:</p> <pre><code class="language-vue"><!-- static-page.html --> <p>Price: <span data-currency>10000</span></p> </code></pre> <p>Now you have to find out the country of the visitor using GeoIP of the MaxMind and then call Autonumeric function to add correct currency. You will do this in the <code>mounted()</code> life cycle of the VueJS as this should be run only at the client side. The best place to do this in the NuxtJS is in your layout file.</p> <pre><code class="language-js">// layouts/default.vue export default { mounted() { geoip2.country( success => { const currencyType = success.country.iso_code === "US" ? "NorthAmerican" : "French"; AutoNumeric.multiple( "[data-currency]", AutoNumeric.getPredefinedOptions()[currencyType] ); }, error => { console.warn("Error occured by getting geolocation."); } ); } }; </code></pre> <p>If the GeoIP is not working or taking to long time to return location, you can show the number without the currency or show the default currency using CSS <code>:before</code> pseudo-element.</p> <pre><code class="language-scss">// fallback for currency span[data-currency]:not([value]) { &::after { content: " €"; } } </code></pre> <p>Don't forget that you could use also the global event bus in the NuxtJS to trigger method on component according to the geolocation. You would add this line <code>this.$nuxt.$emit("geolocationFound", success.country.iso_code)</code> into your mounted cycle.</p> <p>Pretty easy or?</p> ]]></content:encoded> </item> <item> <title><![CDATA[Debugging Templates in Vue.js Components]]></title> <link>https://vuedose.tips/debugging-templates-in-vue-js/</link> <guid>https://vuedose.tips/debugging-templates-in-vue-js/</guid> <pubDate>Thu, 18 Apr 2019 19:00:00 GMT</pubDate> <description><![CDATA[Guide to debug component templates using the browser dev tools.]]></description> <content:encoded><![CDATA[<p>The <a href="https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd">Vue.js DevTools</a> are awesome to inspect your Vue app but sometimes you might want to debug things that are happening in your template and as you heard that you can place JavaScript expressions there maybe you've tried to place a console.log and find this situation:</p> <p><img src="https://convertkit.s3.amazonaws.com/assets/pictures/122145/2065424/content_carbon__55_.png" alt=""></p> <p><img src="https://convertkit.s3.amazonaws.com/assets/pictures/122145/2065425/content_warn.png" alt=""></p> <p>This is due that everything that you placed in the template Vue try to find it in the instance so a quick solution could be put a log method in the component to allow Vue to find it:</p> <p><img src="https://convertkit.s3.amazonaws.com/assets/pictures/122145/2065434/content_carbon__61_.png" alt=""></p> <p>and just use <code>{{ log(message) }}</code>.</p> <p>But this is something that you occasionally want in some different components and it's a little boring to repeat this code all the time so what can we do is to <a href="https://vuejs.org/v2/cookbook/adding-instance-properties.html">add an instance property</a> and use the Vue.prototype to place our custom log method in the main.js:</p> <p><img src="https://convertkit.s3.amazonaws.com/assets/pictures/122145/2065437/content_carbon__59_.png" alt=""></p> <p>so now we can use $log in every component template and if we don't want to interfere with the rendering just use the JS lazy evaluation with an OR operator 😊</p> <p><img src="https://convertkit.s3.amazonaws.com/assets/pictures/122145/2065471/content_unnamed__2_.png" alt=""></p> <p>awesome, right? :D but... what if we want to place a breakpoint in order to quietly debug some variables around the template?</p> <p>If we place a debugger in the template we might find this:</p> <p><img src="https://convertkit.s3.amazonaws.com/assets/pictures/122145/2065466/content_carbon__65_.png" alt=""></p> <p>it is not even compiling the template! 😱 and if we apply the same strategy than before we will find us alone in the prototype function instead of the template that we want to debug:</p> <p><img src="https://convertkit.s3.amazonaws.com/assets/pictures/122145/2065477/content_debugger.png" alt=""></p> <p> so in order to place a breakpoint in the middle of the template we can use a little trick wrapping the debugger in an <a href="https://developer.mozilla.org/en-US/docs/Glossary/IIFE">IIFE</a> (Immediately Invoked Function Expression) like this:</p> <p><img src="https://convertkit.s3.amazonaws.com/assets/pictures/122145/2065489/content_unnamed__1_.png" alt=""></p> <p>and we will find ourselves in the middle of the compiled render function:</p> <p><img src="https://convertkit.s3.amazonaws.com/assets/pictures/122145/2065488/content_image__1_.png" alt=""></p> <p>where in this case the "_vm" variable</p> <p><a href="https://vuejs.org/v2/guide/instance.html">means ViewModel</a></p> <p>and is the instance of our component :). This is also interesting to check the compiled template but for some reason the variables are not available in the function scope of the chrome devtools until we place them after the debugger:</p> <p><img src="https://convertkit.s3.amazonaws.com/assets/pictures/122145/2065487/content_unnamed.png" alt=""></p> <p>and now you can enjoy inspect everything around!</p> <p><img src="https://convertkit.s3.amazonaws.com/assets/pictures/122145/2065496/content_image__2_.png" alt=""></p> <p>That's all! I hope that with these tips you will find yourself more confident debugging Vue templates and also enjoy inspecting the insides of the compiled render functions just for curiosity.</p> ]]></content:encoded> </item> <item> <title><![CDATA[Simple transition effect between pages and layouts in Nuxt.js]]></title> <link>https://vuedose.tips/simple-transition-effect-between-pages-and-layouts-in-nuxt-js/</link> <guid>https://vuedose.tips/simple-transition-effect-between-pages-and-layouts-in-nuxt-js/</guid> <pubDate>Wed, 17 Apr 2019 19:00:00 GMT</pubDate> <description><![CDATA[In Nuxt.js, the Vue.js framework, it's very easy to apply transitions between pages and layouts on route changes. This tutorial explains how to.]]></description> <content:encoded><![CDATA[<p>Vue.js makes the animations and transitions incredibly easy to implement. So you should really use this opportunity to give a little spark to your application/website to shine.</p> <p>Nuxt.js already builds on the provided capabilities of Vue.js. It gives you a possibility to create a very simple transitions between the pages very fast and almost for no effort.</p> <p>All you need to specify in your <code>page.vue</code> file is name of the transition. This name will be used to generate 6 transition classes, which can be use for transition effect between pages.</p> <pre><code class="language-js">export default { transition: "default" }; </code></pre> <p>NuxtJS will interpret this as <code><transition name="default"></code> and it will look for following classes in you css code, which will define the transition between the pages.</p> <pre><code class="language-css">.default-enter { } .default-enter-active { } .default-enter-to { } .default-leave { } .default-leave-active { } .default-leave-to { } </code></pre> <p>You should definitely check <a href="https://vuejs.org/v2/guide/transitions.html#Transition-Classes">Vue.js documentation</a> to understand, when are these classes used and what are transition modes. But lets define very simple transition between pages using the opacity.</p> <pre><code class="language-css">.page-enter-active, .page-leave-active { transition-property: opacity; transition-timing-function: ease-in-out; transition-duration: 500ms; } .page-enter, .page-leave-to { opacity: 0; } </code></pre> <p>After integration of this code into your Nuxt.js application/website, the default transition between pages will take 1000ms and content of the page will fade out and then the new one will fade in. You even don't have to define transition name as the <code>page</code> is the default transition name. You can check this code in the <a href="https://codesandbox.io/embed/2xovlqpv9n">CodeSandbox</a>.</p> <p>If you want create a special transition for one of your pages, then you can specify a name of the transition in <code>page.vue</code> file as I did in this <a href="https://codesandbox.io/embed/2xovlqpv9n">CodeSandbox</a> for <code>intro.vue</code>. As you can see, if you visit <code>intro.vue</code> page the transition is change to two black rectangles.</p> <p><a href="https://codesandbox.io/embed/2xovlqpv9n">https://codesandbox.io/embed/2xovlqpv9n</a></p> <p>Be aware, that all transitions between pages are working only if you are navigating between the pages with the same layout. If you are navigating between the pages with two different layouts you have to use the <a href="https://nuxtjs.org/api/configuration-transition#the-layouttransition-property">layout transition</a> of Nuxt.js. You can see this in our CodeSandbox, if you visit <code>other.vue</code> page.</p> <p>NuxtJS is already setting few defaults on the <a href="https://nuxtjs.org/api/pages-transition/">page transitions</a> and the <a href="https://nuxtjs.org/api/configuration-transition#the-layouttransition-property">layout transitions</a>. These defaults can be override directly in <code>nuxt.config.js</code>:</p> <pre><code class="language-js">module.exports = { /* Layout Transitions */ layoutTransition: { name: "layout", mode: "" }, /* Page Transitions */ pageTransition: { name: "default", mode: "" } }; </code></pre> <p>These are only the basics of transitioning between the pages in Nuxt.js. Under the hood of the Vue.js is hidden much more power, which can be used to create crazy animations and transitions. So keep digging in the documentation and check this <a href="https://github.com/sdras/page-transitions-travelapp">sample</a> from Sarah Drasner.</p> ]]></content:encoded> </item> <item> <title><![CDATA[Lazy load images using v-lazy-image Vue.js component]]></title> <link>https://vuedose.tips/lazy-loading-images-with-v-lazy-image/</link> <guid>https://vuedose.tips/lazy-loading-images-with-v-lazy-image/</guid> <pubDate>Tue, 16 Apr 2019 19:00:00 GMT</pubDate> <description><![CDATA[Learn to improve the web performance of your vue.js application]]></description> <content:encoded><![CDATA[<p>Some time ago I wanted to apply lazy loading in images in order to load them only when they enter the viewport. Researching I found different ways to do it, but they were a bit more cumbersome than what I wanted.</p> <p>I wanted something as simple as having the same <code><img></code> tag that gets lazy loaded. That's why I created <a href="https://github.com/alexjoverm/v-lazy-image">v-lazy-image</a>, a Vue.js component that imitates the <code><img></code> tag API and applies lazy loading.</p> <p>It also makes easy for you to <a href="/achieve-max-performance-loading-your-images-with-v-lazy-image">achieve max performance by using responsive images</a> and <a href="/use-responsive-images-with-v-lazy-image">applying progressive image loading</a> effortlessly, but let me show you the most simple case.</p> <p><code>v-lazy-image</code> <strong>supports Vue 3+</strong>, but it has a compatible Vue 2 version.</p> <p>To use it, once you install it by running <code>npm install v-lazy-image</code>, just import it like a component:</p> <pre><code class="language-vue"><script setup> import VLazyImage from "v-lazy-image"; </script> </code></pre> <p>In <strong>Vue 2</strong>, import it from <code>v-lazy-image/v2</code>:</p> <pre><code class="language-js">import VLazyImage from "v-lazy-image/v2"; export default { components: { VLazyImage } }; </code></pre> <p>And then is as simple to use as using an <code><img></code>:</p> <pre><code class="language-vue"><template> <v-lazy-image src="http://lorempixel.com/400/200/" /> </template> </code></pre> <h2>See it in action</h2> <p>Here's the most simple demo. It shows you how the image is lazy loaded just by using <code><v-lazy-image></code> instead of <code><img></code>.</p> <p>You can check that works by opening the <em>Devtools Network tab</em>, check the <em>Img filter</em> and clear the output. Then, when you scroll down in the demo until the image shows, you'll see the image is loaded when it enters the viewport.</p> <p><em>Psst! See that little bar on the left of that embed? If you scroll right you'll see the demo code</em></p> <p><iframe src="https://codesandbox.io/embed/r5wmj970wm?fontsize=14&hidenavigation=1&module=%2Fsrc%2FApp.vue&theme=dark" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="v-lazy-image" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"</p> <blockquote> <p></iframe></p> </blockquote> <p>Here's what you should see in the <em>Devtools</em>:</p> <p><img src="https://a.storyblok.com/f/83078/1130x572/fd83736dc1/devtools-check-img.png" alt="Img"></p> <h2>Follow up</h2> <p>Do you think that's too basic? Yeah, the idea was to keep it basic, but I understand you want something more advanced.</p> <p>In a real project you want to <strong>achieve max performance</strong>, and for that, <code>v-lazy-image</code> allows you to use <em>responsive images</em> as well as <em>progressive image loading</em>.</p> <p>Check them out and see your web apps flying!</p> <ul> <li><a href="/use-responsive-images-with-v-lazy-image">Use Responsive Images with v-lazy-image</a></li> <li><a href="/achieve-max-performance-loading-your-images-with-v-lazy-image">Progressive Image Loading with v-lazy-image for Max Performance</a></li> </ul> ]]></content:encoded> </item> <item> <title><![CDATA[Dynamic Imports in Vue.js for better performance]]></title> <link>https://vuedose.tips/dynamic-imports-in-vue-js-for-better-performance/</link> <guid>https://vuedose.tips/dynamic-imports-in-vue-js-for-better-performance/</guid> <pubDate>Mon, 15 Apr 2019 19:00:00 GMT</pubDate> <description><![CDATA[Tutorial on code splitting using dynamic import in vue.js applications in order to have better performance.]]></description> <content:encoded><![CDATA[<p>I bet you are already familiar with the terms <strong><a href="https://webpack.js.org/guides/code-splitting/">"code splitting"</a></strong> and <strong>"lazy loading"</strong>. Let's take the latter definition from <a href="https://webpack.js.org/guides/lazy-loading">Webpack's docs</a>:</p> <blockquote> <p>Lazy, or "on demand", loading is a great way to optimize your site or application. This practice essentially involves splitting your code at logical breakpoints, and then loading it once the user has done something that requires, or will require, a new block of code. This speeds up the initial load of the application and lightens its overall weight as some blocks may never even be loaded.</p> </blockquote> <p>This kind of feature should be done by default by the frameworks we use, as <a href="https://twitter.com/slightlylate/status/1018880523446337536">some people have suggested</a>. (Also in the <a href="https://twitter.com/slightlylate/status/1031934342132461568">React ecosystem</a>)</p> <h3>The meat:</h3> <p>Whenever it's possible, I'd recommend to use dynamic imports to import components. They will be lazily loaded (by Webpack) when needed.</p> <pre><code class="language-javascript">// Instead of a usual import import MyComponent from "~/components/MyComponent.js"; // do this const MyComponent = () => import("~/components/MyComponent.js"); </code></pre> <h3>The explanation:</h3> <p>When using Webpack to bundle your application, you may use different ways to work with modules (ES Modules, CJS, AMD...). If you choose the ESM way (which is the recommended), you will have this kind of syntax:</p> <pre><code class="language-javascript">import MyComponent from "~/components/MyComponent.js"; </code></pre> <p>Notice that there are several use cases where we would like to use asyncronous components. As explained by Alex Jover in <a href="https://alexjover.com/blog/lazy-load-in-vue-using-webpack-s-code-splitting/">this article</a>:</p> <ul> <li>In component importing</li> <li>In Vue Router, for components mapping</li> <li>In Vuex modules</li> </ul> <p>Let's take a look at the syntax and focus on the <code>import</code> part.</p> <p>If you are using Webpack (or <a href="https://parceljs.org/">Parcel</a>!), that syntax is going to be transformed on <em>compilation time</em> and these tools are going to use<code>Promise</code>s to load asynchronously your assets/modules/components.</p> <p>Why the need of an arrow function, you might be wondering: As Alex explained, we need to wrap the <code>import</code> with an arrow function to be resolved (remember, promises...) only when executed.</p> <p>To demonstrate that they are fully lazy loaded I've prepared a <a href="https://github.com/gangsthub/dynamic-imports-example">repository</a> (using Nuxt.js). It has 2 pages, each of them use different techniques (<strong>With</strong> and <strong>Without</strong> dynamic imports) to import 2 components (component "A" and component "B").</p> <p>We will see how, when loading the page with dynamic imports, webpack loads 2 separate files after the navigation. But, the page component itself (<code>/without</code>) using regular <code>import</code>s, is heavier because it loads everything at once.</p> <p><img src="/tips/dynamic-imports-in-vue-js-for-better-performance/network.png" alt=""></p> <p>Image showing network waterfall when navigating to both pages. And the differences between both techniques (with and without dynamic imports)</p> <p>Yes, by using this technique, Webpack will create separate files ("chunks") to load them when needed (lazily). Custom chunk naming can be done with <a href="https://webpack.js.org/api/module-methods/#magic-comments">Magic comments</a> but that will be the subject of another article 😉.</p> <p><img src="/tips/dynamic-imports-in-vue-js-for-better-performance/featured.png" alt=""></p> <p>Image showing the result of nuxt build. See how different chunks are created for components A and B when dynamic imports are used!</p> <h3>That's it!</h3> <p>For a deeper exploration of code splitting techniques check:</p> <ul> <li>De facto linked article by Anthony Gore: <a href="https://vuejsdevelopers.com/2017/07/03/vue-js-code-splitting-webpack/">https://vuejsdevelopers.com/2017/07/03/vue-js-code-splitting-webpack/</a></li> <li>Google's web fundamentals article by Addy Osmani and Jeremy Wagner about code splitting: <a href="https://developers.google.com/web/fundamentals/performance/optimizing-javascript/code-splitting/">https://developers.google.com/web/fundamentals/performance/optimizing-javascript/code-splitting/</a></li> <li>Webpack docs: <a href="https://webpack.js.org/guides/code-splitting/">https://webpack.js.org/guides/code-splitting/</a></li> </ul> <p><em>PS: For this example repo I have used webpack@4.29.6 and Nuxt@2.4.0 which uses Vue@2.5.22.</em></p> ]]></content:encoded> </item> <item> <title><![CDATA[Testing logic inside a Vue.js watcher]]></title> <link>https://vuedose.tips/testing-logic-inside-a-vue-js-watcher/</link> <guid>https://vuedose.tips/testing-logic-inside-a-vue-js-watcher/</guid> <pubDate>Sun, 14 Apr 2019 19:00:00 GMT</pubDate> <description><![CDATA[Tutorial on how to test the logic of a vue.js component watcher instead of testing the framework]]></description> <content:encoded><![CDATA[<p>Even though the most of the time we use computed properties to react to data changes, sometimes we have to use a watcher in order to perform an expensive operation or an asynchronous call to our API. Let's keep it simple for this example.</p> <p>Imagine that you have a component that emits an input event when an internal value changes:</p> <pre><code class="language-vue"><template> <input v-model="internalValue" /> </template> <script> export default { data() { return { internalValue: "" }; }, watch: { internalValue(value) { this.$emit("input", value); } } }; </script> </code></pre> <p>When its time to test this feature, the first thing that comes to our minds is that we have to mount the component, change the internalValue data and expect that the watcher has been fired.</p> <p>But you know what? That feature is already tested by Vue core members. Don't do it again. Evan You is pretty confident that a watcher is fired when its associated value changes.</p> <p>You can do a test like this instead:</p> <pre><code class="language-javascript">test("emits input event when interalValue changes", () => { const wrapper = shallowMount(YourComponent); wrapper.vm.$options.watch.internalValue.call(wrapper.vm, 15); expect(wrapper.emitted("input")[0][0]).toBe(15); }); </code></pre> <p>Vue attaches to the <code>$options.watch</code> object each watcher that we define in our component so what we are doing here is invoking the watcher directly using <code>call()</code>.</p> <p>First parameter of <code>call</code> is the context of <code>this</code> inside the function (the component instance). Then we can pass more parameters (the value).</p> <p>So wrapping up: "Don't test the framework"</p> <p>Sometimes is hard to identify the code that you own from the features that have already been tested thousands of times by the library you are using. I guarantee you that a watcher is going to be fired when the data is changed.</p> <p>Test the logic inside of it, don't waste your precious time trying to re-create the scenario and the conditions that would fire the watcher up.</p> ]]></content:encoded> </item> <item> <title><![CDATA[Handle and redirect 404 responses in Nuxt.js]]></title> <link>https://vuedose.tips/redirect-404-not-found-in-nuxt-js/</link> <guid>https://vuedose.tips/redirect-404-not-found-in-nuxt-js/</guid> <pubDate>Sun, 07 Apr 2019 14:00:00 GMT</pubDate> <description><![CDATA[Learn how to redirect a user to the home page in case it navigates to a page that doesn't exists]]></description> <content:encoded><![CDATA[<p>For me, Nuxt is one of the best pieces of software I've ever used. It makes me be so productive building web apps, no matter if they're SPA, Sever Side Rendered (SSR) apps, or static generated sites, following the JAM Stack trend.</p> <p><em>Pss, in fact <a href="https://vuedose.tips/">VueDose website</a> is built on Nuxt as a static site</em> 😉</p> <p>But still, a lot of times find very interesting tricks that are not documented... that needs to stop! Let's share the first hot Nuxt tip!</p> <p>If you're familiar with Nuxt.js, you should know the concept of <a href="https://nuxtjs.org/guide/views/#pages">pages</a>. You should also know that there is a special <a href="https://nuxtjs.org/guide/views#error-page">Error page</a> (well, it goes into the Layouts folder, but it's a page).</p> <p>You can override the default error page and customize it to your needs... but what if we want a different behaviour?</p> <p>In some cases, we might want that when a user visits a non existing page, we redirect him to the home page.</p> <p>Here's the trick: you can do that easily by creating this <code>pages/*.vue</code> component:</p> <pre><code class="language-vue"><!-- pages/*.vue --> <script> export default { asyncData ({ redirect }) { return redirect('/') } } </script> </code></pre> <p>In Nuxt, routes are defined by the file naming convention. So when we create a <code>*.vue</code> file, we're actually using a wildcard route on Vue Router.</p> <p>Then, we use the <code>redirect</code> method from the Nuxt context to perform the redirection, whether it's on the client or server.</p> <p>We do that on the <code>asyncData</code> method since we have the context there, but it would perfectly work with the <code>fetch</code> method as well:</p> <pre><code class="language-vue"><!-- pages/*.vue --> <script> export default { fetch({ redirect }) { return redirect("/"); } }; </script> </code></pre> <p>Go and try it out by typing any non-existent url. You should see how it is be redirected. It'd be helpful if you also share <a href="https://vuedose.tips">VueDose</a> with your colleagues, so they also know about these tips!</p> <p>See you next week.</p> ]]></content:encoded> </item> <item> <title><![CDATA[Run watchers when a Vue.js component is created]]></title> <link>https://vuedose.tips/run-watchers-when-a-vue-js-component-is-created/</link> <guid>https://vuedose.tips/run-watchers-when-a-vue-js-component-is-created/</guid> <pubDate>Sun, 31 Mar 2019 14:00:00 GMT</pubDate> <description><![CDATA[Tutorial on how to make a watcher run instantly when a Vue.js component is created]]></description> <content:encoded><![CDATA[<p>Although Vue.js provides us with computed properties which are very useful for most of the cases, in some situations you might need to use watchers.</p> <p>Watchers by default are run only when the value of the property that is watched changes, and that totally makes sense.</p> <p>Here's how you define a watcher for a dog property:</p> <pre><code class="language-js">export default { data: () => ({ dog: "" }), watch: { dog(newVal, oldVal) { console.log(`Dog changed: ${newVal}`); } } }; </code></pre> <p>So far so good. If you try that code, the dog watch function would be called as soon as its value changes.</p> <p>However, in some situations you need the watcher to be run as soon as the component is created. You could move the logic within to a method, and then call it from both the watcher and the <code>created</code> hook, but there is a simpler way.</p> <p>You can use the long-hand version of the watcher in order to pass the <code>immediate: true</code> option. That will make it run instantly on component's creation.</p> <pre><code class="language-js">export default { data: () => ({ dog: "" }), watch: { dog: { handler(newVal, oldVal) { console.log(`Dog changed: ${newVal}`); }, immediate: true } } }; </code></pre> <p>As you can see, in the long-hand way you need to set the watch callback in the <code>handler</code> key of the object.</p> <p>Do you want to see it in action? Check it out yourself in this <a href="https://codesandbox.io/s/rwxp7pnklo">CodeSandbox</a>!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Simple and performant functional Vue.js components]]></title> <link>https://vuedose.tips/simple-and-performant-functional-vue-js-components/</link> <guid>https://vuedose.tips/simple-and-performant-functional-vue-js-components/</guid> <pubDate>Sun, 24 Mar 2019 14:00:00 GMT</pubDate> <description><![CDATA[Functional Vue.js components]]></description> <content:encoded><![CDATA[<p>Sometimes we don't need complex components, and in some cases we don't even need them to have a state on its own. That can be the case when building UI components that don't have much logic in it.</p> <p>For cases like that, <strong>functional components</strong> can be very appropriated. They're stateless and instanceless, which means that it doesn't even have an instance on its own (so no way to call <code>this.$emit</code> and such).</p> <p>That makes them <strong>simple</strong> to reason about, <strong>faster</strong> and more <strong>lightweight</strong>.</p> <p>The question is, when can I use a functional component? Easy: when they depend <strong>only on props.</strong></p> <p>As an example, the following component:</p> <pre><code class="language-vue"><template> <div> <p v-for="item in items" @click="$emit('item-clicked', item)"> {{ item }} </p> </div> </template> <script> export default { props: ["items"] }; </script> </code></pre> <p>Could be written in a functional way as follows:</p> <pre><code class="language-vue"><template functional> <div> <p v-for="item in props.items" @click="props.itemClick(item);"> {{ item }} </p> </div> </template> </code></pre> <p>Pay attention to the things that changed:</p> <ul> <li>Write <code>functional</code> in the <code>template</code> tag</li> <li>Props are accessible via <code>props</code></li> <li>Since we don't have access to <code>$emit</code>, we can use a function as a prop. That's how the React community has done it the whole time and it worked pretty well.</li> <li>No need for the <code>script</code> part</li> </ul> <p>Do you want to see it in action? Check it out yourself in this <a href="https://codesandbox.io/s/rwxp7pnklo">CodeSandbox</a>!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Listen to lifecycle hooks on third-party Vue.js components]]></title> <link>https://vuedose.tips/listen-to-lifecycle-hooks-on-third-party-vue-js-components/</link> <guid>https://vuedose.tips/listen-to-lifecycle-hooks-on-third-party-vue-js-components/</guid> <pubDate>Sun, 17 Mar 2019 14:00:00 GMT</pubDate> <description><![CDATA[A quick tip on how to avoid repeating a double emit call and listen to lifecycle hooks externally]]></description> <content:encoded><![CDATA[<p>Here's a very useful tip I learnt once from my friend <a href="https://twitter.com/damiandulisz">Damian Dulisz</a>, the Vue.js core team member that created the official <a href="https://news.vuejs.org">Vue newsletter</a> and <a href="https://vue-multiselect.js.org">vue-multiselect</a>.</p> <p>In some cases I needed to know when a component has been created, mounted or updated from a parent component, specially when building components for vanilla JS libraries.</p> <p>You've probably already something like that in your own components, for example, by emitting an event in a lifecycle hook from the child component, like this:</p> <pre><code class="language-js">mounted() { this.$emit("mounted"); } </code></pre> <p>So then you can listen to it from the parent component like <code><Child @mounted="doSomething"/></code>.</p> <p>Let me tell you this: there is no need for that, and in fact you won't be able to do it on third party components.</p> <p>Instead, the solution is as simple as listening to an event with the lifecycle hook name, prefixed by <code>@hook:</code>.</p> <p>For instance, if you want to do something when the third-party component <a href="https://github.com/alexjoverm/v-runtime-template">v-runtime-template</a> renders, you can listen to it's <code>updated</code> lifecycle hook:</p> <pre><code class="language-vue"><v-runtime-template @hook:updated="doSomething" :template="template" /> </code></pre> <p>Still don't trust me? Check it yourself in this <a href="https://codesandbox.io/s/18r05pkmn7">CodeSandbox</a>!</p> ]]></content:encoded> </item> <item> <title><![CDATA[The power of Snapshot Testing in Vue.js]]></title> <link>https://vuedose.tips/the-power-of-snapshot-testing/</link> <guid>https://vuedose.tips/the-power-of-snapshot-testing/</guid> <pubDate>Sun, 10 Mar 2019 11:00:00 GMT</pubDate> <description><![CDATA[Learn why using Jest's snapshots testing for you Vue.js components can make testing much easier and quicker]]></description> <content:encoded><![CDATA[<p>If you are into testing, you probably have used <a href="https://jestjs.io/">Jest</a>: the all-in-one testing framework created by Facebook. Right now is the most popular tool out there and I've been using it since the first time I've tried it.</p> <p>To test Vue.js components, you also have <a href="https://vue-test-utils.vuejs.org/"><em>vue-test-utils</em></a> written by <a href="https://twitter.com/EddYerburgh">Edd Yerburg</a>, the official helper library that makes testing Vue.js apps easier.</p> <p>This is an example of tests using both Jest and vue-test-utils:</p> <pre><code class="language-js">it('has a message list of 3 elements', () => { const cmp = createCmp({ messages: ['msg-1', 'msg-2', 'msg-3'] }) expect(cmp.is('ul')).toBe(true) expect(cmp.find('.message-list').isEmpty()).toBe(false) expect(cmp.find('.message-list').length).toBe(3) }) it('has a message prop rendered as a data-message attribute', () => { const cmp = createCmp({ message: 'Cat' }) expect(cmp.contains('.message')).toBe(true) expect(cmp.find('.message').props('message').toBe('Cat') expect(cmp.find('.message').attributes('data-message').toBe('Cat') // Change the prop message cmp.setProps({ message: 'Dog' }) expect(cmp.find('.message').props('message').toBe('Dog') expect(cmp.find('.message').attributes('data-message').toBe('Dog') }) </code></pre> <p>As you can see, <em>vue-test-utils</em> gives you many methods you can use to check props, classes, content, search, etc. It gives you methods to change stuff, like <code>setProps</code>, which is pretty cool.</p> <p>This test has very specific assertions in the form of <em>"Find an element with a 'message' class and check if it has a 'data-message' set to 'Cat'"</em>.</p> <p>But what if I tell you that... you don't need to do that with Snapshot Testing?</p> <p>Basically, you can rewrite the previous tests by using snapshots in the following way:</p> <pre><code class="language-js">it("has a message list of 4 elements", () => { const cmp = createCmp({ messages: ["msg-1", "msg-2", "msg-3"] }); expect(cmp.element).toMatchSnapshot(); }); it("has a message prop rendered as a data-message attribute", () => { const cmp = createCmp({ message: "Cat" }); expect(cmp.element).toMatchSnapshot(); cmp.setProps({ message: "Dog" }); expect(cmp.element).toMatchSnapshot(); }); </code></pre> <p>The value of the tests will remain the same, or it can have even more since sometimes you find non-related regressions in snapshots.</p> <p>Notice that in this test I've just used <code>toMatchSnapshot</code> for the assertions, and that's all. That makes testing much easier and quicker since you don't need to check every specific thing.</p> <p>The dynamics of testing changed: instead of writing specific assertions, you set the component state in any way you need and take snapshots of it.</p> <p>Snapshots are meant to assert the <strong>rendering state</strong>: they describe how the component is rendered given a specific state, and in later runs the snapshots are compared to check its validity.</p> <p>Keep in mind that not always snapshot testing is what you need. It depends on what you need to test.</p> <p>I can't fit more in a tip, but if you want more information you have the book <em><a href="https://leanpub.com/testingvuejscomponentswithjest">Testing Vue.js Components with Jest</a></em> where I added last week a whole section on snapshot testing, including:</p> <ul> <li>Rethinking in Snapshots</li> <li>Why, how and when to use snapshots</li> <li>When not use them</li> <li>Code examples</li> </ul> <p>Please tell me what you think about it! You can find me <a href="https://twitter.com/alexjoverm">on twitter</a> 🙂</p> ]]></content:encoded> </item> <item> <title><![CDATA[Two less known facts about Vuex]]></title> <link>https://vuedose.tips/two-less-known-facts-about-vuex/</link> <guid>https://vuedose.tips/two-less-known-facts-about-vuex/</guid> <pubDate>Sun, 03 Mar 2019 11:00:00 GMT</pubDate> <description><![CDATA[A short tutorial on how to use watch and subscribeAction methods of Vuex in order to handle side effects, integrate external code and extend your store in Vue.js applications]]></description> <content:encoded><![CDATA[<p>When using Vuex in our Vue.js components we tend to forget the amazing API that it exposes beside the mapping functions.</p> <p>Let's see what we can do with it, but first let's create a basic store for our examples:</p> <pre><code class="language-javascript">const store = new Vuex.Store({ state: { count: 0 }, getters: { getCountPlusOne: state => state.count + 1 }, mutations: { increment(state) { state.count++; } } }); </code></pre> <h2>Watch</h2> <p>The watch method is the most useful to integrate Vuex with external code, be it in your <code>awesomeService</code> or in your <code>catchAllAuthUtils</code>.</p> <p>This is how to use it:</p> <pre><code class="language-javascript">const unsubscribe = store.watch( (state, getters) => { return [state.count, getters.getCountPlusOne]; }, watched => { console.log("Count is:", watched[0]); console.log("Count plus one is:", watched[1]); }, {} ); // To unsubscribe: unsubscribe(); </code></pre> <p>What we are doing is to call the <code>watch</code> method with two functions, one to return what part of the state and/or getters we want to <em>keep an eye on</em> and the other with the function that we want to invoke when <code>state.count</code> or <code>getCountPlusOne</code> change.</p> <p>This is extremely useful to integrate with react code or angular or even... JQuery!</p> <p>See the example in <a href="https://codesandbox.io/s/vm6r05qjq0">this CodeSandbox</a>.</p> <h2>SubscribeAction</h2> <p>Sometimes instead of watching a store property change it's more useful to react to a specific action, <code>login</code> and <code>logout</code> come in mind, vuex has us covered with <code>subscribeAction</code>.</p> <p>Calling subscribe adds a 'callback' that is run at every action and that we can use to call custom code.</p> <p>Let's use it to start and stop a global spinner before and after every action!</p> <pre><code class="language-javascript">const unsubscribe = store.subscribeAction({ before: (action, state) => { startBigLoadingSpinner(); }, after: (action, state) => { stoptBigLoadingSpinner(); } }); // To unsubscribe: unsubscribe(); </code></pre> ]]></content:encoded> </item> <item> <title><![CDATA[Create an ImageSelect component on top of vue-multiselect]]></title> <link>https://vuedose.tips/create-an-image-select-component-on-top-of-vue-multiselect/</link> <guid>https://vuedose.tips/create-an-image-select-component-on-top-of-vue-multiselect/</guid> <pubDate>Sun, 24 Feb 2019 17:00:00 GMT</pubDate> <description><![CDATA[Build an ImageSelect Vue.js component using the popular vue-multiselect package following the Adaptive Components pattern.]]></description> <content:encoded><![CDATA[<p><a href="https://vuedose.tips/tips/adaptive-components-using-v-bind-and-v-on">Two tips before</a> you learnt the concept of Adaptive Components and how you can build the base of it by proxifying props and events using <code>v-bind</code> and <code>v-on</code>.</p> <p>Now it's time to show it in action. By any chance, do you know about <a href="https://vue-multiselect.js.org/">vue-multiselect</a>? It's an amazing select component built by <a href="https://twitter.com/damiandulisz">Damian Dulisz</a>. vue-multiselect can be used in many different ways given how flexible and customisable it is. That's really how a fine third-party component should be in order to be reusable.</p> <p>Based on <a href="https://vue-multiselect.js.org/#sub-custom-option-template">this example</a> from its documentation, let's build an ImageSelect component. To do that, the example redefines some scoped slots that vue-multiselect exposes:</p> <pre><code class="language-vue"><multiselect v-model="value" :options="options"> <template slot="singleLabel" slot-scope="{ option }"> <img class="option__image" :src="option.img" alt="Sth" /> <span class="option__desc"> <span class="option__title">{{ option.title }}</span> </span> </template> <template slot="option" slot-scope="{ option }"> <img class="option__image" :src="option.img" alt="Sth" /> <span class="option__desc"> <span class="option__title">{{ option.title }}</span> <span class="option__small">{{ option.desc }}</span> </span> </template> </multiselect> </code></pre> <p>I'm not getting into scoped slots, just assume that code works in case you don't know about them. The thing here is that I want to build an <code>ImageSelect</code> component on top of that code.</p> <p>From the last tip, you probably know already that you need to use <code>v-bind="$props"</code> and <code>v-on="$listeners"</code> in order to make that proxifying of props and events happen.</p> <p>You also need to redeclare the props from the original vue-multiselect component, and you can take them from the MultiselectMixin of its source code:</p> <pre><code class="language-vue"><template> <multiselect v-bind="$props" v-on="$listeners"> <template slot="singleLabel" slot-scope="{ option }"> <img class="option__image" :src="option.img" alt="No Man’s Sky" /> <span class="option__desc"> <span class="option__title">{{ option.title }}</span> </span> </template> <template slot="option" slot-scope="{ option }"> <img class="option__image" :src="option.img" alt="No Man’s Sky" /> <span class="option__desc"> <span class="option__title">{{ option.title }}</span> <span class="option__small">{{ option.desc }}</span> </span> </template> </multiselect> </template> <script> import Multiselect from "vue-multiselect"; import MultiselectMixin from "vue-multiselect/src/multiselectMixin"; export default { components: { Multiselect }, props: MultiselectMixin.props }; </script> </code></pre> <p>Here's how you can use this <code>ImageSelect</code> component, passing by the minimal properties to make it work:</p> <pre><code class="language-vue"><template> <ImageSelect v-model="imageValue" :options="imageOptions" label="title" track-by="title" :show-labels="false" /> </template> <script> import ImageSelect from "./ImageSelect"; export default { components: { ImageSelect }, data: () => ({ imageValue: null, imageOptions: [ { title: "Random img", img: "https://picsum.photos/300/150" }, { title: "Cool image", img: "https://picsum.photos/300/151" } ] }) }; </script> </code></pre> <p>If you run this code you will notice that there is something not working properly. In particular the <code>show-labels</code> prop. The thing is that it's not a prop, but an attribute! And they're accessible through the <code>$attrs</code> component instance option.</p> <p>Basically we need to proxify not only the props, but also the attributes to make it work.</p> <p>To do that, I'm going to use a computed property to merge both <code>$props</code> and <code>$attrs</code> into the same object:</p> <pre><code class="language-vue"><template> <multiselect v-bind="allBindings" v-on="$listeners"> <!-- ... --> </multiselect> </template> <script> import Multiselect from "vue-multiselect"; import MultiselectMixin from "vue-multiselect/src/multiselectMixin"; export default { components: { Multiselect }, props: MultiselectMixin.props, computed: { allBindings() { // Need to proxify both props and attrs, for example for showLabels return { ...this.$props, ...this.$attrs }; } } }; </script> </code></pre> <p>You can try yourself and run the code on <a href="https://codesandbox.io/s/n71oq781r0">this Codesandbox example</a> I've prepared for you. You'll see it has some additional <em>Adaptive Components</em>, such as <code>SingleSelect</code> and <code>MultiSelect</code>.</p> <p><em>Pss: they have some CSS tricks we'll cover on the next tips</em></p> ]]></content:encoded> </item> <item> <title><![CDATA[Creating a Store without Vuex in Vue.js 2.6]]></title> <link>https://vuedose.tips/creating-a-store-without-vuex-in-vue-js-2-6/</link> <guid>https://vuedose.tips/creating-a-store-without-vuex-in-vue-js-2-6/</guid> <pubDate>Sun, 17 Feb 2019 22:30:00 GMT</pubDate> <description><![CDATA[Tip on creating a simple store in Vue.js 2.6 by using the new Observable API]]></description> <content:encoded><![CDATA[<p>Vue.js 2.6 introduced some new features, and one I really like is the new global <a href="https://vuejs.org/v2/api/#Vue-observable">observable API</a>.</p> <p>Now you can create reactive objects outside the Vue.js components scope. And, when you use them in the components, it will trigger render updates appropriately.</p> <p>In that way, you can create very simple stores without the need of Vuex, perfect for simple scenarios like those cases where you need to share some external state across components.</p> <p>For this tip example, you're going to build a simple count functionality where you externalise the state to our own store.</p> <p>First create <code>store.js</code>:</p> <pre><code class="language-javascript">import Vue from "vue"; export const store = Vue.observable({ count: 0 }); </code></pre> <p>If you feel comfortable with the idea of mutations and actions, you can use that pattern just by creating plain functions to update the data:</p> <pre><code class="language-javascript">import Vue from "vue"; export const store = Vue.observable({ count: 0 }); export const mutations = { setCount(count) { store.count = count; } }; </code></pre> <p>Now you just need to use it in a component. To access the state, just like in Vuex, we'll use computed properties, and methods for the mutations:</p> <pre><code class="language-vue"><template> <div> <p>Count: {{ count }}</p> <button @click="setCount(count + 1);">+ 1</button> <button @click="setCount(count - 1);">- 1</button> </div> </template> <script> import { store, mutations } from "./store"; export default { computed: { count() { return store.count; } }, methods: { setCount: mutations.setCount } }; </script> </code></pre> <p>If you want to try this example yourself, I've compiled for you in <a href="https://codesandbox.io/s/k3kpqz2wz7">this CodeSandbox</a>, go check it out!</p> <p>Remember you can read this tip <a href="https://vuedose.tips/tips/creating-a-store-without-vuex-in-vue-js-2-6">online</a> (with copy/pasteable code), and don't forget to share <a href="https://vuedose.tips">VueDose</a> with your colleagues, so they also know about these tips as well!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Adaptive components using v-bind and v-on]]></title> <link>https://vuedose.tips/adaptive-components-using-v-bind-and-v-on/</link> <guid>https://vuedose.tips/adaptive-components-using-v-bind-and-v-on/</guid> <pubDate>Sun, 10 Feb 2019 22:00:00 GMT</pubDate> <description><![CDATA[Learn to use v-bind and v-on in order to proxify props and events for building a wrapper component in Vue.js.]]></description> <content:encoded><![CDATA[<p>When you're working with component-based technologies, as soon as an application starts to grow, you need to structure and categorise your components in order to keep them as simple and maintainable as possible.</p> <p>Component composition it's a powerful pattern popular that helps reusing and structuring code in component-based technologies... But what the heck is component composition? Although being a generic concept, let's say that when you do component composition you're creating a component by the combination of one or more.</p> <p>One case could be when we have a base component, say <code>BaseList</code>, and you want to create a similar component with some additional functionality on top of it, like <code>SortableList</code>. I call them <strong>Adaptive Components</strong> (also proxy or wrapper components).</p> <p>When building an Adaptive Component, you usually want <code>SortableList</code> to keep the same API that the original <code>BaseList</code> in order to keep the components consistent. Which means that from <code>SortableList</code> you need to pass down all the props and listen to all events of <code>BaseList</code>.</p> <p>Here's the trick: you can do that by using <code>v-bind</code> and <code>v-on</code>:</p> <pre><code class="language-vue"><!-- SortableList --> <template> <AppList v-bind="$props" v-on="$listeners"> <!-- ... --> </AppList> </template> <script> import AppList from "./AppList"; export default { props: AppList.props, components: { AppList } }; </script> </code></pre> <p>The way <code>v-bind</code> works is basically the same than passing one by one all the properties to <code>AppList</code>, but instead passed all at once in an object. <code>$props</code> is the object in the component instance that contains all the properties of that component.</p> <p>As you can imagine, <code>v-on="$listeners"</code> works exactly the same way, but for events.</p> <p>This would work for two-way bound components using <code>v-model</code> as well. In case you didn't know, <code>v-model</code> is a shorthand for passing a <code>value</code> property and listening to a <code>input</code> event.</p> <p>Finally, remember that in Vue.js we have to explicitly declare the props of a component in order to be interpreted as such. A quick way to do that when building an Adaptive Component is to use the base component props to define them, like I do in <code>props: AppList.props</code>.</p> <p>That's it! Maybe you didn't see the practical use of the <em>Adaptive Components</em>? No worries, in the next tip I'll build a real case of an Adaptive Component so you can see it in action. Stay tuned!</p> ]]></content:encoded> </item> <item> <title><![CDATA[How to use the new v-slot directive in Vue.js]]></title> <link>https://vuedose.tips/new-v-slot-directive-in-vue-js-2-6-0/</link> <guid>https://vuedose.tips/new-v-slot-directive-in-vue-js-2-6-0/</guid> <pubDate>Sun, 03 Feb 2019 20:00:00 GMT</pubDate> <description><![CDATA[Simplify the use of scoped slots with the new v-slot syntax introduced in Vue.js 2.6.0 beta 3]]></description> <content:encoded><![CDATA[<p>I'm so excited to see how's people liking VueDose! I received amazing feedback about the performance tips published so far. I'm so grateful about the support and the compliments 🤗.</p> <p>Last week the version 2.6.0-beta.3 of Vue.js was released, enabling a new feature to simplify scoped slots even more.</p> <p>It introduces the new <code>v-slot</code> directive and its shorthand, as described in the <a href="https://github.com/vuejs/rfcs/blob/master/active-rfcs/0001-new-slot-syntax.md">RFC-0001</a> and <a href="https://github.com/vuejs/rfcs/blob/master/active-rfcs/0002-slot-syntax-shorthand.md">RFC-0002</a>.</p> <p>In order to understand how it does simplify the syntax, let's see how's it in current scoped slots. Imagine you have a <code>List</code> the component that exposes a filtered list as its scope.</p> <p>The way you'd use that List scoped slot would be similar to:</p> <pre><code class="language-vue"><template> <List :items="items"> <template slot-scope="{ filteredItems }"> <p v-for="item in filteredItems" :key="item">{{ item }}</p> </template> </List> </template> </code></pre> <p>The implementation of the List component is not too relevant, but in <a href="https://codesandbox.io/s/wwzx6zw47w">this Codesandbox</a> you can check an example of it.</p> <p>With <code>v-slot</code>, you can write the scope of that slot directly on the component tag, avoiding an extra layer:</p> <pre><code class="language-vue"><template> <List v-slot="{ filteredItems }" :items="items"> <p v-for="item in filteredItems" :key="item">{{ item }}</p> </List> </template> </code></pre> <p><em>Keep in mind <code>v-slot</code> can only be used on components and <code>template</code> tags, but not in plain HTML tags</em></p> <p>This makes the code more readable specially when you have nested scoped slots, which can be difficult to reason where a scope comes from.</p> <p>The <code>v-slot</code> directive also introduces a way to combine the <code>slot</code> and the <code>scoped-slots</code> directive, but by separating them with a colon <code>:</code>.</p> <p>For example, this example taken from <a href="https://github.com/posva/vue-promised">vue-promised</a>:</p> <pre><code class="language-vue"><template> <Promised :promise="usersPromise"> <p slot="pending">Loading...</p> <ul slot-scope="users"> <li v-for="user in users">{{ user.name }}</li> </ul> <p slot="rejected" slot-scope="error">Error: {{ error.message }}</p> </Promised> </template> </code></pre> <p>Could be written with <code>v-slot</code> as follows:</p> <pre><code class="language-vue"><template> <Promised :promise="usersPromise"> <template v-slot:pending> <p>Loading...</p> </template> <template v-slot="users"> <ul> <li v-for="user in users">{{ user.name }}</li> </ul> </template> <template v-slot:rejected="error"> <p>Error: {{ error.message }}</p> </template> </Promised> </template> </code></pre> <p>And to end up, <code>v-slot</code> has the symbol <code>#</code> as a shorthand, so the example before could be written as:</p> <pre><code class="language-vue"><template> <Promised :promise="usersPromise"> <template #pending> <p>Loading...</p> </template> <template #default="users"> <ul> <li v-for="user in users">{{ user.name }}</li> </ul> </template> <template #rejected="error"> <p>Error: {{ error.message }}</p> </template> </Promised> </template> </code></pre> <p>Just keep in mind that the shorthand for the default <code>v-slot</code> is <code>#default</code>.</p> <p>Are you excited about this new slot syntax?</p> ]]></content:encoded> </item> <item> <title><![CDATA[Remove unused CSS with PurgeCSS]]></title> <link>https://vuedose.tips/remove-unused-css-with-purge-css/</link> <guid>https://vuedose.tips/remove-unused-css-with-purge-css/</guid> <pubDate>Sun, 27 Jan 2019 22:00:00 GMT</pubDate> <description><![CDATA[Use PurgeCSS to remove dead CSS that is not used, reduce the size of the generated CSS and improve performance of a Vue.js application]]></description> <content:encoded><![CDATA[<p>We can tackle web performance in different ways, and one of them is removing all JS and CSS we don't use in our apps.</p> <p>In the case of CSS, it's even more important when we use frameworks such as Bootstrap, Bulma or Tailwind, as well as when we're facing large or legacy applications.</p> <p>PurgeCSS is a tool that removes unused CSS by applying string comparison. That has some advantages, but also some caveats, so please keep attention to the white-listing part later.</p> <p>As an example, <a href="https://vuedose.tips">VueDose's website</a> is built on Nuxt (static generated) using Tailwind, and it uses PurgeCSS to optimize the generated CSS.</p> <p>If I disable PurgeCSS, you can see that the tailwind css is <strong>485 KB</strong>:</p> <p><img src="/tips/remove-unused-css-with-purge-css/WithoutPurgeCSS-0156b9ea-8b09-40c5-b9c4-70a272b388ea.png" alt=""></p> <p>While if I activate it, it <strong>goes down to 16 KB</strong>:</p> <p><img src="/tips/remove-unused-css-with-purge-css/WithPurgeCSS-a335711a-4251-4915-b814-73ab9b1f61c0.png" alt=""></p> <p>PurgeCSS configuration can be different in each project. It can be set as a webpack plugin or a postcss plugin.</p> <p>In the case of VueDose, I'm using it as a postcss plugin. So I have a <code>postcss.config.js</code> file with this content:</p> <pre><code class="language-javascript">const purgecss = require("@fullhuman/postcss-purgecss"); const plugins = [...]; if (process.env.NODE_ENV === "production") { plugins.push( purgecss({ content: [ "./layouts/**/*.vue", "./components/**/*.vue", "./pages/**/*.vue" ], whitelist: ["html", "body"], whitelistPatternsChildren: [/^token/, /^pre/, /^code/], }) ); } module.exports = { plugins }; </code></pre> <p>Basically, all you need is to tell where to look for the matching classes using the <code>content</code> property.</p> <p>Also, you'd like to <em>whitelist</em> some classes or tags to don't be removed. You'll need to do that at least in <code>html</code> and <code>body</code>, as well as <strong>any dynamic classes.</strong></p> <p>In my case, I use <a href="https://prismjs.com/">prismjs</a> in order to highlight the code blocks, and it adds several <code>token</code> classes, as well as styles in the <code>pre</code> and <code>code</code> tags. In order to exclude them, I need to use the <code>whitelistPatternsChildren</code> property.</p> <p>Tailwind, additionally, needs a <a href="https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css-with-purgecss">custom extractor</a> in order to work properly with PurgeCSS. All in all, the whole <code>postcss.config.js</code> file for VueDose has the following content:</p> <pre><code class="language-javascript">const join = require("path").join; const tailwindJS = join(__dirname, "tailwind.js"); const purgecss = require("@fullhuman/postcss-purgecss"); class TailwindExtractor { static extract(content) { return content.match(/[A-Za-z0-9-_:\/]+/g) || []; } } const plugins = [require("tailwindcss")(tailwindJS), require("autoprefixer")]; if (process.env.NODE_ENV === "production") { plugins.push( purgecss({ content: [ "./layouts/**/*.vue", "./components/**/*.vue", "./pages/**/*.vue" ], whitelist: ["html", "body"], whitelistPatternsChildren: [/^token/, /^pre/, /^code/], extractors: [ { extractor: TailwindExtractor, extensions: ["html", "vue"] } ] }) ); } module.exports = { plugins }; </code></pre> <p>That's it for today!</p> ]]></content:encoded> </item> <item> <title><![CDATA[Measure runtime performance in Vue.js apps]]></title> <link>https://vuedose.tips/measure-runtime-performance-in-vue-js-apps/</link> <guid>https://vuedose.tips/measure-runtime-performance-in-vue-js-apps/</guid> <pubDate>Sun, 20 Jan 2019 22:00:00 GMT</pubDate> <description><![CDATA[Short tutorial on how to measure web performance in Vue.js applications]]></description> <content:encoded><![CDATA[<p>In the <a href="https://vuedose.tips/tips/1">last tip</a> we talked about how to improve performance in large lists. But still we haven't measure how much it really improved.</p> <p>We can do so by using the Performance tab in Chrome DevTools. But in order to have accurate data, we must <strong>activate performance mode</strong> on our Vue app.</p> <p>We can do that by setting the global, in our <code>main.js</code> file or in a plugin in the case of Nuxt:</p> <pre><code class="language-javascript">Vue.config.performance = true; </code></pre> <p>Or if you have your <code>NODE_ENV</code> env variable set correctly, you could use it to set it in non-production environments:</p> <pre><code class="language-javascript">const isDev = process.env.NODE_ENV !== "production"; Vue.config.performance = isDev; </code></pre> <p>That will activate the <a href="https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API">User Timing API</a> that Vue uses internally to mark the components performance.</p> <p>From the last tip, I've created <a href="https://0ql846q66w.codesandbox.io/">this codesandbox</a>. Open it and hit the reload button from the performance tab on Chrome DevTools:</p> <p><img src="/tips/measure-runtime-performance-in-vue-js-apps/PerformanceTab-171255c6-d49e-4b76-b20d-138e5f5e2b58.png" alt=""></p> <p>That will record the page load performance. And thanks to the <code>Vue.config.performance</code> setting that you can see set on <code>main.js</code> you'll be able to see a <em>User Timing</em> section on the profiling:</p> <p><img src="/tips/measure-runtime-performance-in-vue-js-apps/featured.png" alt=""></p> <p>In there, you'll find 3 metrics:</p> <ul> <li><strong>Init</strong>: time it takes to create the component instance</li> <li><strong>Render</strong>: time to create the VDom structure</li> <li><strong>Patch</strong>: time to apply the VDom structure to the real DOM</li> </ul> <p>Back to the curiosity, the results of the previous tip are the following: the normal component <strong>takes 417ms to initalize:</strong></p> <p><img src="/tips/measure-runtime-performance-in-vue-js-apps/Reactive-65ff8639-2b5e-4275-ad75-855ea2e78988.png" alt=""></p> <p>While the non-reactive one using <code>Object.freeze</code> <strong>takes 3.9ms</strong>:</p> <p><img src="/tips/measure-runtime-performance-in-vue-js-apps/NoReactive-c4f316ac-11df-40b6-8a97-d1127b4cea43.png" alt=""></p> <p>Of course this can vary a little from run to run, but still the difference must be quite huge. Since the reactivity issue happens when the component is created, you'll see that difference in the <em>init</em> part of the <em>Reactive</em> and <em>NoReactive</em> components.</p> <p>That's it!</p> <p>Remember you can read this <a href="https://vuedose.tips/tips/measure-runtime-performance-in-vue-js-apps">tip online</a> (with copy/pasteable code) and please share <a href="https://vuedose.tips/">VueDose</a> with all your colleagues if you liked it!</p> <p>See you next week.</p> ]]></content:encoded> </item> <item> <title><![CDATA[Improve performance on large lists in Vue.js]]></title> <link>https://vuedose.tips/improve-performance-on-large-lists-in-vue-js/</link> <guid>https://vuedose.tips/improve-performance-on-large-lists-in-vue-js/</guid> <pubDate>Sun, 13 Jan 2019 21:00:00 GMT</pubDate> <description><![CDATA[Learn how to use Object freeze to improve performance on list rendering in Vue.js by creating a non-reactive array.]]></description> <content:encoded><![CDATA[<p>Hi there! And welcome to the very first VueDose tip! I'm really stocked to start this adventure on VueDose and help devs like you learn some badass tricks.</p> <p>VueDose's tips are going to be very concise, and that's the format I believe people find more useful. So let's go straight to the point.</p> <p>Usually we have the need of fetching a list of objects, say users, items, articles, whatever...</p> <p>And sometimes, we don't even need to modify them, just to display them or having them in our global state (a.k.a. Vuex). A simple code for fetching that list would be something like:</p> <pre><code class="language-javascript">export default { data: () => ({ users: {} }), async created() { const users = await axios.get("/api/users"); this.users = users; } }; </code></pre> <p>Vue by default makes reactive every first-level property for each object in the array.</p> <p>That can be expensive for large arrays of objects. Yeah, sometimes those lists are paginated, but others you just might have that list in the frontend.</p> <p>That's usually the case with Google Maps markers, which in fact are huge objects.</p> <p>So, in these cases, we can gain some performance if we prevent Vue from making reactive that list. And we can do that by using <code>Object.freeze</code> to the list before adding it to the component:</p> <pre><code class="language-javascript">export default { data: () => ({ users: {} }), async created() { const users = await axios.get("/api/users"); this.users = Object.freeze(users); } }; </code></pre> <p>Keep in mind that the same applies to Vuex:</p> <pre><code class="language-javascript">const mutations = { setUsers(state, users) { state.users = Object.freeze(users); } }; </code></pre> <p>By the way, if you need to modify the array, you can still do it by creating a new array. For instance, in order to add an item, you can do it in this way:</p> <pre><code class="language-javascript">state.users = Object.freeze([...state.users, user]); </code></pre> <p>Are you wondering how much is the <strong>performance improvement?</strong> We'll see it in the next tip, so stay tuned!</p> <p>That's it for today! I hope you like this first tip 😛.</p> <p>Remember you can read this <a href="https://vuedose.tips/tips/improve-performance-on-large-lists-in-vue-js">tip online</a> (with copy/pasteable code) and please share <a href="https://vuedose.tips/">VueDose</a> with all your colleagues if you liked it!</p> ]]></content:encoded> </item> <item> <title><![CDATA[How we built our blog as a full-static site with Nuxt, Storyblok and TailwindCSS]]></title> <link>https://vuedose.tips/guides/how-we-built-our-blog-as-a-full-static-site-with-nuxt-storyblok-and-tailwindcss/</link> <guid>https://vuedose.tips/guides/how-we-built-our-blog-as-a-full-static-site-with-nuxt-storyblok-and-tailwindcss/</guid> <pubDate>Tue, 25 Aug 2020 15:25:16 GMT</pubDate> <description><![CDATA[Read the story about how we built VueDose 2.0: a production-ready and finely thought out website from design to development.]]></description> <content:encoded><![CDATA[<p>This is not just a guide for learning how to create a blog with modern technologies such as Nuxt, Storyblok and TailwindCSS. It's also not another tutorial where the author creates a random pet project to show concepts out of the blue.</p> <p>This is a story. The story that tells <strong>how we built VueDose 2.0,</strong> this very site you're reading on. In other words, a production-ready and finely thought out website from design to development.</p> <p>You'll go through this story yourself by creating step by step <em>NarutoDose</em>: a smaller and funnier version of VueDose but themed with Naruto characters. You'll learn to:</p> <ul> <li>Use Nuxt in it's brand new full-static mode for best SEO and max performance</li> <li>Organize UI components in Vue based on a Design System</li> <li>Structure and work with the blog articles, authors and data using Storyblok</li> <li>And much more!</li> </ul> <p><em>Psst: this story wouldn't have been written without the help of <a href="https://www.storyblok.com/developers?utm_source=newsletter&utm_medium=logo&utm_campaign=vuedose">Storyblok</a>, a beloved VueDose sponsor</em> 💚</p> ]]></content:encoded> </item> </channel> </rss>