I need to set up global head in Nuxt for my app, which some subpages will overwrite. Those global head needs to contain translated data.
I created seoHead.js file with code:
import Vue from "vue";
export const $t = (sign) => Vue.prototype.$nuxt.$options.i18n.t(sign);
export default {
title: $t("seoGlobal.title"),
meta: [
{ charset: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{
hid: "description",
name: "description",
content: $t("seoGlobal.description"),
},
{
hid: "ogSiteName",
name: "og:site_name",
content: "Test Page",
},
{
hid: "ogTitle",
name: "og:title",
content: $t("seoGlobal.ogTitle"),
},
(...)
],
};
I import and use this data in my index.vue and other pages like this:
import seoHead from "~/constants/seoHead";
export default {
head() {
const metaI18n = this.$nuxtI18nSeo();
const currentPath = process.env.LP_URL + this.$router.currentRoute.fullPath;
return {
...seoHead,
meta: [
{
hid: "ogLocale",
name: "og:locale",
content: metaI18n.meta[0].content,
},
{
hid: "ogLocaleAlternate",
name: "og:locale:alternate",
content: metaI18n.meta[1].content,
},
{
hid: "ogUrl",
name: "og:url",
content: currentPath,
},
],
};
},
(...)
Unfortunately, I am facing Cannot read property '$options' of undefined error. It's strange for me, because I already used export const $t = (sign) => Vue.prototype.$nuxt.$options.i18n.t(sign); code in another js file. Anyone know why this error appears? You know the best way to translate global head options?
As discussed in the comments, there seems to be a timing issue with the Nuxt lifecycle and your component: at the time your component seoHead.js is imported, Nuxt has not yet injected its $nuxt object into Vue. So an easy workaround would be to delay the execution of your $t function (which accesses $nuxt):
Change your component to export a function which returns the object, instead of directly exporting the object:
export default function() {
return {
title: $t("seoGlobal.title"),
// ...
}
}
In index.vue, change your head function to call seoHead when spreading it:
return {
...seoHead(),
// ...
This way, the code which accesses $nuxt will be executed later -- not when seoHead is imported, but only when the head function is executed. At this time, the Nuxt lifecycle hopefully has finished its startup work and the required object is in place.
As I said, this is merely a workaround; if you would be calling head immediately in index.vue, the same error would appear. So unless you find out a proper way to integrate into the Nuxt lifecycle, I suggest to also put a safeguard into your translation function:
const $t = (sign) => Vue.prototype.$nuxt
? Vue.prototype.$nuxt.$options.i18n.t(sign)
: sign
This will return the i18n key if the required infrastructure is not yet in place. Not great, but better than an exception ;)
Alternatively you might be able to directly import your i18n functionality, without going through Nuxt at all; this way you wouldn't have any dependency on the infrastructure at all -- much better.
I think what you basically need here is a mixin.
export default {
title: $t("seoGlobal.title"),
meta: this.computedMeta,
computed:{
computedMeta(){
return [....] // this contains the array of objects in meta
}
}
methods:{
yourMethod(sign){
return this.$nuxt.$options.i18n.t(sign);
}
}
};
then just import it as a mixin in whatever file you need.
Some colleagues of mine have began a fairly complex web application using Vue.js. They would like to be able to use some of the widgets I've made from scratch in the past using JQuery, as re-implementing them would require a large amount of effort/time.
I know that it's possible to safely use JQuery with Vue.js if you are careful, but the information I've been able to find seems relegated to fairly vague blog posts, and my colleagues have notified me that they are struggling to figure out how to do it. So I am considering the possibility that I could find a way that I can nicely wrap my widgets into a portable cross framework library (for starters that can be used in Vue.js). For example, similar to how people create bindings that provide across language APIs. Ideally, it should make it very easy for someone to use it with Vue.js, and should take away the danger of potential pitfalls. Is there any problem with doing this, and is there any existing work that can be leveraged, or idiomatic way that people do this?
For added context, currently, the widget has an interface that includes a constructor (in which you pass the id of a parent DOM element that it will be appended to), a configure function, and it also emits several signals/events when it changes (although, those could be replaced by a function that checks it's state periodically).
As far as creating a portable and cross-framework library is concerned, I would think of jQuery as simply a dependency that allows you create certain elements and perform certain tasks, which you would intercept and/or modify according to the target framework's requirements. So, you are essentially creating a wrapper component around it, as the top 3 JavaScript frameworks (React, Vue, Angular) today are component-based.
One of the key differences (simply put) is: Reactivity system vs. DOM manipulation.
Now, talking about porting a jQuery plugin to Vue — I'm no expert in both libraries but coming from jQuery myself, I'd say it could be as easy as keeping a reference to a widget/plugin instance on a Vue component internal data and/or props and having it optionally expose the corresponding methods. The reason for the methods exposure part being optional is the same reason that characterizes one library from the other—Vue being more versatile as it scales between both a library and a framework.
In jQuery, you would create an instance of an object and pass it around for its public methods usages; whereas in Vue, you don't explicitly create instances except for the root one (you could, but you typically won't have to)—because the component itself is the (internally constructed) instance. And it is the responsibility of a component to maintain its states and data; the sibling and/or parent components will typically have no direct access to them.
Vue and jQuery are similar in that they both support state/data synchronization. With jQuery, it's obvious since all references are in the global scope; with Vue, one would use either v-model or the .sync modifier (replaced with arguments on v-model in Vue 3). Additionally, they also have event subscription with slightly different approaches.
Let's take the jQuery Autocomplete widget and add some Vue support to it. We'll be focusing on 3 things (Options, Events and Methods) and take 3 of their respective items as an example and comparison. I cannot cover everything here, but this should give you some basic ideas.
Setting up: jQuery
For the sake of complying with your specification in question, let's assume this widget/plugin is a new-able class in the window scope.
In jQuery, you would write the following (on document ready or wrapped in IIFE before the closing <body> tag):
var autocomplete = new Autocomplete({
source: [
'vue',
'react',
'angular',
'jquery'
],
appendTo: '#autocomplete-container',
disabled: false,
change: function(event, ui) { },
focus: function(event, ui) { },
select: function(event, ui) { }
});
// And then some other place needing manual triggers on this instance
autocomplete.close();
var isDisabled = autocomplete.option('disabled');
autocomplete.search('ue'); // Matches 'vue' and 'jquery' ;)
With the target element pre-defined or dynamically created somewhere in the parent scope:
<input type="search" class="my-autocomplete" />
Porting to Vue
Since you didn't mention any specific version of Vue in use, I'm going to assume the Macross (latest stable version: 2.6.12, ATTOW) with ES module; otherwise, try the ES modules compatible build.
And for this particular use case in Vue, we want to instantiate this plugin in the mounted hook, because this is where our target element will have been created and available to literally build upon. Learn more on the Lifecycle Hooks in a diagram here.
Creating component: Autocomplete.vue
<template>
<!--
Notice how this `input` element is added right here rather than we requiring
the parent component to add one, because it's now part of the component. :)
-->
<input type="search" class="my-autocomplete" />
</template>
<script>
export default {
// Basically, this is where you define IMMUTABLE "options", so to speak.
props: {
source: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
}
},
// And this is where to prepare and/or specify the internal options of a component.
data: () => ({
instance: null
}),
mounted() {
// `this` here refers to the local Vue instance
this.instance = new Autocomplete({
source: this.source,
disabled: this.disabled,
appendTo: this.$el // Refers to the `input` element on the template,
change: (event, ui) => {
// You can optionally pass anything in the second argument
this.$emit('change', this.instance);
},
focus: (event, ui) => {
this.$emit('focus', this.instance, event);
},
select: (event, ui) => {
this.$emit('select', this, event, ui);
}
});
},
methods: {
close() {
this.instance.autocomplete('close');
},
getOption(optionName) {
return this.instance.autocomplete('option', optionName);
},
search(keyword) {
this.instance.autocomplete('search', keyword);
}
}
}
</script>
Using the component: Parent.vue (or whatever)
<template>
<div class="parent">
<autocomplete
ref="autocomplete"
:source="items"
:disabled="disabled"
#change="onChange"
#focus="onFocus"
#select="onSelect">
</autocomplete>
</div>
</template>
<script>
import Autocomplete from 'path/to/your-components/Autocomplete.vue';
export default {
data: () => ({
items: [
'vue',
'react',
'angular',
'jquery'
],
disabled: false
}),
methods: {
onChange() {
},
onFocus() {
},
onSelect() {
}
},
mounted() {
// Manually invoke a public method as soon as the component is ready
this.$refs.autocomplete.search('ue');
},
components: {
Autocomplete
}
}
</script>
And we're not there just yet! I purposefully left out the "two-way binding" portion of the above example for us to take a closer look at now. However, this step is optional and should only be done if you need to synchronize data/state between the components (parent ↔ child), for example: You have some logic on the component that sets the input's border color to red when certain values get entered. Now, since you are modifying the parent state (say invalid or error) bound to this component as a prop, you need inform them of its changes by $emit-ting the new value.
So, let's make the following changes (on the same Autocomplete.vue component, with everything else omitted for brevity):
{
model: {
prop: 'source',
event: 'modified' // Custom event name
},
async created() {
// An example of fetching remote data and updating the `source` property.
const newSource = await axios.post('api/fetch-data').then(res => res.data);
// Once fetched, update the jQuery-wrapped autocomplete
this.instance.autocomplete('option', 'source', newSource);
// and tell the parent that it has changed
this.$emit('modified', newSource);
},
watch: {
source(newData, oldData) {
this.instance.autocomplete('option', 'source', newData);
}
}
}
We're basically watch-ing "eagerly" for data changes. If preferred, you could do it lazily with the $watch instance method.
Required changes on the parent side:
<template>
<div class="parent">
<autocomplete
ref="autocomplete"
v-model="items"
:disabled="disabled"
#change="onChange"
#focus="onFocus"
#select="onSelect">
</autocomplete>
</div>
</template>
That's going to enable the aforementioned two-way binding. You could do the same with the rest of the props that you need be "reactive", like the disabled prop in this example—only this time you would use .sync modifier; because in Vue 2, multiple v-model isn't supported. (If you haven't got too far though, I'd suggest going for Vue 3 all the way 🙂).
Finally, there are some caveats and common gotchas that you might want to look out for:
Since Vue performs DOM updates asynchronously, it could be processing something that won't take effect until the next event loop "tick", read more on Async Update Queue.
Due to limitations in JavaScript, there are types of changes that Vue cannot detect. However, there are ways to circumvent them to preserve reactivity.
The this object being undefined, null or in unexpected instance when referenced within a nested method or external function. Go to the docs and search for "arrow function" for complete explanation and how to avoid running into this issue.
And we've created ourselves a Vue-ported version of jQuery Autocomplete! And again, those are just some basic ideas to get you started.
Live Demo
const Autocomplete = Vue.extend({
template: `
<div class="autocomplete-wrapper">
<p>{{label}}</p>
<input type="search" class="my-autocomplete" />
</div>
`,
props: {
source: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
},
label: {
type: String
}
},
model: {
prop: 'source',
event: 'modified'
},
data: () => ({
instance: null
}),
mounted() {
const el = this.$el.querySelector('input.my-autocomplete');
this.instance = $(el).autocomplete({
source: this.source,
disabled: this.disabled,
change: (event, ui) => {
// You can optionally pass anything in the second argument
this.$emit('change', this.instance);
},
focus: (event, ui) => {
this.$emit('focus', this.instance, event);
},
select: (event, ui) => {
this.$emit('select', this, event, ui);
}
});
},
methods: {
close() {
this.instance.autocomplete('close');
},
getOption(optionName) {
return this.instance.autocomplete('option', optionName);
},
search(keyword) {
this.instance.autocomplete('search', keyword);
},
disable(toState) {
this.instance.autocomplete('option', 'disabled', toState);
}
},
watch: {
source(newData, oldData) {
this.instance.autocomplete('option', 'source', newData);
},
disabled(newState, oldState) {
this.disable(newState);
}
}
});
new Vue({
el: '#app',
data: () => ({
items: [
'vue',
'react',
'angular',
'jquery'
],
disabled: false
}),
computed: {
computedItems: {
get() {
return this.items.join(', ');
},
set(val) {
this.items = val.split(', ')
}
}
},
methods: {
onChange() {
// Do something
},
onFocus() {},
onSelect(instance, event, ui) {
console.log(`You selected: "${ui.item.value}"`);
}
},
components: {
Autocomplete
}
})
#app {
display: flex;
justify-content: space-between;
}
#app > div {
flex: 0 0 50%;
}
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" />
<link rel="stylesheet" href="/resources/demos/style.css" />
<script src="https://vuejs.org/js/vue.min.js"></script>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<div id="app">
<autocomplete
v-model="items"
:disabled="disabled"
label='Type something (e.g. "ue")'
#change="onChange"
#focus="onFocus"
#select="onSelect">
</autocomplete>
<div>
<p>Edit this comma-separated list of items and see them reflected on the component</p>
<textarea
v-model.lazy="computedItems"
cols="30"
rows="3">
</textarea>
</div>
</div>
P.S. If these widgets are actually in the global window scope and you are using ESLint, you'll want to ensure they are specified as global variables; otherwise, the no-undef rule will warn on variables that are accessed but not defined within the same file. See this post for the solution.
P.P.S. If you need to ship them as a plugin, see: Writing a Plugin (don't worry, there won't be much extra work required).
I've got a problem, I'm trying to implement Stripe on my nuxt/typescript projet, I use vue-property-decorator as well.
Here's my head in nuxt.config.js
head: {
title: process.env.npm_package_name || '',
meta: [
{charset: 'utf-8'},
{name: 'viewport', content: 'width=device-width, initial-scale=1'},
{hid: 'description', name: 'description', content: process.env.npm_package_description || ''}
],
script: [{
src: "https://js.stripe.com/v3/",
type: "text/javascript"}],
link: [
{rel: 'icon', type: 'image/x-icon', href: '/favicon.ico'}
]
},
My component:
<script lang="ts">
import {Component, Prop, Vue} from "vue-property-decorator";
import {Subscription} from "~/models/Subscription";
#Component({
components: {}
})
export default class PaymentForm extends Vue {
#Prop()
public subscription: Subscription;
mounted() {
let stripe = Stripe(`MY PUBLIC KEY`),
elements = stripe.elements(),
card = undefined;
card = elements.create("card");
card.mount(this.$refs.card);
}
}
</script>
But I got this error
Cannot find name 'Stripe'. Did you mean 'stripe'?
So I can't build my project, can you help me? I tried to just write this component in js and it worked but I'd like to keep my ts.
Thanks!
Stripe property is accessible on client side as window.Stripe after sdk script is loaded. During development TypeScript has no reference for global variable Stripe.
One way to fix it, is to create definitions for Stripe property on global window object.
This can be done by creating (or extending if exists) type definitions of Window class. In type definitions folder create a file global.d.ts (or any other) with following code:
interface Window {
Stripe: any;
}
This will extend default Window class and allow to use Stripe without errors in your code like this:
const stripe = window.Stripe('MY PUBLIC KEY')
Note that this will not provide completion for sdk methods and properties.
I want to use autocomplete.js in my application.
I have installed the package using yarn add #tarekraafat/autocomplete.js. I am using webpack 4.28 to bundle the javascript files and have require("#tarekraafat/autocomplete.js/dist/js/autoComplete"); to import the package into the application and placed the bundled file at the bottom before the closing BODY tag.
In my custom.js file, I am creating a new instance of autoComplete as follows:
new autoComplete({
data: {
src: async () => {
document.querySelector("#autoComplete_results_list").style.display = "none";
document.querySelector("#autoComplete").setAttribute("placeholder", "Loading...");
const source = await fetch("/employee/search");
const data = await source.json();
return data;
},
key: "name"
},
selector: "#autoComplete",
placeHolder: "type employee name to search...",
threshold: 0,
searchEngine: "strict",
highlight: true,
dataAttribute: { tag: "value", value: "" },
maxResults: Infinity,
renderResults: {
destination: document.querySelector("#autoComplete"),
position: "afterend"
},
onSelection: feedback => {
document.querySelector(".selection").innerHTML = feedback.selection.food;
document
.querySelector("#autoComplete")
.setAttribute("placeholder", `${event.target.closest(".autoComplete_result").id}`);
console.log(feedback);
}
});
However, on running the application, I am getting an error Uncaught ReferenceError: autoComplete is not defined with a reference to the location where I am creating the new instance.
I have read the getting started documentation and looked at the demo code and I can't figure out what I am missing. How do I resolve the error?
Your problem is in your import, you are not import the autoComplete correctly, so when you using the new autoComplete you are having error.
Change the require("#tarekraafat/autocomplete.js/dist/js/autoComplete"); to import autoComplete from '#tarekraafat/autocomplete.js';, put this on top of your file, right after jquery or something
Write your code inside
$(document).ready(function(){
// Write your Code Here
});
I have a mobile webview that is injected with some global config object:
Vue.prototype.$configServer = {
MODE: "DEBUG",
isMobile: false,
injected: false,
version: -1,
title:"App",
user: null,
host: "http://127.0.0.1:8080"
}
Later the WebView inject this:
Vue.prototype.$configServer = {
MODE: "DEBUG",
title: "App",
version: "2.0",
isMobile: true,
injected: true,
host: "http://127.0.0.1:8081"
}
And try to use it for the component:
const HomePage = {
key: 'HomePage',
template: '#HomePage',
components: { toolbar },
data() {
return {
items: [
{name:"Login", link:"login"},
]
}
},
computed:{
config() {
return Vue.prototype.$configServer
}
},
};
However the page is not updated. How react to the change?
P.D: I confirm the object is updated with the safari debug tools. Also test in a local html.
Instead of putting the config into the prototype of Vue, you can actually add it as a data option inside the main vue instance which will guarantee you that all your config properties will be reactive. As mentioned in the docs
When you pass a plain JavaScript object to a Vue instance as its data option, Vue will walk through all of its properties and convert them to getter/setters using Object.defineProperty.
Having said that whenever you update your config properties, vue will react to it.
Now let's see how to do it in code:
new Vue(){
data:{ //only place where data is not a function
configServer = {
MODE: "DEBUG",
isMobile: false,
injected: false,
version: -1,
title:"App",
user: null,
host: "http://127.0.0.1:8080"
}
}
}
Now whenever you need to access your config you can directly access it from any component using $root. Like this.$root.configServer.
Well that's it.
I hope it helps.
There are 3 ways to acheive what you want
1- Make sure you import vue in your component
import 'Vue' from vue
...
...
...
computed:{
config() {
return Vue.prototype.$configServer
}
2- If you don't want to import vue the you can directly access prototype using proto from any instance.
computed:{
config() {
return this.__proto__.$configServer
}
3- As you have added the config in the prototype you can actually access is directly from the vue instance using this
computed:{
config() {
return this.$configServer
}
},
Well whatever style matches yours you can choose that.
But I would personally recommend using the 3rd one, because accessing the prototype of instance is sort of an anti-pattern.
I hope it helps.