KnockoutJS: components binding, working with objects as data type - components

I'm new to Knockout JS and I find this library very powerful, but quite hard to understand sometimes. The documentation is hudge, but it's always (too) small code snippets, so it's difficult to have the big picture, unless your coding style & philosophy paradigm are the same as KO developers.
I come from angular world, and I'm used to have an array where each item is an object with properties (id, name, etc). When I click a button, I "send" this object to a new component that will render it in a form.
I'm sure I'm missing something obvious, but I don't understand how to make things work, even with plugins like ko.mapping and ko.postbox.
Does anyone can help me to find the solution? In the working code above, I've posted my 3 very specific questions in the javascript area.
EDIT: I answered to them, but I don't know if it's a best practice or not.
var
// User class to give to each property the observable capacity
User = function (rawData) {
var self = this,
data = rawData || {};
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
},
// List component. initially in a separate file
// (small modifs so all can be in the same file for this demo)
cmplist = {
viewModel: function () {
var self = this;
self.users = ko.observableArray([
new User({id: 1, name: 'John'}),
new User({id: 2, name: 'Jack'}),
new User({id: 3, name: 'Smith'})
]);
// #ANSWER 1: initialize postbox event
self.user = ko.observable(new User()).publishOn('userEdit');
self.showEdit = function (user) {
// #QUESTION 1: how do I send this object to the
// users-form component. ko.postbox?
// #ANSWER 1: change the observable
self.user(user);
console.log('show', user);
};
},
template: ''
+ '<ul data-bind="foreach: users">'
+ '<li>'
+ '<button data-bind="click: $parent.showEdit">Edit</button>'
+ ' <span data-bind="text: name"></span>'
+ '</li>'
+ '</ul>'
},
// Form component, initially in a separate file
// (small modifs so all can be in the same file for this demo)
cmpform = {
viewModel: function () {
var self = this;
// #QUESTION 2: how do I recept the object sent by the
// list?
// #ANSWER 2: make the observable subscribe to event
self.user = ko.observable(new User()).subscribeTo('userEdit');
self.save = function () {
// #QUESTION 3: how do I notify the users-list cmp
// that object has changed? ko.postbox?
window.alert('save ' + ko.toJSON(self.user()));
console.log('save');
};
},
// #ANSWER 2: call user() with parenthesis to access properties
template: ''
+ '<form>'
+ '<p>Edit user <span data-bind="text: user().name"></span> '
+ 'with id <span data-bind="text: user().id"></span></p>'
+ '<input data-bind="textInput: user().name" />'
+ '<button data-bind="click: save">Save</button>'
+ '</form>'
};
// KO bootstrap, initially in a 3rd file
// (small modifs so all can be in the same file for this demo)
ko.components.register('users-list', cmplist);
ko.components.register('users-form', cmpform);
ko.applyBindings({});
ul {
border: 1px solid blue;
list-style: none;
float: left;
}
li {
border: 1px solid green;
}
form {
border: 1px solid red;
float: right;
margin-top: 20px;
}
ul, li, form {
padding: 5px;
}
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout-postbox/0.5.2/knockout-postbox.min.js"></script>
</head>
<body>
<users-list></users-list>
<users-form></users-form>
</body>
</html>

Related

NetSuite: How can I add a record to a Advanced PDF/HMTL Template?

So I know that I can use the N/render to generate a template and I can use the addRecord to add record objects to the print template to make them available in the FTL.
My question is if I can do something similar when the native print button is clicked and prints a Advanced PDF/HTML Template. I know that I can catch the PRINT event in the User Event script but beyond that I am stuck.
I know the question is a little general I will add context on request. I just don't know which way to go.
EDIT: I am familiar with the option of adding a custpage field to the form and then extracting the JSON in the FTL.
In this specific situation it would be much more convenient if I could simply add a full record. Meaning I am on a Item Fulfillment print and want to add the FULL parent Sales Order record to the print so that I can access it in the FTL by salesorder.memo etc. Something similar to:
require(['N/render'], function(render) {
var renderer = render.create();
renderer.addRecord('customer', record.load({ type: record.Type.CUSTOMER, id: customer }));
})
The issue is that I only know how to do this for completely custom prints but not prints that are printed from the Native print buttons on transactions.
I need this to do line matching from the Sales Order lines to the Item Fulfillment lines and would rather do it this way if possible instead of creating a custpage and inserting a custom made object.
I refer to one of my previous answer.
Use the beforeLoad hook in a UserEventScript to set extra data on the context.form. You'll be able to access this data on the template.
/**
* #NApiVersion 2.x
* #NScriptType UserEventScript
*/
define(['N/ui/serverWidget'], function(serverWidget) {
function beforeLoad(context) {
// var request = context.request;
// var newRecord = context.newRecord;
var form = context.form;
var type = context.type;
var UserEventType = context.UserEventType;
// only execute during printing...
if (type != UserEventType.PRINT) return
var customData = {
hello: 'world'
}
var field = form.addField({
id : 'custpage_custom_data',
label: 'Custom Data',
type : serverWidget.FieldType.LONGTEXT
});
field.defaultValue = JSON.stringify(customData);
}
return {
beforeLoad: beforeLoad
};
})
You can access the data within the template through:
<#if record.custpage_custom_data?has_content>
<#assign custom_data = record.custpage_custom_data?eval />
</#if>
As per your question you want to add item sublist data also from sales order on print of item fulfillment. if it is so, then here I have used for same situation.
Steps:
Write a user event before load script on print mode only and then create a saved search to get the data of item and save it in custom field with long text type with space as label.
Customize your standard pdf template that is attached to item fulfillment record.
GoTo- customization- forms- Advanced pdf template-Customize preferred template for item fulfillment.
Add a table there with that custom field.
It will work on standard print button. I have done it for work order record. You may edit in search using sales order saved search.
UserEvent
/**
*#NApiVersion 2.x
*#NScriptType UserEventScript
*/
define(['N/record', 'N/search', 'N/ui/serverWidget'], function (record, search, serverWidget) {
function beforeLoad(scriptContext) {
try {
if (scriptContext.type == 'print') {
var currentRec = scriptContext.newRecord;
var recid = currentRec.id;
columns[0] = search.createColumn({
name: "sequence",
join: "manufacturingOperationTask",
sort: search.Sort.ASC,
label: "Operation Sequence"
});
columns[1] = search.createColumn({
name: "custevent_custom_op_name",
join: "manufacturingOperationTask",
label: "Operation Name(Instruction)"
});
columns[2] = search.createColumn({
name: "manufacturingworkcenter",
join: "manufacturingOperationTask",
label: "Manufacturing Work Center"
});
columns[3] = search.createColumn({
name: "formulanumeric",
formula: "Round({manufacturingoperationtask.runrate}*{quantity}/60,2)",
label: "BudgetHours"
});
//Creating search to get all the values for work order
var mySearch = search.create({
type: "workorder",
filters:
[
["type", "anyof", "WorkOrd"],
"AND",
["internalid", "anyof", recid],
"AND",
["mainline", "is", "T"]
],
columns: columns
});
var searchResultCount = mySearch.runPaged().count;
mySearch.run().each(function (result) {
// .run().each has a limit of 4,000 results
results.push(result);
return true;
});
//populate current printout with custom record entries
var customRecords = { columns: columns, results: results };
var columns = customRecords.columns, results = customRecords.results;
var custrecord = scriptContext.form.addField({ id: 'custpage_custrecord_to_print', type: serverWidget.FieldType.LONGTEXT, label: " " }),
custrecordArray = [];
if (results && results instanceof Array) {
for (var i = 0; i < results.length; i++) {
var singleLine = {};
for (var j = 0; j < columns.length; j++) {
if (i == i && j == 2) {
var value = results[i].getText(columns[j]);
} else {
var value = results[i].getValue(columns[j]);
}
if (j == 0 || j == 1 || j == 2) {
if (value.indexOf('.') == 0 || value.indexOf(',') == 0 || value.indexOf('-.') == 0 || value.indexOf('-,') == 0) {
value = '0' + value;
}
}
singleLine["col" + j] = (value) ? value : '';
}
custrecordArray.push(singleLine);
}
custrecord.defaultValue = JSON.stringify(custrecordArray);
}
}
} catch (e) {
log.error("ERROR", e);
}
}
return {
beforeLoad: beforeLoad,
};
});
In Advanced Pdf Template:-
<#if record.custpage_custrecord_to_print?has_content>
<#assign customrecord = record.custpage_custrecord_to_print?eval />
<table width="100%" class="second_table" style="page-break-inside: auto; width: 100%; margin-top: 2px; padding-top: 0px">
<#list customrecord as customrecord_line>
<tr width="100%" border-top="solid black" margin-top="10px" style="margin-top:10px; page-break-before: auto;">
<th width="25%" align="left" style="padding: 2px 2px;">Step</th>
<th width="25%" align="center" style="padding: 2px 2px;">Activity</th>
<th width="25%" align="center" style="padding: 2px 2px;">Run Rate(Min/Unit)</th>
<th width="25%" align="center" style="padding: 2px 2px;">BudgetHours</th></tr>
<tr width="100%" style="page-break-inside: auto;">
<td width="25%" align="left" style="padding: 2px 2px;">0${customrecord_line.col0}</td>
<td width="25%" align="center" style="padding: 2px 2px;">${customrecord_line.col2}</td>
<td width="25%" align="center" style="padding: 2px 2px;">${customrecord_line.col3}</td>
<td width="25%" align="center" style="padding: 2px 2px;">${customrecord_line.col4}</td>
</tr>
</list>
</table>
</#if>
It will be helpful.
Thanks,

Empty Div Preventing Interaction with amCharts5 MapChart on Vue3

I decided to dip my toes in Vue and have had an idea for a website for a while which I'd like to use amCharts5 for.
I had some issues initially as all the info I could find was related to Vue2, but I think I've somewhat wrapped my head around Vue3 and its composition API.
The MapChart is created, however there is always a div slapped on top of it which prevent any interaction. If I delete this element via DevTools, the MapChart becomes interactive.
I've tried debugging this and commenting sections of the code out, regardless this div is always created. And I simply can't figure out if it's injected by Vue or if amCharts 5 is the culprit.
The highlighted element is the one I must delete for it to become interactive.
Here's how the component is setup;
<template>
<div class="testClass" ref="chartdiv">
</div>
</template>
<script setup lang="ts">
import * as am5 from "#amcharts/amcharts5";
import * as am5map from "#amcharts/amcharts5/map";
import am5geodata_worldLow from "#amcharts/amcharts5-geodata/worldLow";
import am5themes_Animated from '#amcharts/amcharts5/themes/Animated';
import { ref, onMounted, onUnmounted } from "vue";
const chartdiv = ref<HTMLElement | null>()
var root!: am5.Root;
onMounted(() => {
if (chartdiv.value) {
// Create the Root
var root = am5.Root.new(chartdiv.value);
// Setup the MapChart
var chart = root.container.children.push(
am5map.MapChart.new(root, {
panX: "rotateX",
panY: "rotateY",
projection: am5map.geoOrthographic(),
centerMapOnZoomOut: false
})
);
// Setup Animations
root.setThemes([
am5themes_Animated.new(root)
]);
// Create MapPolygons
var polygonSeries = chart.series.push(
am5map.MapPolygonSeries.new(root, {
geoJSON: am5geodata_worldLow
})
);
// Setup MapPolygon Styling
polygonSeries.mapPolygons.template.setAll({
tooltipText: "{name}",
fill: am5.color("#909090")
});
// Setup MapPolygon Hover Styling
polygonSeries.mapPolygons.template.states.create("hover", {
fill: am5.color("#FF0000"),
stroke: am5.color("#00FF00"),
strokeWidth: 2
});
polygonSeries.mapPolygons.template.events.on("click", function(event) {
//console.log("Clicked: {0}", event.target);
});
// Setup Background
var backgroundSeries = chart.series.unshift(
am5map.MapPolygonSeries.new(root, {})
);
backgroundSeries.mapPolygons.template.setAll({
fill: am5.color(0x2c84d0),
stroke: am5.color(0x2c84d0)
});
backgroundSeries.data.push({
geometry: am5map.getGeoRectangle(90, 180, -90, -180)
});
}
});
onUnmounted(() => {
if (root) {
root.dispose();
}
});
</script>
<style scoped>
.testClass {
width: 50vw;
height: 50vh;
}
</style>
When you create a Vite-powered Vue project, it automatically creates a bunch of CSS files for you. One of those is base.css.
Inside this file, you'll find these lines which causes all the headache;
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
position: relative;
font-weight: normal;
}
Removing those lines will fix the issue.

Text size changes when using html-pdf local vs online

I'm actually using node js and html-pdf lib to generate a pdf on the server which is return as a blob to the client. When I did it on my local machine everything is fine but when I upload it to the server and test it out. The text size increases when in production.
I've created a function that takes an object as a parameter:
async function generatePDF(object) {
let htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body,
html {
font-family: arial, sans-serif;
color: rgb(46, 46, 46);
}
</style>
</head>
<body>
<br />
Date: ${object.todayDate}
<div style="margin-top: 60px">
<h3 style="border-top: 1px solid black">Dr. ${object.name} ${object.firstname}</h3>
</div>
</html>
`;
Down below is the code to generate PDF from the HTML file created:
let sideMargin = "1.5cm";
var options = {
format: "A5",
border: {
top: "1.5cm", // default is 0, units: mm, cm, in, px
right: sideMargin,
bottom: "3.0cm",
left: sideMargin,
},
};
console.log(options);
var path = require("path");
var appDir = path.dirname(require.main.filename);
let date = Date.now();
let newPDF = `${appDir}/files/${date}new.pdf`;
let newHTML = `${appDir}/files/${date}new.html`;
htmlData["fileName"] = newHTML;
console.log(`Generating new PDF '${newPDF}`);
await generatePDF(htmlData);
var html = fs.readFileSync(`${newHTML}`, "utf8");
pdf.create(html, options).toFile(`${newPDF}`, function (err, res) {
if (err) return console.log(err);
console.log(res);
r.sendFile(`${newPDF}`, (error) => {
if (!error) {
fs.unlinkSync(`${newPDF}`);
fs.unlinkSync(`${newHTML}`);
}
});
});
The problem is that locally on my machine everything is fine, but online the text size of the PDF is way bigger, and instead of returning one page it's returning back two pages (of course the content I've set here in the HTML is not the result I want, it is just an example).
Hey i've found what was the problem,
there is no solution to this problem yet. The work around is to add
html {
zoom: 0.55;
}
to the CSS file/code
There's actually an open issue for this.

Display a normal chart or D3Js in Node-red dashboard (over template ?)

Has somebody already achieved to display a chart, not based on timeseries (like the chart from the dashboard), but on data passed to it?
I wonder if it is, perhaps, possible to use a template to do that? But I've no clue how to do it. If someone has a small example, it would be great.
I've found a solution, using some code sample from the library...
The link between msg.payload and data is still missing, but not far away
<style>
#chart svg {
width: 800px;
height: 600px;
}
</style>
<h3>test</h3>
<div id="chart">
<svg></svg>
</div>
<script src="https://raw.githubusercontent.com/novus/nvd3/master/build/nv.d3.js"></script>
<script>
(function(scope) {
console.log('Position 1');
console.dir(scope);
console.log(scope.msg);
scope.$watch('msg.payload', function(data) {
console.log('Position 2');
console.dir(data);
});
})(scope);
function data() {
var sin = [],
cos = [];
for (var i = 0; i < 100; i++) {
sin.push({x: i, y: Math.sin(i/10)});
cos.push({x: i, y: .5 * Math.cos(i/10)});
}
return [
{
values: sin,
key: 'normal',
color: '#ff7f0e'
},
{
values: cos,
key: 'defect',
color: '#2ca02c'
}
];
}
nv.addGraph(function() {
var chart = nv.models.lineChart()
.useInteractiveGuideline(true)
;
chart.xAxis
.axisLabel('frequence')
.tickFormat(d3.format(',r'))
;
chart.yAxis
.axisLabel('amplitude')
.tickFormat(d3.format('.02f'))
;
d3.select('#chart svg')
.datum(data())
.transition().duration(500)
.call(chart)
;
nv.utils.windowResize(chart.update);
return chart;
});
</script>

YUI Uploader hangs after choosing file

Below is my entire code from a User control that contains the YUI Uploader. Is there something I'm missing. Right now, when I step through the javascript code in Firebug, it hangs on the first line of the upload() function. I have a breakpoint on the first line of the ashx that handles the file, but it is never called. So, it doesn't get that far. I figure I'm just missing something stupid. I've used this control many times before with no issues. I'm using all the css files and graphics provided by the samples folder in the YUI download.
If I'm not missing anything, is there a more comprehensive way of debuging this issue then through stepping through the javascript with FireBug. I've tried turning the logging for YUI on and off, and never get any logs anywhere. I'm not sure where to go now.
<style type="text/css">
#divFile
{
background-color:White;
border:2px inset Ivory;
height:21px;
margin-left:-2px;
margin-right:9px;
width:125px;
}
</style>
<ajaxToolkit:RoundedCornersExtender runat="server" Corners="All" Radius="6" ID="rceContainer" TargetControlID="pnlMMAdmin" />
<asp:Panel ID="pnlMMAdmin" runat="server"
Width="100%" BackColor="Silver" ForeColor="#ffffff" Font-Bold="true" Font-Size="16px">
<div style="padding: 5px; text-align:center; width: 100%;">
<table style="width: 100% ; border: none; text-align: left;">
<tr>
<td style="width: 460px; vertical-align: top;">
<!-- information panel -->
<ajaxToolkit:RoundedCornersExtender runat="server" Corners="All" Radius="6" ID="RoundedCornersExtender1" TargetControlID="pnlInfo" />
<asp:Panel ID="pnlInfo" runat="server"
Width="100%" BackColor="Silver" ForeColor="#ffffff" Font-Bold="true" Font-Size="16px">
<div id="infoPanel" style="padding: 5px; text-align:left; width: 100%;">
<table>
<tr><td>Chart</td><td>
<table><tr><td><div id="divFile" ></div></td><td><div id="uploaderContainer" style="width:60px; height:25px"></div></td></tr>
<tr><td colspan="2"><div id="progressBar"></div></td></tr></table>
</td></tr>
</table>
</div></asp:Panel>
<script type="text/javascript" language="javascript">
WYSIWYG.attach('<%= txtComment.ClientID %>', full);
var uploader = new YAHOO.widget.Uploader("uploaderContainer", "assets/buttonSkin.jpg");
uploader.addListener('contentReady', handleContentReady);
uploader.addListener('fileSelect', onFileSelect)
uploader.addListener('uploadStart', onUploadStart);
uploader.addListener('uploadProgress', onUploadProgress);
uploader.addListener('uploadCancel', onUploadCancel);
uploader.addListener('uploadComplete', onUploadComplete);
uploader.addListener('uploadCompleteData', onUploadResponse);
uploader.addListener('uploadError', onUploadError);
function handleContentReady() {
// Allows the uploader to send log messages to trace, as well as to YAHOO.log
uploader.setAllowLogging(false);
// Restrict selection to a single file (that's what it is by default,
// just demonstrating how).
uploader.setAllowMultipleFiles(false);
// New set of file filters.
var ff = new Array({ description: "Images", extensions: "*.jpg;*.png;*.gif" });
// Apply new set of file filters to the uploader.
uploader.setFileFilters(ff);
}
var fileID;
function onFileSelect(event) {
for (var item in event.fileList) {
if (YAHOO.lang.hasOwnProperty(event.fileList, item)) {
YAHOO.log(event.fileList[item].id);
fileID = event.fileList[item].id;
}
}
uploader.disable();
var filename = document.getElementById("divFile");
filename.innerHTML = event.fileList[fileID].name;
var progressbar = document.getElementById("progressBar");
progressbar.innerHTML = "Please wait... Starting upload.... ";
upload(fileID);
}
function upload(idFile) {
// file hangs right here. **************************
progressBar.innerHTML = "Upload starting... ";
if (idFile != null) {
uploader.upload(idFile, "AdminFileUploader.ashx", "POST");
fileID = null;
}
}
function handleClearFiles() {
uploader.clearFileList();
uploader.enable();
fileID = null;
var filename = document.getElementById("divFile");
filename.innerHTML = "";
var progressbar = document.getElementById("progressBar");
progressbar.innerHTML = "";
}
function onUploadProgress(event) {
prog = Math.round(300 * (event["bytesLoaded"] / event["bytesTotal"]));
progbar = "<div style=\"background-color: #f00; height: 5px; width: " + prog + "px\"/>";
var progressbar = document.getElementById("progressBar");
progressbar.innerHTML = progbar;
}
function onUploadComplete(event) {
uploader.clearFileList();
uploader.enable();
progbar = "<div style=\"background-color: #f00; height: 5px; width: 300px\"/>";
var progressbar = document.getElementById("progressBar");
progressbar.innerHTML = progbar;
alert('File Uploaded');
}
function onUploadStart(event) {
alert('upload start');
}
function onUploadError(event) {
alert('upload error');
}
function onUploadCancel(event) {
alert('upload cancel');
}
function onUploadResponse(event) {
alert('upload response');
}
</script>
It seems that there is a case mismatch in the name of the progressbar variable: you refer to it as progressbar everywhere else, but as progressBar in the upload() function.
An even bigger problem is that you define the progressbar variable inside the onFileSelect function. Because of that, the variable is limited in scope and should not be accessible anywhere else.
See if moving the definition for progressbar out of there (or freshly grabbing it from the DOM everywhere it's used by using getElementById) and fixing the case mismatch solves your issues.
YUI 2.8 has issues with events and the uploader. It won't work unless you use 2.9 event and uploader. I wasted more time than I want to admit trying to get 2.8 to work. I hope this saves someone that time.

Resources