vue: Populate select options dynamically - vue.js

I have vuejs based application with with a form with a drop-down menu. The options in the menu should eventually be populated from a rest call. Right now I use a very hard coded list:
<template>
<ModalForm v-show="visible" #close="visible = false" title="Add location">
<div class="form-outline mb-4">
<select
class="form-control form-control-lg"
placeholder="Organisation"
v-model="formData.organisation"
>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</select>
</div>
<div class="text-center mt-5">
<button class="btn btn-lg btn-primary">
Add location
</button>
</div>
</ModalForm>
</template>
<script setup>
import { reactive, ref } from "#vue/reactivity";
import { watch } from "#vue/runtime-core";
import ModalForm from "../../../components/ModalForm.vue";
const visible = ref(false);
const formDataDefaults = {
organisation : "",
};
const formData = reactive({ ...formDataDefaults });
watch(visible, async (newValue) => {
if (newValue == false) {
for (var key in formData) formData[key] = formDataDefaults[key];
}
});
function show() {
visible.value = true;
}
defineExpose({ show });
</script>
and that kind of works - but when I try to populate the select options in a slightly more dynamic way things fail in the build stage. From googling around I have tried this:
<option v-for="org in organisations" :value="org.name" :key="org.id">
...
<script setup>
export default {
data() {
return { organisation: [{name: "Org1", id: 1}, {name: "Org2", id: 2}] };
}
};
</script>
When compiling/running this with npm run serve I get the following error message:
error in ./src/pages/Locations/components/AddLocationModal.vue?vue&type=script&setup=true&lang=js
Syntax Error: TypeError: Cannot read properties of null (reading 'content')
which I do not understand, but I think it is related to the data export in the <script> section. Of course the next step is to actually populate with a rest call - but one step at a time.
Any hints appreciated!
Disclaimer: I am quite new to vue (and anything beyond basic JS), so this is in it's entirety based on CopyPaste and Google.
Update: the error message comes when I only add the
<script setup>
export default {
data() {
return { organisation: [{name: "Org1", id: 1}, {name: "Org2", id: 2}] };
}
};
</script>
part - without making any changes to the <template> part. So it seems there is "something" wrong with the data export. I guess problem is I struggle to understand the model for flow of data/state in this framework.
Update 2: As pointed out in answer below there was a missing { in the original data export section. Fixing that did not solve the problem.
Update 3/solution: As pointed out by several I had a mix of vue2 and vue3 syntax. This example solved it for me.

<script setup> is only for use in Vue 3 projects using the Composition API. Your current code within <script setup> however is written using the Options API. You can either:
Refactor your code to use the Composition API using script setup,
Remove the keyword setup from <script setup> to stick with your current code which should then work using the Options API. Vue 2 projects must use the Options API.

In the example code you have a syntax error, you're missing a brace:
data() {
˅
return { organisation: [{name: "Org1", id: 1}, {name: "Org2", id: 2}] };
˄
}
Check the official documentation about SELECT or the dynamic example

Related

[Nuxt3][Vue3][TailwindCSS] TailwindCSS don't effect when passing props

<script setup lang="ts">
const props = defineProps({
px: {
type: String,
required: false,
},
bg: {
type: String,
default: 'transparent',
},
rounded: {
type: String,
default: 'none',
}
})
const classes = []
for (const [key, value] of Object.entries(props)) {
(value !== undefined) && classes.push(`${key}-${value}`)
}
</script>
<template>
<div :class="classes" class="overflow-hidden">
<slot></slot>
</div>
</template>
Because the props is reactive, so I think the DOM will finish rendering before script's process.
How can I let the component wait for script's process? Thanks.
the classed has been added but it's reactive so they don't effect...
Don't construct class names dynamically
If you use string interpolation or concatenate partial class names together, Tailwind will not find them and therefore will not generate the corresponding CSS
How to use dynamic class names in tailwindcss -> use dynamic class name in tailwindcss
<!-- wrong ❌ -->
<div class="text-{{ error ? 'red' : 'green' }}-600"></div>
<!-- correct ✅ -->
<div class="{{ error ? 'text-red-600' : 'text-green-600' }}"></div>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
pxClass: {
type: String,
required: false,
default: 'px-4'
},
bgClass: {
type: String,
default: 'bg-transparent',
},
roundedClass: {
type: String,
default: 'rouned-none',
}
});
const classes = computed(() => {
let result = [];
for (const [key, value] of Object.entries(props)) {
result.push(value)
}
return result;
})
</script>
<template>
<div :class="classes" class="overflow-hidden">
<slot></slot>
</div>
</template>
This answer do have a really valid point of warning about dynamic classes. If you don't do that, you'll get your whole CSS payload bloated because Tailwind will not be able to generate only the needed utility classes.
Defeating the whole purpose of Tailwind. You can't have dynamic classes + utility classes at the same time mainly.
Here is an approach on how to do dynamic stuff in Vue with Tailwind's classes if you prefer to have an Options API approach on how to solve things.
As of today, this mapping is still the official way to go (confirmed by founders on Github discussions).

vue 2 [GSI_LOGGER]: The value of 'callback' is not a function. Configuration ignored

I'm trying to put a google sign in button inside my Vue2 project, so I tried to follow the instructions here https://developers.google.com/identity/gsi/web/guides/display-button#html
So I put this code below into my Hello.vue component
<template>
<section>
<div id="g_id_onload"
data-client_id="YOUR_GOOGLE_CLIENT_ID"
data-callback=myCallbackFunction
data-auto_prompt="false">
</div>
<div class="g_id_signin"
data-type="standard"
data-size="large"
data-theme="outline"
data-text="sign_in_with"
data-shape="rectangular"
data-logo_alignment="left">
</div>
</section>
</template>
<script>
export default {
methods: {
myCallbackFunction(){
}
}
}
</script>
and when I reloaded my page/component, it will display the error [GSI_LOGGER]: The value of 'callback' is not a function. Configuration ignored.
I think the problem is data-callback couldn't find or recognize myCallbackFunction which I already declared under methods. I've also tried to put myCallbackFunction under computed instead, but it still return the same error. So is there any way I can make this work?
Ok, I think I got it—but I switched from using the HTML documentation to the JavaScript documentation, since VueJS works better with this.
Still, I don't know if mounted is the best option, but it's at least working as intended.
Just use the callback function created at methods, and that's it.
mounted: function () {
google.accounts.id.initialize({
client_id:
'xxxxxxx.apps.googleusercontent.com',
callback: this.handleCredentialResponse,
})
google.accounts.id.prompt()}
working for me in Vue 2
<template>
<div>
<div id="signin_button"></div>
</div>
</template>
<script>
export default {
components: {
},
methods: {
handleCredentialResponse(response) {
console.log(response);
}
},
mounted: function () {
let googleScript = document.createElement('script');
googleScript.src = 'https://accounts.google.com/gsi/client';
document.head.appendChild(googleScript);
window.addEventListener('load', () => {
console.log(window.google);
window.google.accounts.id.initialize({
client_id: "xxxxxxx.apps.googleusercontent.com",
callback: this.handleCredentialResponse
});
window.google.accounts.id.renderButton(
document.getElementById("signin_button"),
{ theme: "outline", size: "large" } // customization attributes
);
})
}
}
</script>
use globalThis.yourcallbackfunction

how to use require.js define a template like vue component and connect the data

In projects that use express + require.js and vue cdn, I try to use require.js to define a template similar to vue components
In index.js, I have a data list, I want to use v-for in the index.html display list item, but I cannot connect the data list in html
This is my code, is there any mistake?
index.js
define([
'text!js/components/search/index.html',
'jquery',
], function (template) {
var $ = require('jquery');
var Vue = require('vue');
var ajax = require('js/ajax');
return {
name: 'search',
template: require('text!js/components/search/index.html'),
props: {
},
data: function () {
return {
// data list
Options: [],
};
},
mounted: function () {
this.loadOptions();
},
methods: {
//data list
loadOptions() {
ajax.get('/options/options').then(function (data) {
this.Options = data.Options;
console.log('this.Options Successful get data')
});
},
},
};
});
index.html
<div class="col-12 col-md-6 col-xl-6 ">
<a class="dropdown-item"
href="#"
v-for="opt in Options"
:key="opt.value">
<p :title="opt.label">
{{ opt.label }}
</p>
</a>
</div>
Maybe because of this?
I assume that console.log('this.Options Successful get data') works as expected. Try to change your loadOptions method as follows:
loadOptions() {
ajax.get('/options/options').then(data => {
this.Options = data.Options;
console.log('this.Options Successful get data')
});
},
Arrow function should help to point this to your Vue component instance.
From the code, your template file lives at components/search/index.html, is the file being referenced properly?
Better still, Please put your code in a sandbox/jsfiddle and share the link. So, it would be easy for anyone to look at it for you.

Passing props to Vue root instance via attributes on element the app is mounted on

I am terribly new to Vue, so forgive me if my terminology is off. I have a .NET Core MVC project with small, separate vue pages. On my current page, I return a view from the controller that just has:
#model long;
<div id="faq-category" v-bind:faqCategoryId="#Model"></div>
#section Scripts {
<script src="~/scripts/js/faqCategory.js"></script>
}
Where I send in the id of the item this page will go grab and create the edit form for. faqCategory.js is the compiled vue app. I need to pass in the long parameter to the vue app on initialization, so it can go fetch the full object. I mount it with a main.ts like:
import { createApp } from 'vue'
import FaqCategoryPage from './FaqCategoryPage.vue'
createApp(FaqCategoryPage)
.mount('#faq-category');
How can I get my faqCategoryId into my vue app to kick off the initialization and load the object? My v-bind attempt seems to not work - I have a #Prop(Number) readonly faqCategoryId: number = 0; on the vue component, but it is always 0.
My FaqCategoryPAge.vue script is simply:
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Prop } from 'vue-property-decorator'
import Card from "#/Card.vue";
import axios from "axios";
import FaqCategory from "../shared/FaqCategory";
#Options({
components: {
Card,
},
})
export default class FaqCategoryPage extends Vue {
#Prop(Number) readonly faqCategoryId: number = 0;
mounted() {
console.log(this.faqCategoryId);
}
}
</script>
It seems passing props to root instance vie attributes placed on element the app is mounting on is not supported
You can solve it using data- attributes easily
Vue 2
const mountEl = document.querySelector("#app");
new Vue({
propsData: { ...mountEl.dataset },
props: ["message"]
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Vue 3
const mountEl = document.querySelector("#app");
Vue.createApp({
props: ["message"]
}, { ...mountEl.dataset }).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Biggest disadvantage of this is that everything taken from data- attributes is a string so if your component expects something else (Number, Boolean etc) you need to make conversion yourself.
One more option of course is pushing your component one level down. As long as you use v-bind (:counter), proper JS type is passed into the component:
Vue.createApp({
components: {
MyComponent: {
props: {
message: String,
counter: Number
},
template: '<div> {{ message }} (counter: {{ counter }}) </div>'
}
},
}).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app">
<my-component :message="'Hello from HTML'" :counter="10" />
</div>
Just an idea (not a real problem)
Not really sure but it can be a problem with Props casing
HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you're using in-DOM templates, camelCased prop names need to use their kebab-cased (hyphen-delimited) equivalents
Try to change your MVC view into this:
<div id="faq-category" v-bind:faq-category-id="#Model"></div>
Further to Michal Levý's answer regarding Vue 3, you can also implement that pattern with a Single File Component:
app.html
<div id="app" data-message="My Message"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const mountEl = document.querySelector("#app");
Vue.createApp(MyComponent, { ...mountEl.dataset }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
Or you could even grab data from anywhere on the parent HTML page, eg:
app.html
<h1>My Message</h1>
<div id="app"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const message = document.querySelector('h1').innerText;
Vue.createApp(MyComponent, { message }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
To answer TheStoryCoder's question: you would need to use a data prop. My answers above demonstrate how to pass a value from the parent DOM to the Vue app when it is mounted. If you wanted to then change the value of message after it was mounted, you would need to do something like this (I've called the data prop myMessage for clarity, but you could also just use the same prop name message):
<template>
{{ myMessage }}
<button #click="myMessage = 'foo'">Foo me</button>
</template>
<script>
export default {
props: {
message: String
},
data() {
return {
myMessage: this.message
}
}
};
</script>
So I'm not at all familiar with .NET and what model does, but Vue will treat the DOM element as a placeholder only and it does not extend to it the same functionality as the components within the app have.
so v-bind is not going to work, even without the value being reactive, the option is not there to do it.
you could try a hack to access the value and assign to a data such as...
const app = Vue.createApp({
data(){
return {
faqCategoryId: null
}
},
mounted() {
const props = ["faqCategoryId"]
const el = this.$el.parentElement;
props.forEach((key) => {
const val = el.getAttribute(key);
if(val !== null) this[key] = (val);
})
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="12">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
where you get the value from the html dom element, and assign to a data. The reason I'm suggesting data instead of props is that props are setup to be write only, so you wouldn't be able to override them, so instead I've used a variable props to define the props to look for in the dom element.
Another option
is to use inject/provide
it's easier to just use js to provide the variable, but assuming you want to use this in an mvc framework, so that it is managed through the view only. In addition, you can make it simpler by picking the exact attributes you want to pass to the application, but this provides a better "framework" for reuse.
const mount = ($el) => {
const app = Vue.createApp({
inject: {
faqCategoryId: {
default: 'optional'
},
},
})
const el = document.querySelector($el)
Object.keys(app._component.inject).forEach(key => {
if (el.getAttribute(key) !== null) {
app.provide(key, el.getAttribute(key))
}
})
app.mount('#app')
}
mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="66">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
As i tried in the following example
https://codepen.io/boussadjra/pen/vYGvXvq
you could do :
mounted() {
console.log(this.$el.parentElement.getAttribute("faqCategoryId"));
}
All other answers might be valid, but for Vue 3 the simple way is here:
import {createApp} from 'vue'
import rootComponent from './app.vue'
let rootProps = {};
createApp(rootComponent, rootProps)
.mount('#somewhere')

Vue.js Dynamic Component - Template not showing components data

I'm trying to build a quiz-game with VueJs and up until now everything worked out smoothly, but now that I'm started using dynamic components I'm running into issues with displaying the data.
I have a start component (Start View) that I want to be replaced by the actual Quiz component ("In Progress") when the user clicks on the start button. This works smoothly. But then, in the second components template, the data referenced with {{ self.foo }} does not show up anymore, without any error message.
The way I implemented is the following:
startComponent:
startComponent = {
template: '#start-component',
data: function () {
return {
QuizStore: QuizStore.data
}
},
methods: {
startQuiz: function () {
this.QuizStore.currentComponent = 'quiz-component';
}
}
}
};
And the template:
<script type="x-template" id="start-component">
<div>
<button v-on:click="startQuiz()">
<span>Start Quiz</span>
</button>
</div>
</script>
Note: I'm using x-templates since it somehow makes the most sense with the rest of the application being Python/Flask. But everything is wrapped in {% raw %} so the brackets are not the issue.
Quiz Component:
quizComponent = {
template: '#quiz-component',
data: function () {
return {
QuizStore: QuizStore.data,
question: 'foo',
}
};
And the template:
<script type="x-template" id="quiz-component">
<div>
<p>{{ self.question }}</p>
</div>
</script>
And as you might have seen I'm using a QuizStore that stores all the states.
The store:
const QuizStore = {
data: {
currentComponent: 'start-component',
}
};
In the main .html I'm implementing the dynamic component as follows:
<div id="app">
<component :is="QuizStore.currentComponent"></component>
</div>
So what works:
The Start screen with the button shows up.
When I click on the Start Button, the quizComponent shows up as expected.
What does not work:
The {{ self.question }} data in the QuizComponent template does not show up. And it does not throw an error message.
it also does not work with {{ question }}.
What I don't understand:
If I first render the quizComponent with setting QuizStore.currentComponent = 'startComponent', the data shows up neatly.
If I switch back to <quiz-component></quiz-component> (rather than the dynamic components), it works as well.
So it seems to be the issue that this. does not refer to currently active dynamic component - so I guess here is the mistake? But then again I don't understand why there is no error message...
I can't figure out what the issue is here - anyone?
You may have some issues with your parent component not knowing about its child components, and your construct for QuizStore has a data layer that you don't account for when you set currentComponent.
const startComponent = {
template: '#start-component',
data: function() {
return {
QuizStore: QuizStore.data
}
},
methods: {
startQuiz: function() {
this.QuizStore.currentComponent = 'quiz-component';
}
}
};
const QuizStore = {
data: {
currentComponent: 'start-component',
}
};
new Vue({
el: '#app',
data: {
QuizStore
},
components: {
quizComponent: {
template: '#quiz-component',
data: function() {
return {
QuizStore: QuizStore.data,
question: 'foo'
}
}
},
startComponent
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<script type="x-template" id="start-component">
<div>
<button v-on:click="startQuiz()">
<span>Start Quiz</span>
</button>
</div>
</script>
<script type="x-template" id="quiz-component">
<div>
<p>{{ question }}</p>
</div>
</script>
<div id="app">
<component :is="QuizStore.data.currentComponent"></component>
</div>
The following worked in the end:
I just wrapped <component :is="QuizStore.currentComponent"></component> in a parent component ("index-component") instead of putting it directly in the main html file:
<div id="app">
<index-component></index-component>
</div>
And within the index-component:
<script type="x-template" id="index-component">
<div>
<component :is="QuizStore.currentComponent"></component>
</div>
</script>
Maybe this would have been the right way all along, or maybe not, but it works now :) Thanks a lot Roy for your help!