Instantiate Vue.js components from existing HTML? - javascript

I'm trying to make Vue.js instantiate it's components from existing HTML templates and I cannot find any reasonable information on doing that. I'm using non-JavaScript backend framework and Vue.js added in as a normal .js file in the view stack.
Let's say I have the HTML structure looking like this:
<div id="vueApp">
<div class="vueComponent">...</div>
<div class="vueComponent">...</div>
<div class="vueComponent">...</div>
</div>
Then I'm trying to enable it with JavaScript like this (mixing jQuery and Vue for safety):
<script>
// declare Vue component
Vue.component('irrelevantName', {
template: '.vueComponent',
data: function() {
return { something: false }
}
});
// launch Vue app
jQuery(document).ready(function() {
var vueApp = new Vue({
el: '#vueApp',
created: function() {
// TODO: some method that will read/parse the component data, do binding, whatever
}
});
});
</script>
This kind of approach worked perfectly for whenever I had a precisely declared and pointed <template> tag within the HTML structure and the component was supposed to be just a single one. In this case though I'm trying to instantiate n ones identified by a CSS class.
I am aware that one possible approach would be passing all the component data as hidden <input> with JSON-ized array/Collection data, and then asking vueApp to create single-template-driven components on the fly from this data, though that seems a bit tedious approach.
Any suggestions?
Thanks in advance!

I believe you might be thinking about this the wrong way. Vue binds to a single element, so if you want multiple elements to be manipulated, what you can do is create a new Vue instance around div#vueApp and create the components inside of that with Vue templates, like so:
Vue.component('vue-comp', {
data: function () {
return {
something: false
}
},
template: '<div class="vue-comp"></div>'
})
let vueApp = new Vue({
el: '#vueApp',
created: function() {
}
});
div#vueApp {
padding: 1rem;
background: #CD545b;
display: inline-block;
}
div.vue-comp {
height: 50px;
width: 50px;
margin: 0 1rem;
background: #236192;
display: inline-block;
}
<script src="https://unpkg.com/vue"></script>
<div id="vueApp">
<vue-comp></vue-comp>
<vue-comp></vue-comp>
<vue-comp></vue-comp>
</div>

Related

Solved - Vue dynamically add style to :active pseudo class

HIGHLIGHT: This is a solved problem.
Edit: Solved the problem using #Amaarockz's advice!
I haven't had much experience in CSS variables & complicated :style structures, but turned out they're great!
Original question:
I have a menu built with Vue.js (and Vuetify), data passed in from the backend like:
[
{
id: 13241243,
color: "#123456",
activeColor: "#abcdef",
text: "Asadpyqewri"
},
{
id: 742378104,
color: "#234567",
activeColor: "#bcdefa",
text: "Iudaofqepr"
}
]
Menu looks like:
<v-btn-toggle>
<v-btn v-for="item in items" :key="item.id" :color="item.color">
{{item.text}}
</v-btn>
</v-btn-toggle>
Problem
I want to know, how can I dynamically make the items in a different color when they have :active pseudo-class?
What I've tried:
Write something in "style" attribute of list items:
Failed, as I can't add a selector in <element style="">, only styles.
Use an attribute like "active-color" defined by Vuetify:
Failed, such things don't exist.
Dynamically add colors to the "v--btn-active" class, which Vuetify adds automatically:
Failed, I can't find a way to do this seperately for each button.
Watch when the :active pseudo-class appears, add style in the listener:
Somehow MutationObserver didn't work for me.
getElementsByClassName[0] keeps getting "null". I tried writing that in windows.onload, nothing changed.
One time it returned the correct node, and I was able to watch the class mutation. But I can't reproduce that even though nothing changed in the code.
mounted(){
window.onload = function(){
const targetNodes = document.getElementsByClassName('asdff');
var a = targetNodes.item(0);
//"targetNodes" is a HTMLCollection with 3 elements, but "a" is null.
}
}
Add one different class to every button, and write seperate CSS for them using JavaScript:
Perhaps doable, but code's going to be extremely ugly & difficult to maintain.
Besides, it's hard to overwrite Vuetify's default style outsides <element style>; you have to add !important.
Last two attempts are what I think more hopeful. Is there any way to work through?
Try using pseudo class like
Vue.component('pseudo', {
data() {
return {
msg: 'Hover on me',
}
},
props: {
color: {
type: String,
},
text: {
type: String,
}
},
computed: {
cssAttrs() {
return {
'--color': this.color,
'--text': JSON.stringify(this.text),
}
}
},
template: `<button class="content" :style="cssAttrs">{{msg}}</button>`,
});
var vm = new Vue({
el: '#app',
});
.content {
color: red;
}
.content:focus {
content: var(--text);
color: var(--color);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<pseudo color="blue" text="Changing!!"></pseudo>
</div>

Vue: Bind Change event to dynamically inserted content

I have a Vue app which get an html from API that contains some code like <div class="play-video"> <input type="text" class="input-1"/></div>
Calling the API with axios via a promise, it is inserted into the dom something like:
<div v-if="content" v-html="content"></div>
How can I bind on change events to the children inputs with the .input-1 class?
You could query the container for those inputs, and add an event handler to each:
Apply a template ref (named container) on the container div:
<div ref="container">
Add a watcher on content that queries the container (via this.$refs.container.querySelectorAll()) for <input class="input-1">, and adds an event handler to each input. Note that the handler needs to wait until $nextTick(), after which the v-html directive would have had a chance to update.
export default {
watch: {
content: {
async handler(content) {
// wait til v-html directives takes effect
await this.$nextTick()
this.$refs.container
.querySelectorAll('.input-1')
.forEach((input) => input.addEventListener('change', this.onInputChange))
},
immediate: true,
},
},
methods: {
onInputChange(e) {
console.log('input change')
},
},
}
demo
There are two approaches that rise to the top for me:
From inside a vue app, use vanilla javascript to append those nodes to the DOM and then query for their inputs and add event handlers. Since you're inside the Vue app you can write the handlers to interact with Vue components. Or you could probably do something fancy with vuex getters returning those elements (if they're still attached to the document).
Use Vue.compile to create render functions from the html or add an async omponent. However, this will only work if you have a robust way to add #change="doSomething" to the template. For this reason, I'd probably should go with option 1) unless you control the source of the templates.
Whichever you do, keep malicious injections in mind.
const someHtml = '<div class="play-video"><input type="text" class="input-1"/></div>'
var app = new Vue({
el: '#app',
mounted() {
const dynamicContent = document.createElement('div');
dynamicContent.innerHTML = someHtml;
dynamicContent.querySelector('input').addEventListener('change',
e => this.inputValue = e.target.value);
this.$refs.container.appendChild(dynamicContent);
},
data() {
return {
name: 'Vue',
inputValue: ''
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div>type something below then click this text</div>
<div ref="container"></div>
<div>The user entered: {{inputValue}}</div>
</div>

Using Vue.js directives within component template

I'm new to Vue.js and trying to create a component that connects to one object within some global-scope data and displays differently based on the specifics of each object. I think I'm misunderstanding how the directives v-if and v-on work within component templates. (Apologies if this should actually be two different questions, but my guess is that the root of my misunderstanding is the same for both issues).
Below is a minimal working example. My goal is to have each member entry only display the Disable button if the associated member is active, and enable changing their status via the button. (I also want to keep the members data at the global scope, since in the actual tool there will be additional logic happening outside of the app itself).
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<member-display
v-for="member in members"
v-bind:member="member"
></member-display>
</div>
<script>
var members = [
{name: "Alex", status: "On"},
{name: "Bo", status: "On"},
{name: "Charley", status: "Off"}
]
Vue.component('member-display', {
props: ['member'],
computed: {
active: function() {
// Placeholder for something more complicated
return this.member.status == "On";}
},
methods: {
changeStatus: function() {
this.member.status = 'Off';
}
},
// WHERE MY BEST-GUESS FOR THE ISSUE IS:
template: `
<div>
{{member.name}} ({{member.status}})
<button v-if:active v-on:changeStatus>Disable</button>
</div>
`
});
var app = new Vue({
el: "#app",
data: {
members: members
}
})
</script>
</body>
</html>
Thanks for your help!
The code v-if and the v-on for the button just have the wrong syntax. The line should look like this:
<button v-if="active" v-on:click=changeStatus>Disable</button>

How do you attach Vue.js events on dynamic elements added with jQuery.append()

I'm using Laravel & Vue.js. When I append some elements to page and change DOM using jQuery the Vue.js, events like #click & #blur will not work.
Is there any method to update DOM?
dropzone_success(file, response) {
$('.dz-details').append('<button type="button" class="thumb_button" id="'+response.path+'" #click="somemethod($event)">Make Default</button>');
}
And my method for example:
somemethod(event)
{
console.log(event.target);
}
You shouldn't probably be using Vue.js with jQuery in the first place, as they work in a much different concept. The following should give you some rough idea how this could be done in Vue.js alone:
const vm = new Vue({
data() {
return {
dropzone: {},
dropzoneOpts: {
// ...
},
responsePath: ''
}
},
mounted() {
this.dropzone = new Dropzone('#dropzone_id', {
...this.dropzoneOpts,
success: this.dropzone_success
});
},
methods: {
dropzone_success(file, response) {
this.responsePath = response.path;
},
somemethod(evt) {
// ...
}
}
});
<div class="dz-details">
<button
v-if="responsePath"
:id="responsePath"
class="thumb_button"
#click="somemethod">Make Default</button>
</div>
The point is, you don't do direct DOM manipulation with Vue.js as this framework is really building virtual DOM instead of the real DOM, it supports conditional rendering, two-way data binding, etc.
Check out this nice article on Declarative and Imperative programming.

Vue 2 component styles without Vue loader

Considering that there is single file component (as shown in the guide),
<style>
.example {
color: red;
}
</style>
<template>
<div class="example">hi</div>
</template>
How can the same thing be done without Vue loader in non-modular ES5/ES6 environment?
Considering that the style is scoped,
<style scoped>
.example {
color: red;
}
</style>
Is there a way to implement scoped CSS in non-modular environment, too? If there's none, is there a way to implement it in modular environment (Webpack), but without Vue loader and custom .vue format?
Instead of using the template instance in the Vue component, you can harness a 'closer-to-the-compiler alternative' with the render function without the need for the Vue Loader or compiler. You can add any additional attributes with the second parameter in the createElement function and this will give you a lot of flexibility on top of just styles.
See the Render Functions section in the guide for more info and the full options allowed in the data obj:
https://v2.vuejs.org/v2/guide/render-function
https://v2.vuejs.org/v2/guide/render-function#The-Data-Object-In-Depth
Note: The caveat here is that the style will only apply to the component it is declared in, so it might not be able to used across multiple components of the same class like CSS would be. Not sure if thats also what you want to achieve.
An example from the docs catered to this use case:
Vue.component('example', {
// render function as alternative to 'template'
render: function (createElement) {
return createElement(
// {String | Object | Function}
// An HTML tag name, component options, or function
// returning one of these. Required.
'h2',
// {Object}
// A data object corresponding to the attributes
// you would use in a template. Optional.
{
style: {
color: 'red',
fontSize: '28px',
},
domProps: {
innerHTML: 'My Example Header'
}
},
// {String | Array}
// Children VNodes. Optional.
[]
)}
});
var example = new Vue({
el: '#yourExampleId'
});
It can be achieved putting the scope manually, as vue-loader does automatically.
This is the example of the documentation. Adding some kind of ID, "_v-f3f3eg9" in this case, to scope the class only for that element.
<style>
.example[_v-f3f3eg9] {
color: red;
}
</style>
Vue.component('my-component', {
template: '<div class="example" _v-f3f3eg9>hi</div>'
});
I use Rollup (+ Bublé) + Vue.js all the time. It's pretty simple and fast.
The Rollup config is like:
import vue from 'rollup-plugin-vue';
import resolve from 'rollup-plugin-node-resolve';
import buble from 'rollup-plugin-buble';
const pkg = require('./package.json');
const external = Object.keys(pkg.dependencies);
export default {
external,
globals: { vue: 'Vue' },
entry: 'src/entry.js',
plugins: [
resolve(),
vue({ compileTemplate: true, css: true }),
buble({ target: { ie: 9 }})
],
targets: [
{ dest: 'dist/vue-rollup-example.cjs.js', format: 'cjs' },
{ dest: 'dist/vue-rollup-example.umd.js', format: 'umd' }
]
};
I've made a boilerplate repo:
git clone https://github.com/jonataswalker/vue-rollup-example.git
cd vue-rollup-example
npm install
npm run build

Categories

Resources