Load Vue view-model with data from existing HTML - vue.js

I'd like to use Vue with pre-rendered HTML. In the long run we might not, but for now it seems easier to leave the server code in place and use Vue to manage the client logic of the web app.
I can't seem to find anything that describes how to have the view model load its data from the HTML. My example will be the same as https://github.com/vuejs/Discussion/issues/56. Even though that issue was closed (and fixed?), I can't seem to get that to work in either 1.x or 2.x.
When I set up some HTML
<div id="myvue">
<span v-text="message">Hello!</span>
</div>
and then create a view-model for it
var vm = new Vue({
el: '#myvue'
});
I'm hoping to have the vm object hydrated with data from the HTML so that vm.message == 'Hello!' is true. But that doesn't happen. I just end up with an error saying that message is not defined with version 1. With version 2 I get a warning that says Property or method "message" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
But when I declare the data property in the vm like so
var myvue = new Vue({
el: '#myvue',
data: {
message: <something>
}
});
no matter what <something> is (null, a string, undefined), it always replaces what's in the HTML element. Is there any way to have the Vue object load data from the HTML?

It is theoretically impossible to hydrate your state directly from HTML, because rendering is not a bijective function. Two simple examples:
<p>{{ foo }}</p> renders to <p>2</p>. Is foo a string or a number?
<p>{{ foo + bar }}</p> renders to <p>6</p>. What are the values of foo and bar?
The simplest way to hydrate your state is to take the data you use for rendering on the server, serialize it to JSON and render it using <script>window.hydrationData = ...</script> directly in your document. This way, you can use it for data when you create the Vue instance.
This approach is also used in the official Vue demo app.

It's not related to Vue, but Knockout.js can kind of do this when used with ErikSchierboom's knockout-pre-rendered custom binding.
This combo essentially lets you define an input or something
<div id="mydiv">
<input data-bind="init, textInput: content" value="foo"/>
</div>
And then bind a model object to it
function ViewModel() {
this.content = ko.observable();
}
var vm = new ViewModel();
ko.applyBindings(vm, $('#mydiv')[0])
With the init binding on the input, the vm content gets loaded with the value from the input when it gets bound.
Again, it's not Vue. But thought I'd put it up incase it helps someone.

One option, if your rendered HTML doesn't depend on computed properties, might be to parse your generated HTML to grab the values you need for your data (say using jQuery or vanilla-js to create a data object to pass to your VM when you instantiate it:
$(function(){
var app = '#app';
var $app = $(app);
var data = {
a: 0, // some initial value not present in HTML
b: $app.find('.foo').text(),
c: $app.find('input[name=c]').val()
};
var vm = new Vue({
el: app,
template: '...template...', // if needed
data: data,
...
});
});
When instantiated, the data model should match the rendered HTML. You may need to supply the template if not all template components are part for the rendered HTML. Note that this most likely will cause the app to re-render once instantiated. You might be able to add the server-rendered="true" attribute to your root app element to prevent this if the pre-rendered HTML is identical to what the vm would render.
Another, not so automated option would be to have your template generate a <script> tag just below your element which the data model in it:
<script>
window.MYSTATE = window.MYSTATE || {};
window.MYSTATE['#app'] = {
"a": 0,
"b": "Some text",
"c": "Some value"
};
</script>
Make sure your data is properly escaped into JSON format. Then in your page scripts where you instantiate your app , use the window.MYSTATE['#app'] as the data object (doing a deep clone to prevent changes in the window.MYSTATE instance affecting your model.
var vm = new Vue({
el: '#app',
template: '...template if needed...',
data: $.extend(true, {}, window.MYSTATE['#app'])
...
});
Just make sure you do the instantiation after the page has loaded (i.e. by placing this at the bottom of the page before the closing body tag)

Related

Vue replaces "open" attribute to value "open" in any tag

I'm using vue.js (v2.6.12) components in laravel blade templates.
For the project, I'm also using MathML in which I need to use the open attribute of <mfenced> tag to be set to some custom values. Here is the example of the math expressing in mathml.
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mi>f</mi>
<mfenced close="]" open="[">
<mrow><mi>a</mi><mo>,</mo><mi>b</mi></mrow>
</mfenced>
</math>
But as soon as the page renders, the open attribute is converted into this open="open". I'm 100% sure there is no other library or script is loaded that updates like so, just plain vue. This actually breaks the math expression. So it looks like this:
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mi>f</mi>
<mfenced close="]" open="open">
<mrow><mi>a</mi><mo>,</mo><mi>b</mi></mrow>
</mfenced>
</math>
Later I realized that not only in math expression, litaratily any tag, be it <div open="anything">...</div>, <span open="anything">...</span>, <custom-element open="something">...</custom-element> having open attribute behaves the same. even if I use v-pre attribute to exclude it from vue js templete compiler.
And this do not happen, as soon I disable the vue app initialization.
The question here are:
Why vue is changing the open attribute like so?
How can I stop this behaviour, to the entire page within the vue application area or at least where I choose (something like using v-pre), is there ary config or any other way around?
Why
In HTML spec there are some attributes called boolean attributes. Spec dictates what can be a value of such attribute:
If the attribute is present, its value must either be the empty string or a value that is an ASCII case-insensitive match for the attribute's canonical name, with no leading or trailing whitespace.
The values "true" and "false" are not allowed on boolean attributes. To represent a false value, the attribute has to be omitted altogether.
open is one of the boolean attributes - it is defined for the <details> element
Problem with Vue 2 is, that it treats most of the boolean attributes as global - without considering the element it is placed on. Result is that open attribute is always rendered with value "open" or removed if the value is falsy (when v-binding). This is fixed in Vue 3 as shown in 2nd example...
How
The use of v-pre is the way to go but unfortunately for you there is a bug.
See this issue. The bug was already fixed with this commit(Sep 21, 2020) but it was not released yet...
example - the "With v-pre" should work in Vue version > 2.6.12
const vm = new Vue({
el: '#app',
data() {
return {
message: 'Hi!',
html: `<div open="[" close="]">Hi from html</div>`
}
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.js"></script>
<div id="app">
<div open="[" close="]">{{ message }}</div>
<div v-html="html"></div>
<div v-pre>
<p open="[" close="]">With v-pre</p>
</div>
</div>
example - it works in Vue 3 - open is treated as boolean attribute only if placed on <details>
const app = Vue.createApp({
data() {
return {
message: 'This works in Vue 3!',
}
},
})
app.mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.11/vue.global.js" integrity="sha512-1gHWIGJfX0pBsPJHfyoAV4NiZ0wjjE1regXVSwglTejjna0/x/XG8tg+i3ZAsDtuci24LLxW8azhp1+VYE5daw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div id="app">
<div open="[" close="]">{{ message }}</div>
<details open="[">
<summary>Details</summary>
open attribute on details element is treated as boolean (renders empty value)
</details>
</div>
One workaround is to create a directive (named "attr") that sets the attribute:
Vue.directive('attr', (el, binding) => el.setAttribute(binding.arg, binding.value || ''))
Then use it in your template like v-bind but with v-attr:
<mfenced v-attr:open="'['">
Vue.directive('attr', (el, binding) => el.setAttribute(binding.arg, binding.value || ''))
new Vue({ el: '#app' })
<script src="https://unpkg.com/vue#2.6.12"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<div id="app">
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mi>f</mi>
<mfenced close="]" v-attr:open="'['">
<mrow><mi>a</mi><mo>,</mo><mi>b</mi></mrow>
</mfenced>
</math>
</div>
I've found a simple hack to solve this problem.
Why hack?
Because it is eventually going to be fixed in the comming release as pointed by #Michal, so just a quick & dirty hack is enough for now to go for it.
What I did is I placed the math content in the content and also added it to the data attribute and replacing the original content after vue has done its bad work (sorry just using blade syntax here, but it will make sense). I keep it in both places just for SEO purposes.
The template where I need math expression to be displayed.
...
<div class="proxy-content" data-proxy-content="{{ $article->content }}">
{!! $article->content !!}
</div>
...
I was using it along with jQuery, but you can easily substitute with vue.js' $el. This is what it looks in my app.js file.
...
const app = new Vue({
el: '#app',
methods: {
proxyContent() {
// Set Proxy Content.
jQuery('.proxy-content').each((i, el) => {
const $el = jQuery(el);
$el.html( jQuery('<textarea />').html( $el.data('proxy-content')).text() );
});
}
loadMathJax() {
// Load & Initialize MathJax Library.
const script = document.createElement("script");
script.type = "text/javascript";
script.src = "https://cdn.jsdelivr.net/npm/mathjax#3/es5/tex-mml-chtml.js";
document.getElementsByTagName("head")[0].appendChild(script);
}
}
mounted(){
// Enable proxy content after mount, so we are sure no more rendering issue for templates.
this.proxyContent();
// Load MathJax library with a little delay to make sure everything is ready before loading the library.
setTimeout(() => this.loadMathJax(), 10);
}
});
...
One might argue, that I'm mixing up things outside of the scope of the vue application. For me that is not an issue, as the whole page is using vue.js and also the single thing don't make any harm even if there is another scope that is using mathml (though it depends on actual implementation).
In that case, if you want to scope it well, just use $el of vue.

Does Vue have bilateral bind?

<p contenteditable="true" v-bind="message"></p>
Object #message does not change when I edit the HTML element. Is there any simple way to do this using Vue.js?
Unfortunately, contenteditable doesn't work with Vue bindings such as v-model, it's recommended that you use a library such as medium.js to build your own component.
However, if you're just trying to do something simple and keep the data in sync you can probably do that yourself:
View Model
new Vue({
el: '#app',
methods: {
updateMessage() {
this.message = this.$refs.message.innerText;
}
},
watch: {
message() {
this.$refs.message.innerText = this.message;
}
},
data: {
message: 'Hello'
}
})
HTML
<p ref="message" contenteditable="true" #keyup="updateMessage" class="editable">{{message}}</p>
 
As you can see you have to deal with updating the DOM yourself when using contenteditable. What I'm actually doing here is using a ref to target the contenteditable, so I can get the inner text via this.$refs.message.innerText. I've then added a #keyup event which calls the updateMessage method to update the message data property. I've then added a watcher which reverses this process, so when message is updated it updates the contenteditable.
Here's the JSFiddle: https://jsfiddle.net/3ngc9486/
Yes it has a two-way binding directive v-model, but it works only with input elements. So, instead of using a p element and handling that with complex JS, use a textarea with v-model and it will work out of the box.
<textarea v-model="message"></textarea>
here is an example.

Why does one Vue instance update with the data while another view instance doesn't?

I am using Vue for the first time, and am confused about why one of my Vue objects keeps updating when the data changes, and the other Vue object won't.
I have a JavaScript module like this to basically just hold data:
var MyModule = (function(){
const info = {
requestCount: 20,
arrayOfItems: []
}
return info;
})();
I have this html:
<span id='requestCounter' class='badge'>{{count}}</span>
<table id='vueTable'><tr v-for="item in items"><td>{{item.description}}</td></tr></table>
And lastly, I have this JavaScript file to do everything:
window.onload = function(){
var mainTable = new Vue({
el: '#vueTable',
data: {
items: MyModule.arrayOfItems
}
});
var requestCounterVue = new Vue({
el: '#requestCounter',
data: {
count: MyModule.requestCount
}
});
}
On the first runthrough, everything works as intended. Changing the MyModule.arrayOfItems works correctly, the table updates depending on what is in the array. However, changing MyModule.requestCount doesn't do anything. Can anybody tell me what I'm doing wrong here?
(Note, my example is simplified to make my question easier)
You are initializing the data in your Vue objects from MyModule. This does not make MyModule a proper store or controller. The weird behavior is not that requestCount doesn't react, but that arrayOfItems does.
The reason the array behaves as it does is that objects are passed by reference, so when you initialize items, Vue gets hold of the actual array object and does its magic on it. Effectively, MyModule provides an external handle to your Vue data.
requestCount, on the other hand, is a simple value, not an object, so it is passed by value, and there is absolutely no relationship between the member in MyModule and the data item in requestCounterVue.
Roy is exactly right. I'm just leaving this here because I was typing while he answered, and possibly more explanation couldn't hurt.
MyModule doesn't become reactive just because you use it to initialize values in your Vues. I expect the reason arrayOfItems is working the way you expect is because you are not actually setting it to a new array, you are mutating the existing array.
For example, your Vue using the array is defined here.
var mainTable = new Vue({
el: '#vueTable',
data: {
items: MyModule.arrayOfItems
}
});
If you add a value to MyModule.arrayOfItems like this
MyModule.arrayOfItems.push('my new value')
then your Vue will show the new value. This is because mainTable.items and MyModule.arrayOfItems are pointers to the same array.
If instead, however, you modified MyModule.arrayOfItems like this
MyModule.arrayOfItems = ['my','new','array']
Then mainTable will not show the updated array. This is because you've changed it to a completely different array. In other words, MyModule.arrayOfItems !== mainTable.items.
This is also the reason changing MyModule.count to a new value is not reflected in requestCounterVue. MyModule.count is a primitive value and when you initialized requestCounterVue with count, it used a copy of the value and made the copy reactive. Changing MyModule.count will have no effect.
If you wanted to see the changes reflected, what you might do is initialize your Vues this way
var mainTable = new Vue({
el: '#vueTable',
data: MyModule
});
When the Vue is instantiated, MyModule will be turned into a reactive object and changes will be reflected for both count and arrayOfItems.

Passing in two-way binding prop to a slot

I'm somewhat new to Vue, and I'm having particular difficulty in passing in formData to my individual child nodes. Ideally each child node simply updates the parent formData object allowing me to submit the form data later on as a whole.
I've setup a JSFiddle to illustrate: https://jsfiddle.net/3nm1mrLo/
My current thinking is that I should v-bind:field="formData.name" but that throws an error. It seems as though formData doesn't exist in the slot based HTML.
Any pointers would be gratefully received. Thanks!
As you rightly said, you need to use v-bind:field="formData.name" or :field="formData.name".
It is not working because you have defined the main app template directly in your html, and used "content distribution" to include <example-form> and <example-input>.
As defined in the docs, this content-distribution (or "transclusion" if you are familiar with Angular 1.x) works fine, but the scope belongs to the main app (the root instance, because the template belongs to it).
Ref: https://v2.vuejs.org/v2/guide/components.html#Compilation-Scope
Quote:
A simple rule of thumb for component scope is:
Everything in the parent template is compiled in parent scope; everything in the child template is compiled in child scope.
If you are curious, try changing your main app (root instance) as follows:
new Vue({
el: '*[my-app]',
data: function() {
return {
formData: { name: 'abc', location: 'xyz'}
};
}
});
Now you will see that formData is not undefined anymore.
But a more proper method is to include <example-input> as part of the template of example-form component as follows:
Vue.component('example-form', {
template: `
<div class="my-example-form">
<form><pre>{{formData}}</pre><slot></slot></form>
<example-input :field="formData.name"></example-input>
<example-input :field="formData.location"></example-input>
</div>
`,
data: function() {
return {
formData: { name: '', location: ''}
};
}
});
Now it will bind to the right formData as you would expect.
But this will still not work for you because props is a one-way binding only. The child component (example-input) will get the value, but will not pass the data changes back to parent component (example-form)
For child to pass data back to parent, the right way is to use $emit as explained in this question: Updating parent data via child component?
If you want to have an <example-input> component to work as form elements, here is a related answer which works like what you expect: https://stackoverflow.com/a/40337942/654825 - there is a working jsFiddle also.

vuejs 2 v-for :key not working, html being replaced?

I'm rendering some HTML in a v-for
But everytime I change any of the data, all my html gets replaced (input fields lose their values)
I tried giving the :key all kinds of different values
I didn't have this problem in vue v1, only in v2
http://jsbin.com/jomuzexihu/1/edit?html,js,output
I had a little play with this and it appears that Vue does not re-render the entire list when using <input /> or if you use a component but it does with v-html. Heres the fiddle for the comparison:
https://jsfiddle.net/cxataxcf/
The key actually isn't needed here because the list isn't being re-ordered, so your issue isn't to do with :key but rather with v-html. Heres what the docs say about v-html:
The contents are inserted as plain HTML - data bindings are ignored. Note that you cannot use v-html to compose template partials, because Vue is not a string-based templating engine. Instead, components are preferred as the fundamental unit for UI reuse and composition.
So I guess this is where the problem lies.
It might be worth raising an issue on Vue's github page to see whether this is the expected behavior for v-html, but Vue 2.0 is much more heavily focused on components than vue 1.x and doesn't appear to recommend using v-html, so it may just be that you need to re-factor your code to use components instead.
Edit
The solution to this problem is to simply wrap the code in a component and pass the HTML as a prop:
Vue.component('unknown-html', {
props: {
html: ""
},
template: '<div><div v-html="html"></div>'
})
The markup:
<unknown-html :html="thing.html"></unknown-html>
And the View model:
var app = new Vue({
el: '#app',
data: {
numInputs: 1,
stuff: [{
'html':'<input />'
}, {
'html':'<button>Foo</button>'
}]
}
})
Here's the JSFiddle: https://jsfiddle.net/wrox5acb/
You are trying to inject raw html directly into the DOM. Probably it was possible in earlier versions of Vue.js, but it is definitely not the recommended way.
You can instead have an array of objects and bind it to html as shown in this jsFiddle: https://jsfiddle.net/43xz6xqz/
Vue.js version: 2.0.3
In the example above, vue.js is responsible for creating the input elements and also for binding these input elements to the object values using v-model.
To extract these values, you may use a computed property as shown in the sample code.
I guess, for performance optimization, when the key is not change, Vue will not rerender the dom, but will update the data import through directive.So when your input element is import through an directive (v-html), it will be rerendered everytime when stuff changes.
Due to the vue is not a string template engines, but template based on dom, so in the case of #craig_h 's example , to use the incomming html in a string template within a component:
Vue.component('unknown-html', {
props: {
html: ""
},
template: '<div><div v-html="html"></div>'
})
view:
<unknown-html :html="thing.html"></unknown-html>
So when the stuff changes, it will not to rerender the template declare in string, for vue is not a string template engine.