Create an embeddable "widget" from the JS script - javascript

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)

Related

Loading HTML file content in a Vue component

I'm having troubles loading the content of an HTML file in a Vue component. Basically i have a Django backend that generates an HTML file using Bokeh and a library called backtesting.py. My frontend is using Nuxt/Vue, so i can't just load the HTML on the page dynamically.
Here is what the HTML file looks like (it was too long to post here): https://controlc.com/aad9cb7f
The content of that file should be loaded in a basic component:
<template>
<div>
<h1>Some content here</h1>
</div>
</template>
<script>
export default {
components: {
},
data() {
return {
}
},
mounted() {
},
methods: {
}
}
</script>
The problem is that i really don't know how to do that. If i just copy and paste the content in the vue component, i'll get a lot of error due to the fact that i'm using a <script> tag in a component. The only thing i managed to do was to load the BokehJS CDN in my index.html file, but even after that i'll get a Bokeh is undefined error in the component.
What can i do to accomplish this? Any kind of advice is appreciated
Tao's answer is spot on and is very similar to how I've solved this issue for myself in the past.
However, I'd like to throw in an alternative iframe approach that could work in case reactivity is important. Here's a codesandbox link
The only difference is that this approach loads the code/HTML via XHR and writes it manually into the iframe. Using this approach, you should be able to add some reactivity if necessary.
<script>
export default {
components: {},
data() {
return {};
},
async mounted() {
this.initialize();
},
methods: {
async initialize() {
const html = await this.loadHTML();
const doc = this.htmlToDocument(html);
this.updateIframe(doc);
},
async loadHTML() {
const response = await fetch("/plot");
const text = await response.text();
return text;
},
htmlToDocument(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
return doc;
},
updateIframe(doc) {
const iframe = this.$refs.frame;
const iframeDocument = iframe.contentWindow.document;
iframeDocument.open();
iframeDocument.write(doc.documentElement.innerHTML);
iframeDocument.close();
}
},
};
</script>
In the codesandbox, I've thrown in two additional methods to give you an example of how reactivity can work with this approach:
modify() {
if (this.orig) {
// Only for the purpose of this example.
// It's already been modified. Just short-circuit so we don't overwrite it
return;
}
const bokehDoc = this.$refs.frame.contentWindow.Bokeh.documents[0];
// Get access to the data..not sure if there's a better/proper way
const models = [...bokehDoc._all_models.values()];
const modelWithData = models.find((x) => x.data);
const { data } = modelWithData;
const idx = Math.floor(data.Close.length / 2);
// Store old data so we can reset it
this.orig = data.Close[idx];
data.Close[Math.floor(data.Close.length / 2)] = 0;
modelWithData.change.emit();
},
reset() {
if (!this.orig) {
return;
}
const bokehDoc = this.$refs.frame.contentWindow.Bokeh.documents[0];
// Get access to the data..not sure if there's a better/proper way
const models = [...bokehDoc._all_models.values()];
const modelWithData = models.find((x) => x.data);
const { data } = modelWithData;
const idx = Math.floor(data.Close.length / 2);
data.Close[idx] = this.orig;
modelWithData.change.emit();
delete this.orig;
}
Probably the simplest way is to make your HTML available at the URL of your choice, on your server (regardless of Vue).
Then, in your app, use an <iframe> and point its src to that html. Here's an example, using codesandbox.io, where I placed what you posted into the index.html. Below you can see it working with both <iframe> and <object> tags:
Vue.config.productionTip = false;
Vue.config.devtools = false;
new Vue({
'el': '#app'
})
body {
margin: 0;
}
h1, h3 {padding-left: 1rem;}
object, iframe {
border: none;
height: 800px;
width: 100%;
min-height: calc(100vh - 125px);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h1>This content is placed in Vue</h1>
<h3>Vue doesn't really care.</h3>
<iframe src="https://1gk6z.csb.app/"></iframe>
<h1><code><object></code> works, too:</h1>
<object type="text/html" data="https://1gk6z.csb.app/"></object>
</div>
Note: if the domain serving the graph and the one displaying it differ, you'll need server-side configuration to allow the embed (most domains have it turned off by default).
Strategy:
insert and init bokeh in head tag of public/index.html
read file in a string via ajax/xhr and parse as dom tree
extract each needed dom element from the parsed tree
recreate and append each element
No iframe needed. window.Bokeh is directly accessible.
A skeletal example of reactivity is suggested through the method logBkh that logs the global Bokeh object when clicking on the graph
<template>
<div id="app">
<div id="page-container" #click="logBkh"></div>
</div>
</template>
<script>
// loaded from filesystem for test purposes
import page from 'raw-loader!./assets/page.txt'
// parse as dom tree
const extDoc = new DOMParser().parseFromString(page, 'text/html');
export default {
methods: {
logBkh(){
console.log(window.Bokeh)
}
},
mounted() {
const pageContainer = document.querySelector('#page-container')
// generate and append root div
const dv = document.createElement('div')
const { attributes } = extDoc.querySelector('.bk-root')
for(const attr in attributes) {
dv.setAttribute(attributes[attr].name, attributes[attr].value)
}
pageContainer.append(dv)
for(const _scrpt of extDoc.body.querySelectorAll('script')) {
// generate and append each script
const scrpt = document.createElement('script')
for(const attr in _scrpt.attributes) {
scrpt.setAttribute(
_scrpt.attributes[attr].name,
_scrpt.attributes[attr].value
)
}
scrpt.innerHTML = _scrpt.innerHTML
pageContainer.append(scrpt)
}
}
}
</script>
result:

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.

how can I run an JavaScript function once a <script> appear

hey so I have a problem with my HTML script
so first I had this
index.html
<script src="index.js"></script>
<script src="loadplayer.js"></script>
index.js
const script = document.createElement("div")
script.innerHTML = urlvid
const player = document.getElementById("head")
player.appendChild(script)
in the script, there is a function named loadvideoplayer()
loadplayer.js
function loadvideoplayer(title){
let array = {title}
console.log(array)
console.log(array.title.sources[0].file)
console.log("Loaded")
}
the problem is the function on the script tag from index.js is not executed after the is created
how can i make it executed after the created ?
btw the urlvid contain this
loadvideoplayer(
{
title : '',
tracks: [{
file: '',
kind: 'captions',
'default': true
}],
sources: [{'file':'vidurl','type':'video/mp4'}],
image: "imgurl",
captions:
{
color:'#FFFF00',fontSize:17,backgroundOpacity:50
},
}
);
Move your HTML declaration like this:
<!-- import libraries before executing code -->
<script src="loadplayer.js"></script>
<script src="index.js"></script>
Then you can do something like this:
function loadVideoPlayer(title) {
console.log('Loaded Video: ', title);
}
let script = document.createElement('script');
script.innerHTML = 'console.log("Appended script executed!"); myObject = { title: "Wonderful Stack Overflow Answers" };'
document.head.appendChild(script);
loadVideoPlayer(myObject.title)
Obviously you must know in advance what are the variables in the script you are injecting into your page (like myObject in the example). Without knowing the script you are loading you can't use its variables.
Note that as we are using innerHTML and not src in the appended script, you can just trigger the function just after appended the child into the document head.
If you use src, the browser will ask asynchronously the script, so you must do:
I also warn you on the fact that injecting a script into a page is always a security risk, so you must rely on what you are injecting (it is a secure source?).
script.addEventListener('load', function() {
loadVideoPlayer(myObject.title)
});
Side Note: you could also use eval (MDN), many will state that eval is evil, but you are actually ignoring the same security risks by evaluating the code in the script tag:
function loadVideoPlayer(title) {
console.log('Loaded Video: ', title);
}
let script = 'console.log("Appended script executed!"); myObject = { title: "Wonderful Stack Overflow Answers" };'
eval(script);
loadVideoPlayer(myObject.title)

Vue and Prismic rich text: add event listener to a span node

The content of my Vue app is fetched from Prismic (an API CMS). I have a rich text block, some parts of which are wrapped inside span tags with a specific class. I want to get those span nodes with Vue and add to them an event listener.
With JS, this code would work:
var selectedSpanElements = document.querySelectorAll('.className');
selectedSpanElements[0].style.color = "red"
But when I use this code in Vue, I can see that it works just a fraction of a second before Vue updates the DOM. I've tried using this code on mounted, beforeupdate, updated, ready hooks... Nothing has worked.
Update: Some hours later, I found that with the HTMLSerializer I can add HTML code to the span tag. But this is regular HTML, I cannot access to Vue methods.
#Bruja
I was able to find a solution using a closure. The folks at Prismic reminded/showed me.
Of note, per Phil Snow's comment above: If you are using Nuxt you won't have access to Vue's functionality and will have to go old-school JS.
Here is an example where you can pass in component-level props, data, methods, etc... to the prismic htmlSerializer:
<template>
<div>
<prismic-rich-text
:field="data"
:htmlSerializer="anotherHtmlSerializer((startNumber = list.start_number))"
/>
</div>
</template>
import prismicDOM from 'prismic-dom';
export default {
methods: {
anotherHtmlSerializer(startNumber = 1) {
const Elements = prismicDOM.RichText.Elements;
const that = this;
return function(type, element, content, children) {
// To add more elements and customizations use this as a reference:
// https://prismic.io/docs/vuejs/beyond-the-api/html-serializer
that.testMethod(startNumber);
switch (type) {
case Elements.oList:
return `<ol start=${startNumber}>${children.join('')}</ol>`;
}
// Return null to stick with the default behavior for everything else
return null;
};
},
testMethod(startNumber) {
console.log('test method here');
console.log(startNumber);
}
}
};
I believe you are on the right track looking into the HTML Serializer. If you want all your .specialClass <span> elements to trigger a click event that calls specialmethod() this should work for you:
import prismicDOM from 'prismic-dom';
const Elements = prismicDOM.RichText.Elements;
export default function (type, element, content, children) {
// I'm not 100% sure if element.className is correct, investigate with your devTools if it doesn't work
if (type === Elements.span && element.className === "specialClass") {
return `<span #click="specialMethod">${content}</span>`;
}
// Return null to stick with the default behavior for everything else
return null;
};

How to generate Vue components from 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>

Categories

Resources