I can't figure out how to embed a codepen using the recommended HTML method i a Vue application.
As <script> tag cannot be part of a Vue component template, I tried to add it to index.html where my Vue application is injected without luck. However, when I tried to paste the html code outside the div where Vue resides, the code got turned into an iFrame as it should.
Here is the HTML embed:
<p data-height="265" data-theme-id="0" data-slug-hash="JyxKMg" data-default-tab="js,result" data-user="sindael" data-embed-version="2" data-pen-title="Fullscreen image gallery using Wallop, Greensock and Flexbox" class="codepen">See the Pen Fullscreen image gallery using Wallop, Greensock and Flexbox by Dan (#sindael) on CodePen.</p>
And the script:
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
Embedding an iFrame directly works fine, but I wonder. Is there a way how to get the html working?
Look into the https://static.codepen.io/assets/embed/ei.js, then you will see it executes two steps:
check document.getElementsByClassName if exists, create it if not.
one IIFE to execute the embed.
So one hacky way as below simple demo:
copy the source codes from https://static.codepen.io/assets/embed/ei.js
copy the codes of first step then wrap it as one function = _codepen_selector_contructor
copy the codes of second step and remove () from the end, then wrap it as one function = _codepen_embed_method
create one vue-directive (I prefer using the directive to support the features which directly process Dom elements, you can use other solutions), then execute _codepen_selector_contructor and _codepen_embed_method
Probably you want to replace document inside _codepen_embed_method with el instead, then execute _codepen_embed_method(el). so that it will not affects other elements.
Below demo uses the hook='inserted', you could use other hooks if inserted can't meet your requirements.
let vCodePen = {}
vCodePen.install = function install (Vue) {//copy from https://static.codepen.io/assets/embed/ei.js
let _codepen_selector_contructor = function () {
document.getElementsByClassName||(document.getElementsByClassName=function(e){var n,t,r,a=document,o=[];if(a.querySelectorAll)return a.querySelectorAll("."+e);if(a.evaluate)for(t=".//*[contains(concat(' ', #class, ' '), ' "+e+" ')]",n=a.evaluate(t,a,null,0,null);r=n.iterateNext();)o.push(r);else for(n=a.getElementsByTagName("*"),t=new RegExp("(^|\\s)"+e+"(\\s|$)"),r=0;r<n.length;r++)t.test(n[r].className)&&o.push(n[r]);return o})
}
let _codepen_embed_method = //copy from https://static.codepen.io/assets/embed/ei.js then removed `()` from the end
function(){function e(){function e(){for(var e=document.getElementsByClassName("codepen"),t=e.length-1;t>-1;t--){var u=a(e[t]);if(0!==Object.keys(u).length&&(u=o(u),u.user=n(u,e[t]),r(u))){var c=i(u),l=s(u,c);f(e[t],l)}}m()}function n(e,n){if("string"==typeof e.user)return e.user;for(var t=0,r=n.children.length;t<r;t++){var a=n.children[t],o=a.href||"",i=o.match(/codepen\.(io|dev)\/(\w+)\/pen\//i);if(i)return i[2]}return"anon"}function r(e){return e["slug-hash"]}function a(e){for(var n={},t=e.attributes,r=0,a=t.length;r<a;r++){var o=t[r].name;0===o.indexOf("data-")&&(n[o.replace("data-","")]=t[r].value)}return n}function o(e){return e.href&&(e["slug-hash"]=e.href),e.type&&(e["default-tab"]=e.type),e.safe&&("true"===e.safe?e.animations="run":e.animations="stop-after-5"),e}function i(e){var n=u(e),t=e.user?e.user:"anon",r="?"+l(e),a=e.preview&&"true"===e.preview?"embed/preview":"embed",o=[n,t,a,e["slug-hash"]+r].join("/");return o.replace(/\/\//g,"//")}function u(e){return e.host?c(e.host):"file:"===document.location.protocol?"https://codepen.io":"//codepen.io"}function c(e){return e.match(/^\/\//)||!e.match(/https?:/)?document.location.protocol+"//"+e:e}function l(e){var n="";for(var t in e)""!==n&&(n+="&"),n+=t+"="+encodeURIComponent(e[t]);return n}function s(e,n){var r;e["pen-title"]?r=e["pen-title"]:(r="CodePen Embed "+t,t++);var a={id:"cp_embed_"+e["slug-hash"].replace("/","_"),src:n,scrolling:"no",frameborder:"0",height:d(e),allowTransparency:"true",allowfullscreen:"true",allowpaymentrequest:"true",name:"CodePen Embed",title:r,"class":"cp_embed_iframe "+(e["class"]?e["class"]:""),style:"width: "+p+"; overflow: hidden;"},o="<iframe ";for(var i in a)o+=i+'="'+a[i]+'" ';return o+="></iframe>"}function d(e){return e.height?e.height:300}function f(e,n){if(e.parentNode){var t=document.createElement("div");t.className="cp_embed_wrapper",t.innerHTML=n,e.parentNode.replaceChild(t,e)}else e.innerHTML=n}function m(){"function"==typeof __CodePenIFrameAddedToPage&&__CodePenIFrameAddedToPage()}var p="100%";e()}function n(e){/in/.test(document.readyState)?setTimeout("window.__cp_domReady("+e+")",9):e()}var t=1;window.__cp_domReady=n,window.__CPEmbed=e,n(function(){new __CPEmbed})}
let defaultProps = {class: 'codepen', 'data-height':265, 'data-theme-id':0, 'data-slug-hash':'', 'data-default-tab':'js,result', 'data-user':'sindael', 'data-embed-version':'2', 'data-pen-title':''}
Vue.directive('code-pen', {
inserted: function (el, binding, vnode) {
let options = Object.assign({}, defaultProps, binding.value)
Object.entries(options).forEach((item) => {
el.setAttribute(item[0], item[1])
})
setTimeout(() => {
_codepen_selector_contructor()
_codepen_embed_method() //_codepen_embed_method(el); you can pass el to take place of `document`
}, 100)
},
componentUpdated: function (el, binding, vnode) {
}
})
}
Vue.use(vCodePen)
Vue.config.productionTip = false
app = new Vue({
el: "#app",
data: {
keyword: '',
},
mounted: function () {
},
methods: {
}
})
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<p v-code-pen="{class: 'codepen', 'data-height':'265', 'data-theme-id':0, 'data-slug-hash':'JyxKMg', 'data-default-tab':'js,result', 'data-user':'sindael', 'data-embed-version':'2', 'data-pen-title':'Test'}">
</p>
</div>
Related
I'm using vue 2. I have a text get from api.
"Hello everyone! My name is [input]. I'm [input] year old".
Now, I have to replace the [input] with an html input and handle the onKeyUp for this input.
What I have to do?
I used computed render html, but it not work with v-on:xxx.
content.replaceAll('[answer]', '<input type="text" class="input_answer" v-on:click="handleInput()"/>')
Thanks!
After spending an hour and so on this requirement, I came up with the solution.
Here you go (I added all the descriptive comments/steps in the below code snippet itself) :
// Template coming from API
var textFromAPI = "<p>Hello everyone! My name is [input]. I'm [input] year old</p>";
// getting the array of input tags. So that we can loop and create the proper input element.
const matched = textFromAPI.match(/(input)/g);
// Iterating over an array of matched substrings and creating a HTML element along with the required attributes and events.
matched.forEach((el, index) => {
textFromAPI = textFromAPI.replace('[input]', `<input type="text" id="${index + 1}" v-model="inputValue[${index}]" v-on:keyup="getValue"/>`);
})
// Here, we are compiling the whole string so that it will behave in a Vue way.
var res = Vue.compile(textFromAPI)
var app = new Vue({
el: '#app',
data: {
compiled: null,
inputValue: []
},
render: res.render,
staticRenderFns: res.staticRenderFns,
mounted() {
setTimeout(() => {
this.compiled = res;
})
},
methods: {
getValue() {
// Here you will get the updated values of the inputs.
console.log(this.inputValue);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
</div>
Thanks # Rohit Jíndal, but!
When I use vue2 and it doesn't work. And there is an error:
TypeError: Cannot read properties of undefined (reading 'string')
I build this to a component and use anywhere in my project.
<render-html :text="question.quest_content" #handleAnswer="handleAnswer"></render-html>
I used and it's work.
this.$options.staticRenderFns = string.staticRenderFns;
return string.render.bind(this)(h)
Thanks so much!
I need to add events in some buttons in qweb template or maybe make a widget for this template. But I can't load js in this template, even if I add js file in web.assets_backend or web.assets_frontend.
controller.py
from odoo import http
class LogData(http.Controller):
#http.route("/log_data", type="http", auth="user")
def log_data_view(self, **kwargs):
return http.request.render(
"table_relation.log_data_template"
)
log_data_template.xml
<odoo>
<template id="log_data_template" name="Log Data">
<t t-call="web.layout">
<t t-set="head">
<t t-call-asssets="web.assets_common" t-js="false"/>
<t t-call-asssets="web.assets_frontend" t-js="false"/>
</t>
<div id="wrap" class="container">
<h1>Log Data</h1>
<div class="o_log_data">
<button id="start-log">日志</button>
<button id="cancel-log">停止</button>
<div id="log-content" style="height:500px;overflow: scroll;"/>
</div>
<button type="button" class="demo-btn">demo button</button>
</div>
</t>
</template>
</odoo>
log_data.js
odoo.define('log_data', function (require){
'use strict';
var publicWidget = require('web.public.widget');
console.log('==========')
publicWidget.registry.LogData = publicWidget.Widget.extend({
selector: '.o_log_data',
events: {
'click #start-log': '_startLog',
'click #cancel-log': '_cancelLog',
},
init: function () {
console.log('o_log_data')
},
start: function () {
console.log('o_log_data')
},
_startLog: function () {
console.log('_startLog')
},
});
publicWidget.registry.DemoBtn = publicWidget.Widget.extend({
selector: '.demo-btn',
events: {
click: '_onClick'
},
_onClick: function (e) {
console.log('_onClick')
},
});
})
manifest.py
'assets': {
'web.assets_backend': [
...
'table_relation/static/src/js/log_data.js',
]
'web.assets_frontend': [
...
'table_relation/static/src/js/log_data.js',
]
...
}
enter image description here
It seems not to load assets_backend bundle on this page, and log_data.js is not working.
As per the code you have mentioned above, it seems like you are trying to create a controller i.e a route that can be accessed by the User only but from the website side like the portal or eCommerce part.
So if that is the case, then you need to add your js files to web.assets_frontend instead of web.assets_backend in manifest.py file.
The answer is late but in case you still need this you can try the following code. It works in Odoo14 and should definitely work in Odoo15 as well since class Widget still have this statement:
// Now this class can simply be used with the following syntax::
// var myWidget = new MyWidget(this);
// myWidget.appendTo($(".some-div"));
You can find the reference here:
https://github.com/odoo/odoo/blob/f1f3fcef6cc471fc01da574da26712e643315da6/addons/web/static/src/legacy/js/core/widget.js#L49-L52
And your code refactored by following those instructions.
odoo.define('log_data', function (require){
'use strict';
var publicWidget = require('web.public.widget');
console.log('==========')
publicWidget.registry.LogData = publicWidget.Widget.extend({
selector: '.o_log_data',
events: {
'click #start-log': '_startLog',
'click #cancel-log': '_cancelLog',
},
init: function () {
console.log('o_log_data')
},
start: function () {
console.log('o_log_data')
},
_startLog: function () {
console.log('_startLog')
},
});
var LogData = new publicWidget.registry.LogData(this);
LogData.appendTo($(".o_log_data"));
publicWidget.registry.DemoBtn = publicWidget.Widget.extend({
selector: '.demo-btn',
events: {
click: '_onClick'
},
_onClick: function (e) {
console.log('_onClick')
},
});
var DemoBtn = new publicWidget.registry.DemoBtn(this);
DemoBtn.appendTo($(".demo-btn"));
});
I think that publicWidget are only rendered, as Krutarth Patel said, in the scope of some layout like 'website.layout'. I cannot provide much info about this because I still have to figure out the layouts, for example 'portal.layout' scope was not rendering the publicWidget extension for me.
But the Widget class is designed in a way that allows to 'force' render of the widget by inserting it into the dom, render, bound to specific selector and events.
So you can (probably?) automatically render by wrapping your template with the proper t-call, otherwise you can use the 'appendTo' syntax and append the widget to the DOM, make an istance out of it and use that istance.
I've been struggling with this for a couple of days before I could make it work, and I found a couple of post like this.
I hope this will help you, or other users to figure out some of the use you can make of odoo widgets.
I want to display an unorderd list or table in a leaflet popup.
The number of items and their content are different and depend on the type of element which was clicked.
So ideally the popup content should be created on the click event.
I tried to build the list inside the bindPopup function, but it's not working.
L.marker([mapElement.y * -1, mapElement.x], {
uniqueID: mapElement.elementID,
mapIconWidth: mapElement.width,
icon: new mapIcon({
iconUrl: icon.mapIcon.imageData,
iconSize: [elementSize, elementSize]
})
})
.addTo(markers)
.bindPopup(mapElement.element.nbr + ' ' + mapElement.element.name + "<br/<ul> <li v-for='state in mapElement.element.states'>{{ state.definition.stateTypeTitle }}</li> </ul>");
That's the output:
Any ideas would be great!
Thanks!
Edited code (get this error message: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.):
LoadMarker(mapID) {
console.log("Load Markers");
map.removeLayer(markers);
markers.clearLayers();
fetch(this.$apiUrl + "/mapelements/" + mapID)
.then(response => response.json())
.then(received => {
let mapElements = received;
let mapZoom = map.getZoom();
mapElements.forEach(function(mapElement) {
let elementSize = (mapElement.width / 8) * mapZoom;
let popup = new Vue({
template:
mapElement.element.nbr +
" " +
mapElement.element.name +
"<br/<ul> <li v-for='state in mapElement.element.states'>{{ state.definition.stateTypeTitle }}</li> </ul>",
data: {
mapElement
}
}).$mount().$el;
let icon = mapIconSchemas.find(
schema => schema.mapIconSchemaID == mapElement.mapIconSchemaID
);
if (icon != null) {
L.marker([mapElement.y * -1, mapElement.x], {
uniqueID: mapElement.elementID,
mapIconWidth: mapElement.width,
icon: new mapIcon({
iconUrl: icon.mapIcon.imageData,
iconSize: [elementSize, elementSize]
})
})
.addTo(markers)
.bindPopup(popup);
}
});
});
map.addLayer(markers);
},
You can not use Vue templating syntax in the HTML String for the popup. But as can be seen from the docs the .bindPopup method can also accept HTML element. So your way to go would be like this:
first create the popup element:
let popup = new Vue({
template: mapElement.element.nbr + ' ' + mapElement.element.name + "<br/<ul> <li v-for='state in mapElement.element.states'>{{ state.definition.stateTypeTitle }}</li> </ul>",
data: {
mapElement
}
}).$mount().$el
and then use it in the .bindPopup method:
/*...*/
.bindPopup(popup)
There is a solution, if you want to use the vue templating engine to fill the popup content.
I explained it for this question.
You create a component with the content you want to display in the popup, but you hide it :
<my-popup-content v-show=False ref='foo'><my-popup-content>
Then you can access the generated html of that component in your code like this :
const template = this.$refs.foo.$el.innerHTML
and use it to fill your popup.
The big advantage of that method is that you can generate the popup content with all the vue functionalities (v-if, v-bind, whatever) and you don't need messy string concatenations anymore.
import Component from './Component.vue'
import router from './router'
import store from './store'
bindPopup(() => new Vue({
// router,
// store,
render: h => h(Component)
}).$mount().$el)
perfect
I'm trying to use Twitter's typeahead.js in a Vue component, but although I have it set up correctly as tested out outside any Vue component, when used within a component, no suggestions appear, and no errors are written to the console. It is simply as if it is not there. This is my typeahead setup code:
var codes = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('code'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
prefetch: contextPath + "/product/codes"
});
$('.typeahead').typeahead({
hint: true,
highlight: true,
minLength: 3
},
{
name: 'codes',
display: 'code',
source: codes,
templates: {
suggestion: (data)=> {
return '<div><strong>' + data.code + '</strong> - ' + data.name + '</div>';
}
}
});
I use it with this form input:
<form>
<input id="item" ref="ttinput" autocomplete="off" placeholder="Enter code" name="item" type="text" class="typeahead"/>
</form>
As mentioned, if I move this to a div outside Vue.js control, and put the Javascript in a document ready block, it works just fine, a properly formatted set of suggestions appears as soon as 3 characters are input in the field. If, however, I put the Javascript in the mounted() for the component (or alternatively in a watch, I've tried both), no typeahead functionality kicks in (i.e., nothing happens after typing in 3 characters), although the Bloodhound prefetch call is made. For the life of me I can't see what the difference is.
Any suggestions as to where to look would be appreciated.
LATER: I've managed to get it to appear by putting the typeahead initialization code in the updated event (instead of mounted or watch). It must have been some problem with the DOM not being in the right state. I have some formatting issues but at least I can move on now.
The correct place to initialize Twitter Typeahead/Bloodhound is in the mounted() hook since thats when the DOM is completely built. (Ref)
Find below the relevant snippet: (Source: https://digitalfortress.tech/js/using-twitter-typeahead-with-vuejs/)
mounted() {
// configure datasource for the suggestions (i.e. Bloodhound)
this.suggestions = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('title'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
identify: item => item.id,
remote: {
url: http://example.com/search + '/%QUERY',
wildcard: '%QUERY'
}
});
// get the input element and init typeahead on it
let inputEl = $('.globalSearchInput input');
inputEl.typeahead(
{
minLength: 1,
highlight: true,
},
{
name: 'suggestions',
source: this.suggestions,
limit: 5,
display: item => item.title,
templates: {
suggestion: data => `${data.title}`;
}
}
);
}
You can also find a working example: https://gospelmusic.io/
and a Reference Tutorial to integrate twitter typeahead with your VueJS app.
I am using Riot JS and in my index.html, I have 3 custom tags - header, login-panel and candidates-panel inside my body. In my main app.js, in the callback function of $(document).ready, I execute the current route and also register a route change handler function. In my switchView, I unmount all custom tags and then try to mount only the tag pertaining to the current view being switched. Here is my code. If I do unmount, then nothing is displayed on the page
index.html
<body>
<header label="Hire Zen" icon="img/user-8-32.png"></header>
<login-panel class="viewTag" id="loginView"></login-panel>
<candidates-panel id="candidatesView" class="viewTag"></candidates-panel>
<script src="js/bundle.js"></script>
</body>
app.js
function switchView(view) {
if(!view || view === '') {
view = 'login'
}
//unmount all other panels and mount only the panel that is required
//TODO: unmount all view panels and mounting only required panel is not working
//riot.unmount('.viewTag')
riot.mount(view+'-panel')
$('.viewTag').hide()
$(view+'-panel').show()
}
$(document).ready(function () {
RiotControl.addStore(new AuthStore())
RiotControl.addStore(new CandidatesStore())
riot.mount('header')
//register route change handler
riot.route(function (collection, id, action) {
switchView(collection)
})
riot.route.exec(function (collection, id, action) {
switchView(collection)
})
})
Answer for riot.js v2.1.0:
The function
riot.unmount(...)
is not available as far as I know. However, you can unmount saved tags.
mytag.unmount(true)
Source
The trick is to remember the mounted tags to be able to unmount them later:
var viewTag = riot.mount(document.getElementById('viewTag'))
viewTag.unmount(true)
You can store all those view tags in an object and loop them to unmount all and mount only the active one.
Source
Answer for 2.3.18
Based on the previous answer and this tutorial I have created following concept:
app.currentPage = null;
var goTo = function(page){
if (app.currentPage) {
app.currentPage.unmount(true); //unmount and keep parent tag
}
app.currentPage = riot.mount(page)[0]; //remember current page
};
riot.route(function() {
console.info("this page is not defined");
//do nothing (alternatively go to 404 page or home)
});
riot.route('/inventory', function(){
goTo('inventory');
});
riot.route('/options', function() {
goTo('options');
});
I think you are looking for riot.util.tags.unmountAll(tags)
How to achieve the goal?
index.html
var tags = [];
some.tag.html
var some = this;
tags.push(some);
unmountAllTags.js
riot.util.tags.unmountAll(tags);