How to handle #Prop() logic using getters/setters within StencilJS - stenciljs

I would like to add a getter / setter to my StencilJS component. As I'm currently aware, you cannot use the get / set logic with #Prop(). How would I generate similar logic in a StencilJS component?
For example, say I have the following Angular component, I could do the following:
#Input()
get enabled(): boolean {
return this._enabledOverride == null ? this._getDefaultEnabled() : this.enabledOverride;
}
set enabled(value: boolean) {
this._enabledOverride = coerceBooleanProperty(value);
}
_enabledOverride: boolean|null = null;
private _getDefaultEnabled() {
return this.stepControl ? this.stepControl.valid && this.interacted : this.interacted;
}
My attempt at a StencilJS component would be the following:
Child Component
_enabled: boolean;
#Prop() enabled: boolean;
#Watch('enabled')
watchHandler(newValue: boolean, oldValue: boolean) {
this._enabledOverride = coerceBooleanProperty(newValue);
this._enabled = this._enabledOverride == null ? this.getDefaultEnabled() : this._enabledOverride;
}
_enabledOverride: boolean | null = null;
Here, I Watch for enabled to change, do logic, and set a private instance variable of _enabled to the updated value. This is great, for use within the instance. But what if I need to access the enabled property from the parent component, also my logic wouldn't run on page load since #Watch() isn't triggered on page load. Once it does run, there is now a mismatch in value between enabled and _enabled. So for example in my parent component if I had:
Parent Component
export class Parent {
#Element() el: HTMLElement;
componentWillLoad() {
const childElements = this.el.querySelectorAll('app-child-component');
childElements.forEach((el) => {
// attempting to access the enabled property would be out of sync with the private _enabled
console.log(el.enabled)
})
}
}
As seen above, attempting to access the enabled property would be out of sync with the private _enabled giving me an out-of-date property value. How should I go about this? Thanks!

You could add a method with the #Method decorator to act as a getter:
#Method()
async getEnabled() {
return this._enabledOverride == null ? this._getDefaultEnabled() : this.enabledOverride;
}
And then use it like
const childElements = this.el.querySelectorAll('app-child-component');
await Promise.all(
childElements.map(async el => console.log(await el.getEnabled()))
)

Related

mobx challenge: getters and setters into an observable array

I'm trying to write getters and setters into an observable array and it isn't working. The code below gives me the following error: Error: [MobX] No annotations were passed to makeObservable, but no decorator members have been found either
I've tried different combinations of decorators, but nothing seems to work. The behavior I want is whenever AppModel.text is updated, any UI rending the getter for text should update. Also whenever gonext() is called on the object, then any UI rending from AppModel.text should update and render data from the new 0 item on the array.
class DataThing
{
#observable text?: string = "foo";
}
class AppModel
{
get text() { return this.items[0].text}
set text(value: string | undefined) { this.items[0].text = value;}
items: DataThing[] = observable( new Array<DataThing>());
constructor() {
makeObservable(this);
this.items.push(new DataThing());
}
gonext() { this.items.unshift(new DataThing()); }
}
EDIT:
I ended up doing the following, but would still like to understand how to index into an array in an observable way.
class DataThing
{
#observable text?: string = "zorp";
constructor(){makeObservable(this);}
}
class AppModel
{
#observable _current?:DataThing;
get current() {return this._current;}
items: DataThing[] = observable( new Array<DataThing>());
constructor() {
makeObservable(this);
this.gonext();
}
gonext() {
this.items.unshift(new DataThing());
this._current = this.items[0];
}
}

How to mutate a property within a stenciljs component without triggering watch

I have a component with a viewport property. I want to listen for changes to this property, do some calculations and reflect a possibly changed value back to the property. My first attempt looked something like this:
class MyComponent {
#Prop()
viewport: ViewportData
#Watch('viewport)
viewportChanged(newValue: ViewportData, oldValue:ViewportData) {
... do some calculations
// Reflect value back as property
this.viewport = computedViewport;
}
}
This results in a stack overflow because reflecting the value back triggers another call to the watch function. I could prevent it by having a flag saying if this is an internal change or not. Something like this:
class MyComponent {
internalViewportChange = false;
#Prop()
viewport: ViewportData
#Watch('viewport)
viewportChanged(newValue: ViewportData, oldValue:ViewportData) {
if (this.internalViewportChange) {
this.internalViewportChange = false;
return;
}
... do some calculations
// Reflect value back as property
this.internalViewportChange = true;
this.viewport = computedViewport;
}
}
I don't like this approach. And is looking for something better. This problem could normally be solved by using getters and setters and a private variable keeping the actual state:
class MyComponent {
private _viewport: ViewportData
get viewport() {
return this._viewport;
}
set viewport() {
... do some calculations
// Reflect value back as property
this.viewport = computedViewport;
}
}
However, using Stenciljs the getters and setters are autogenerated. Any good ideas?
I'd probably break the two-way prop setting, and
create a unidirectional data flow, and emit events instead. Something like:
class MyComponent
#Event() viewportChanged: EventEmitter;
#Prop() viewport: ViewportData;
#State() _computedViewport: ViewportData;
#Watch('viewport') onViewportChanged(newValue) {
// do calculations
this._computedViewport = computedViewport;
this.viewportChanged.emit(this._computedViewport);
}
Internally, you'd only work on _computedViewport, and the public viewportProp is only there for users to update themselves. Ostensibly you could also expose a #Method() that does the same thing.

Aurelia-Validation - Clarity on object validation w/addObject()

I'm attempting to manually validate an object which is updated via a customEvent.
//home.html
<type-ahead change.delegate="updateValue($event.detail, true/false)"></type-ahead>
I have two of the above elements, which I want to assign to the properties of origin and destination based on a boolean value.
I also want to validate both of these properties against the same custom rule, which I've defined in setupValidation on my viewmodel below.
Both of these are objects, that need some complex validation (I've simplified it for demonstration purposes), so I've used .ensureObject() and manually add both of these objects to the validation controller.
I would expect that I would only have to add the objects once (during my initial setupValidation(), but I've found that I have to remove, and then re-add the object to the validation controller whenever it changes.
If you look at updateValue(...), you'll see that I'm expecting this.destination to validate to the updated object, but I'm seeing my results still be null on validation. However, this.origin does update and the validation succeeds (as I'm manually updating the controller).
I would expect not to have to manually update the controller. Is this expected?
//home.ts
#autoinject
export class Home {
origin = null;
destination = null;
private controller: ValidationController;
public canSubmit: boolean = false;
public error: string;
private rules;
constructor(controllerFactory: ValidationControllerFactory) {
this.controller = controllerFactory.createForCurrentScope();
this.controller.validateTrigger = validateTrigger.manual;
}
bind() {
this.setupValidation();
}
private validate() {
this.controller.validate()
.then(results => {
this.canSubmit = results.valid;
});
}
private setupValidation() {
ValidationRules.customRule(
'rule1',
(value) => {
if(value.property && value.property.length === 3)
return true;
else
return false;
},
`\${$displayName} must be 3 characters long`
);
this.rules = ValidationRules
.ensureObject()
.required()
.satisfiesRule('rule1')
.rules;
this.controller.addObject(this.origin, this.rules);
this.controller.addObject(this.destination, this.rules);
}
updateValue(newValue, isOrigin) {
if(isOrigin) {
this.controller.removeObject(this.origin);
this.origin = newValue;
this.controller.addObject(this.origin, this.rules);
}
else
this.destination = newValue;
this.validate();
}
}
Thank you, let me know if there are any additional details needed.

View not updating after changing value of component array

I need to update my view on changing array in my *.component.ts
I use
public getFolders() : void {
this.webService.getFolders({client_id : this.local.get('clientUser').client_id}).subscribe( this.processSkills.bind(this, this.local.get('clientUser')))
}
processSkills(res: any, myobj): void {
if(res.status){
myobj.folders = res.folders;
this.local.set('clientUser', myobj);
this.userObj = this.local.get('clientUser');
}
}
It updates my array i saw in console it update my session value which i saw after pressing F5 but it doesn't update my view
Initially i am assigning my array to variable from my session object.
import { BehaviorSubject } from 'rxjs';
private messageSource = new BehaviorSubject(this.local.get('clientUser'));
currentMessage = this.messageSource.asObservable();
I resolved it and found a solution to pass our array into session and make the code into our provider which works as observable to my array and then recieve
currentMessage to our receiver function to update on view.
this.webService.currentMessage.subscribe(message => {
this.userObj = message;
})
will receive updated value and will reflect on view.

Aurelia DataTables Recompile

I've been exploring Aurelia and so far have loved what I've seen. I've come accross an issue that I'm not really sure how to solve. I used jquery datatables for large results in my current app with angular, using server side fetches. Datatables has a function you can call whenever a new row is added to the table (fnRowCallback - http://legacy.datatables.net/ref#fnRowCallback, or "createdRow" - https://datatables.net/examples/advanced_init/row_callback.html#) - This is really handy as you can recompile the dom after each row (costly I know).
This enables you to reference functions that exist in the current scope (or viewModel) that the datatable exists in. For example:
In my view model:
export class DataTableTest{
test(){
alert('this is a test');
}
}
In the return results from a datatable fetch:
{name:'blah',age:40,actions:"<a click.delegate='test();'>Test</a>"}
For some reason I can't seem to figure out how to recompile an element once it has been added to the dom.
Does anyone have any ideas how you could do this?
UPDATE:
These are the original options I pass to datatables:
var options = {
"fnRowCallback": function (nRow) {
$compile($(nRow).contents())(scope);
}
};
I've tried the following after injecting that compiler service:
"fnRowCallback": function (nRow) {
this.compiler.compile($(nRow).contents()).fragment.innerHTML;
},
But I always get Uncaught TypeError: Cannot read property 'compile' of undefined - I do this in the "attached" function.. If I console.log(this.compiler) outside of these options, it's available. Also, we don't need to return html back to datatables, just run the compile on the contents. Many thanks for all your help!
You can use a compiler service to compile the element:
import {inject, ViewCompiler, ViewResources, Container} from 'aurelia-framework';
/**
* Compiler service
*
* compiles an HTML element with aurelia
*/
#inject(ViewCompiler, ViewResources, Container)
export class Compiler {
viewCompiler: any;
resources: any;
container: any;
constructor(viewCompiler, resources, container) {
this.viewCompiler = viewCompiler;
this.resources = resources;
this.container = container;
}
compile(templateOrFragment, ctx = null, viewSlot = null):any {
if (typeof templateOrFragment === "string") {
var temp = document.createElement('span');
temp.innerHTML = templateOrFragment;
templateOrFragment = temp;
}
var view = this.viewCompiler.compile(templateOrFragment, this.resources).create(this.container, ctx);
return view;
}
}
I use this in Kendo in the cell template callback function (it lets you return a string that will become the cell contents)
function(dataItem) {
var cellctx = { "$item": dataItem, "$parent": ctx };
return this.compiler.compile(templateString, cellctx).fragment.innerHTML;
}
(this happens in Aurelia's bind callback so the ctx is the executionContext)
I just wrap the current data item up in a context and alias it as $item so I can work with it.
Looks something like this:
<kendo-grid>
<kendo-grid-col title="Main Office" field="IsMainOffice">
<kendo-template><img if.bind="$item.IsMainOffice" src="/content/img/accept.png" /></kendo-template>
</kendo-grid-col>
</kendo-grid>