How to render variable stored DOM Nodes on VueJS? - javascript

I'm using Tokbox, a WebRTC SDK and one of their methods returns a video object into a variable.
With Vanilla JS I'd simply use append to add it to my DOM, but with VueJS, I'm not sure how can I accomplish this. I tried using v-html but it outputs the object as a JSON. Here is a screenshot of the object representation on chrome's console:
I don't want to use vanilla JS to append it, I'd rather expect VueJS to convert it to its own Virtual DOM object so I can freely manipulate it and don't worry about wrong states for this object.
I don't know if I'm making sense over here, but I hope you get the idea.
Thanks.

I believe you could use $refs, something like
<video ref="tokboxVideo" />
then
mounted() {
this.$refs.tokboxVideo.srcObject = this.tokboxVideoElementCreated;
}

You might have better success with the streamCreated event on the session. Create a Subscriber component like below:
<template>
<div>
</div>
</template>
<script>
import OT from '#opentok/client';
export default {
name: 'subscriber',
props: {
stream: {
type: OT.Stream,
required: true
},
session: {
type: OT.Session,
required: true
},
opts: {
type: Object,
required: false
}
},
mounted: function() {
const subscriber = this.session.subscribe(
this.stream,
this.$el,
this.opts,
err => {
if (err) {
this.$emit('error', err);
} else {
this.$emit('subscriberConnected', subscriber);
}
}
);
this.$emit('subscriberCreated', subscriber);
}
};
</script>
This will insert the video element inside the component. Then you can control how the component is displayed in your app.
Check out the basic vue sample in our repos.

Related

Is there a nice way to wrap a JQuery based widget into a module that can be easily used in Vue.js?

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).

VueJs + Laravel - like button component

I'm trying to get a good understanding of VueJS, and I'm using it with Laravel 5.7 for a personal project, but I can't exactly figure out how to do a, probably, simple task a "like" button\icon.
So, here's the situation, I have a page, displaying various posts from my database, and at the bottom of each post I want a "like toogle" button, which I made with an icon followed by the number of likes on that post; At first the button will contain the data retrieved from the corresponding database table, but if you click it will increase the displayed number by one and insert a new like in the database.
I made the "like" icon as a component :
<section class="bottomInfo">
<p>
<likes now="{{ $article->likes }}"></likes>
<span class="spacer"></span>
<span class="entypo-chat">
{{ $article->comments }}
</p>
</section> <!-- end .bottomInfo -->
As you can see there's a <likes> in which I added a prop now, by what I'm understanding till now about components, in this way I can insert the data from my db as a starting value (now contains the db row value), problem is, I don't know where\how to keep that value in my app, in which I'm gonna also use axios for increasing the likes.
Here's the component:
<template>
<span class="entypo-heart"> {{ now }}</span>
</template>
<script>
export default {
props: ['now'],
data() {
return {
like: this.now
}
},
mounted() {
console.log('Component mounted.');
}
}
</script>
What I tried to do (and I don't know if it's correct) is to pass the value of now to the data function inside a property named like, so, if I understood correctly, that variable like is now part of my properties in my main Vue instance, which is this one
const app = new Vue({
el: '#main',
mounted () {
console.log("The value of 'like' property is " + this.like)
},
methods: {
toggleLike: function() {
} //end toggleLike
}
});
The mounted function should print that property value, but instead I get
The value of 'like' property is undefined
Why? Is this how it works? How can I make it so I can get that value and also update it if clicked, to then do a request to my API? (I mean, I'm not asking how to do those single tasks, just where\how to implement it in this situation). Am i getting the component logic right?
Probably a bit more verbosity never hurt:
props: {
now: {
type: Number,
required: true
}
}
Instead of using the data function, use a computed property:
computed: {
likes: {
get: function() {
return this.now
}
}
}
However, here comes the problem.
If you need to change the # of likes after the user clicks like, you have to update this.now. But you can't! It's a property, and properties are pure. Vue will complain about mutating a property
So now you can introduce a data variable to determine if the user has clicked that like button:
data() {
return {
liked: 0
}
}
Now we can update our computed property:
likes: {
get: function() {
return this.now + this.liked
}
}
However, what are we liking? Now we need another property:
props: {
id: {
type: Number,
required: true
},
now: {
type: Number,
required: true
}
}
And we add a method:
methods: {
add: function() {
//axios?
axios.post(`/api/articles/${this.id}/like`)
.then (response => {
// now we can update our `liked` proper
this.liked = 1
}).catch(error => {
// handle errors if you need to
)}
}
}
And, let's make sure clicking our heart fires that event:
<span class="entypo-heart" #click="add"> {{ now }}</span>
Finally our likes component requires an id property from our article:
<likes now="{{ $article->likes }}" id="{{ $article->id }}"></likes>
With all this in place; you're a wizard now, Harry.
Edit
It should be noted that a user will be forever able to like this, over and over again. So you need some checks in the click function to determine if they like it. You also need a new prop or computed property to determine if it was already liked. This isn't the full monty yet.

Vue-meta: metaInfo doesn't have an access to computed properties

I'm using vue-meta to dynamically change my meta tags. I want to change it only on some particular pages.
I'm using metaInfo function and try to change, for example, a title. But data from my getter is undefined which is why I cannot change the title in meta tags. It seems like metaInfo function try to access data before the component actually has it.
Here is my code in the component:
<template>
...
</template>
<script>
export default {
metaInfo() {
return {
title: this.getViewPage.data.meta.title, // data is undefined
};
},
created() {
this.loadViewPage();
},
computed: {
...mapGetters(['getViewPage']),
},
methods: {
...mapActions(['loadViewPage']),
};
</script>
vue-meta just creates computed property from your metaInfo function (according to plugin source code), so I assume that your loadViewPage action fills data object asynchronously and your problem just transforms to null-checking problem.
So you should check data before using its properties, and when data will be loaded metaInfo will update object as well:
metaInfo() {
// don't know your return object structure,
// maybe you should check whole this.getViewPage
let data = this.getViewPage.data;
return {
title: data ? data.meta.title : "some placeholder title",
}
};

Is there any way to 'watch' for localstorage in Vuejs?

I'm attempting to watch for localstorage:
Template:
<p>token - {{token}}</p>
Script:
computed: {
token() {
return localStorage.getItem('token');
}
}
But it doesn't change, when token changes. Only after refreshing the page.
Is there a way to solve this without using Vuex or state management?
localStorage is not reactive but I needed to "watch" it because my app uses localstorage and didn't want to re-write everything so here's what I did using CustomEvent.
I would dispatch a CustomEvent whenever you add something to storage
localStorage.setItem('foo-key', 'data to store')
window.dispatchEvent(new CustomEvent('foo-key-localstorage-changed', {
detail: {
storage: localStorage.getItem('foo-key')
}
}));
Then where ever you need to watch it do:
mounted() {
window.addEventListener('foo-key-localstorage-changed', (event) => {
this.data = event.detail.storage;
});
},
data() {
return {
data: null,
}
}
Sure thing! The best practice in my opinion is to use the getter / setter syntax to wrap the localstorage in.
Here is a working example:
HTML:
<div id="app">
{{token}}
<button #click="token++"> + </button>
</div>
JS:
new Vue({
el: '#app',
data: function() {
return {
get token() {
return localStorage.getItem('token') || 0;
},
set token(value) {
localStorage.setItem('token', value);
}
};
}
});
And a JSFiddle.
The VueJs site has a page about this.
https://v2.vuejs.org/v2/cookbook/client-side-storage.html
They provide an example.
Given this html template
<template>
<div id="app">
My name is <input v-model="name">
</div>
<template>
They provide this use of the lifecycle mounted method and a watcher.
const app = new Vue({
el: '#app',
data: {
name: ''
},
mounted() {
if (localStorage.name) {
this.name = localStorage.name;
}
},
watch: {
name(newName) {
localStorage.name = newName;
}
}
});
The mounted method assures you the name is set from local storage if it already exists, and the watcher allows your component to react whenever the name in local storage is modified. This works fine for when data in local storage is added or changed, but Vue will not react if someone wipes their local storage manually.
Update: vue-persistent-state is no longer maintained. Fork or look else where if it doesn't fit your bill as is.
If you want to avoid boilerplate (getter/setter-syntax), use vue-persistent-state to get reactive persistent state.
For example:
import persistentState from 'vue-persistent-state';
const initialState = {
token: '' // will get value from localStorage if found there
};
Vue.use(persistentState, initialState);
new Vue({
template: '<p>token - {{token}}</p>'
})
Now token is available as data in all components and Vue instances. Any changes to this.token will be stored in localStorage, and you can use this.token as you would in a vanilla Vue app.
The plugin is basically watcher and localStorage.set. You can read the code here. It
adds a mixin to make initialState available in all Vue instances, and
watches for changes and stores them.
Disclaimer: I'm the author of vue-persistent-state.
you can do it in two ways,
by using vue-ls and then adding the listener on storage keys, with
Vue.ls.on('token', callback)
or
this.$ls.on('token', callback)
by using storage event listener of DOM:
document.addEventListener('storage', storageListenerMethod);
LocalStorage or sessionStorage are not reactive. Thus you can't put a watcher on them. A solution would be to store value from a store state if you are using Vuex for example.
Ex:
SET_VALUE:(state,payload)=> {
state.value = payload
localStorage.setItem('name',state.value)
or
sessionStorage.setItem('name',state.value)
}

Firebase React Binding

I'm somewhat new to React, and using the re-base library to work with Firebase.
I'm currently trying to render a table, but because of the way my data is structured in firebase, I need to get a list of keys from two locations- the first one being a list of user keys that are a member of a team, and the second being the full user information.
The team node is structured like this: /teams/team_id/userkeys, and the user info is stored like this: /Users/userkey/{email, name, etc.}
My table consists of two react components: a table component and a row component.
My table component has props teamid passed to it, and I'm using re-base's bindToState functionality to get the associated user keys in componentWillMount(). Then, I use bindToState again to get the full user node, like so:
componentWillMount() {
this.ref = base.bindToState(`/teams/${this.props.data}/members`, {
context: this,
state: 'members',
asArray: true,
then() {
this.secondref = base.bindToState('/Users', {
context: this,
state: 'users',
asArray: true,
then() {
let membersKeys = this.state.members.map(function(item) {
return item.key;
});
let usersKeys = this.state.members.map(function(item) {
return item.key;
});
let onlyCorrectMembersKeys = intersection(membersKeys, usersKeys);
this.setState({
loading: false
});
}
});
}
});
}
As you can see, I create membersKeys and usersKeys and then use underscore.js's intersection function to get all the member keys that are in my users node (note: I do this because there are some cases where a user will be a member of a team, but not be under /Users).
The part I'm struggling with is adding an additional rebase call to create the full members array (ie. the user data from /Users for the keys in onlyCorrectMembersKeys.
Edit: I've tried
let allKeys = [];
onlyCorrectMembersKeys.forEach(function(element) {
base.fetch(`/Users/${element}`, {
asArray: true,
then(data) {
allKeys.prototype.concat(data);
}
});
});
But I'm receiving the error Error: REBASE: The options argument must contain a context property of type object. Instead, got undefined
I'm assuming that's because onlyCorrectMembersKeys hasn't been fully computed yet, but I'm struggling with how to figure out the best way to solve this..
For anyone dealing with this issue as well, I seemed to have found (somewhat) of a solution:
onlyCorrectMembersKeys.map(function(item) {
base.fetch(`/Users/${item}`, {
context: this,
asObject: true,
then(data) {
if (data) {
allKeyss.push({item,data});
this.setState({allKeys: allKeyss});
}
this.setState({loading: false});
},
onFailure(err) {
console.log(err);
this.setState({loading: false});
}
})
}, this);
}
This works fine, but when users and members state is updated, it doesn't update the allkeys state. I'm sure this is just due to my level of react knowledge, so when I figure that out I'll post the solution.
Edit: using listenTo instead of bindToState is the correct approach as bindToState's callback is only fired once.

Categories