usage of ref in vue3 - vuejs3

I wrote a vue3 component which uses the VirtualScroller from PrimeVue and I would like to scroll to the end of the scroller each time I'm adding new elements. For that, there is scrollInView method which is defined on the component and documented here
My code looks like this (it's typescript with vue-class-component and single file syntax):
<template>
...
<VirtualScroller :items="content" :itemSize="50" class="streamscroller" ref="streamscroller">
<template v-slot:item="{ item }">
<pre>{{ item }}</pre>
</template>
</VirtualScroller>
...
</template>
<script lang="ts">
...
import { ref, ComponentPublicInstance } from "vue";
import VirtualScroller from "primevue/virtualscroller";
...
#Options({
components: {
VirtualScroller,
...
},
})
export default class StreamResultViewer extends Vue {
streamscroller = ref<ComponentPublicInstance<VirtualScroller>>();
content: string [] = [ "No output" ];
...
mounted(): void {
...
console.debug("scroller mounted: ", this.streamscroller.value); // <=== here, already the value is indefined
}
onData(msg: string): void {
const lines = msg.split('\n');
const content = [...this.content, ...lines];
this.content = content;
console.debug("scroller: ", this.streamscroller.value); // <== always undefined
this.streamscroller.value?.scrollInView(this.content.length, 'to-end', 'smooth'); // <== so never called
}
...
The virtual scroller works well (I can add lines each time they arrives and the scroll bar moves...) but I can never call the scroll method because the ref is undefined...
I'd be very grateful for any clue...
Thank you

The only workaround I found is too use $refs like this:
onData(msg: string): void {
const lines = msg.split('\n');
const content = [...this.content, ...lines];
this.content = content;
const scroller = this.$refs.streamscroller as VirtualScroller;
scroller.scrollInView(this.content.length, 'to-end', 'smooth');
}
This way, I am able to call the scrolling method and it works fine.
If someone can explain how it should work normally with ref<T>() in the vue-class-component + typescript mode, I'd be glad to hear that.

Related

Using composables with OptionsAPI

Our team is converting an app from Vue 2 to 3 and I am working on the final steps.
I am investigating whether we can convert our mixin files to composables. I have yet to find any documentation to support whether you can use composables with optionsAPI.
I have tried a little sample but I am seeing the limitations:
COMPOSABLE file useComposables:
import { ref, computed } from 'vue'
export default () => {
let first = ref('First')
let last = ref('Last')
let mycomputed = computed(() => {
return `${first.value} *** ${last.value}`
})
return {
first, mycomputed
}
}
COMPONENT:
import useComposables from '#/utils/useComposable'
created () {
let { first, mycomputed } = useComposables()
console.log('first', first.value)
console.log('mycomputed', mycomputed.value)
},
<template>
mycomputed {{ mycomputed }}
</template>
So, I see when I try to do interpolation on mycomputed computed variable in the template, the component doesn't have access to the computed variable because it is not in the computed option and doesn't belong to 'this'.
I can't seem to find any documentation to support using composables with options API.
Am I missing something or is this a no-go?
Thanks
OP achieved to solve that by using the following:
setup() {
let { first, mycomputed } = useComposables()
return {
first, mycomputed
}
},

stenciljs: Issue with slot

I have created a simple web (stencil) component AuthGuard, not to be be confused with Angular's AuthGuard.
The purpose of this component is to check if the user is logged in.
If yes, render the slot html.
If not, render the Signup button.
The component code is as follows:
import { Component, Host, h } from '#stencil/core';
import { Build, State } from '#stencil/core';
import { AuthService } from 'auth/auth.service';
import { ConfigService } from 'common/config.service';
#Component({
tag : 'auth-guard',
styleUrl : 'auth-guard.css',
shadow : true,
})
export class AuthGuard {
#State() canRender : boolean = false;
componentWillLoad() {
if (Build.isBrowser) {
const timerId = setInterval(() => {
if (AuthService.isInitialized) {
AuthService.vol$.subscribe(_u => {
this.canRender= true;
});
clearInterval(timerId);
}
}, ConfigService.loadTime);
}
}
render() {
console.log('auth guard :: render', this.canRender, AuthService.me);
return (
<Host>
{
this.canRender ? (
AuthService.me && AuthService.me.id.length > 0 ? (
<slot></slot>
) : (
<ion-button
href="/signup"
routerDirection="forward"
color="danger">
Signup
</ion-button>
)
): null
}
</Host>
);
}
}
Now in the other file, I use the following code:
<auth-guard slot='end'>
<volunteer-mini volunteer={AuthService.me}></volunteer-mini>
</auth-guard>
With this what I am expecting is
Nothing to be rendered, till this.canRender becomes true.
Once this.canRender becomes true, If AuthService.me is valid, render the slot HTML,
If AuthService.me is null, render signup button.
But seems when this.canRender is false, it tried to render volunteer-mini the slot HTML, which is a problem. Since volunteer-mini internally depends on AuthService.me, which is not yet initialized.
But once this.canRender becomes true, other 2 scenarios are working fine.
It's in general a bad idea to write an auth-guard using stencil. The core problem is that your slot exists before your component has initialized.
Therefor, using your current code, you'd have to manually remove the slot after you decided that you don't have the rights.
Additionally, if you do not define a slot position, but still provide a slot-content in your parent, it will still be appended to your inner children.
To resolve this problem, you can refactor your component to an function, like <Host>, but this has other pits to consider.

Binding to 'style.grid' throws 'sanitizing unsafe style value' warning

I'm creating a mechanism for defining and calculating my own reusable grids. Here's an example of what's returned
[left-bleed-start] 10.245000000001024px [left-bleed-end content-col-start] 1331.8500000001332px [content-col-end right-bleed-start] 10.245000000001024px [right-bleed-end]/[top-bleed-start] 10.245000000001024px [top-bleed-end link-row-start] 81.9600000000082px [content-row-end footer-row-start] 163.9200000000164px [footer-row-end bottom-bleed-start] 10.245000000001024px [bottom-bleed-end]
When applying it like this
<div class="appCompGrid" [style.grid]="Grid.GridCode"></div>
I get the santization warning. However if I copy and paste the value in like this
<div class="appCompGrid" style="grid: (same code);"></div>
everything works. The css class is where I define the display as grid seeing that it'll be consistent no matter what size the screen is. The only thing I could think to do was go back into the function and add + ';' to the end of where the grid code is put together figuring maybe that was throwing something off but it still gives the same error. I tried applying display: grid; inline to see if maybe there was a problem with it reading both from a css class and inline for some odd reason.
I'm using #HostListener to re-calculate the grid as the size or orientation changes, so far I haven't run into a problem with Angular functioning in this manner so I don't understand where to begin with figuring out why this is happening. Here's how I have my component classes set up.
Base Class
export class GridBuilder {
Settings : GridInit = new GridInit();
GridData : Array<GridType> = new Array();
Grid : GridOutput = new GridOutput();
constructor() { this.GridData = GridDefs; }
public buildGrid() {
const coreSettings : GridInit = this.Settings;
const gridData : GridType[] = this.GridData;
const w: number = multiply( coreSettings.Size.Width, coreSettings.Size.PixelRatio );
const h: number = multiply( coreSettings.Size.Height, coreSettings.Size.PixelRatio );
const o: string = checkOrientation( w, h );
const c: CellSpecs = calcCell( o, w );
const t: GridType = gridData.find( a => a.GridName == coreSettings.GridStyle );
const cols: string = calcArea( t.Columns, c );
const rows: string = calcArea( t.Rows, c );
this.Grid.GridCode = cols + '/' + rows + ';';
this.Grid.GridAreas = t.Areas;
}
}
Secondary class for app component/ any top tier container
export class SiteGrid extends GridBuilder {
constructor(){
super();
this.applySizeSettings();
}
applySizeSettings(){
this.Settings.Size.Width = window.innerWidth;
this.Settings.Size.Height = window.innerHeight;
this.Settings.Size.PixelRatio = window.devicePixelRatio;
}
}
the AppComponent
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent extends SiteGrid {
title = 'app';
#HostListener( 'window: resize', [ '$event' ] )onResize( event ){ this.applySizeSettings(); this.buildGrid(); }
constructor(){
super();
this.Settings.GridStyle = 'SiteGridA';
this.buildGrid();
}
}
I don't know how relevant this may be in helping figure out the solution but thought I'd show how things are flowing just incase. Anyone know why this warning is occurring?
You need to implement a sanitizer to cleanse your css, or bypass it...
constructor(private sanitizer: DomSanitizer) {
this.sanitizedCSS = sanitizer.bypassSecurityTrustStyle(Grid.GridCode) ;
}
As for why, this blog explains it pretty well, as does the DomSanitizer documentation.
DomSanitizer helps preventing Cross Site Scripting Security bugs (XSS) by sanitizing values to be safe to use in the different DOM contexts.

Why is componentDidMount method not being called in reactjs.net when I write on JSX

I'm a noob and I'm trying to compile a JSX following this tutorial :
http://xabikos.com/2015/03/18/Using-Reactjs-net-in-Web-Forms/
using reactjs.net``
In my helloWorld.jsx
class PageContent extends React.Component {
componentDidMount() {
this.state = { name: "Hello World from ComponentDidMount" };
}
constructor(props) {
super(props);
this.state = { name: "Hello World" };
}
componentWillMount() {
this.state = { name: "Hello World from ComponentWIllMount" };
}
render() {
return <h1>{this.state.name}</h1>
}
}
In my ReactConfig.cs
ReactSiteConfiguration.Configuration
.AddScript("~/Scripts/components/helloWorld.jsx");
In my Default.aspx.cs
var env = AssemblyRegistration.Container.Resolve<IReactEnvironment>
();
var objectModel = new { user = "React User" };
var reactComponent = env.CreateComponent("PageContent",
objectModel);
PageContent.Text = reactComponent.RenderHtml();
The page seems to works fine
it prints
"Hello World from Component Will Mount"
But when I comment the Component Will Mount, I dont receive anything from DidMount, it only prints
"Hello World"
Does anybody know why is this method never called ?
Thank you in advance
Your problem is you're not using this.setState. You are manually assigning a new value in this.state. React does not know to call render again to update your component, which is why your component doesn't update. It's not that this.state has not changed. It's that React hasn't been told to update what's shown on the screen. So, here's what happens with your code in terms of life cycles:
constructor: initializes everything (duh)
componentWillMount: In your case, setting this.state.name. render does not occur yet
render: renders this.state.name, which you have set in your componentWillMount
componentDidMount: sets this.state.name to your new value. no instructions to update your render function
If you use this.forceUpdate() in your componentDidMount, after you assign this.state.name to its new value, I believe it would update, but this is very bad practice. Use this.setState instead.
componentDidMount() {
this.setState({ name: "Hello World from ComponentDidMount" });
}
componentWillMount() {
this.setState({ name: "Hello World from ComponentWillMount" });
}
As you've noticed in your code, this.setState isn't entirely necessary in componentWillMount, because render has not been called yet. But it's good to just keep things consistent.

Selecting a Row in React Griddle, and changing tr background color

I'm just wondering if someone has already been able to change the color of a row, in React Griddle, by clicking on it (just once).
I'm experimenting things with JQuery, and even with Griddle Metadata, but it may be done in a cleaner way ?
Edit : I'm using React 15, Griddle inside MantraJS/Meteor, getting the data in my react Component using a Mantra container.
I can get the data by using onClick event, but not able to switch the background color in the onClick event, or playing with Metadatas.
Thanks !
EDIT : I use another view to display the content of the table, so for now I don't need to change the background of my tables cells, but if I found a solution I'll complete this post
You can use react-griddle props rowMetadata and onRowClick to do this:
class ComponentWithGriddle extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedRowId: 0,
};
}
onRowClick(row) {
this.setState({ selectedRowId: row.props.data.id });
}
render() {
const rowMetadata = {
bodyCssClassName: rowData => (rowData.id === this.state.selectedRowId ? 'selected' : ''),
};
return (
<Griddle
...
rowMetadata={rowMetadata}
onRowClick={this.onRowClick.bind(this)}
/>
);
}
}
Now this adds a selected class to the selected <tr> elements, so you can use custom styles to add colors or whatever styles you want to apply to the selected row.
Note that a more convenient API for selecting rows has been called for in the Griddle Github issues.
For whatever reason, I couldn't get Waiski's answer to work for me at all. I'm assuming that something must have changed in Griddle over the past two years. It looks like the current prevailing advice on the Web is to "implement row selection as a plugin", but I couldn't find any examples of that either. After a long hard look at the code for the Position plugin’s TableEnhancer on GitHub and a bunch of trial and error I eventually managed to cobble together the following row selection plugin for Griddle in TypeScript:
import * as React from "react";
import * as Redux from "redux";
import Griddle, { connect, GriddlePlugin, components } from "griddle-react";
export type RowId = string | number;
export type RowClickHandler = (event: React.MouseEvent<Element>, rowId: RowId) => void;
export type RowIdGetter<TData> = (rowData: TData) => RowId;
export interface IRowEnhancerProps {
rowClickHandler: RowClickHandler;
rowId: RowId;
isSelected: boolean;
}
export class RowSelector<TData> {
private _rowClickHandler: RowClickHandler = null;
private _rowIdGetter: RowIdGetter<TData>;
constructor(rowClickHandler: RowClickHandler, rowIdGetter: (rowData: TData) => RowId) {
this._rowClickHandler = rowClickHandler;
this._rowIdGetter = rowIdGetter;
}
public rowIdToSelect: RowId;
public plugin: GriddlePlugin = {
components: {
RowEnhancer: (OriginalComponent: React.ComponentClass<components.RowProps>) =>
this.rowSelectionEnhancer(OriginalComponent)
}
}
private rowSelectionEnhancer(
OriginalComponent: React.ComponentClass<components.RowProps>
): React.ComponentClass<components.RowProps> {
const rowDataSelector = (state, { griddleKey }) => {
return state
.get('data')
.find(rowMap => rowMap.get('griddleKey') === griddleKey)
.toJSON();
};
return Redux.compose(
connect((state, props) => {
const rowData: TData = rowDataSelector(state, props as { griddleKey });
const rowId: RowId = this._rowIdGetter(rowData);
return {
...props,
rowClickHandler: this._rowClickHandler,
rowId: rowId,
isSelected: rowId.toString() === this.rowIdToSelect.toString()
};
})
)(class extends React.Component<IRowEnhancerProps, any>{
public render() {
return (
<OriginalComponent
{...this.props}
onClick={(event) => this.props.rowClickHandler(event, this.props.rowId)}
className={this.props.isSelected ? "selected" : ""}
/>
);
}
});
}
}
Here's a rough outline of how it's used by a component. (Note that I had to selectively extract this example from a much larger and more complicated component, so there might be some errors/inconsistencies; sorry about that. It should still give a good overall idea of the approach.)
import * as React from "react";
import Griddle, { RowDefinition, plugins, GriddlePlugin} from "griddle-react";
import * as MyGriddlePlugins from "../GriddlePlugins";
export interface IPartInfo {
serialNumber: number,
name: string,
location: string
}
export interface IPartListProps{
parts: IPartInfo[],
selectedSerialNumber: number
}
export class PartList extends React.Component<IPartListProps, void > {
private rowSelector: MyGriddlePlugins.RowSelector<IPartInfo>;
private rowIdGetter: MyGriddlePlugins.RowIdGetter<IPartInfo>;
constructor(props?: IPartListProps, context?: any) {
super(props, context);
this._rowClickHandler = this._rowClickHandler.bind(this);
this.rowSelector = new MyGriddlePlugins.RowSelector(
this._rowClickHandler,
this._rowIdGetter);
}
private _rowClickHandler: MyGriddlePlugins.RowClickHandler =
(event: React.MouseEvent<Element>, selectedSerialNumber: MyGriddlePlugins.RowId) => {
if (selectedSerialNumber !== this.props.selectedSerialNumber) {
/*
Set state, dispatch an action, do whatever. The main point is that you
now have the actual event from the click on the row and the id value from
your data in a function on your component. If you can trigger another
render pass from here and set a fresh value for this.rowSelector.rowIdToSelect
then the "selected" CSS class will be applied to whatever row this click
event just came form so you can style it however you like.
*/
}
}
private _rowIdGetter: (rowData: IPartInfo) => MyGriddlePlugins.RowId =
(rowData: IPartInfo) => rowData.serialNumber;
public render(): JSX.Element {
this.rowSelector.rowIdToSelect = this.props.selectedSerialNumber;
return (
<div>
<Griddle
data={this.props.parts}
plugins={[plugins.LocalPlugin, this.rowSelector.plugin]}
>
<RowDefinition>
<ColumnDefinition id="name" title="Part Name" />
<ColumnDefinition id="location" title="Installed Location" />
<ColumnDefinition id="serailNumber" title="Serial Number" />
</RowDefinition>
</Griddle>
</div>
);
}
}
So, what's actually going on here? The component creates an instance of the plugin class at instantiation time, passing in an event handler to capture the click on the row and an accessor function to retrieve your ID value (not an inscrutable internal ID) from a row of your data. Just before the component returns its rendering, a value is set on the component's instance of the plugin, that way, when Griddle renders the plugin has the data to figure out when it's on a selected row and then adjust the CSS accordingly. The handler function from your component is then assigned to the row's onClick handler so your component can get the data from the click and do whatever it needs to do.
This passes the "It works for me" test (on React 15.6) which in my case is a straightforward master/detail view driven by a traditional table implemented through Griddle. I have no idea how well it would work with some of Griddle's more advanced features.

Resources