Lit web component not updated on attribute change - web

I'm changing an attribute of a Lit web component, but the changed value won't render.
I have an observed array: reports[] that will be populated in firstUpdated() with reports urls fetched from rest apis. The loading of the array is done by:
this.reports.push({ "name" : report.Name, "url" : this.apiUrl + "/" + report.Name + "?rs:embed=true" });
see below:
import { LitElement, html, css } from 'lit';
import {apiUrl, restApiUrl} from '../../config';
export default class Homepage extends LitElement {
static properties = {
apiUrl: '',
restApiUrl: '',
reports: []
}
...
constructor() {
super();
this.apiUrl = apiUrl;
this.restApiUrl= restApiUrl;
this.reports = [];
}
firstUpdated() {
...
// Fetch all reports from restApiUrl:
rsAPIDetails(restApiUrl).then(reports =>{
for(const report of reports.value)
{
rsAPIDetails(restApiUrl + "(" + report.Id + ")/Policies").then(policies => {
for(const policy of policies.Policies)
{
if(policy.GroupUserName.endsWith(usernamePBI))
{
for(const role of policy.Roles)
{
if(role != null && (role.Name== "Browser" || role.Name== "Content Manager"))
{
// User has access to this report so i'll push it to the list of reports that will show in the navbar:
this.reports.push({ "name" : report.Name, "url" : this.apiUrl + "/" + report.Name + "?rs:embed=true" });
}
}
}
}
});
}
}).then(q => {
console.log(this.reports);
});
}
render() {
return html`
<div id="sidenav" class="sidenav">
...
<div class="menucateg">Dashboards</div>
${this.reports.map((report) =>
html`<a #click=${() => this.handleMenuItemClick(report.url)}>${report.name}</a>`
)}
<div class="menucateg">Options</div>
</div>
`;
}
At console I can clearly see that the array is loaded with the correct values.
But the render() function won't update the web component with the new values of reports[]:
The links should be added inside 'Dashboards' div
If instead I statically populate reports[] with values (in the ctor), it renders the links just fine.
So why isn't the component updated when the observed array is changed ?
Thank you!

Array.push mutates the array, but doesn't change the actual value in memory.
To have LitElement track updates to arrays and objects, the update to the value needs to be immutable.
For example, we can make your example work by doing it this way:
const newReports = this.reports.slice();
newReports.push({ "name" : report.Name, "url" : this.apiUrl + "/" + report.Name + "?rs:embed=true" });
this.reports = newReports;
Or by using array spread
this.reports = [...this.reports, { "name" : report.Name, "url" : this.apiUrl + "/" + report.Name + "?rs:embed=true" }]
The reason why this works is that when you do this.reports.push(), you're not actually changing the "reference" of this.reports, you're just adding an object to it. On the other hand, when you re-define the property with this.reports = ..., you are changing the "reference", so LitElement knows the value has changed, and it will trigger a re-render.
This is also true for objects. Let's say you have a property obj. If you updated the object by just adding a property to, the element wouldn't re-render.
this.obj.newProp = 'value';
But if you re-define the object property as a whole by copying the object and adding a property, it will cause the element to update properly.
this.obj = {...this.obj, newProp: 'value'}
You can see the values that are getting tracked and updated by using the updated method.

Related

Svelte access a store variable in child component script tag

I have a Svelte & Sapper app where I am using a Svelte writable store to set up a variable with an initial blank string value:
import { writable } from 'svelte/store';
export let dbLeaveYear = writable('');
In my Index.svelte file I am importing this and then working out the value of this variable and setting it (I am doing this within the onMount function of ```Index.svelte if this is relevant):
<script>
import {dbLeaveYear} from "../stores/store.js"
function getCurrentLeaveYear() {
const today = new Date();
const currYear = today.getFullYear();
const twoDigitYear = currYear.toString().slice(-2);
const cutoffDate = `${twoDigitYear}-04-01`
const result = compareAsc(today, new Date(cutoffDate));
if (result === -1) {
$dbLeaveYear = `${parseInt(twoDigitYear, 10) - 1}${twoDigitYear}`;
} else {
$dbLeaveYear = `${twoDigitYear}${parseInt(twoDigitYear, 10) + 1}`;
}
}
onMount(() => {
getCurrentLeaveYear();
});
</script>
I have a child component being rendered in the Index.svelte
<Calendar />
Inside the Calendar child component I am importing the variable and trying to access it to perform a transform on it but I am getting errors that it is still blank - it is seemingly not picking up the assignment from Index.svelte:
<script>
import {dbLeaveYear} from "../stores/store.js"
const calStart = $dbLeaveYear.slice(0, 2)
</script>
However if I use the value in an HTML element in the same Calendar child component with <p>{$dbLeaveYear}</p> it is populated with the value from the calculation in Index.svelte.
How can I access the store variable inside the <script> tag of the child component? Is this even possible? I've tried assiging in onMount, I've tried assigning in a function - nothing seems to work and it always says that $dbLeaveYear is a blank string.
I need the value to be dynamic as the leave year value can change.
Before digging deeper into your problem, let me say that you shouldn't mutate the store variable directly, but use the provided set or update method. This avoids hard-to-debug bugs:
if (result === -1) {
dbLeaveYear.set(() => `${parseInt(twoDigitYear, 10) - 1}${twoDigitYear}`);
} else {
dbLeaveYear.set(`${twoDigitYear}${parseInt(twoDigitYear, 10) + 1}`);
}
With that out of the way, the problem seems to be that your auto-subscribe to the store is not ideal for your use case. You need to use the subscribe property for that:
<script>
import { dbLeaveYear } from "../stores/store.js"
import { onDestroy, onMount } from "svelte"
let yearValue;
// Needed to avoid memory leaks
let unsubscribe
onMount(() => {
unsubscribe = dbLeaveYear.subscribe(value => yearValue = value.slice(0, 2));
})
onDestroy(unsubscribe);
</script>
Another thing that could cause your problem is a race condition. So the update from the parent component is not finished when the child renders. Then you would need to add a sanity check in the rendering child component.
The answer here is a combination of Sapper preload and the ability to export a function from a store.
in store.js export the writable store for the variable you want and also a function that will work out the value and set the writable store:
export let dbLeaveYear = writable('');
export function getCurrentLeaveYear() {
const today = new Date();
const currYear = today.getFullYear();
const twoDigitYear = currYear.toString().slice(-2);
const cutoffDate = `${twoDigitYear}-04-01`
const result = compareAsc(today, new Date(cutoffDate));
if (result === -1) {
dbLeaveYear.set(`${parseInt(twoDigitYear, 10) - 1}${twoDigitYear}`);
} else {
dbLeaveYear.set(`${twoDigitYear}${parseInt(twoDigitYear, 10) + 1}`);
}
}
In the top-level .svelte file, use Sapper's preload() function inside a "module" script tag to call the function that will work out the value and set the writable store:
<script context="module">
import {getCurrentLeaveYear} from '../stores/store'
export async function preload() {
getCurrentLeaveYear();
}
</script>
And then in the component .svelte file, you can import the store variable and because it has been preloaded it will be available in the <script> tag:
<script>
import {dbLeaveYear} from '../stores/store'
$: startDate = `20${$dbLeaveYear.slice(0, 2)}`
$: endDate = `20${$dbLeaveYear.slice(-2)}`
</script>

How do I download data trees to CSV?

How can I export nested tree data as a CSV file when using Tabulator? I tried using the table.download("csv","data.csv") function, however, only the top-level data rows are exported.
It looks like a custom file formatter or another option may be necessary to achieve this. It seems silly to re-write the CSV downloader, so while poking around the csv downloader in the download.js module, it looks like maybe adding a recursive function to the row parser upon finding a "_children" field might work.
I am having difficulty figuring out where to get started.
Ultimately, I need to have the parent-to-child relationship represented in the CSV data with a value in a parent ID field in the child rows (this field can be blank in the top-level parent rows because they have no parent). I think I would need to include an ID and ParentID in the data table to achieve this, and perhaps enforce the validation of that key using some additional functions as data is inserted into the table.
Below is currently how I am exporting nested data tables to CSV. This will insert a new column at the end to include a parent row identifier of your choice. It would be easy to take that out or make it conditional if you do not need it.
// Export CSV file to download
$("#export-csv").click(function(){
table.download(dataTreeCSVfileFormatter, "data.csv",{nested:true, nestedParentTitle:"Parent Name", nestedParentField:"name"});
});
// Modified CSV file formatter for nested data trees
// This is a copy of the CSV formatter in modules/download.js
// with additions to recursively loop through children arrays and add a Parent identifier column
// options: nested:true, nestedParentTitle:"Parent Name", nestedParentField:"name"
var dataTreeCSVfileFormatter = function(columns, data, options, setFileContents, config){
//columns - column definition array for table (with columns in current visible order);
//data - currently displayed table data
//options - the options object passed from the download function
//setFileContents - function to call to pass the formatted data to the downloader
var self = this,
titles = [],
fields = [],
delimiter = options && options.delimiter ? options.delimiter : ",",
nestedParentTitle = options && options.nestedParentTitle ? options.nestedParentTitle : "Parent",
nestedParentField = options && options.nestedParentField ? options.nestedParentField : "id",
fileContents,
output;
//build column headers
function parseSimpleTitles() {
columns.forEach(function (column) {
titles.push('"' + String(column.title).split('"').join('""') + '"');
fields.push(column.field);
});
if(options.nested) {
titles.push('"' + String(nestedParentTitle) + '"');
}
}
function parseColumnGroup(column, level) {
if (column.subGroups) {
column.subGroups.forEach(function (subGroup) {
parseColumnGroup(subGroup, level + 1);
});
} else {
titles.push('"' + String(column.title).split('"').join('""') + '"');
fields.push(column.definition.field);
}
}
if (config.columnGroups) {
console.warn("Download Warning - CSV downloader cannot process column groups");
columns.forEach(function (column) {
parseColumnGroup(column, 0);
});
} else {
parseSimpleTitles();
}
//generate header row
fileContents = [titles.join(delimiter)];
function parseRows(data,parentValue="") {
//generate each row of the table
data.forEach(function (row) {
var rowData = [];
fields.forEach(function (field) {
var value = self.getFieldValue(field, row);
switch (typeof value === "undefined" ? "undefined" : _typeof(value)) {
case "object":
value = JSON.stringify(value);
break;
case "undefined":
case "null":
value = "";
break;
default:
value = value;
}
//escape quotation marks
rowData.push('"' + String(value).split('"').join('""') + '"');
});
if(options.nested) {
rowData.push('"' + String(parentValue).split('"').join('""') + '"');
}
fileContents.push(rowData.join(delimiter));
if(options.nested) {
if(row._children) {
parseRows(row._children, self.getFieldValue(nestedParentField, row));
}
}
});
}
function parseGroup(group) {
if (group.subGroups) {
group.subGroups.forEach(function (subGroup) {
parseGroup(subGroup);
});
} else {
parseRows(group.rows);
}
}
if (config.columnCalcs) {
console.warn("Download Warning - CSV downloader cannot process column calculations");
data = data.data;
}
if (config.rowGroups) {
console.warn("Download Warning - CSV downloader cannot process row groups");
data.forEach(function (group) {
parseGroup(group);
});
} else {
parseRows(data);
}
output = fileContents.join("\n");
if (options.bom) {
output = "\uFEFF" + output;
}
setFileContents(output, "text/csv");
};
as of version 4.2 it is currently not possible to include tree data in downloads, this will be comming in a later release

Syncing React-Slick with Query Params

I'm using React-Slick to render <Report /> components in a carousel. I would like to sync each <Report />'s reportId with query params.
For example, a user would be able to see a specific report by going to myapp.com/reports?id=1 and it would take them to that specific "slide".
The problem I'm having is that the report data is being loaded before the slides are initialized. I can't find any good examples of react-slick's onInit or onReInit.
Instead of using onInit or onReInit, I just utilized the initialSlide setting and used componentDidUpdate().
componentDidUpdate = (prevProps, prevState) => {
const queryParams = qs.parse(this.props.location.search)
if (prevProps.reports !== this.props.reports) {
const sortedReports = this.sortReportsByDate(this.props.reports)
this.setSlideIndex(sortedReports.findIndex(i => i.id === parseInt(queryParams.reportId, 10)))
this.setQueryParams({
reportId: this.sortReportsByDate(this.props.reports)[this.state.slideIndex].id
})
}
if (prevState.slideIndex !== this.state.slideIndex) {
this.setQueryParams({
reportId: this.sortReportsByDate(this.props.reports)[this.state.slideIndex].id
})
}
}
And the settings:
const settings = {
...
initialSlide: reportsSortedByDate.findIndex(i => i.id === parseInt(queryParams.reportId, 10))
...
}
I hope someone finds this useful!

Trying to encapsulate TextInput, getting the addField error

I'm trying to encapsulate a TextInput such that when the value changes it does a serverside lookup and based on the result shows a notification to the user: "That group name already exists". I've been using this as my example to start from: https://marmelab.com/admin-on-rest/Actions.html#the-simple-way
My current error is
Error: The TextInput component wasn't called within a redux-form . Did you decorate it and forget to add the addField prop to your component? See https://marmelab.com/admin-on-rest/Inputs.html#writing-your-own-input-component for details.
but even if I add in
NameLookupTextInput.defaultProps = {
addField: true, // require a <Field> decoration
}
I still get the error. Heres my code
class NameLookupTextInput extends TextInput {
handleChange = eventOrValue => {
console.log("handleChange",eventOrValue);
this.props.onChange(eventOrValue);
this.props.input.onChange(eventOrValue);
if(this.timeoutHandle){
clearTimeout(this.timeoutHandle);
}
console.log(fetch);
this.timeoutHandle = setTimeout(function(){
let value = this.props.input.value;
fetchUtils.fetchJson(API_ENDPOINT+'/groupNameCheck/'+value, { method: 'GET'})
.then((data) => {
console.log(data);
let exists = data.json.exists;
let name = data.json.name;
if(exists){
console.log(this.props.showNotification('The group name "'+name+'" already exists.'));
}else{
console.log(this.props.showNotification('The group name "'+name+'" does not exist.'));
}
})
.catch((e) => {
console.error(e);
//showNotification('Error: comment not approved', 'warning')
});
}.bind(this),500);
};
}
export default connect(null, {
showNotification: showNotificationAction
})(NameLookupTextInput);

Reference to component

Using knockout.js in node, how can I get a reference to a child VM component, which was invoked from a template?
Illustration
I have a Question Model, VM, containining a custom Resource component, with it's respective Model and VM. The Resource VM registers a custom component and receives a Resource Model object as a parameter, which was constructed by the parent Question Model. This constructed resource is passed as a parameter with the template:
QuestionModel.js
this = new QuestionModel(...);
this.resource = new ResourceModel(some data);
question-template.html
<div data-bind="foreach: { data: questions, as: 'question' }">
<!-- question related -->
<resource params="resource: question.resource"></resource>
</div>
ResourceVM.js
define(function(require, exports, module) {
var ko = require('knockout');
var ResourceViewModel = function ResourceViewModel(params) {
this.resource = params.resource;
this.somethingSpecific = function() {
return 'some value manipulate from this model';
}
}
ko.components.register('resource', {
viewModel: {
createViewModel: function(params, componentInfo) {
return new ResourceViewModel(params);
}
},
template: {
require: 'text!/resource-template.html'
}
});
return ResourceViewModel;
});
I want to be able to call functions of the Resource VM (like question.resourceVM.somethingSpecific()).
What is a proper way of getting a reference to a component child?
The only solution I can think of is to pass the parent object with the parameters and extend it from child, which is obviously bad.
Your QuestionModel already has access to this.resource, so the way forward might be by doing it through the data models, instead of through the view models. Having somethingSpecific() as an attribute on either QuestionModel or ResourceModel instead of ResourceViewModel would solve the problem nicely.
I would argue that manipulating data is the responsibility of the entity that holds it; the job of the ResourceViewModel is only to provide glue between the data model and the DOM.
var QuestionModel = function QuestionModel() {
this.somethingSpecific = function somethingSpecific() {
this.resource.doStuff();
};
};
this = new QuestionModel();
this.resource = new ResourceModel(some data);
You could then give your resource component access to the question instead of the child resource:
var ResourceViewModel = function ResourceViewModel(params) {
this.question = params.question;
this.resource = this.question.resource;
}

Resources