odoo15: How can I load js in qweb template? - odoo

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.

Related

Unable to set focus to a textarea, when the page loads

I am trying to set the focus on a syncfusion textarea but I am unable to do so. I have used the this.$nextTick when the component mounts as defined here but the system still does not focus on the textarea.
I have added the same "focus to textarea" code in the created event because somehow the created event is triggered after the mounted event.
I have re-created the issue here.
I also see that this.$refs.vocabularies.$el returns input#vocabularies.e-control.e-textbox.e-lib.
What am I doing wrong?
<template>
<ejs-textbox cssClass="height:500px;" id='vocabularies' :multiline="true" placeholder="Enter your vocabularies" floatLabelType="Auto" :input= "inputHandler" v-model="vocabularies" ref="vocabularies"/>
</template>
<script>
import '#syncfusion/ej2-base/styles/material.css';
import '#syncfusion/ej2-vue-inputs/styles/material.css';
export default
{
data() {
return {
vocabularies: '',
inputHandler: (args) =>
{
args.event.currentTarget.style.height = "auto";
args.event.currentTarget.style.height = (args.event.currentTarget.scrollHeight)+"px";
},
}
},
mounted()
{
this.$nextTick(function()
{
this.$refs.vocabularies.$el.style.height = "auto";
this.$refs.vocabularies.$el.style.height = (this.$refs.vocabularies.$el.scrollHeight)+"px";
this.$refs.vocabularies.$el.focus();
console.log(`mounted run`);
});
},
async created()
{
this.$nextTick(function()
{
this.$refs.vocabularies.$el.style.height = "auto";
this.$refs.vocabularies.$el.style.height = (this.$refs.vocabularies.$el.scrollHeight)+"px";
this.$refs.vocabularies.$el.focus();
console.log(`created run`);
});
},
</script>
So, here's how I've solved it. I am not so sure regarding how good of an approach this is as I haven't worked with syncfusion, so can't say if there might be a better way.
<ejs-textbox cssClass="test" id='vocabularies' :multiline="true" placeholder="Enter your vocabularies" floatLabelType="Auto" :input= "inputHandler" v-model="vocabularies" ref="vocabularies"/>
Then in mounted I did
mounted() {
let a = document.getElementsByClassName('test')[0];
a.children[1].focus();
}
I was able to fix the issue by using this.$refs.vocabularies.focusIn(); in the mounted() method, based on the documentation here
You can focus the text area by using the focusIn public method of the TextBox component in the created event. Kindly refer the below code,
<ejs-textbox cssClass="height:500px;" id='vocabularies' :multiline="true" placeholder="Enter your vocabularies" floatLabelType="Auto" :input= "inputHandler" v-model="vocabularies" ref="vocabularies" :created='onCreated' />
onCreated:function(){
this.$refs.vocabularies.ej2Instances.focusIn()
}
Please find the sample from the below link,
Sample Link:
https://www.syncfusion.com/downloads/support/directtrac/general/ze/quickstart1111977605

Why does variable substitution not work for my case?

I'm trying to create a custom widget using templates, but variable substitution does not seem to be working for me.
I can see the property value being updated inside the widget, but the DOM does not change. For example, when I use the get() method, I can see the new value of the widget's property. However, the DOM never changes its value.
Here is my template:
<div class="outerContainer">
<div class="container">
<span class="mySpan">My name is ${name}</span>
</div>
</div>
Now, here is my widget code:
define([
"dojo/_base/declare",
"dijit/_WidgetBase",
"dijit/_TemplatedMixin",
"dojo/text!/templates/template.html",
], function (declare, _WidgetBase, _TemplatedMixin, template) {
return declare([_WidgetBase, _TemplatedMixin], {
templateString: template,
name: "",
constructor: function (args) {
console.log("calling constructor of the widget");
this.name = args.name;
},
startup: function() {
this.inherited(arguments);
this.set("name", "Robert"); // this does not work
},
postCreate: function() {
this.inherited(arguments);
this.set("name, "Robert"); // this does not work either
},
_setNameAttr: function(newName) {
// I see this printed in the console.
console.log("Setting name to " + newName);
this._set("name", newName);
// I also see the correct value when I get()
console.log(this.get("name")); // This prints Robert
}
});
});
I was expecting the DOM node to say "My name is Robert" i.e. the new value, but it never updates. Instead, it reads "My name is ". It does not overwrite the default value.
I'm sure I'm missing a silly step somewhere, but can someone help me figure out what?
You should bind the property to that point in the dom. So you will need to change the template to this:
<span class="mySpan">My name is <span data-dojo-attach-point='nameNode'></span></span>
And in your widget you should add this function to bind it whenever name changes:
_setNameAttr: { node: "nameNode", type: "innerHTML" },
Now when name changes, it will change the innerHTML of the nameNode inside your mySpan span. If you need to know more about this binding I recommend checking the docs out.
Hope this helps!

How to embed codepen as HTML in vue.js

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>

Twitter typeahead.js not working in Vue component

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.

how to do pagination in vuejs

hi i want to do pagination in my view page.can anyone tell me how to do that in vuejs..
Here is my view page:
<div class="container">
<div class="row">
<el-row :gutter="12">
<el-col>
<p>View Candidates</p>
</el-col>
</el-row>
<el-row :gutter="12">
<template v-for="c in candidates">
<el-col :span="6">
<Candidate :c="c" :key="c.id"></Candidate>
</el-col>
</template>
</el-row>
</div>
here is my js page:
const app = new Vue({
el: '#app',
data() {
return {
candidates: window.data.candidates,
}
},
components: { Candidate }
});
i am working on laravel5.4 and vuejs 2
Please can anyone help me..how to do this..
For real pagination you will need to ensure that your endpoints (from your post I'd say something like /candidates) will return json AND that it will return a pagniated object ofcourse.
In Laravel you'd do it like
public function index() {
return Candidates::paginate(10);
}
EDIT: for more information regarding laravel pagination you can take a look at their examples and docs: https://laravel.com/docs/5.4/pagination
A full example is rather hard to give but here a really short one
routes/web.php
Route::get('candidates', 'CandidateController#index');
app/http/controller/CandidateController
public function index() {
$candidates = App\Candidate::paginate(10);
return $candidates;
}
For a more detailed version of the laravel part you should provide your Controller, Migration, Routing setup.
In Vue I'd suggest you load all your data from within Vue and not with blade. Even though you could keep it as it is - it would be more "unified".
data: function() {
return { paginator: null }
},
created: function() {
// load initial first 10 entries
axios.get('/candidates').then(function(response) {
this.paginator = response.data;
}.bind(this));
}
Ok so now you have the initial load as you had it before. You can loop through pagniator.data which is your actual list now. Small example:
<ul v-if="paginator"><!-- important !not loaded on initial render-->
<li v-for="paginator.data as candidate">{{ candidate.name }}</li>
</ul>
Now to the load more. Let's say you want a button for that. The paginator has a pro called next_page_url to give you the next http endpoint. If it's null - now data is left to load.
<button v-if="paginator && paginator.next_page_url" #click.prevent="loadMore">Load more</button>
Button is setup - now the load more
methods: {
loadMore: function() {
// load next 10 elements
axios.get(this.paginator.next_page_url).then(function(response) {
// you have two options now. Either replace the paginator fully - then you will actually "page" your results.
// only 10 will be visible at any time
this.paginator = response.data;
}.bind(this));
}
}
There you go this is an actual pagination. If you want to loadMore to add 10 elements to your current list it is a little bit more tricky because you don't want to replace the paginator.data with the new loaded stuff. You want to concat it.
...
axios.get(this.paginator.next_page_url).then(function(response) {
response.data.data = this.paginator.data.concat(response.data.data);
this.paginator = response.data;
}.bind(this));