How to generate Vue components from Javascript - javascript

I'm new to Vue.js (with a background in Computer Science and programming, including interactive Javascript webpages) and as I'm a teacher, I have a quiz site I use to give homework to my students.
My codebase is messy, so I decided to migrate the whole thing to Vue, with the idea that I could use a component for each individual type of question -- separation of concerns, and all that.
However, I can't seem to find a way to generate appropriate components on the fly and include them in my page.
Here's a simplified version of my framework, with two question types. If I include the components directly in the HTML, they work fine.
Vue.component("Freetext",{
props: ["prompt","solution"],
data : function() {return {
response:""
}},
methods : {
check : function () {
if (this.solution == this.response) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'<span><h1>{{prompt}}</h1> <p><input type="text" v-model="response"></input></p> <p><button class="LG_checkbutton" #click="check()">Check</button></p></span>'
})
Vue.component("multi",{
props : { prompt: String,
options : Array,
key_index : Number // index of correct answer
},
data : function() {return {
response:""
}},
methods : {
check : function (k) {
if (k == this.key_index) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'<span><h1>{{prompt}}</h1><button v-for="(v,k) in options" #click="check(k)">{{v}}</button></span>'
})
</script>
<div id="app">
<Freetext prompt="Type 'correct'." solution="correct"></freetext>
<multi prompt="Click the right answer." :options='["right","wrong","very wrong"]' :key_index=0></multi>
</div>
<script>
var app = new Vue({
el: "#app",
data : {
questions:[ {type:"Multi",
prompt: "Click the right answer.",
options:["right","wrong","very wrong"],
key:0},
{type:"Freetext",
prompt:"Type 'correct'.",
solution:"correct"}
],
question_number:0
},
methods : {
nextQuestion : function () {
this.question_number ++;
}
}
})
</script>
But what I want to do is generate the contents of the div app on the fly, based on using the data member app.question_number as an index to app.questions, and the .type member of the question indicated (i.e. app.questions[app.question_number].type)
If I try to make the app of the form:
{{question}}
</div>
<script>
//...
computed : {
question : function () {
var typ = this.questions[this.question_number].type;
return "<"+typ+"></"+typ+">";
}
...I just get as plain text, and it isn't parsed as HTML.
If I try document.getElementById("app").innerHTML = "<multi prompt='sdf'></multi>"; from the console, the tag shows up in the DOM inspector, and isn't processed by Vue, even if I call app.$forceUpdate().
Is there any way round this?

While Keith's answer works for most of what I need to do, there's another way to handle this that I've just found out about, which I thought I'd share in case anyone else is looking for it: giving a block level HTML element a v-html property.
For me, this is handy as a short term fix as I'm migrating a codebase that generates dynamic HTML as strings, and I can quickly integrate some of my existing code without reworking it completely.
For example, I have a function makeTimetable that takes a custom datastructure representing a week's actively and turns it into a table with days across the top and times down the left-hand side, setting appropriate rowspans for all the activities. (It's a bit of a convoluted function, but it does what I need and isn't really worth refactoring at this point.)
So I can use this as follows:
<script type="text/x-template" id="freetext-template">
<span>
<div v-html="tabulated_timetable"></div>
<p>{{prompt}}</p>
<p><input type="text" v-model="response"></input></p>
<p><button class="LG_checkbutton" #click="check()">Check</button></p>
</span>
</script>
<script>
var freetext = Vue.component("Freetext",{
props: {"prompt":String,
"timetable":Object,
"solution":String,
data : function() {return {
response:""
}},
computed : {
tabulated_timetable : function () {
return makeTimetable (this.timetable);
}},
methods : {
check : function () {
if (this.solution == this.response) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'#freetext-template'
})
</script>
(I suppose I could put `tabulated_timetable` in `methods` rather than `computed`, as it's set once and never changed, but I don't know if there would be any performance benefit to doing it that way.)

I think maybe a slightly different approach, Vue supports the concept of "dynamic components"
see https://v2.vuejs.org/v2/guide/components-dynamic-async.html
this will let you define what component to use on each question which would look something like
<component v-bind:is="question.component" :question="question"></component>

Related

Create an embeddable "widget" from the JS script

I would like to be able to embed the content that gets created via this snippet anywhere I like, using one line of code - same way that you get any kind of snippet somewhere, so that it renders an iframe or similar in return. I am not sure where to start with that, and if this what I already have is usable/ready to "convert" to an embeddable snippet. I went through a few tutorials but it was quite confusing since it was using some backend stuff I don't really understand...
Obviously, it should be hosted somewhere, but the part where I need to "call" the snippet is not really clear to me. Now, it just appears everywhere on my website since it's just a normal JS file which gets included, same as all other files.
It would be great if I could just pack it somehow and call it like this let's say:
<script src="link-to-my-snippet.js"></script>
If someone could direct me a bit that would be great.
const data = [
{
name: "John Doe",
age: 40
},
{
name: "Jane Doe",
age: 50
}
];
window.addEventListener("DOMContentLoaded", function() {
function Item(configurationObject) {
apiCall(data);
}
function apiCall(data) {
// Do some API call and get back the data
// With the Unique ID that wass passed in via the
// configuration Object
// "data" is faked just for the demonstration
createHTML(data);
}
function createHTML(data) {
const mainDiv = document.createElement("div");
document.body.appendChild(mainDiv);
let html = '';
data.forEach((user) => {
html += `
<div class="test">
<p>${user.name}</p>
<p>${user.age}</p>
</div>
`;
});
mainDiv.innerHTML = html;
createStylesheet();
}
function createStylesheet() {
const style = document.createElement("style");
style.innerHTML = `
.test {
background-color: lightgreen;
color: white;
}
`;
document.head.appendChild(style);
}
let configurationObject = {
uniqueID: 1234
}
let initialize = new Item(configurationObject);
});
There are two ways:
Using modern javascript - ES6, Webpack, SCSS, and then bundle all in a single file using NPM
Follow: https://blog.jenyay.com/building-javascript-widget/
Pure JavaScript - Custom Created.
You can create a self-executable anonymous function like this and write your complete widget code - including your HTML, CSS, etc inside this. It will be executed once your page is loaded with this script.
(function() {
// The following code will be enclosed within an anonymous function
var foo = "Hello World!";
document.write("<p>Inside our anonymous function foo means '" + foo + '".</p>');
})(); // We call our anonymous function immediately
For the second type solution you can also follow following article:
https://gomakethings.com/the-anatomy-of-a-vanilla-javascript-plugin/
I'm assuming that you have some local static HTML/CSS page.
First off, you don't need to render the generated HTML by the javascript in an iframe, almost any element will do. The purpose of JS is to create, manipulate and read DOM-elements, so don't feel limited.
Secondly, some of that code is useless for your purpose, unless you plan on doing stuff with an API (which I assume not), and have an actual need for a unique ID. In that code, that unique Id isn't unique anyway and isn't used for anything.
There is so many ways to implement this script on any page of your choice, but here's one:
You have a HTML-file, in that one, put:
<div id="users-list"></div>
wherever you want the list to appear.
Create a file called whatever you want, but for example users-list.js. Check the demo in my answer for the JS code to put in that file.
In any HTML file where you have added an element with the ID of 'users-list', simply also add the script in that same HTML file. Preferably before the ending tag.
<script src="/path/to/users-list.js"></script>
Of course, you make this in so many ways, and expand on it infinitely. For example, something like this could be cool to have:
<div id="some-div-id"></div>
...
<script src="/path/users-list.js">
getUsers({
element: 'some-div-id'
theme: 'dark',
layout: 'boxes' // or 'rows'
});
</script>
Then you could invoke the script on different pages with different generated HTML, but of course, that would require some modification of your JS code, to actually print out different css content, and getting the right element to place the data in.
In any case, it wouldn't be hard to do.
But back on topic, working demo:
const users = [
{
name: "John Doe",
age: 40
},
{
name: "Jane Doe",
age: 50
}
];
const styles = `
.user {
background-color: lightgreen;
color: white;
}
.age { font-weight: bold; }
`;
function setStyles() {
const styleElement = document.createElement('style');
styleElement.innerHTML = styles;
document.head.appendChild(styleElement);
}
function setUsers(users) {
let element = document.getElementById('users-list')
let usersHtml = '';
users.forEach(user => {
usersHtml += `
<div class="user">
<p class="name">${user.name}</p>
<p class="age">${user.age}</p>
</div>
`;
})
if (element) element.innerHTML = usersHtml;
}
window.addEventListener("DOMContentLoaded", function () {
setUsers(users);
setStyles(styles);
});
<div id="users-list"></div>
Here is an example of a self invoking recursive IIFE checking for the document readyState, better than the accepted answers solution
const myPlugin = () => {
// stuff
}
/**
* Checks the document readyState until it's ready
*/
(ready = (delay) => {
// this is always 'complete' if everything on the page is loaded,
// if you want to check for a state when all html/js is loaded but not all assets, check for 'interactive'
if (document.readyState == 'complete') {
myPlugin() // your stuff being invoked when doc is ready
} else {
console.log('Retrying!')
setTimeout(() => { ready(delay) }, delay)
}
})(50)

Custom Element (Web Component) won't accept keyboard input when inserted by a CKEditor 5 plugin

I'm in the initial stages of developing a plugin that will allow the user to insert placeholder elements into HTML content that will be processed server-side and used to incorporate some simple logic into a generated PDF document. To this end, I'm attempting to insert a custom element that I've defined using the web components API.
class NSLoop extends HTMLElement {
constructor() {
super();
}
get source() {
return this.getAttribute('source');
}
get as() {
return this.getAttribute('as');
}
}
window.customElements.define('ns-loop', NSLoop);
The contents of loopediting.js:
import Plugin from "#ckeditor/ckeditor5-core/src/plugin";
import Widget from "#ckeditor/ckeditor5-widget/src/widget";
import {viewToModelPositionOutsideModelElement} from "#ckeditor/ckeditor5-widget/src/utils";
import LoopCommand from "./loopcommand";
export default class LoopEditing extends Plugin {
static get requires() {
return [Widget];
}
constructor(editor) {
super(editor);
}
init() {
this._defineSchema();
this._defineConverters();
this.editor.commands.add('loop', new LoopCommand(this.editor));
this.editor.editing.mapper.on('viewToModelPosition', viewToModelPositionOutsideModelElement(this.editor.model, viewElement => viewElement.is('element', 'ns-loop')));
}
_defineSchema() {
const schema = this.editor.model.schema;
schema.register('loop', {
isBlock: false,
isLimit: false,
isObject: false,
isInline: false,
isSelectable: false,
isContent: false,
allowWhere: '$block',
allowAttributes: ['for', 'as'],
});
schema.extend( '$text', {
allowIn: 'loop'
} );
schema.extend( '$block', {
allowIn: 'loop'
} );
}
_defineConverters() {
const conversion = this.editor.conversion;
conversion.for('upcast').elementToElement({
view: {
name: 'ns-loop',
},
model: (viewElement, {write: modelWriter}) => {
const source = viewElement.getAttribute('for');
const as = viewElement.getAttribute('as');
return modelWriter.createElement('loop', {source: source, as: as});
}
});
conversion.for('editingDowncast').elementToElement({
model: 'loop',
view: (modelItem, {writer: viewWriter}) => {
const widgetElement = createLoopView(modelItem, viewWriter);
return widgetElement;
}
});
function createLoopView(modelItem, viewWriter) {
const source = modelItem.getAttribute('source');
const as = modelItem.getAttribute('as');
const loopElement = viewWriter.createContainerElement('ns-loop', {'for': source, 'as': as});
return loopElement;
}
}
}
This code works, in the sense that an <ns-loop> element is successfully inserted into the editor content; however, I am not able to edit this element's content. Any keyboard input is inserted into a <p> before the <ns-loop> element, and any text selection disappears once the mouse stops moving. Additionally, it is only possible to place the cursor at the beginning of the element.
If I simply swap out 'ns-loop' as the tag name for 'div' or 'p', I am able to type within the element without issue, so I suspect that I am missing something in the schema definition to make CKEditor aware that this element is "allowed" to be typed in, however I have no idea what I may have missed -- as far as I'm aware, that's what I should be achieving with the schema.extend() calls.
I have tried innumerable variations of allowedIn, allowedWhere, inheritAllFrom, isBlock, isLimit, etc within the schema definition, with no apparent change in behaviour.
Can anyone provide any insight?
Edit: Some additional information I just noticed - when the cursor is within the <ns-loop> element, the Heading/Paragraph dropdown menu is empty. That may be relevant.
Edit 2: Aaand I found the culprit staring me in the face.
this.editor.editing.mapper.on('viewToModelPosition', viewToModelPositionOutsideModelElement(this.editor.model, viewElement => viewElement.is('element', 'ns-loop')));
I'm new to the CKE5 plugin space, and was using other plugins as a reference point, and I guess I copied that code from another plugin. Removing that code solves the problem.
As noted in the second edit, the culprit was the code,
this.editor.editing.mapper.on('viewToModelPosition', viewToModelPositionOutsideModelElement(this.editor.model, viewElement => viewElement.is('element', 'ns-loop')));
which I apparently copied from another plugin I was using for reference. Removing this code has solved the immediate problem.
I'll accept this answer and close the question once the 2-day timer is up.

vue.js computed beginner

I am trying to learn vue.js despite not having any background with javascript. I ran into some code when following a video that was teaching about 'computed', and I tried experimenting on it and had a bit of trouble along the way.
<div id='app'>
<p>Do you see me?</p>
<p v-if="show">Do you also see me?</p>
<button #click="showToggle1">Switch!</button>
</div>
new Vue({
el:'#app',
data:{
show = true;
},
computed:{
showToggle1:function(){
return this.show = !this.show
}
},
methods:{
showToggle2:function(){
this.show = !this.show;
}
});
Basically it's making "Do you also see me?" disappear and appear depending on the value of "show". I know that if you write #click:'showToggle2()' instead of #click:'showToggle1' at the button, the value changes and it works. I'm just having some trouble understanding how computed works and why showToggle1 doesn't change the value of show when I click the button
Some problems.
First, you have syntactical problems. data is an object, so instead of:
data:{
show = true;
}
Should be:
data:{
show: true
}
Next, computed properties are to be used like... properties. For example, like declared in data. So, typicall, you read from them. You don't execute computed properties in #click events. So this code:
<button #click="showToggle1">Switch!</button>
Is not correct. It will error because showToggle1 is not a method, it is, as said, a computed property. What you should have in click is a method, like:
<button #click="showToggle2">Switch!</button>
This will work because showToggle2 is a method. And you should use methods to perform changes.
Not, before going into the last and most tricky part, here's a working demo:
new Vue({
el: '#app',
data: {
show: true
},
computed: {
/*showToggle1: function() {
return this.show = !this.show;
}*/
},
methods: {
showToggle2: function() {
this.show = !this.show;
}
}
});
<script src="https://unpkg.com/vue"></script>
<div id='app'>
<p>Do you see me?</p>
<p v-if="show">Do you also see me?</p>
<hr>
Value of show: {{ show }}<br>
<button #click="showToggle2">Switch2!</button>
</div>
The tricky part is your computed property (which I commented out in the code above):
computed:{
showToggle1:function(){
return this.show = !this.show
}
},
Basically what it is doing is it is automatically negating the value of show whenever it changes.
This happens because the computed property is calculated whenever show updates. And what is happening is:
You initialize data with true (because of data: {show: true}).
The showToggle1 computed auto-recalculates, because it has this.show inside of it (it depends on it).
When recalculating, showToggle1 sets the value of show to false (because of return this.show = !this.show).
That's why show becomes false.
And that's also why whenever you change (even from the method, which is the correct place) the value of show to true it will automatically go back to false. Because any change in show triggers the showToggle1 computed recalculation, which sets show back to false.
In summary:
Use methods to perform changes.
Don't change properties inside computed properties.

VueJs manipulate inline template and reinitialize it

this question is similar to VueJS re-compile HTML in an inline-template component and also to How to make Vue js directive working in an appended html element
Unfortunately the solution in that question can't be used anymore for the current VueJS implementation as $compile was removed.
My use case is the following:
I have to use third party code which manipulates the page and fires an event afterwards. Now after that event was fired I would like to let VueJS know that it should reinitialize the current DOM.
(The third party which is written in pure javascript allows an user to add new widgets to a page)
https://jsfiddle.net/5y8c0u2k/
HTML
<div id="app">
<my-input inline-template>
<div class="wrapper">
My inline template<br>
<input v-model="value">
<my-element inline-template :value="value">
<button v-text="value" #click="click"></button>
</my-element>
</div>
</my-input>
</div>
Javascript - VueJS 2.2
Vue.component('my-input', {
data() {
return {
value: 1000
};
}
});
Vue.component('my-element', {
props: {
value: String
},
methods: {
click() {
console.log('Clicked the button');
}
}
});
new Vue({
el: '#app',
});
// Pseudo code
setInterval(() => {
// Third party library adds html:
var newContent = document.createElement('div');
newContent.innerHTML = `<my-element inline-template :value="value">
<button v-text="value" #click="click"></button>
</my-element>`; document.querySelector('.wrapper').appendChild(newContent)
//
// How would I now reinialize the app or
// the wrapping component to use the click handler and value?
//
}, 5000)
After further investigation I reached out to the VueJs team and got the feedback that the following approach could be a valid solution:
/**
* Content change handler
*/
function handleContentChange() {
const inlineTemplates = document.querySelector('[inline-template]');
for (var inlineTemplate of inlineTemplates) {
processNewElement(inlineTemplate);
}
}
/**
* Tell vue to initialize a new element
*/
function processNewElement(element) {
const vue = getClosestVueInstance(element);
new Vue({
el: element,
data: vue.$data
});
}
/**
* Returns the __vue__ instance of the next element up the dom tree
*/
function getClosestVueInstance(element) {
if (element) {
return element.__vue__ || getClosestVueInstance(element.parentElement);
}
}
You can try it in the following fiddle
Generally when I hear questions like this, they seem to always be resolved by using some of Vue's more intimate and obscured inner beauty :)
I have used quite a few third party libs that 'insist on owning the data', which they use to modify the DOM - but if you can use these events, you can proxy the changes to a Vue owned object - or, if you can't have a vue-owned object, you can observe an independent data structure through computed properties.
window.someObjectINeedtoObserve = {...}
yourLib.on('someEvent', (data) => {
// affect someObjectINeedtoObserve...
})
new Vue ({
// ...
computed: {
myObject () {
// object now observed and bound and the dom will react to changes
return window.someObjectINeedtoObserve
}
}
})
If you could clarify the use case and libraries, we might be able to help more.

How do I create a dynamic quiz using JavaScript?

I'm looking into making a quiz where the user will be greeted with a "card" interface with both simple yes/no questions and some multiple choice questions. When answering one question, the next questions change to fit the previous answers.
Example flow:
Do you want to eat out or at home? I want to eat out. Do you want to eat at a Korean restaurant? Yes.
The problem I have is that I want this on the frontend without having multiple routes. Currently, I'm using Vue.js and Vue-Router. And this is what I have so for(don't mind the naming conventions, just temporary):
<template>
<div>
<div v-show="isQuestion(0)">
<p id="question_1">Question 1</p>
<p #click="answerQuestion()">Answer 1</p>
<a #click="answerQuestion()">Answer 2</a>
</div>
<div v-show="isQuestion(1)">
<p id="question_2">Question 2</p>
<div v-show="isAnswer(0)">
<p>Game?</p>
<a #click="answerQuestion()">Yes</a>
<a #click="nextAnswer()">No</a>
</div>
<div v-show="isAnswer(1)">
<p>Read?</p>
<a #click="answerQuestion()">Yes</a>
<a #click="nextAnswer()">No</a>
</div>
<div v-show="isAnswer(2)">
<p>Redo?</p>
<a #click="resetAnswer()">Reset</a>
</div>
<a #click="search()">Search</a>
</div>
</div>
</template>
<script>
export default {
data () {
return {
question: 0,
answer: 0,
answers: {}
}
},
methods: {
answerQuestion () {
this.answers[this.question] = this.answer
this.question++
this.answer = 0
},
nextAnswer () {
this.answer++
},
resetAnswer () {
this.answer = 0
},
isQuestion (n) {
return n === this.question
},
isAnswer (n) {
return n === this.answer
}
}
}
</script>
One option I'm thinking about would perhaps to put the questions with the answers in a database so that the frontend can fetch them as JSON and then populate the so called "cards". But then I have a problem with how to show the "correct" next question responding to the previous answers.
I don't feel comfortable hard coding everything as it seems like a bad practice but I'm having a hard time doing this any other way.
I think the point on your case is proper data structure. In your case I will use:
data () {
return {
curentQuesionIndex: 0,
curentQuesion: null,
questions: [
{
question: 'Do you want to eat out or at home?',
options: [{0:'No'}, {1:'Yes'}, {'special':'reset'}], // answer options
answer: 1 // if NULL then user not yet give answer
// if 'special' then you process it differently
},
... // next questions here
]
}
}
Using this array of questions you can render it in automatic way using Vue, (you can read it from ajax json), show next questions and other stuff. If answer in some question is NULL then you know that this is the 'next question' to show...
In your vue component create variable curentQuesionIndex=2 and currentQuestion= {..} which you will use to show, save (into your query array), and operate (for instance of 'special' answer like 'reset').
You will use only one #click function: saveAnswer(value) where 'value' is the one option from question.options. Inside this funciotn you can save answer to questions list, set newq question to currentQuestion variable (which is rendered on screen) and depends of value you will make different actions - for instance: you will put if statement there: if questions[currentQuestionIndex].options[value] == 'reset' then you will reset...

Categories

Resources